├── screenshot └── image.png ├── .gitignore ├── .env.example ├── src ├── services │ ├── whatsapp │ │ ├── index.js │ │ ├── WhatsAppManager.js │ │ ├── MessageFormatter.js │ │ └── BaileysStore.js │ └── websocket │ │ └── WebSocketManager.js ├── middleware │ └── apiKeyAuth.js ├── config │ ├── swagger.js │ └── swagger-paths.js └── routes │ └── whatsapp.js ├── .dockerignore ├── package.json ├── LICENSE ├── Dockerfile ├── docker-compose.yml ├── index.js ├── public └── websocket-test.html └── README.md /screenshot/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farinchan/chatery_whatsapp/HEAD/screenshot/image.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | .DS_Store 4 | *.log 5 | auth_info/ 6 | sessions/ 7 | public/media/ 8 | n8n-nodes-chatery-whatsapp/ -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | NODE_ENV=development 3 | 4 | DASHBOARD_USERNAME=admin 5 | DASHBOARD_PASSWORD=securepassword123 6 | API_KEY=your_api_key_here -------------------------------------------------------------------------------- /src/services/whatsapp/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WhatsApp Service Module 3 | * 4 | * Struktur file: 5 | * - BaileysStore.js : Custom in-memory store untuk Baileys v7 6 | * - MessageFormatter.js : Utility untuk format pesan 7 | * - WhatsAppSession.js : Class untuk mengelola satu sesi WhatsApp 8 | * - WhatsAppManager.js : Singleton untuk mengelola semua sesi 9 | * - index.js : Entry point (file ini) 10 | */ 11 | 12 | const WhatsAppManager = require('./WhatsAppManager'); 13 | 14 | // Singleton instance 15 | const whatsappManager = new WhatsAppManager(); 16 | 17 | module.exports = whatsappManager; 18 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | npm-debug.log 4 | yarn-debug.log 5 | yarn-error.log 6 | 7 | # Git 8 | .git 9 | .gitignore 10 | 11 | # Docker 12 | Dockerfile 13 | Dockerfile.dev 14 | docker-compose*.yml 15 | .dockerignore 16 | 17 | # IDE 18 | .vscode 19 | .idea 20 | *.swp 21 | *.swo 22 | 23 | # Environment files (use docker-compose env instead) 24 | .env 25 | .env.local 26 | .env.*.local 27 | 28 | # Sessions and data (mounted as volumes) 29 | sessions/ 30 | store/ 31 | 32 | # Media files (mounted as volume) 33 | public/media/* 34 | !public/media/.gitkeep 35 | 36 | # Documentation 37 | README.md 38 | CHANGELOG.md 39 | LICENSE 40 | docs/ 41 | screenshot/ 42 | 43 | # N8N nodes package 44 | n8n-nodes-chatery-whatsapp/ 45 | 46 | # Tests 47 | test/ 48 | tests/ 49 | coverage/ 50 | *.test.js 51 | *.spec.js 52 | 53 | # Build artifacts 54 | dist/ 55 | build/ 56 | 57 | # Logs 58 | logs/ 59 | *.log 60 | 61 | # OS files 62 | .DS_Store 63 | Thumbs.db 64 | -------------------------------------------------------------------------------- /src/middleware/apiKeyAuth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * API Key Authentication Middleware 3 | * 4 | * Validates X-Api-Key header against the API_KEY environment variable. 5 | * If API_KEY is not set or empty, authentication is skipped (open access). 6 | */ 7 | 8 | const apiKeyAuth = (req, res, next) => { 9 | const apiKey = process.env.API_KEY; 10 | 11 | // If no API key is configured, skip authentication 12 | if (!apiKey || apiKey === '' || apiKey === 'your_api_key_here') { 13 | return next(); 14 | } 15 | 16 | const providedKey = req.headers['x-api-key']; 17 | 18 | if (!providedKey) { 19 | return res.status(401).json({ 20 | success: false, 21 | message: 'Missing X-Api-Key header' 22 | }); 23 | } 24 | 25 | if (providedKey !== apiKey) { 26 | return res.status(403).json({ 27 | success: false, 28 | message: 'Invalid API key' 29 | }); 30 | } 31 | 32 | next(); 33 | }; 34 | 35 | module.exports = apiKeyAuth; 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatery_whatsapp", 3 | "version": "1.0.0", 4 | "description": "Backend for chatery.app", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "dev": "node --watch index.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/farinchan/chatery_whatsapp.git" 14 | }, 15 | "keywords": [ 16 | "chatery" 17 | ], 18 | "author": "Fajri Rinaldi Chan", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/farinchan/chatery_whatsapp/issues" 22 | }, 23 | "homepage": "https://github.com/farinchan/chatery_whatsapp#readme", 24 | "dependencies": { 25 | "@whiskeysockets/baileys": "^7.0.0-rc.9", 26 | "cors": "^2.8.5", 27 | "dotenv": "^17.2.3", 28 | "express": "^5.2.1", 29 | "pino": "^10.1.0", 30 | "qrcode": "^1.5.4", 31 | "socket.io": "^4.8.1", 32 | "swagger-jsdoc": "^6.2.8", 33 | "swagger-ui-express": "^5.0.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Fajri Rinaldi Chan 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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:20-alpine AS builder 3 | 4 | WORKDIR /app 5 | 6 | # Copy package files 7 | COPY package*.json ./ 8 | 9 | # Install dependencies 10 | RUN npm ci --only=production 11 | 12 | # Production stage 13 | FROM node:20-alpine 14 | 15 | # Add labels 16 | LABEL maintainer="Farin Azis Chan " 17 | LABEL description="Chatery WhatsApp API - Multi-session WhatsApp API with Baileys" 18 | LABEL version="1.0.0" 19 | 20 | # Create non-root user for security 21 | RUN addgroup -g 1001 -S chatery && \ 22 | adduser -S -D -H -u 1001 -G chatery chatery 23 | 24 | WORKDIR /app 25 | 26 | # Copy dependencies from builder 27 | COPY --from=builder /app/node_modules ./node_modules 28 | 29 | # Copy application files 30 | COPY . . 31 | 32 | # Create directories for sessions and media with proper permissions 33 | RUN mkdir -p /app/sessions /app/public/media /app/store && \ 34 | chown -R chatery:chatery /app 35 | 36 | # Switch to non-root user 37 | USER chatery 38 | 39 | # Expose port 40 | EXPOSE 3000 41 | 42 | # Health check 43 | HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ 44 | CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1 45 | 46 | # Start the application 47 | CMD ["node", "index.js"] 48 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | chatery: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | container_name: chatery-whatsapp-api 9 | restart: unless-stopped 10 | ports: 11 | - "${PORT:-3000}:3000" 12 | environment: 13 | - NODE_ENV=production 14 | - PORT=3000 15 | - CORS_ORIGIN=${CORS_ORIGIN:-*} 16 | - DASHBOARD_USERNAME=${DASHBOARD_USERNAME:-admin} 17 | - DASHBOARD_PASSWORD=${DASHBOARD_PASSWORD:-admin123} 18 | - API_KEY=${API_KEY:-} 19 | volumes: 20 | # Persist WhatsApp sessions 21 | - chatery_sessions:/app/sessions 22 | # Persist received media files 23 | - chatery_media:/app/public/media 24 | # Persist message store 25 | - chatery_store:/app/store 26 | healthcheck: 27 | test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/"] 28 | interval: 30s 29 | timeout: 10s 30 | retries: 3 31 | start_period: 10s 32 | logging: 33 | driver: "json-file" 34 | options: 35 | max-size: "10m" 36 | max-file: "3" 37 | networks: 38 | - chatery-network 39 | 40 | volumes: 41 | chatery_sessions: 42 | driver: local 43 | chatery_media: 44 | driver: local 45 | chatery_store: 46 | driver: local 47 | 48 | networks: 49 | chatery-network: 50 | driver: bridge 51 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const path = require('path'); 4 | const http = require('http'); 5 | const swaggerUi = require('swagger-ui-express'); 6 | const swaggerSpec = require('./src/config/swagger'); 7 | require('dotenv').config(); 8 | 9 | const app = express(); 10 | const server = http.createServer(app); 11 | const PORT = process.env.PORT || 3000; 12 | 13 | // Import Routes 14 | const whatsappRoutes = require('./src/routes/whatsapp'); 15 | 16 | // Import Middleware 17 | const apiKeyAuth = require('./src/middleware/apiKeyAuth'); 18 | 19 | // Import WebSocket Manager 20 | const wsManager = require('./src/services/websocket/WebSocketManager'); 21 | 22 | // Initialize WebSocket 23 | wsManager.initialize(server, { 24 | cors: { 25 | origin: process.env.CORS_ORIGIN || '*' 26 | } 27 | }); 28 | 29 | // Middleware 30 | app.use(cors()); 31 | app.use(express.json()); 32 | app.use(express.urlencoded({ extended: true })); 33 | 34 | // Serve static files from public folder (for media access) 35 | app.use('/media', express.static(path.join(__dirname, 'public', 'media'))); 36 | 37 | // Serve Dashboard 38 | app.get('/dashboard', (req, res) => { 39 | res.sendFile(path.join(__dirname, 'public', 'dashboard.html')); 40 | }); 41 | 42 | // Serve WebSocket test page 43 | app.get('/ws-test', (req, res) => { 44 | res.sendFile(path.join(__dirname, 'public', 'websocket-test.html')); 45 | }); 46 | 47 | // Swagger UI Options 48 | const swaggerUiOptions = { 49 | customCss: ` 50 | .swagger-ui .topbar { display: none } 51 | .swagger-ui .info { margin: 20px 0 } 52 | .swagger-ui .info .title { color: #25D366 } 53 | `, 54 | customSiteTitle: 'Chatery WhatsApp API - Documentation', 55 | customfavIcon: '/media/favicon.ico' 56 | }; 57 | 58 | // API Documentation (Swagger UI) at root 59 | app.use('/', swaggerUi.serve); 60 | app.get('/', swaggerUi.setup(swaggerSpec, swaggerUiOptions)); 61 | 62 | // Swagger JSON endpoint 63 | app.get('/api-docs.json', (req, res) => { 64 | res.setHeader('Content-Type', 'application/json'); 65 | res.send(swaggerSpec); 66 | }); 67 | 68 | app.get('/api/health', (req, res) => { 69 | res.json({ 70 | success: true, 71 | message: 'Server is running', 72 | timestamp: new Date().toISOString() 73 | }); 74 | }); 75 | 76 | // Dashboard Login 77 | app.post('/api/dashboard/login', (req, res) => { 78 | const { username, password } = req.body; 79 | 80 | const validUsername = process.env.DASHBOARD_USERNAME || 'admin'; 81 | const validPassword = process.env.DASHBOARD_PASSWORD || 'admin123'; 82 | 83 | if (username === validUsername && password === validPassword) { 84 | res.json({ 85 | success: true, 86 | message: 'Login successful' 87 | }); 88 | } else { 89 | res.status(401).json({ 90 | success: false, 91 | message: 'Invalid username or password' 92 | }); 93 | } 94 | }); 95 | 96 | // WebSocket Stats 97 | app.get('/api/websocket/stats', (req, res) => { 98 | res.json({ 99 | success: true, 100 | data: wsManager.getStats() 101 | }); 102 | }); 103 | 104 | // WhatsApp Routes (with API Key Authentication) 105 | app.use('/api/whatsapp', apiKeyAuth, whatsappRoutes); 106 | 107 | // 404 Handler 108 | app.use((req, res) => { 109 | res.status(404).json({ 110 | success: false, 111 | message: 'Route not found' 112 | }); 113 | }); 114 | 115 | // Error Handler 116 | app.use((err, req, res, next) => { 117 | console.error(err.stack); 118 | res.status(500).json({ 119 | success: false, 120 | message: 'Internal Server Error' 121 | }); 122 | }); 123 | 124 | // Start Server 125 | server.listen(PORT, () => { 126 | console.log(`Chatery WhatsApp API running on http://localhost:${PORT}`); 127 | console.log(`WebSocket server running on ws://localhost:${PORT}`); 128 | console.log(`API Documentation: http://localhost:${PORT}`); 129 | }); 130 | -------------------------------------------------------------------------------- /src/services/whatsapp/WhatsAppManager.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const WhatsAppSession = require('./WhatsAppSession'); 4 | 5 | /** 6 | * WhatsApp Manager Class 7 | * Mengelola semua sesi WhatsApp (Singleton) 8 | */ 9 | class WhatsAppManager { 10 | constructor() { 11 | this.sessions = new Map(); 12 | this.sessionsFolder = path.join(process.cwd(), 'sessions'); 13 | this.initExistingSessions(); 14 | } 15 | 16 | /** 17 | * Load existing sessions on startup 18 | */ 19 | async initExistingSessions() { 20 | try { 21 | if (!fs.existsSync(this.sessionsFolder)) { 22 | fs.mkdirSync(this.sessionsFolder, { recursive: true }); 23 | return; 24 | } 25 | 26 | const sessionDirs = fs.readdirSync(this.sessionsFolder); 27 | for (const sessionId of sessionDirs) { 28 | const sessionPath = path.join(this.sessionsFolder, sessionId); 29 | if (fs.statSync(sessionPath).isDirectory()) { 30 | console.log(`🔄 Restoring session: ${sessionId}`); 31 | // Session will load its own config from file 32 | const session = new WhatsAppSession(sessionId, {}); 33 | this.sessions.set(sessionId, session); 34 | await session.connect(); 35 | } 36 | } 37 | } catch (error) { 38 | console.error('Error initializing sessions:', error); 39 | } 40 | } 41 | 42 | /** 43 | * Create a new session or reconnect existing 44 | * @param {string} sessionId - Session identifier 45 | * @param {Object} options - Session options 46 | * @param {Object} options.metadata - Custom metadata to store with session 47 | * @param {Array} options.webhooks - Array of webhook configs [{ url, events }] 48 | * @returns {Object} 49 | */ 50 | async createSession(sessionId, options = {}) { 51 | // Validate session ID 52 | if (!sessionId || !/^[a-zA-Z0-9_-]+$/.test(sessionId)) { 53 | return { 54 | success: false, 55 | message: 'Invalid session ID. Use only letters, numbers, underscore, and dash.' 56 | }; 57 | } 58 | 59 | // Check if session already exists 60 | if (this.sessions.has(sessionId)) { 61 | const existingSession = this.sessions.get(sessionId); 62 | 63 | // Update config if provided 64 | if (options.metadata || options.webhooks) { 65 | existingSession.updateConfig(options); 66 | } 67 | 68 | if (existingSession.connectionStatus === 'connected') { 69 | return { 70 | success: false, 71 | message: 'Session already connected', 72 | data: existingSession.getInfo() 73 | }; 74 | } 75 | // Reconnect existing session 76 | await existingSession.connect(); 77 | return { 78 | success: true, 79 | message: 'Reconnecting existing session', 80 | data: existingSession.getInfo() 81 | }; 82 | } 83 | 84 | // Create new session with options 85 | const session = new WhatsAppSession(sessionId, options); 86 | session._saveConfig(); // Save initial config 87 | this.sessions.set(sessionId, session); 88 | await session.connect(); 89 | 90 | return { 91 | success: true, 92 | message: 'Session created', 93 | data: session.getInfo() 94 | }; 95 | } 96 | 97 | /** 98 | * Get session by ID 99 | * @param {string} sessionId 100 | * @returns {WhatsAppSession|undefined} 101 | */ 102 | getSession(sessionId) { 103 | return this.sessions.get(sessionId); 104 | } 105 | 106 | /** 107 | * Get all sessions info 108 | * @returns {Array} 109 | */ 110 | getAllSessions() { 111 | const sessionsInfo = []; 112 | for (const [sessionId, session] of this.sessions) { 113 | sessionsInfo.push(session.getInfo()); 114 | } 115 | return sessionsInfo; 116 | } 117 | 118 | /** 119 | * Delete a session 120 | * @param {string} sessionId 121 | * @returns {Object} 122 | */ 123 | async deleteSession(sessionId) { 124 | const session = this.sessions.get(sessionId); 125 | if (!session) { 126 | return { success: false, message: 'Session not found' }; 127 | } 128 | 129 | await session.logout(); 130 | this.sessions.delete(sessionId); 131 | return { success: true, message: 'Session deleted successfully' }; 132 | } 133 | 134 | /** 135 | * Get session QR code info 136 | * @param {string} sessionId 137 | * @returns {Object|null} 138 | */ 139 | getSessionQR(sessionId) { 140 | const session = this.sessions.get(sessionId); 141 | if (!session) { 142 | return null; 143 | } 144 | return session.getInfo(); 145 | } 146 | } 147 | 148 | module.exports = WhatsAppManager; 149 | -------------------------------------------------------------------------------- /src/services/whatsapp/MessageFormatter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Message Formatter Utility 3 | * Format pesan WhatsApp untuk response API 4 | */ 5 | class MessageFormatter { 6 | /** 7 | * Format message untuk response 8 | * @param {Object} msg - Raw message object dari Baileys 9 | * @returns {Object|null} 10 | */ 11 | static formatMessage(msg) { 12 | if (!msg || !msg.message) return null; 13 | 14 | const messageContent = msg.message; 15 | let type = 'unknown'; 16 | let content = null; 17 | let caption = null; 18 | let mimetype = null; 19 | let filename = null; 20 | 21 | if (messageContent?.conversation) { 22 | type = 'text'; 23 | content = messageContent.conversation; 24 | } else if (messageContent?.extendedTextMessage) { 25 | type = 'text'; 26 | content = messageContent.extendedTextMessage.text; 27 | } else if (messageContent?.imageMessage) { 28 | type = 'image'; 29 | caption = messageContent.imageMessage.caption || null; 30 | mimetype = messageContent.imageMessage.mimetype || null; 31 | } else if (messageContent?.videoMessage) { 32 | type = 'video'; 33 | caption = messageContent.videoMessage.caption || null; 34 | mimetype = messageContent.videoMessage.mimetype || null; 35 | } else if (messageContent?.audioMessage) { 36 | type = messageContent.audioMessage.ptt ? 'ptt' : 'audio'; 37 | mimetype = messageContent.audioMessage.mimetype || null; 38 | } else if (messageContent?.documentMessage) { 39 | type = 'document'; 40 | filename = messageContent.documentMessage.fileName || null; 41 | mimetype = messageContent.documentMessage.mimetype || null; 42 | } else if (messageContent?.stickerMessage) { 43 | type = 'sticker'; 44 | mimetype = messageContent.stickerMessage.mimetype || null; 45 | } else if (messageContent?.locationMessage) { 46 | type = 'location'; 47 | content = { 48 | latitude: messageContent.locationMessage.degreesLatitude, 49 | longitude: messageContent.locationMessage.degreesLongitude, 50 | name: messageContent.locationMessage.name || null, 51 | address: messageContent.locationMessage.address || null 52 | }; 53 | } else if (messageContent?.contactMessage) { 54 | type = 'contact'; 55 | content = { 56 | displayName: messageContent.contactMessage.displayName, 57 | vcard: messageContent.contactMessage.vcard 58 | }; 59 | } else if (messageContent?.contactsArrayMessage) { 60 | type = 'contacts'; 61 | content = messageContent.contactsArrayMessage.contacts?.map(c => ({ 62 | displayName: c.displayName, 63 | vcard: c.vcard 64 | })); 65 | } else if (messageContent?.reactionMessage) { 66 | type = 'reaction'; 67 | content = { 68 | emoji: messageContent.reactionMessage.text, 69 | targetMessageId: messageContent.reactionMessage.key?.id 70 | }; 71 | } else if (messageContent?.protocolMessage) { 72 | type = 'protocol'; 73 | content = messageContent.protocolMessage.type; 74 | } 75 | 76 | return { 77 | id: msg.key.id, 78 | chatId: msg.key.remoteJid, 79 | fromMe: msg.key.fromMe || false, 80 | sender: msg.key.participant || msg.key.remoteJid, 81 | senderPhone: (msg.key.participant || msg.key.remoteJid)?.split('@')[0], 82 | senderName: msg.pushName || null, 83 | timestamp: typeof msg.messageTimestamp === 'object' 84 | ? msg.messageTimestamp.low 85 | : msg.messageTimestamp, 86 | type: type, 87 | content: content, 88 | caption: caption, 89 | mimetype: mimetype, 90 | filename: filename, 91 | mediaUrl: msg._mediaPath || null, // URL to access saved media 92 | isGroup: msg.key.remoteJid?.includes('@g.us') || false, 93 | quotedMessage: msg.message?.extendedTextMessage?.contextInfo?.quotedMessage ? { 94 | id: msg.message.extendedTextMessage.contextInfo.stanzaId, 95 | sender: msg.message.extendedTextMessage.contextInfo.participant 96 | } : null 97 | }; 98 | } 99 | 100 | /** 101 | * Format last message preview untuk chat overview 102 | * @param {Object} msg - Raw message object 103 | * @returns {Object|null} 104 | */ 105 | static formatLastMessagePreview(msg) { 106 | if (!msg || !msg.message) return null; 107 | 108 | const content = msg.message; 109 | let type = 'unknown'; 110 | let text = null; 111 | 112 | if (content.conversation) { 113 | type = 'text'; 114 | text = content.conversation; 115 | } else if (content.extendedTextMessage?.text) { 116 | type = 'text'; 117 | text = content.extendedTextMessage.text; 118 | } else if (content.imageMessage) { 119 | type = 'image'; 120 | text = content.imageMessage.caption || '📷 Photo'; 121 | } else if (content.videoMessage) { 122 | type = 'video'; 123 | text = content.videoMessage.caption || '🎥 Video'; 124 | } else if (content.audioMessage) { 125 | type = content.audioMessage.ptt ? 'ptt' : 'audio'; 126 | text = content.audioMessage.ptt ? '🎤 Voice message' : '🎵 Audio'; 127 | } else if (content.documentMessage) { 128 | type = 'document'; 129 | text = `📄 ${content.documentMessage.fileName || 'Document'}`; 130 | } else if (content.stickerMessage) { 131 | type = 'sticker'; 132 | text = '🏷️ Sticker'; 133 | } else if (content.locationMessage) { 134 | type = 'location'; 135 | text = '📍 Location'; 136 | } else if (content.contactMessage) { 137 | type = 'contact'; 138 | text = `👤 ${content.contactMessage.displayName || 'Contact'}`; 139 | } else if (content.reactionMessage) { 140 | type = 'reaction'; 141 | text = content.reactionMessage.text || '👍'; 142 | } 143 | 144 | return { 145 | type: type, 146 | text: text ? (text.length > 100 ? text.substring(0, 100) + '...' : text) : null, 147 | fromMe: msg.key?.fromMe || false, 148 | timestamp: msg.messageTimestamp || 0 149 | }; 150 | } 151 | } 152 | 153 | module.exports = MessageFormatter; 154 | -------------------------------------------------------------------------------- /src/config/swagger.js: -------------------------------------------------------------------------------- 1 | const swaggerJsdoc = require('swagger-jsdoc'); 2 | 3 | const options = { 4 | definition: { 5 | openapi: '3.0.0', 6 | info: { 7 | title: 'Chatery WhatsApp API', 8 | version: '1.0.0', 9 | description: ` 10 | A powerful WhatsApp API backend built with Express.js and Baileys library. 11 | 12 | ## Quick Links 13 | - [🎛️ Dashboard](/dashboard) - Admin Dashboard with API Tester 14 | - [🔌 WebSocket Test](/ws-test) - Test real-time WebSocket events 15 | - [📄 OpenAPI JSON](/api-docs.json) - Download API specification 16 | 17 | 18 | ## Features 19 | - Multi-Session Support 20 | - Real-time WebSocket Events 21 | - Group Management 22 | - Send Messages (Text, Image, Document, Location, Contact) 23 | - Auto-Save Media 24 | - Persistent Store 25 | - API Key Authentication 26 | 27 | ## Authentication 28 | All API endpoints require \`X-Api-Key\` header (if API_KEY is configured in .env). 29 | 30 | ## Full Documentation 31 | - [https://docs.chatery.app](https://docs.chatery.app) 32 | - [https://chatery-whatsapp-documentation.appwrite.network](https://chatery-whatsapp-documentation.appwrite.network) 33 | 34 | ## ⭐ Support This Project 35 | - [⭐ Star on GitHub](https://github.com/farinchan/chatery_backend) - Give us a star! 36 | - [☕ Buy Me a Coffee (saweria)](https://saweria.co/fajrichan) - Support the developer 37 | 38 | `, 39 | contact: { 40 | name: 'Fajri Rinaldi Chan', 41 | email: 'fajri@gariskode.com', 42 | url: 'https://github.com/farinchan' 43 | }, 44 | license: { 45 | name: 'MIT', 46 | url: 'https://opensource.org/licenses/MIT' 47 | } 48 | }, 49 | servers: [ 50 | { 51 | url: '/', 52 | description: 'Current Server' 53 | }, 54 | { 55 | url: 'http://localhost:3000', 56 | description: 'Local Development' 57 | } 58 | ], 59 | tags: [ 60 | { name: 'Health', description: 'Health check endpoints' }, 61 | { name: 'Sessions', description: 'WhatsApp session management' }, 62 | { name: 'Messaging', description: 'Send messages (text, image, document, etc.)' }, 63 | { name: 'Chat History', description: 'Get chats, messages, contacts' }, 64 | { name: 'Groups', description: 'Group management operations' }, 65 | { name: 'WebSocket', description: 'WebSocket connection info' } 66 | ], 67 | components: { 68 | securitySchemes: { 69 | ApiKeyAuth: { 70 | type: 'apiKey', 71 | in: 'header', 72 | name: 'X-Api-Key', 73 | description: 'API Key for authentication (configured in .env)' 74 | } 75 | }, 76 | schemas: { 77 | SuccessResponse: { 78 | type: 'object', 79 | properties: { 80 | success: { type: 'boolean', example: true }, 81 | message: { type: 'string', example: 'Operation successful' }, 82 | data: { type: 'object' } 83 | } 84 | }, 85 | ErrorResponse: { 86 | type: 'object', 87 | properties: { 88 | success: { type: 'boolean', example: false }, 89 | message: { type: 'string', example: 'Error message' } 90 | } 91 | }, 92 | Session: { 93 | type: 'object', 94 | properties: { 95 | sessionId: { type: 'string', example: 'mysession' }, 96 | status: { type: 'string', enum: ['connecting', 'connected', 'disconnected'], example: 'connected' }, 97 | isConnected: { type: 'boolean', example: true }, 98 | phoneNumber: { type: 'string', example: '628123456789' }, 99 | name: { type: 'string', example: 'John Doe' } 100 | } 101 | }, 102 | Message: { 103 | type: 'object', 104 | properties: { 105 | id: { type: 'string' }, 106 | chatId: { type: 'string' }, 107 | fromMe: { type: 'boolean' }, 108 | timestamp: { type: 'integer' }, 109 | type: { type: 'string' }, 110 | content: { type: 'object' } 111 | } 112 | }, 113 | Chat: { 114 | type: 'object', 115 | properties: { 116 | id: { type: 'string' }, 117 | name: { type: 'string' }, 118 | isGroup: { type: 'boolean' }, 119 | unreadCount: { type: 'integer' }, 120 | lastMessage: { type: 'object' } 121 | } 122 | }, 123 | Group: { 124 | type: 'object', 125 | properties: { 126 | id: { type: 'string' }, 127 | subject: { type: 'string' }, 128 | owner: { type: 'string' }, 129 | creation: { type: 'integer' }, 130 | participants: { 131 | type: 'array', 132 | items: { 133 | type: 'object', 134 | properties: { 135 | id: { type: 'string' }, 136 | admin: { type: 'string', nullable: true } 137 | } 138 | } 139 | } 140 | } 141 | }, 142 | Webhook: { 143 | type: 'object', 144 | properties: { 145 | url: { type: 'string', format: 'uri', example: 'https://your-server.com/webhook' }, 146 | events: { 147 | type: 'array', 148 | items: { type: 'string' }, 149 | example: ['message', 'connection.update'] 150 | } 151 | } 152 | } 153 | } 154 | }, 155 | security: [{ ApiKeyAuth: [] }] 156 | }, 157 | apis: ['./src/config/swagger-paths.js'] 158 | }; 159 | 160 | const swaggerSpec = swaggerJsdoc(options); 161 | 162 | module.exports = swaggerSpec; 163 | -------------------------------------------------------------------------------- /src/services/websocket/WebSocketManager.js: -------------------------------------------------------------------------------- 1 | const { Server } = require('socket.io'); 2 | 3 | /** 4 | * WebSocket Manager (Singleton) 5 | * Mengelola koneksi WebSocket dan event broadcasting 6 | */ 7 | class WebSocketManager { 8 | constructor() { 9 | this.io = null; 10 | this.sessionRooms = new Map(); // sessionId -> Set of socket IDs 11 | } 12 | 13 | /** 14 | * Initialize Socket.IO server 15 | * @param {http.Server} httpServer - HTTP server instance 16 | * @param {Object} options - Socket.IO options 17 | */ 18 | initialize(httpServer, options = {}) { 19 | this.io = new Server(httpServer, { 20 | cors: { 21 | origin: options.cors?.origin || '*', 22 | methods: ['GET', 'POST'], 23 | credentials: true 24 | }, 25 | pingTimeout: 60000, 26 | pingInterval: 25000, 27 | ...options 28 | }); 29 | 30 | this._setupConnectionHandlers(); 31 | console.log('🔌 WebSocket server initialized'); 32 | return this.io; 33 | } 34 | 35 | /** 36 | * Setup connection event handlers 37 | */ 38 | _setupConnectionHandlers() { 39 | this.io.on('connection', (socket) => { 40 | console.log(`🔗 Client connected: ${socket.id}`); 41 | 42 | // Client subscribes to a session 43 | socket.on('subscribe', (sessionId) => { 44 | if (!sessionId) { 45 | socket.emit('error', { message: 'Session ID is required' }); 46 | return; 47 | } 48 | 49 | // Join the session room 50 | socket.join(`session:${sessionId}`); 51 | 52 | // Track socket in session room 53 | if (!this.sessionRooms.has(sessionId)) { 54 | this.sessionRooms.set(sessionId, new Set()); 55 | } 56 | this.sessionRooms.get(sessionId).add(socket.id); 57 | 58 | console.log(`📡 Client ${socket.id} subscribed to session: ${sessionId}`); 59 | socket.emit('subscribed', { 60 | sessionId, 61 | message: `Subscribed to session ${sessionId}` 62 | }); 63 | }); 64 | 65 | // Client unsubscribes from a session 66 | socket.on('unsubscribe', (sessionId) => { 67 | if (!sessionId) return; 68 | 69 | socket.leave(`session:${sessionId}`); 70 | 71 | if (this.sessionRooms.has(sessionId)) { 72 | this.sessionRooms.get(sessionId).delete(socket.id); 73 | if (this.sessionRooms.get(sessionId).size === 0) { 74 | this.sessionRooms.delete(sessionId); 75 | } 76 | } 77 | 78 | console.log(`📴 Client ${socket.id} unsubscribed from session: ${sessionId}`); 79 | socket.emit('unsubscribed', { sessionId }); 80 | }); 81 | 82 | // Handle disconnect 83 | socket.on('disconnect', (reason) => { 84 | console.log(`🔌 Client disconnected: ${socket.id} - Reason: ${reason}`); 85 | 86 | // Remove socket from all session rooms 87 | for (const [sessionId, sockets] of this.sessionRooms.entries()) { 88 | if (sockets.has(socket.id)) { 89 | sockets.delete(socket.id); 90 | if (sockets.size === 0) { 91 | this.sessionRooms.delete(sessionId); 92 | } 93 | } 94 | } 95 | }); 96 | 97 | // Ping-pong for connection health check 98 | socket.on('ping', () => { 99 | socket.emit('pong', { timestamp: Date.now() }); 100 | }); 101 | }); 102 | } 103 | 104 | /** 105 | * Emit event to specific session subscribers 106 | * @param {string} sessionId - Session ID 107 | * @param {string} event - Event name 108 | * @param {Object} data - Event data 109 | */ 110 | emitToSession(sessionId, event, data) { 111 | if (!this.io) { 112 | console.warn('WebSocket server not initialized'); 113 | return; 114 | } 115 | 116 | this.io.to(`session:${sessionId}`).emit(event, { 117 | sessionId, 118 | timestamp: new Date().toISOString(), 119 | ...data 120 | }); 121 | } 122 | 123 | /** 124 | * Emit event to all connected clients 125 | * @param {string} event - Event name 126 | * @param {Object} data - Event data 127 | */ 128 | broadcast(event, data) { 129 | if (!this.io) { 130 | console.warn('WebSocket server not initialized'); 131 | return; 132 | } 133 | 134 | this.io.emit(event, { 135 | timestamp: new Date().toISOString(), 136 | ...data 137 | }); 138 | } 139 | 140 | // ==================== WhatsApp Event Emitters ==================== 141 | 142 | /** 143 | * Emit QR code event 144 | */ 145 | emitQRCode(sessionId, qrCode) { 146 | this.emitToSession(sessionId, 'qr', { qrCode }); 147 | } 148 | 149 | /** 150 | * Emit connection status change 151 | */ 152 | emitConnectionStatus(sessionId, status, details = {}) { 153 | this.emitToSession(sessionId, 'connection.update', { 154 | status, 155 | ...details 156 | }); 157 | } 158 | 159 | /** 160 | * Emit new message received 161 | */ 162 | emitMessage(sessionId, message) { 163 | this.emitToSession(sessionId, 'message', { message }); 164 | } 165 | 166 | /** 167 | * Emit message sent confirmation 168 | */ 169 | emitMessageSent(sessionId, message) { 170 | this.emitToSession(sessionId, 'message.sent', { message }); 171 | } 172 | 173 | /** 174 | * Emit message status update (read, delivered, etc) 175 | */ 176 | emitMessageStatus(sessionId, update) { 177 | this.emitToSession(sessionId, 'message.update', { update }); 178 | } 179 | 180 | /** 181 | * Emit message deleted/revoked 182 | */ 183 | emitMessageRevoke(sessionId, key, participant) { 184 | this.emitToSession(sessionId, 'message.revoke', { key, participant }); 185 | } 186 | 187 | /** 188 | * Emit chat update (archive, mute, pin, etc) 189 | */ 190 | emitChatUpdate(sessionId, chats) { 191 | this.emitToSession(sessionId, 'chat.update', { chats }); 192 | } 193 | 194 | /** 195 | * Emit new chat created 196 | */ 197 | emitChatsUpsert(sessionId, chats) { 198 | this.emitToSession(sessionId, 'chat.upsert', { chats }); 199 | } 200 | 201 | /** 202 | * Emit chat deleted 203 | */ 204 | emitChatDelete(sessionId, chatIds) { 205 | this.emitToSession(sessionId, 'chat.delete', { chatIds }); 206 | } 207 | 208 | /** 209 | * Emit contact update 210 | */ 211 | emitContactUpdate(sessionId, contacts) { 212 | this.emitToSession(sessionId, 'contact.update', { contacts }); 213 | } 214 | 215 | /** 216 | * Emit presence update (typing, online, etc) 217 | */ 218 | emitPresence(sessionId, presence) { 219 | this.emitToSession(sessionId, 'presence.update', { presence }); 220 | } 221 | 222 | /** 223 | * Emit group participants update 224 | */ 225 | emitGroupParticipants(sessionId, update) { 226 | this.emitToSession(sessionId, 'group.participants', { update }); 227 | } 228 | 229 | /** 230 | * Emit group update (name, description, etc) 231 | */ 232 | emitGroupUpdate(sessionId, update) { 233 | this.emitToSession(sessionId, 'group.update', { update }); 234 | } 235 | 236 | /** 237 | * Emit call event 238 | */ 239 | emitCall(sessionId, call) { 240 | this.emitToSession(sessionId, 'call', { call }); 241 | } 242 | 243 | /** 244 | * Emit labels update 245 | */ 246 | emitLabels(sessionId, labels) { 247 | this.emitToSession(sessionId, 'labels', { labels }); 248 | } 249 | 250 | /** 251 | * Emit session logged out 252 | */ 253 | emitLoggedOut(sessionId) { 254 | this.emitToSession(sessionId, 'logged.out', { 255 | message: 'Session has been logged out' 256 | }); 257 | } 258 | 259 | /** 260 | * Get connection stats 261 | */ 262 | getStats() { 263 | if (!this.io) return null; 264 | 265 | const rooms = {}; 266 | for (const [sessionId, sockets] of this.sessionRooms.entries()) { 267 | rooms[sessionId] = sockets.size; 268 | } 269 | 270 | return { 271 | totalConnections: this.io.engine?.clientsCount || 0, 272 | sessionRooms: rooms 273 | }; 274 | } 275 | } 276 | 277 | // Singleton instance 278 | const wsManager = new WebSocketManager(); 279 | 280 | module.exports = wsManager; 281 | -------------------------------------------------------------------------------- /public/websocket-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chatery WebSocket Test 7 | 8 | 188 | 189 | 190 |
191 |

🔌 Chatery WebSocket Tester

192 |

Real-time WhatsApp event monitoring

193 | 194 |
195 | 196 |
197 | 198 |
199 |
200 |
201 | Disconnected 202 |
203 | 204 |
205 | 206 | 207 |
208 | 209 | 210 |
211 | 212 | 213 |
214 |

📡 Session Subscription

215 | 216 |
217 | 218 |
219 | 220 | 221 |
222 | 223 | 224 |
225 | 226 | 227 | 231 |
232 | 233 | 234 |
235 |
236 |

📨 Events

237 | 238 |
239 |
240 |
241 |
242 |
243 | 244 | 489 | 490 | 491 | -------------------------------------------------------------------------------- /src/services/whatsapp/BaileysStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom in-memory store for Baileys 3 | * Optimized version with pre-computed caches for fast queries 4 | */ 5 | class BaileysStore { 6 | constructor(sessionId = null) { 7 | this.sessionId = sessionId; 8 | 9 | // Core data stores 10 | this.chats = new Map(); 11 | this.contacts = new Map(); 12 | this.messages = new Map(); 13 | this.groupMetadata = new Map(); 14 | 15 | // Optimized caches for fast queries 16 | this.chatsOverview = new Map(); // Pre-computed chat overview 17 | this.profilePictures = new Map(); // Cached profile pictures 18 | this.contactsCache = new Map(); // Cached contacts with profile pics 19 | 20 | // Media files tracking: messageId -> filePath 21 | this.mediaFiles = new Map(); 22 | 23 | // Cache timestamps 24 | this.lastOverviewUpdate = 0; 25 | this.lastContactsUpdate = 0; 26 | this.cacheTimeout = 30000; // 30 seconds cache validity 27 | } 28 | 29 | /** 30 | * Bind store to Baileys socket events 31 | */ 32 | bind(ev) { 33 | // Handle chat updates 34 | ev.on('chats.set', ({ chats }) => { 35 | for (const chat of chats) { 36 | this.chats.set(chat.id, chat); 37 | } 38 | this._invalidateOverviewCache(); 39 | }); 40 | 41 | ev.on('chats.upsert', (chats) => { 42 | for (const chat of chats) { 43 | this.chats.set(chat.id, { ...this.chats.get(chat.id), ...chat }); 44 | this._updateSingleChatOverview(chat.id); 45 | } 46 | }); 47 | 48 | ev.on('chats.update', (updates) => { 49 | for (const update of updates) { 50 | const existing = this.chats.get(update.id); 51 | if (existing) { 52 | this.chats.set(update.id, { ...existing, ...update }); 53 | this._updateSingleChatOverview(update.id); 54 | } 55 | } 56 | }); 57 | 58 | ev.on('chats.delete', (ids) => { 59 | for (const id of ids) { 60 | this.chats.delete(id); 61 | this.chatsOverview.delete(id); 62 | this.messages.delete(id); 63 | } 64 | }); 65 | 66 | // Handle contact updates 67 | ev.on('contacts.set', ({ contacts }) => { 68 | for (const contact of contacts) { 69 | this.contacts.set(contact.id, contact); 70 | } 71 | this._invalidateContactsCache(); 72 | }); 73 | 74 | ev.on('contacts.upsert', (contacts) => { 75 | for (const contact of contacts) { 76 | this.contacts.set(contact.id, { ...this.contacts.get(contact.id), ...contact }); 77 | } 78 | this._invalidateContactsCache(); 79 | }); 80 | 81 | ev.on('contacts.update', (updates) => { 82 | for (const update of updates) { 83 | const existing = this.contacts.get(update.id); 84 | if (existing) { 85 | this.contacts.set(update.id, { ...existing, ...update }); 86 | } 87 | } 88 | this._invalidateContactsCache(); 89 | }); 90 | 91 | // Handle message updates - OPTIMIZED 92 | ev.on('messages.set', ({ messages, isLatest }) => { 93 | for (const msg of messages) { 94 | const chatId = msg.key.remoteJid; 95 | if (!this.messages.has(chatId)) { 96 | this.messages.set(chatId, new Map()); 97 | } 98 | this.messages.get(chatId).set(msg.key.id, msg); 99 | this._updateSingleChatOverview(chatId, msg); 100 | } 101 | }); 102 | 103 | ev.on('messages.upsert', ({ messages, type }) => { 104 | for (const msg of messages) { 105 | const chatId = msg.key.remoteJid; 106 | if (!this.messages.has(chatId)) { 107 | this.messages.set(chatId, new Map()); 108 | } 109 | this.messages.get(chatId).set(msg.key.id, msg); 110 | this._updateSingleChatOverview(chatId, msg); 111 | } 112 | }); 113 | 114 | ev.on('messages.update', (updates) => { 115 | for (const { key, update } of updates) { 116 | const chatMessages = this.messages.get(key.remoteJid); 117 | if (chatMessages) { 118 | const existing = chatMessages.get(key.id); 119 | if (existing) { 120 | chatMessages.set(key.id, { ...existing, ...update }); 121 | } 122 | } 123 | } 124 | }); 125 | 126 | ev.on('messages.delete', (item) => { 127 | if ('keys' in item) { 128 | for (const key of item.keys) { 129 | const chatMessages = this.messages.get(key.remoteJid); 130 | if (chatMessages) { 131 | chatMessages.delete(key.id); 132 | // Also delete associated media file 133 | this._deleteMediaFile(key.id); 134 | } 135 | } 136 | } 137 | }); 138 | 139 | // Handle group metadata 140 | ev.on('groups.upsert', (groups) => { 141 | for (const group of groups) { 142 | this.groupMetadata.set(group.id, group); 143 | } 144 | }); 145 | 146 | ev.on('groups.update', (updates) => { 147 | for (const update of updates) { 148 | const existing = this.groupMetadata.get(update.id); 149 | if (existing) { 150 | this.groupMetadata.set(update.id, { ...existing, ...update }); 151 | } 152 | } 153 | }); 154 | } 155 | 156 | /** 157 | * Update single chat overview (called on message events) 158 | */ 159 | _updateSingleChatOverview(chatId, newMessage = null) { 160 | const chat = this.chats.get(chatId); 161 | const chatMessages = this.messages.get(chatId); 162 | 163 | if (!chatMessages || chatMessages.size === 0) { 164 | this.chatsOverview.delete(chatId); 165 | return; 166 | } 167 | 168 | // Find latest message 169 | let latestMessage = newMessage; 170 | if (!latestMessage) { 171 | const messagesArray = Array.from(chatMessages.values()); 172 | messagesArray.sort((a, b) => (b.messageTimestamp || 0) - (a.messageTimestamp || 0)); 173 | latestMessage = messagesArray[0]; 174 | } 175 | 176 | const contact = this.contacts.get(chatId); 177 | const isGroup = chatId.endsWith('@g.us'); 178 | const groupMeta = isGroup ? this.groupMetadata.get(chatId) : null; 179 | 180 | this.chatsOverview.set(chatId, { 181 | id: chatId, 182 | name: groupMeta?.subject || contact?.name || contact?.notify || chat?.name || chatId.replace('@s.whatsapp.net', '').replace('@g.us', ''), 183 | isGroup, 184 | unreadCount: chat?.unreadCount || 0, 185 | lastMessage: { 186 | id: latestMessage?.key?.id, 187 | timestamp: latestMessage?.messageTimestamp, 188 | preview: this._extractMessagePreview(latestMessage), 189 | fromMe: latestMessage?.key?.fromMe || false 190 | }, 191 | profilePicture: this.profilePictures.get(chatId) || null, 192 | conversationTimestamp: chat?.conversationTimestamp || latestMessage?.messageTimestamp 193 | }); 194 | } 195 | 196 | /** 197 | * Extract message preview text 198 | */ 199 | _extractMessagePreview(message) { 200 | if (!message?.message) return ''; 201 | 202 | const msg = message.message; 203 | 204 | if (msg.conversation) return msg.conversation.substring(0, 100); 205 | if (msg.extendedTextMessage?.text) return msg.extendedTextMessage.text.substring(0, 100); 206 | if (msg.imageMessage) return '📷 Image'; 207 | if (msg.videoMessage) return '🎥 Video'; 208 | if (msg.audioMessage) return '🎵 Audio'; 209 | if (msg.documentMessage) return `📄 ${msg.documentMessage.fileName || 'Document'}`; 210 | if (msg.stickerMessage) return '🎭 Sticker'; 211 | if (msg.contactMessage) return `👤 Contact: ${msg.contactMessage.displayName}`; 212 | if (msg.locationMessage) return '📍 Location'; 213 | if (msg.buttonsMessage) return msg.buttonsMessage.contentText || 'Buttons'; 214 | if (msg.templateMessage) return 'Template Message'; 215 | if (msg.listMessage) return msg.listMessage.title || 'List'; 216 | 217 | return 'Message'; 218 | } 219 | 220 | /** 221 | * Invalidate overview cache 222 | */ 223 | _invalidateOverviewCache() { 224 | this.lastOverviewUpdate = 0; 225 | } 226 | 227 | /** 228 | * Invalidate contacts cache 229 | */ 230 | _invalidateContactsCache() { 231 | this.lastContactsUpdate = 0; 232 | this.contactsCache.clear(); 233 | } 234 | 235 | /** 236 | * Set profile picture (called from session) 237 | */ 238 | setProfilePicture(jid, url) { 239 | this.profilePictures.set(jid, url); 240 | // Update overview if exists 241 | const overview = this.chatsOverview.get(jid); 242 | if (overview) { 243 | overview.profilePicture = url; 244 | } 245 | } 246 | 247 | /** 248 | * Get cached profile picture 249 | */ 250 | getProfilePicture(jid) { 251 | return this.profilePictures.get(jid) || null; 252 | } 253 | 254 | /** 255 | * FAST: Get chats overview (uses pre-computed cache) 256 | */ 257 | getChatsOverviewFast(options = {}) { 258 | const { limit = 50, offset = 0 } = options; 259 | 260 | // Build overview if empty 261 | if (this.chatsOverview.size === 0) { 262 | this._rebuildOverviewCache(); 263 | } 264 | 265 | // Convert to array and sort by timestamp 266 | let overview = Array.from(this.chatsOverview.values()); 267 | overview.sort((a, b) => { 268 | const timeA = a.conversationTimestamp || a.lastMessage?.timestamp || 0; 269 | const timeB = b.conversationTimestamp || b.lastMessage?.timestamp || 0; 270 | return timeB - timeA; 271 | }); 272 | 273 | // Apply pagination 274 | return { 275 | total: overview.length, 276 | offset, 277 | limit, 278 | data: overview.slice(offset, offset + limit) 279 | }; 280 | } 281 | 282 | /** 283 | * Rebuild overview cache from scratch 284 | */ 285 | _rebuildOverviewCache() { 286 | this.chatsOverview.clear(); 287 | 288 | for (const [chatId, chatMessages] of this.messages) { 289 | if (chatMessages.size > 0) { 290 | this._updateSingleChatOverview(chatId); 291 | } 292 | } 293 | } 294 | 295 | /** 296 | * FAST: Get contacts (optimized) 297 | */ 298 | getContactsFast(options = {}) { 299 | const { limit = 100, offset = 0, search = '' } = options; 300 | 301 | let contacts = Array.from(this.contacts.values()) 302 | .filter(c => c.id.endsWith('@s.whatsapp.net')) 303 | .map(c => ({ 304 | id: c.id, 305 | name: c.name || c.notify || c.id.replace('@s.whatsapp.net', ''), 306 | notify: c.notify, 307 | verifiedName: c.verifiedName, 308 | profilePicture: this.profilePictures.get(c.id) || null 309 | })); 310 | 311 | // Apply search filter 312 | if (search) { 313 | const searchLower = search.toLowerCase(); 314 | contacts = contacts.filter(c => 315 | c.name?.toLowerCase().includes(searchLower) || 316 | c.notify?.toLowerCase().includes(searchLower) || 317 | c.id.includes(search) 318 | ); 319 | } 320 | 321 | // Sort by name 322 | contacts.sort((a, b) => (a.name || '').localeCompare(b.name || '')); 323 | 324 | return { 325 | total: contacts.length, 326 | offset, 327 | limit, 328 | data: contacts.slice(offset, offset + limit) 329 | }; 330 | } 331 | 332 | /** 333 | * Get all chats 334 | */ 335 | getAllChats() { 336 | return Array.from(this.chats.values()); 337 | } 338 | 339 | /** 340 | * Get messages for a specific chat 341 | */ 342 | getMessages(chatId, options = {}) { 343 | const { limit = 50, before = null } = options; 344 | const chatMessages = this.messages.get(chatId); 345 | 346 | if (!chatMessages) return []; 347 | 348 | let messages = Array.from(chatMessages.values()) 349 | .filter(m => m && m.key && m.messageTimestamp); // Filter invalid messages 350 | 351 | messages.sort((a, b) => { 352 | const timeA = typeof a.messageTimestamp === 'object' ? (a.messageTimestamp.low || 0) : (a.messageTimestamp || 0); 353 | const timeB = typeof b.messageTimestamp === 'object' ? (b.messageTimestamp.low || 0) : (b.messageTimestamp || 0); 354 | return timeB - timeA; 355 | }); 356 | 357 | if (before) { 358 | const beforeIndex = messages.findIndex(m => m.key?.id === before); 359 | if (beforeIndex > -1) { 360 | messages = messages.slice(beforeIndex + 1); 361 | } 362 | } 363 | 364 | return messages.slice(0, limit); 365 | } 366 | 367 | /** 368 | * Get a specific contact 369 | */ 370 | getContact(jid) { 371 | return this.contacts.get(jid) || null; 372 | } 373 | 374 | /** 375 | * Get group metadata 376 | */ 377 | getGroupMetadata(groupId) { 378 | return this.groupMetadata.get(groupId) || null; 379 | } 380 | 381 | /** 382 | * Get chat by ID 383 | */ 384 | getChat(chatId) { 385 | return this.chats.get(chatId) || null; 386 | } 387 | 388 | /** 389 | * Safe JSON serialization (handles circular references and binary data) 390 | */ 391 | _safeSerialize(data) { 392 | const seen = new WeakSet(); 393 | 394 | return JSON.stringify(data, (key, value) => { 395 | // Skip binary data and buffers 396 | if (value instanceof Uint8Array || value instanceof ArrayBuffer) { 397 | return undefined; 398 | } 399 | if (Buffer.isBuffer && Buffer.isBuffer(value)) { 400 | return undefined; 401 | } 402 | 403 | // Skip functions 404 | if (typeof value === 'function') { 405 | return undefined; 406 | } 407 | 408 | // Handle circular references 409 | if (typeof value === 'object' && value !== null) { 410 | if (seen.has(value)) { 411 | return undefined; 412 | } 413 | seen.add(value); 414 | } 415 | 416 | return value; 417 | }, 2); 418 | } 419 | 420 | /** 421 | * Write store to file (for persistence) - FIXED JSON serialization 422 | */ 423 | writeToFile(filePath) { 424 | const fs = require('fs'); 425 | const path = require('path'); 426 | 427 | try { 428 | // Ensure directory exists 429 | const dir = path.dirname(filePath); 430 | if (!fs.existsSync(dir)) { 431 | fs.mkdirSync(dir, { recursive: true }); 432 | } 433 | 434 | // Convert Maps to arrays for serialization 435 | const data = { 436 | chats: Array.from(this.chats.entries()), 437 | contacts: Array.from(this.contacts.entries()), 438 | messages: Array.from(this.messages.entries()).map(([chatId, msgs]) => [ 439 | chatId, 440 | Array.from(msgs.entries()).slice(-100) // Keep only last 100 messages per chat 441 | ]), 442 | groupMetadata: Array.from(this.groupMetadata.entries()), 443 | profilePictures: Array.from(this.profilePictures.entries()) 444 | }; 445 | 446 | // Use safe serialization to avoid .enc or corrupted files 447 | const jsonContent = this._safeSerialize(data); 448 | 449 | // Write to temp file first, then rename (atomic write) 450 | const tempPath = filePath + '.tmp'; 451 | fs.writeFileSync(tempPath, jsonContent, 'utf8'); 452 | 453 | // Rename temp to final (atomic on most filesystems) 454 | if (fs.existsSync(filePath)) { 455 | fs.unlinkSync(filePath); 456 | } 457 | fs.renameSync(tempPath, filePath); 458 | 459 | return true; 460 | } catch (error) { 461 | console.error('Error writing store to file:', error.message); 462 | return false; 463 | } 464 | } 465 | 466 | /** 467 | * Read store from file (for restoration) 468 | */ 469 | readFromFile(filePath) { 470 | const fs = require('fs'); 471 | 472 | try { 473 | if (!fs.existsSync(filePath)) { 474 | return false; 475 | } 476 | 477 | const content = fs.readFileSync(filePath, 'utf8'); 478 | 479 | // Validate JSON before parsing 480 | if (!content || content.trim() === '') { 481 | console.warn('Store file is empty'); 482 | return false; 483 | } 484 | 485 | // Check if file is corrupted (e.g., .enc issue) 486 | if (!content.startsWith('{')) { 487 | console.warn('Store file appears corrupted, skipping restore'); 488 | // Delete corrupted file 489 | fs.unlinkSync(filePath); 490 | return false; 491 | } 492 | 493 | const data = JSON.parse(content); 494 | 495 | // Restore Maps 496 | if (data.chats) { 497 | this.chats = new Map(data.chats); 498 | } 499 | if (data.contacts) { 500 | this.contacts = new Map(data.contacts); 501 | } 502 | if (data.messages) { 503 | this.messages = new Map( 504 | data.messages.map(([chatId, msgs]) => [chatId, new Map(msgs)]) 505 | ); 506 | } 507 | if (data.groupMetadata) { 508 | this.groupMetadata = new Map(data.groupMetadata); 509 | } 510 | if (data.profilePictures) { 511 | this.profilePictures = new Map(data.profilePictures); 512 | } 513 | 514 | // Rebuild overview cache after restore 515 | this._rebuildOverviewCache(); 516 | 517 | return true; 518 | } catch (error) { 519 | console.error('Error reading store from file:', error.message); 520 | return false; 521 | } 522 | } 523 | 524 | /** 525 | * Clear all data 526 | */ 527 | clear() { 528 | // Clean up all media files first 529 | this._cleanupAllMedia(); 530 | 531 | this.chats.clear(); 532 | this.contacts.clear(); 533 | this.messages.clear(); 534 | this.groupMetadata.clear(); 535 | this.chatsOverview.clear(); 536 | this.profilePictures.clear(); 537 | this.contactsCache.clear(); 538 | this.mediaFiles.clear(); 539 | } 540 | 541 | /** 542 | * Get store statistics 543 | */ 544 | getStats() { 545 | let totalMessages = 0; 546 | for (const [, chatMessages] of this.messages) { 547 | totalMessages += chatMessages.size; 548 | } 549 | 550 | return { 551 | chats: this.chats.size, 552 | contacts: this.contacts.size, 553 | messages: totalMessages, 554 | groups: this.groupMetadata.size, 555 | mediaFiles: this.mediaFiles.size 556 | }; 557 | } 558 | 559 | /** 560 | * Register a media file for a message 561 | */ 562 | registerMediaFile(messageId, filePath) { 563 | this.mediaFiles.set(messageId, filePath); 564 | } 565 | 566 | /** 567 | * Delete media file for a message 568 | */ 569 | _deleteMediaFile(messageId) { 570 | const fs = require('fs'); 571 | const filePath = this.mediaFiles.get(messageId); 572 | if (filePath) { 573 | try { 574 | if (fs.existsSync(filePath)) { 575 | fs.unlinkSync(filePath); 576 | console.log(`🗑️ [${this.sessionId}] Media deleted: ${filePath}`); 577 | } 578 | } catch (e) { 579 | // Silent fail 580 | } 581 | this.mediaFiles.delete(messageId); 582 | } 583 | } 584 | 585 | /** 586 | * Cleanup all media files 587 | */ 588 | _cleanupAllMedia() { 589 | const fs = require('fs'); 590 | for (const [messageId, filePath] of this.mediaFiles) { 591 | try { 592 | if (fs.existsSync(filePath)) { 593 | fs.unlinkSync(filePath); 594 | } 595 | } catch (e) { 596 | // Silent fail 597 | } 598 | } 599 | this.mediaFiles.clear(); 600 | } 601 | 602 | /** 603 | * Cleanup old media files (keep only last N messages per chat) 604 | */ 605 | cleanupOldMedia(maxMessagesPerChat = 100) { 606 | const fs = require('fs'); 607 | const messagesToKeep = new Set(); 608 | 609 | // Collect message IDs that should be kept 610 | for (const [chatId, chatMessages] of this.messages) { 611 | const msgs = Array.from(chatMessages.values()) 612 | .filter(m => m && m.messageTimestamp) 613 | .sort((a, b) => { 614 | const timeA = typeof a.messageTimestamp === 'object' ? (a.messageTimestamp.low || 0) : (a.messageTimestamp || 0); 615 | const timeB = typeof b.messageTimestamp === 'object' ? (b.messageTimestamp.low || 0) : (b.messageTimestamp || 0); 616 | return timeB - timeA; 617 | }) 618 | .slice(0, maxMessagesPerChat); 619 | 620 | for (const msg of msgs) { 621 | if (msg.key?.id) { 622 | messagesToKeep.add(msg.key.id); 623 | } 624 | } 625 | } 626 | 627 | // Delete media files for messages that will be removed 628 | for (const [messageId, filePath] of this.mediaFiles) { 629 | if (!messagesToKeep.has(messageId)) { 630 | try { 631 | if (fs.existsSync(filePath)) { 632 | fs.unlinkSync(filePath); 633 | console.log(`🗑️ [${this.sessionId}] Old media cleaned: ${filePath}`); 634 | } 635 | } catch (e) { 636 | // Silent fail 637 | } 638 | this.mediaFiles.delete(messageId); 639 | } 640 | } 641 | } 642 | } 643 | 644 | module.exports = BaileysStore; 645 | -------------------------------------------------------------------------------- /src/routes/whatsapp.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const whatsappManager = require('../services/whatsapp'); 4 | 5 | // Get all sessions 6 | router.get('/sessions', (req, res) => { 7 | try { 8 | const sessions = whatsappManager.getAllSessions(); 9 | res.json({ 10 | success: true, 11 | message: 'Sessions retrieved', 12 | data: sessions.map(s => ({ 13 | sessionId: s.sessionId, 14 | status: s.status, 15 | isConnected: s.isConnected, 16 | phoneNumber: s.phoneNumber, 17 | name: s.name 18 | })) 19 | }); 20 | } catch (error) { 21 | res.status(500).json({ 22 | success: false, 23 | message: error.message 24 | }); 25 | } 26 | }); 27 | 28 | // Create/Connect a session 29 | router.post('/sessions/:sessionId/connect', async (req, res) => { 30 | try { 31 | const { sessionId } = req.params; 32 | const { metadata, webhooks } = req.body; 33 | 34 | const options = {}; 35 | if (metadata) options.metadata = metadata; 36 | if (webhooks) options.webhooks = webhooks; 37 | 38 | const result = await whatsappManager.createSession(sessionId, options); 39 | 40 | res.json({ 41 | success: result.success, 42 | message: result.message, 43 | data: result.data 44 | }); 45 | } catch (error) { 46 | res.status(500).json({ 47 | success: false, 48 | message: error.message 49 | }); 50 | } 51 | }); 52 | 53 | // Get session status 54 | router.get('/sessions/:sessionId/status', (req, res) => { 55 | try { 56 | const { sessionId } = req.params; 57 | const session = whatsappManager.getSession(sessionId); 58 | 59 | if (!session) { 60 | return res.status(404).json({ 61 | success: false, 62 | message: 'Session not found' 63 | }); 64 | } 65 | 66 | const info = session.getInfo(); 67 | res.json({ 68 | success: true, 69 | message: 'Status retrieved', 70 | data: { 71 | sessionId: info.sessionId, 72 | status: info.status, 73 | isConnected: info.isConnected, 74 | phoneNumber: info.phoneNumber, 75 | name: info.name, 76 | metadata: info.metadata, 77 | webhooks: info.webhooks 78 | } 79 | }); 80 | } catch (error) { 81 | res.status(500).json({ 82 | success: false, 83 | message: error.message 84 | }); 85 | } 86 | }); 87 | 88 | // Update session config (metadata, webhooks) 89 | router.patch('/sessions/:sessionId/config', (req, res) => { 90 | try { 91 | const { sessionId } = req.params; 92 | const { metadata, webhooks } = req.body; 93 | 94 | const session = whatsappManager.getSession(sessionId); 95 | 96 | if (!session) { 97 | return res.status(404).json({ 98 | success: false, 99 | message: 'Session not found' 100 | }); 101 | } 102 | 103 | const options = {}; 104 | if (metadata !== undefined) options.metadata = metadata; 105 | if (webhooks !== undefined) options.webhooks = webhooks; 106 | 107 | const updatedInfo = session.updateConfig(options); 108 | 109 | res.json({ 110 | success: true, 111 | message: 'Session config updated', 112 | data: { 113 | sessionId: updatedInfo.sessionId, 114 | metadata: updatedInfo.metadata, 115 | webhooks: updatedInfo.webhooks 116 | } 117 | }); 118 | } catch (error) { 119 | res.status(500).json({ 120 | success: false, 121 | message: error.message 122 | }); 123 | } 124 | }); 125 | 126 | // Add a webhook to session 127 | router.post('/sessions/:sessionId/webhooks', (req, res) => { 128 | try { 129 | const { sessionId } = req.params; 130 | const { url, events } = req.body; 131 | 132 | if (!url) { 133 | return res.status(400).json({ 134 | success: false, 135 | message: 'Missing required field: url' 136 | }); 137 | } 138 | 139 | const session = whatsappManager.getSession(sessionId); 140 | 141 | if (!session) { 142 | return res.status(404).json({ 143 | success: false, 144 | message: 'Session not found' 145 | }); 146 | } 147 | 148 | const updatedInfo = session.addWebhook(url, events || ['all']); 149 | 150 | res.json({ 151 | success: true, 152 | message: 'Webhook added', 153 | data: { 154 | sessionId: updatedInfo.sessionId, 155 | webhooks: updatedInfo.webhooks 156 | } 157 | }); 158 | } catch (error) { 159 | res.status(500).json({ 160 | success: false, 161 | message: error.message 162 | }); 163 | } 164 | }); 165 | 166 | // Remove a webhook from session 167 | router.delete('/sessions/:sessionId/webhooks', (req, res) => { 168 | try { 169 | const { sessionId } = req.params; 170 | const { url } = req.body; 171 | 172 | if (!url) { 173 | return res.status(400).json({ 174 | success: false, 175 | message: 'Missing required field: url' 176 | }); 177 | } 178 | 179 | const session = whatsappManager.getSession(sessionId); 180 | 181 | if (!session) { 182 | return res.status(404).json({ 183 | success: false, 184 | message: 'Session not found' 185 | }); 186 | } 187 | 188 | const updatedInfo = session.removeWebhook(url); 189 | 190 | res.json({ 191 | success: true, 192 | message: 'Webhook removed', 193 | data: { 194 | sessionId: updatedInfo.sessionId, 195 | webhooks: updatedInfo.webhooks 196 | } 197 | }); 198 | } catch (error) { 199 | res.status(500).json({ 200 | success: false, 201 | message: error.message 202 | }); 203 | } 204 | }); 205 | 206 | // Get QR Code for session 207 | router.get('/sessions/:sessionId/qr', (req, res) => { 208 | try { 209 | const { sessionId } = req.params; 210 | const sessionInfo = whatsappManager.getSessionQR(sessionId); 211 | 212 | if (!sessionInfo) { 213 | return res.status(404).json({ 214 | success: false, 215 | message: 'Session not found. Please create session first.' 216 | }); 217 | } 218 | 219 | if (sessionInfo.isConnected) { 220 | return res.json({ 221 | success: true, 222 | message: 'Already connected to WhatsApp', 223 | data: { 224 | sessionId: sessionInfo.sessionId, 225 | status: 'connected', 226 | qrCode: null 227 | } 228 | }); 229 | } 230 | 231 | if (!sessionInfo.qrCode) { 232 | return res.status(404).json({ 233 | success: false, 234 | message: 'QR Code not available yet. Please wait...', 235 | data: { status: sessionInfo.status } 236 | }); 237 | } 238 | 239 | res.json({ 240 | success: true, 241 | message: 'QR Code ready', 242 | data: { 243 | sessionId: sessionInfo.sessionId, 244 | qrCode: sessionInfo.qrCode, 245 | status: sessionInfo.status 246 | } 247 | }); 248 | } catch (error) { 249 | res.status(500).json({ 250 | success: false, 251 | message: error.message 252 | }); 253 | } 254 | }); 255 | 256 | // Get QR Code as Image for session 257 | router.get('/sessions/:sessionId/qr/image', (req, res) => { 258 | try { 259 | const { sessionId } = req.params; 260 | const sessionInfo = whatsappManager.getSessionQR(sessionId); 261 | 262 | if (!sessionInfo || !sessionInfo.qrCode) { 263 | return res.status(404).send('QR Code not available'); 264 | } 265 | 266 | // Konversi base64 ke buffer dan kirim sebagai image 267 | const base64Data = sessionInfo.qrCode.replace(/^data:image\/png;base64,/, ''); 268 | const imgBuffer = Buffer.from(base64Data, 'base64'); 269 | 270 | res.set('Content-Type', 'image/png'); 271 | res.send(imgBuffer); 272 | } catch (error) { 273 | res.status(500).send('Error generating QR image'); 274 | } 275 | }); 276 | 277 | // Delete/Logout a session 278 | router.delete('/sessions/:sessionId', async (req, res) => { 279 | try { 280 | const { sessionId } = req.params; 281 | const result = await whatsappManager.deleteSession(sessionId); 282 | 283 | res.json({ 284 | success: result.success, 285 | message: result.message 286 | }); 287 | } catch (error) { 288 | res.status(500).json({ 289 | success: false, 290 | message: error.message 291 | }); 292 | } 293 | }); 294 | 295 | // ==================== CHAT API ==================== 296 | 297 | // Middleware untuk check session dari body 298 | const checkSession = (req, res, next) => { 299 | if (!req.body) { 300 | return res.status(400).json({ 301 | success: false, 302 | message: 'Request body is required' 303 | }); 304 | } 305 | 306 | const { sessionId } = req.body; 307 | 308 | if (!sessionId) { 309 | return res.status(400).json({ 310 | success: false, 311 | message: 'Missing required field: sessionId' 312 | }); 313 | } 314 | 315 | const session = whatsappManager.getSession(sessionId); 316 | 317 | if (!session) { 318 | return res.status(404).json({ 319 | success: false, 320 | message: 'Session not found' 321 | }); 322 | } 323 | 324 | if (session.connectionStatus !== 'connected') { 325 | return res.status(400).json({ 326 | success: false, 327 | message: 'Session not connected. Please scan QR code first.' 328 | }); 329 | } 330 | 331 | req.session = session; 332 | next(); 333 | }; 334 | 335 | // Send text message 336 | router.post('/chats/send-text', checkSession, async (req, res) => { 337 | try { 338 | const { chatId, message, typingTime = 0 } = req.body; 339 | 340 | if (!chatId || !message) { 341 | return res.status(400).json({ 342 | success: false, 343 | message: 'Missing required fields: chatId, message' 344 | }); 345 | } 346 | 347 | const result = await req.session.sendTextMessage(chatId, message, typingTime); 348 | res.json(result); 349 | } catch (error) { 350 | res.status(500).json({ 351 | success: false, 352 | message: error.message 353 | }); 354 | } 355 | }); 356 | 357 | // Send image 358 | router.post('/chats/send-image', checkSession, async (req, res) => { 359 | try { 360 | const { chatId, imageUrl, caption, typingTime = 0 } = req.body; 361 | 362 | if (!chatId || !imageUrl) { 363 | return res.status(400).json({ 364 | success: false, 365 | message: 'Missing required fields: chatId, imageUrl' 366 | }); 367 | } 368 | 369 | const result = await req.session.sendImage(chatId, imageUrl, caption || '', typingTime); 370 | res.json(result); 371 | } catch (error) { 372 | res.status(500).json({ 373 | success: false, 374 | message: error.message 375 | }); 376 | } 377 | }); 378 | 379 | // Send document 380 | router.post('/chats/send-document', checkSession, async (req, res) => { 381 | try { 382 | const { chatId, documentUrl, filename, mimetype, typingTime = 0 } = req.body; 383 | 384 | if (!chatId || !documentUrl || !filename) { 385 | return res.status(400).json({ 386 | success: false, 387 | message: 'Missing required fields: chatId, documentUrl, filename' 388 | }); 389 | } 390 | 391 | const result = await req.session.sendDocument(chatId, documentUrl, filename, mimetype, typingTime); 392 | res.json(result); 393 | } catch (error) { 394 | res.status(500).json({ 395 | success: false, 396 | message: error.message 397 | }); 398 | } 399 | }); 400 | 401 | // Send location 402 | router.post('/chats/send-location', checkSession, async (req, res) => { 403 | try { 404 | const { chatId, latitude, longitude, name, typingTime = 0 } = req.body; 405 | 406 | if (!chatId || latitude === undefined || longitude === undefined) { 407 | return res.status(400).json({ 408 | success: false, 409 | message: 'Missing required fields: chatId, latitude, longitude' 410 | }); 411 | } 412 | 413 | const result = await req.session.sendLocation(chatId, latitude, longitude, name || '', typingTime); 414 | res.json(result); 415 | } catch (error) { 416 | res.status(500).json({ 417 | success: false, 418 | message: error.message 419 | }); 420 | } 421 | }); 422 | 423 | // Send contact 424 | router.post('/chats/send-contact', checkSession, async (req, res) => { 425 | try { 426 | const { chatId, contactName, contactPhone, typingTime = 0 } = req.body; 427 | 428 | if (!chatId || !contactName || !contactPhone) { 429 | return res.status(400).json({ 430 | success: false, 431 | message: 'Missing required fields: chatId, contactName, contactPhone' 432 | }); 433 | } 434 | 435 | const result = await req.session.sendContact(chatId, contactName, contactPhone, typingTime); 436 | res.json(result); 437 | } catch (error) { 438 | res.status(500).json({ 439 | success: false, 440 | message: error.message 441 | }); 442 | } 443 | }); 444 | 445 | // Send button message 446 | router.post('/chats/send-button', checkSession, async (req, res) => { 447 | try { 448 | const { chatId, text, footer, buttons, typingTime = 0 } = req.body; 449 | 450 | if (!chatId || !text || !buttons || !Array.isArray(buttons)) { 451 | return res.status(400).json({ 452 | success: false, 453 | message: 'Missing required fields: chatId, text, buttons (array)' 454 | }); 455 | } 456 | 457 | const result = await req.session.sendButton(chatId, text, footer || '', buttons, typingTime); 458 | res.json(result); 459 | } catch (error) { 460 | res.status(500).json({ 461 | success: false, 462 | message: error.message 463 | }); 464 | } 465 | }); 466 | 467 | // Send presence update (typing indicator) 468 | router.post('/chats/presence', checkSession, async (req, res) => { 469 | try { 470 | const { chatId, presence = 'composing' } = req.body; 471 | 472 | if (!chatId) { 473 | return res.status(400).json({ 474 | success: false, 475 | message: 'Missing required field: chatId' 476 | }); 477 | } 478 | 479 | const validPresences = ['composing', 'recording', 'paused', 'available', 'unavailable']; 480 | if (!validPresences.includes(presence)) { 481 | return res.status(400).json({ 482 | success: false, 483 | message: `Invalid presence. Must be one of: ${validPresences.join(', ')}` 484 | }); 485 | } 486 | 487 | const result = await req.session.sendPresenceUpdate(chatId, presence); 488 | res.json(result); 489 | } catch (error) { 490 | res.status(500).json({ 491 | success: false, 492 | message: error.message 493 | }); 494 | } 495 | }); 496 | 497 | // Check if number is registered on WhatsApp 498 | router.post('/chats/check-number', checkSession, async (req, res) => { 499 | try { 500 | const { phone } = req.body; 501 | 502 | if (!phone) { 503 | return res.status(400).json({ 504 | success: false, 505 | message: 'Missing required field: phone' 506 | }); 507 | } 508 | 509 | const result = await req.session.isRegistered(phone); 510 | res.json(result); 511 | } catch (error) { 512 | res.status(500).json({ 513 | success: false, 514 | message: error.message 515 | }); 516 | } 517 | }); 518 | 519 | // Get profile picture 520 | router.post('/chats/profile-picture', checkSession, async (req, res) => { 521 | try { 522 | const { phone } = req.body; 523 | 524 | if (!phone) { 525 | return res.status(400).json({ 526 | success: false, 527 | message: 'Missing required field: phone' 528 | }); 529 | } 530 | 531 | const result = await req.session.getProfilePicture(phone); 532 | res.json(result); 533 | } catch (error) { 534 | res.status(500).json({ 535 | success: false, 536 | message: error.message 537 | }); 538 | } 539 | }); 540 | 541 | // ==================== CHAT HISTORY API ==================== 542 | 543 | /** 544 | * Get chats overview - hanya chat yang punya pesan 545 | * Body: { sessionId, limit?, offset?, type? } 546 | * type: 'all' | 'personal' | 'group' 547 | */ 548 | router.post('/chats/overview', checkSession, async (req, res) => { 549 | try { 550 | const { limit = 50, offset = 0, type = 'all' } = req.body; 551 | const result = await req.session.getChatsOverview(limit, offset, type); 552 | res.json(result); 553 | } catch (error) { 554 | res.status(500).json({ 555 | success: false, 556 | message: error.message 557 | }); 558 | } 559 | }); 560 | 561 | /** 562 | * Get contacts list - semua kontak yang tersimpan 563 | * Body: { sessionId, limit?, offset?, search? } 564 | */ 565 | router.post('/contacts', checkSession, async (req, res) => { 566 | try { 567 | const { limit = 100, offset = 0, search = '' } = req.body; 568 | const result = await req.session.getContacts(limit, offset, search); 569 | res.json(result); 570 | } catch (error) { 571 | res.status(500).json({ 572 | success: false, 573 | message: error.message 574 | }); 575 | } 576 | }); 577 | 578 | /** 579 | * Get messages from any chat (personal or group) 580 | * Body: { sessionId, chatId, limit?, cursor? } 581 | * chatId: phone number (628xxx) or group id (xxx@g.us) 582 | */ 583 | router.post('/chats/messages', checkSession, async (req, res) => { 584 | try { 585 | const { chatId, limit = 50, cursor = null } = req.body; 586 | 587 | if (!chatId) { 588 | return res.status(400).json({ 589 | success: false, 590 | message: 'Missing required field: chatId' 591 | }); 592 | } 593 | 594 | const result = await req.session.getChatMessages(chatId, limit, cursor); 595 | res.json(result); 596 | } catch (error) { 597 | res.status(500).json({ 598 | success: false, 599 | message: error.message 600 | }); 601 | } 602 | }); 603 | 604 | /** 605 | * Get chat info/detail (personal or group) 606 | * Body: { sessionId, chatId } 607 | */ 608 | router.post('/chats/info', checkSession, async (req, res) => { 609 | try { 610 | const { chatId } = req.body; 611 | 612 | if (!chatId) { 613 | return res.status(400).json({ 614 | success: false, 615 | message: 'Missing required field: chatId' 616 | }); 617 | } 618 | 619 | const result = await req.session.getChatInfo(chatId); 620 | res.json(result); 621 | } catch (error) { 622 | res.status(500).json({ 623 | success: false, 624 | message: error.message 625 | }); 626 | } 627 | }); 628 | 629 | /** 630 | * Mark a chat as read 631 | * Body: { sessionId, chatId, messageId? } 632 | */ 633 | router.post('/chats/mark-read', checkSession, async (req, res) => { 634 | try { 635 | const { chatId, messageId } = req.body; 636 | 637 | if (!chatId) { 638 | return res.status(400).json({ 639 | success: false, 640 | message: 'Missing required field: chatId' 641 | }); 642 | } 643 | 644 | const result = await req.session.markChatRead(chatId, messageId); 645 | res.json(result); 646 | } catch (error) { 647 | res.status(500).json({ 648 | success: false, 649 | message: error.message 650 | }); 651 | } 652 | }); 653 | 654 | // ==================== GROUP MANAGEMENT ==================== 655 | 656 | /** 657 | * Create a new group 658 | * Body: { sessionId, name, participants: ['628xxx', '628yyy'] } 659 | */ 660 | router.post('/groups/create', checkSession, async (req, res) => { 661 | try { 662 | const { name, participants } = req.body; 663 | 664 | if (!name || !participants) { 665 | return res.status(400).json({ 666 | success: false, 667 | message: 'Missing required fields: name, participants' 668 | }); 669 | } 670 | 671 | const result = await req.session.createGroup(name, participants); 672 | res.json(result); 673 | } catch (error) { 674 | res.status(500).json({ 675 | success: false, 676 | message: error.message 677 | }); 678 | } 679 | }); 680 | 681 | /** 682 | * Get all participating groups 683 | * Body: { sessionId } 684 | */ 685 | router.post('/groups', checkSession, async (req, res) => { 686 | try { 687 | const result = await req.session.getAllGroups(); 688 | res.json(result); 689 | } catch (error) { 690 | res.status(500).json({ 691 | success: false, 692 | message: error.message 693 | }); 694 | } 695 | }); 696 | 697 | /** 698 | * Get group metadata 699 | * Body: { sessionId, groupId } 700 | */ 701 | router.post('/groups/metadata', checkSession, async (req, res) => { 702 | try { 703 | const { groupId } = req.body; 704 | 705 | if (!groupId) { 706 | return res.status(400).json({ 707 | success: false, 708 | message: 'Missing required field: groupId' 709 | }); 710 | } 711 | 712 | const result = await req.session.groupGetMetadata(groupId); 713 | res.json(result); 714 | } catch (error) { 715 | res.status(500).json({ 716 | success: false, 717 | message: error.message 718 | }); 719 | } 720 | }); 721 | 722 | /** 723 | * Add participants to a group 724 | * Body: { sessionId, groupId, participants: ['628xxx', '628yyy'] } 725 | */ 726 | router.post('/groups/participants/add', checkSession, async (req, res) => { 727 | try { 728 | const { groupId, participants } = req.body; 729 | 730 | if (!groupId || !participants) { 731 | return res.status(400).json({ 732 | success: false, 733 | message: 'Missing required fields: groupId, participants' 734 | }); 735 | } 736 | 737 | const result = await req.session.groupAddParticipants(groupId, participants); 738 | res.json(result); 739 | } catch (error) { 740 | res.status(500).json({ 741 | success: false, 742 | message: error.message 743 | }); 744 | } 745 | }); 746 | 747 | /** 748 | * Remove participants from a group 749 | * Body: { sessionId, groupId, participants: ['628xxx', '628yyy'] } 750 | */ 751 | router.post('/groups/participants/remove', checkSession, async (req, res) => { 752 | try { 753 | const { groupId, participants } = req.body; 754 | 755 | if (!groupId || !participants) { 756 | return res.status(400).json({ 757 | success: false, 758 | message: 'Missing required fields: groupId, participants' 759 | }); 760 | } 761 | 762 | const result = await req.session.groupRemoveParticipants(groupId, participants); 763 | res.json(result); 764 | } catch (error) { 765 | res.status(500).json({ 766 | success: false, 767 | message: error.message 768 | }); 769 | } 770 | }); 771 | 772 | /** 773 | * Promote participants to admin 774 | * Body: { sessionId, groupId, participants: ['628xxx', '628yyy'] } 775 | */ 776 | router.post('/groups/participants/promote', checkSession, async (req, res) => { 777 | try { 778 | const { groupId, participants } = req.body; 779 | 780 | if (!groupId || !participants) { 781 | return res.status(400).json({ 782 | success: false, 783 | message: 'Missing required fields: groupId, participants' 784 | }); 785 | } 786 | 787 | const result = await req.session.groupPromoteParticipants(groupId, participants); 788 | res.json(result); 789 | } catch (error) { 790 | res.status(500).json({ 791 | success: false, 792 | message: error.message 793 | }); 794 | } 795 | }); 796 | 797 | /** 798 | * Demote participants from admin 799 | * Body: { sessionId, groupId, participants: ['628xxx', '628yyy'] } 800 | */ 801 | router.post('/groups/participants/demote', checkSession, async (req, res) => { 802 | try { 803 | const { groupId, participants } = req.body; 804 | 805 | if (!groupId || !participants) { 806 | return res.status(400).json({ 807 | success: false, 808 | message: 'Missing required fields: groupId, participants' 809 | }); 810 | } 811 | 812 | const result = await req.session.groupDemoteParticipants(groupId, participants); 813 | res.json(result); 814 | } catch (error) { 815 | res.status(500).json({ 816 | success: false, 817 | message: error.message 818 | }); 819 | } 820 | }); 821 | 822 | /** 823 | * Update group subject (name) 824 | * Body: { sessionId, groupId, subject } 825 | */ 826 | router.post('/groups/subject', checkSession, async (req, res) => { 827 | try { 828 | const { groupId, subject } = req.body; 829 | 830 | if (!groupId || !subject) { 831 | return res.status(400).json({ 832 | success: false, 833 | message: 'Missing required fields: groupId, subject' 834 | }); 835 | } 836 | 837 | const result = await req.session.groupUpdateSubject(groupId, subject); 838 | res.json(result); 839 | } catch (error) { 840 | res.status(500).json({ 841 | success: false, 842 | message: error.message 843 | }); 844 | } 845 | }); 846 | 847 | /** 848 | * Update group description 849 | * Body: { sessionId, groupId, description } 850 | */ 851 | router.post('/groups/description', checkSession, async (req, res) => { 852 | try { 853 | const { groupId, description } = req.body; 854 | 855 | if (!groupId) { 856 | return res.status(400).json({ 857 | success: false, 858 | message: 'Missing required field: groupId' 859 | }); 860 | } 861 | 862 | const result = await req.session.groupUpdateDescription(groupId, description); 863 | res.json(result); 864 | } catch (error) { 865 | res.status(500).json({ 866 | success: false, 867 | message: error.message 868 | }); 869 | } 870 | }); 871 | 872 | /** 873 | * Update group settings 874 | * Body: { sessionId, groupId, setting: 'announcement'|'not_announcement'|'locked'|'unlocked' } 875 | */ 876 | router.post('/groups/settings', checkSession, async (req, res) => { 877 | try { 878 | const { groupId, setting } = req.body; 879 | 880 | if (!groupId || !setting) { 881 | return res.status(400).json({ 882 | success: false, 883 | message: 'Missing required fields: groupId, setting' 884 | }); 885 | } 886 | 887 | const result = await req.session.groupUpdateSettings(groupId, setting); 888 | res.json(result); 889 | } catch (error) { 890 | res.status(500).json({ 891 | success: false, 892 | message: error.message 893 | }); 894 | } 895 | }); 896 | 897 | /** 898 | * Update group profile picture 899 | * Body: { sessionId, groupId, imageUrl } 900 | */ 901 | router.post('/groups/picture', checkSession, async (req, res) => { 902 | try { 903 | const { groupId, imageUrl } = req.body; 904 | 905 | if (!groupId || !imageUrl) { 906 | return res.status(400).json({ 907 | success: false, 908 | message: 'Missing required fields: groupId, imageUrl' 909 | }); 910 | } 911 | 912 | const result = await req.session.groupUpdateProfilePicture(groupId, imageUrl); 913 | res.json(result); 914 | } catch (error) { 915 | res.status(500).json({ 916 | success: false, 917 | message: error.message 918 | }); 919 | } 920 | }); 921 | 922 | /** 923 | * Leave a group 924 | * Body: { sessionId, groupId } 925 | */ 926 | router.post('/groups/leave', checkSession, async (req, res) => { 927 | try { 928 | const { groupId } = req.body; 929 | 930 | if (!groupId) { 931 | return res.status(400).json({ 932 | success: false, 933 | message: 'Missing required field: groupId' 934 | }); 935 | } 936 | 937 | const result = await req.session.groupLeave(groupId); 938 | res.json(result); 939 | } catch (error) { 940 | res.status(500).json({ 941 | success: false, 942 | message: error.message 943 | }); 944 | } 945 | }); 946 | 947 | /** 948 | * Join a group using invitation code/link 949 | * Body: { sessionId, inviteCode } - Can be full URL or just the code 950 | */ 951 | router.post('/groups/join', checkSession, async (req, res) => { 952 | try { 953 | const { inviteCode } = req.body; 954 | 955 | if (!inviteCode) { 956 | return res.status(400).json({ 957 | success: false, 958 | message: 'Missing required field: inviteCode' 959 | }); 960 | } 961 | 962 | const result = await req.session.groupJoinByInvite(inviteCode); 963 | res.json(result); 964 | } catch (error) { 965 | res.status(500).json({ 966 | success: false, 967 | message: error.message 968 | }); 969 | } 970 | }); 971 | 972 | /** 973 | * Get group invitation code/link 974 | * Body: { sessionId, groupId } 975 | */ 976 | router.post('/groups/invite-code', checkSession, async (req, res) => { 977 | try { 978 | const { groupId } = req.body; 979 | 980 | if (!groupId) { 981 | return res.status(400).json({ 982 | success: false, 983 | message: 'Missing required field: groupId' 984 | }); 985 | } 986 | 987 | const result = await req.session.groupGetInviteCode(groupId); 988 | res.json(result); 989 | } catch (error) { 990 | res.status(500).json({ 991 | success: false, 992 | message: error.message 993 | }); 994 | } 995 | }); 996 | 997 | /** 998 | * Revoke group invitation code 999 | * Body: { sessionId, groupId } 1000 | */ 1001 | router.post('/groups/revoke-invite', checkSession, async (req, res) => { 1002 | try { 1003 | const { groupId } = req.body; 1004 | 1005 | if (!groupId) { 1006 | return res.status(400).json({ 1007 | success: false, 1008 | message: 'Missing required field: groupId' 1009 | }); 1010 | } 1011 | 1012 | const result = await req.session.groupRevokeInvite(groupId); 1013 | res.json(result); 1014 | } catch (error) { 1015 | res.status(500).json({ 1016 | success: false, 1017 | message: error.message 1018 | }); 1019 | } 1020 | }); 1021 | 1022 | module.exports = router; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Chatery WhatsApp API 2 | 3 | ![Chatery](https://sgp.cloud.appwrite.io/v1/storage/buckets/6941a5b70012d918c7aa/files/6941a69000028dec52d2/view?project=694019b0000abc694483&token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbklkIjoiNjk0MWE4NjRjZGNhZGUxOTZmNTMiLCJyZXNvdXJjZUlkIjoiNjk0MWE1YjcwMDEyZDkxOGM3YWE6Njk0MWE2OTAwMDAyOGRlYzUyZDIiLCJyZXNvdXJjZVR5cGUiOiJmaWxlcyIsInJlc291cmNlSW50ZXJuYWxJZCI6IjE0NTE6MSIsImlhdCI6MTc2NTkxMDYyOH0.6DyBMKwzA6x__pQZn3vICDLdBfo0mEUlyMVAc3qEnyo) 4 | A powerful WhatsApp API backend built with Express.js and Baileys library. Supports multi-session management, real-time WebSocket events, group management, and media handling. 5 | 6 | ![Node.js](https://img.shields.io/badge/Node.js-18+-green.svg) 7 | ![Express.js](https://img.shields.io/badge/Express.js-5.x-blue.svg) 8 | ![Baileys](https://img.shields.io/badge/Baileys-7.x-orange.svg) 9 | ![Socket.IO](https://img.shields.io/badge/Socket.IO-4.x-purple.svg) 10 | ![License](https://img.shields.io/badge/License-MIT-yellow.svg) 11 | 12 | ## ✨ Features 13 | 14 | - 📱 **Multi-Session Support** - Manage multiple WhatsApp accounts simultaneously 15 | - 🔌 **Real-time WebSocket** - Get instant notifications for messages, status updates, and more 16 | - 👥 **Group Management** - Create, manage, and control WhatsApp groups 17 | - 📨 **Send Messages** - Text, images, documents, locations, contacts, and buttons 18 | - 📥 **Auto-Save Media** - Automatically save incoming media to server 19 | - 💾 **Persistent Store** - Message history with optimized caching 20 | - 🔐 **Session Persistence** - Sessions survive server restarts 21 | - 🎛️ **Admin Dashboard** - Web-based dashboard with real-time monitoring and API tester 22 | 23 | ## 📖 Full Documentation 24 | 25 | For complete and detailed documentation, please visit: 26 | 27 | | 🌐 Documentation | Link | 28 | |------------------|------| 29 | | **Primary Docs** | [https://docs.chatery.app](https://docs.chatery.app/) | 30 | | **Mirror Docs** | [https://chatery-whatsapp-documentation.appwrite.network](https://chatery-whatsapp-documentation.appwrite.network) | 31 | 32 | > 📚 The documentation includes complete API reference, examples, troubleshooting guides, and more. 33 | 34 | ## 📋 Table of Contents 35 | 36 | - [Full Documentation](#-full-documentation) 37 | - [Installation](#-installation) 38 | - [Standard Installation](#option-1-standard-installation) 39 | - [Docker Installation](#option-2-docker-installation) 40 | - [Configuration](#-configuration) 41 | - [API Key Authentication](#-api-key-authentication) 42 | - [Quick Start](#-quick-start) 43 | - [Dashboard](#-dashboard) 44 | - [API Documentation](#-api-documentation) 45 | - [Sessions](#sessions) 46 | - [Messaging](#messaging) 47 | - [Chat History](#chat-history) 48 | - [Group Management](#group-management) 49 | - [WebSocket Events](#-websocket-events) 50 | - [Examples](#-examples) 51 | 52 | ## 🛠 Installation 53 | 54 | ### Option 1: Standard Installation 55 | 56 | ```bash 57 | # Clone the repository 58 | git clone https://github.com/farinchan/chatery_whatsapp.git 59 | cd chatery_whatsapp 60 | 61 | # Install dependencies 62 | npm install 63 | 64 | # Create environment file 65 | cp .env.example .env 66 | 67 | # Start the server 68 | npm start 69 | 70 | # Or development mode with auto-reload 71 | npm run dev 72 | ``` 73 | 74 | ### Option 2: Docker Installation 75 | 76 | ```bash 77 | # Clone the repository 78 | git clone https://github.com/farinchan/chatery_whatsapp.git 79 | cd chatery_whatsapp 80 | 81 | # Create environment file 82 | cp .env.example .env 83 | 84 | # Build and run with Docker Compose 85 | docker-compose up -d 86 | 87 | # View logs 88 | docker-compose logs -f 89 | 90 | # Stop the container 91 | docker-compose down 92 | ``` 93 | 94 | #### Docker Commands 95 | 96 | | Command | Description | 97 | |---------|-------------| 98 | | `docker-compose up -d` | Start container in background | 99 | | `docker-compose down` | Stop and remove container | 100 | | `docker-compose logs -f` | View live logs | 101 | | `docker-compose restart` | Restart container | 102 | | `docker-compose build --no-cache` | Rebuild image | 103 | 104 | #### Docker Volumes 105 | 106 | The following data is persisted across container restarts: 107 | 108 | | Volume | Path | Description | 109 | |--------|------|-------------| 110 | | `chatery_sessions` | `/app/sessions` | WhatsApp session data | 111 | | `chatery_media` | `/app/public/media` | Received media files | 112 | | `chatery_store` | `/app/store` | Message history store | 113 | 114 | ## ⚙ Configuration 115 | 116 | Create a `.env` file in the root directory: 117 | 118 | ```env 119 | PORT=3000 120 | CORS_ORIGIN=* 121 | 122 | # Dashboard Authentication 123 | DASHBOARD_USERNAME=admin 124 | DASHBOARD_PASSWORD=securepassword123 125 | 126 | # API Key Authentication (optional - leave empty or 'your_api_key_here' to disable) 127 | API_KEY=your_secret_api_key_here 128 | ``` 129 | 130 | ## 🔐 API Key Authentication 131 | 132 | All WhatsApp API endpoints are protected with API key authentication. Include the `X-Api-Key` header in your requests. 133 | 134 | ### How to Enable 135 | 136 | 1. Set a strong API key in your `.env` file: 137 | ```env 138 | API_KEY=your_super_secret_key_12345 139 | ``` 140 | 141 | 2. Include the header in all API requests: 142 | ```bash 143 | curl -X GET http://localhost:3000/api/whatsapp/sessions \ 144 | -H "X-Api-Key: your_super_secret_key_12345" 145 | ``` 146 | 147 | ### Disable Authentication 148 | 149 | To disable API key authentication, leave `API_KEY` empty or set it to `your_api_key_here` in `.env`: 150 | ```env 151 | API_KEY= 152 | # or 153 | API_KEY=your_api_key_here 154 | ``` 155 | 156 | ### Error Responses 157 | 158 | | Status | Message | Description | 159 | |--------|---------|-------------| 160 | | 401 | Missing X-Api-Key header | API key not provided in request | 161 | | 403 | Invalid API key | API key doesn't match | 162 | 163 | ### Dashboard Integration 164 | 165 | When logging into the dashboard, you'll be prompted to enter your API key (optional). This allows the dashboard to make authenticated API calls. 166 | 167 | ## 🚀 Quick Start 168 | 169 | 1. **Start the server** 170 | ```bash 171 | npm start 172 | ``` 173 | 174 | 2. **Create a session** 175 | ```bash 176 | curl -X POST http://localhost:3000/api/whatsapp/sessions/mysession/connect \ 177 | -H "X-Api-Key: your_api_key" \ 178 | -H "Content-Type: application/json" 179 | ``` 180 | 181 | 3. **Get QR Code** - Open in browser or scan 182 | ``` 183 | http://localhost:3000/api/whatsapp/sessions/mysession/qr/image 184 | ``` 185 | Note: QR image endpoint also requires API key. Use curl or include header. 186 | 187 | 4. **Send a message** 188 | ```bash 189 | curl -X POST http://localhost:3000/api/whatsapp/chats/send-text \ 190 | -H "X-Api-Key: your_api_key" \ 191 | -H "Content-Type: application/json" \ 192 | -d '{"sessionId": "mysession", "chatId": "628123456789", "message": "Hello!"}' 193 | ``` 194 | 195 | --- 196 | 197 | ## 🎛️ Dashboard 198 | 199 | Access the admin dashboard at `http://localhost:3000/dashboard` 200 | 201 | ### 🔐 Authentication 202 | 203 | Dashboard requires login with username and password configured in `.env` file. 204 | 205 | | Field | Default Value | 206 | |-------|---------------| 207 | | Username | `admin` | 208 | | Password | `admin123` | 209 | 210 | ### ✨ Dashboard Features 211 | 212 | | Feature | Description | 213 | |---------|-------------| 214 | | 📊 **Real-time Stats** | Monitor total sessions, connected/disconnected status, and WebSocket clients | 215 | | 📱 **Session Management** | Create, connect, reconnect, and delete WhatsApp sessions | 216 | | 📷 **QR Code Scanner** | Scan QR codes directly from the dashboard | 217 | | 📡 **Live Events** | Real-time WebSocket event viewer with filtering | 218 | | 💬 **Quick Send** | Send messages quickly to any number | 219 | | 🧪 **API Tester** | Test all 30+ API endpoints with pre-filled templates | 220 | | 🚪 **Logout** | Secure logout button in header | 221 | 222 | ### 📸 Screenshots 223 | 224 | ![Dashboard Screenshot](./screenshot/image.png) 225 | 226 | The dashboard provides a modern dark-themed interface: 227 | - **Session Cards** - View all sessions with status indicators 228 | - **QR Modal** - Full-screen QR code for easy scanning 229 | - **Event Log** - Live scrolling event feed with timestamps 230 | - **API Tester** - Dropdown with all endpoints and auto-generated request bodies 231 | 232 | --- 233 | 234 | ## 📚 API Documentation 235 | 236 | Base URL: `http://localhost:3000/api/whatsapp` 237 | 238 | ### Sessions 239 | 240 | #### List All Sessions 241 | ```http 242 | GET /sessions 243 | ``` 244 | 245 | **Response:** 246 | ```json 247 | { 248 | "success": true, 249 | "message": "Sessions retrieved", 250 | "data": [ 251 | { 252 | "sessionId": "mysession", 253 | "status": "connected", 254 | "isConnected": true, 255 | "phoneNumber": "628123456789", 256 | "name": "John Doe" 257 | } 258 | ] 259 | } 260 | ``` 261 | 262 | #### Create/Connect Session 263 | ```http 264 | POST /sessions/:sessionId/connect 265 | ``` 266 | 267 | **Body (Optional):** 268 | ```json 269 | { 270 | "metadata": { 271 | "userId": "user123", 272 | "plan": "premium", 273 | "customField": "any value" 274 | }, 275 | "webhooks": [ 276 | { "url": "https://your-server.com/webhook", "events": ["all"] }, 277 | { "url": "https://backup-server.com/webhook", "events": ["message"] } 278 | ] 279 | } 280 | ``` 281 | 282 | | Parameter | Type | Description | 283 | |-----------|------|-------------| 284 | | `metadata` | object | Optional. Custom metadata to store with session | 285 | | `webhooks` | array | Optional. Array of webhook configs `[{ url, events }]` | 286 | 287 | **Response:** 288 | ```json 289 | { 290 | "success": true, 291 | "message": "Session created", 292 | "data": { 293 | "sessionId": "mysession", 294 | "status": "qr_ready", 295 | "qrCode": "data:image/png;base64,...", 296 | "metadata": { "userId": "user123" }, 297 | "webhooks": [ 298 | { "url": "https://your-server.com/webhook", "events": ["all"] } 299 | ] 300 | } 301 | } 302 | ``` 303 | 304 | #### Update Session Config 305 | ```http 306 | PATCH /sessions/:sessionId/config 307 | ``` 308 | 309 | **Body:** 310 | ```json 311 | { 312 | "metadata": { "newField": "value" }, 313 | "webhooks": [ 314 | { "url": "https://new-webhook.com/endpoint", "events": ["message", "connection.update"] } 315 | ] 316 | } 317 | ``` 318 | 319 | **Response:** 320 | ```json 321 | { 322 | "success": true, 323 | "message": "Session config updated", 324 | "data": { 325 | "sessionId": "mysession", 326 | "metadata": { "userId": "user123", "newField": "value" }, 327 | "webhooks": [ 328 | { "url": "https://new-webhook.com/endpoint", "events": ["message", "connection.update"] } 329 | ] 330 | } 331 | } 332 | ``` 333 | 334 | #### Add Webhook 335 | ```http 336 | POST /sessions/:sessionId/webhooks 337 | ``` 338 | 339 | **Body:** 340 | ```json 341 | { 342 | "url": "https://another-server.com/webhook", 343 | "events": ["message", "connection.update"] 344 | } 345 | ``` 346 | 347 | #### Remove Webhook 348 | ```http 349 | DELETE /sessions/:sessionId/webhooks 350 | ``` 351 | 352 | **Body:** 353 | ```json 354 | { 355 | "url": "https://another-server.com/webhook" 356 | } 357 | ``` 358 | 359 | #### Get Session Status 360 | ```http 361 | GET /sessions/:sessionId/status 362 | ``` 363 | 364 | #### Get QR Code (JSON) 365 | ```http 366 | GET /sessions/:sessionId/qr 367 | ``` 368 | 369 | #### Get QR Code (Image) 370 | ```http 371 | GET /sessions/:sessionId/qr/image 372 | ``` 373 | Returns a PNG image that can be displayed directly in browser or scanned. 374 | 375 | #### Delete Session 376 | ```http 377 | DELETE /sessions/:sessionId 378 | ``` 379 | 380 | --- 381 | 382 | ### Messaging 383 | 384 | > **💡 Typing Indicator**: All messaging endpoints support `typingTime` parameter (in milliseconds) to simulate typing before sending the message. This makes the bot appear more human-like. 385 | 386 | #### Send Text Message 387 | ```http 388 | POST /chats/send-text 389 | ``` 390 | 391 | **Body:** 392 | ```json 393 | { 394 | "sessionId": "mysession", 395 | "chatId": "628123456789", 396 | "message": "Hello, World!", 397 | "typingTime": 2000 398 | } 399 | ``` 400 | 401 | | Parameter | Type | Description | 402 | |-----------|------|-------------| 403 | | `sessionId` | string | Required. Session ID | 404 | | `chatId` | string | Required. Phone number (628xxx) or group ID (xxx@g.us) | 405 | | `message` | string | Required. Text message to send | 406 | | `typingTime` | number | Optional. Typing duration in ms before sending (default: 0) | 407 | 408 | #### Send Image 409 | ```http 410 | POST /chats/send-image 411 | ``` 412 | 413 | **Body:** 414 | ```json 415 | { 416 | "sessionId": "mysession", 417 | "chatId": "628123456789", 418 | "imageUrl": "https://example.com/image.jpg", 419 | "caption": "Check this out!", 420 | "typingTime": 1500 421 | } 422 | ``` 423 | 424 | | Parameter | Type | Description | 425 | |-----------|------|-------------| 426 | | `sessionId` | string | Required. Session ID | 427 | | `chatId` | string | Required. Phone number or group ID | 428 | | `imageUrl` | string | Required. Direct URL to image file | 429 | | `caption` | string | Optional. Image caption | 430 | | `typingTime` | number | Optional. Typing duration in ms (default: 0) | 431 | 432 | #### Send Document 433 | ```http 434 | POST /chats/send-document 435 | ``` 436 | 437 | **Body:** 438 | ```json 439 | { 440 | "sessionId": "mysession", 441 | "chatId": "628123456789", 442 | "documentUrl": "https://example.com/document.pdf", 443 | "filename": "document.pdf", 444 | "mimetype": "application/pdf", 445 | "typingTime": 1000 446 | } 447 | ``` 448 | 449 | | Parameter | Type | Description | 450 | |-----------|------|-------------| 451 | | `sessionId` | string | Required. Session ID | 452 | | `chatId` | string | Required. Phone number or group ID | 453 | | `documentUrl` | string | Required. Direct URL to document | 454 | | `filename` | string | Required. Filename to display | 455 | | `mimetype` | string | Optional. MIME type (default: application/pdf) | 456 | | `typingTime` | number | Optional. Typing duration in ms (default: 0) | 457 | 458 | #### Send Location 459 | ```http 460 | POST /chats/send-location 461 | ``` 462 | 463 | **Body:** 464 | ```json 465 | { 466 | "sessionId": "mysession", 467 | "chatId": "628123456789", 468 | "latitude": -6.2088, 469 | "longitude": 106.8456, 470 | "name": "Jakarta, Indonesia", 471 | "typingTime": 1000 472 | } 473 | ``` 474 | 475 | | Parameter | Type | Description | 476 | |-----------|------|-------------| 477 | | `sessionId` | string | Required. Session ID | 478 | | `chatId` | string | Required. Phone number or group ID | 479 | | `latitude` | number | Required. GPS latitude | 480 | | `longitude` | number | Required. GPS longitude | 481 | | `name` | string | Optional. Location name | 482 | | `typingTime` | number | Optional. Typing duration in ms (default: 0) | 483 | 484 | #### Send Contact 485 | ```http 486 | POST /chats/send-contact 487 | ``` 488 | 489 | **Body:** 490 | ```json 491 | { 492 | "sessionId": "mysession", 493 | "chatId": "628123456789", 494 | "contactName": "John Doe", 495 | "contactPhone": "628987654321", 496 | "typingTime": 500 497 | } 498 | ``` 499 | 500 | | Parameter | Type | Description | 501 | |-----------|------|-------------| 502 | | `sessionId` | string | Required. Session ID | 503 | | `chatId` | string | Required. Phone number or group ID | 504 | | `contactName` | string | Required. Contact display name | 505 | | `contactPhone` | string | Required. Contact phone number | 506 | | `typingTime` | number | Optional. Typing duration in ms (default: 0) | 507 | 508 | #### Send Button Message 509 | ```http 510 | POST /chats/send-button 511 | ``` 512 | 513 | **Body:** 514 | ```json 515 | { 516 | "sessionId": "mysession", 517 | "chatId": "628123456789", 518 | "text": "Please choose an option:", 519 | "footer": "Powered by Chatery", 520 | "buttons": ["Option 1", "Option 2", "Option 3"], 521 | "typingTime": 2000 522 | } 523 | ``` 524 | 525 | | Parameter | Type | Description | 526 | |-----------|------|-------------| 527 | | `sessionId` | string | Required. Session ID | 528 | | `chatId` | string | Required. Phone number or group ID | 529 | | `text` | string | Required. Button message text | 530 | | `footer` | string | Optional. Footer text | 531 | | `buttons` | array | Required. Array of button labels (max 3) | 532 | | `typingTime` | number | Optional. Typing duration in ms (default: 0) | 533 | 534 | #### Send Presence Update 535 | ```http 536 | POST /chats/presence 537 | ``` 538 | 539 | **Body:** 540 | ```json 541 | { 542 | "sessionId": "mysession", 543 | "chatId": "628123456789", 544 | "presence": "composing" 545 | } 546 | ``` 547 | 548 | | Parameter | Type | Description | 549 | |-----------|------|-------------| 550 | | `sessionId` | string | Required. Session ID | 551 | | `chatId` | string | Required. Phone number or group ID | 552 | | `presence` | string | Required. `composing`, `recording`, `paused`, `available`, `unavailable` | 553 | 554 | #### Check Phone Number 555 | ```http 556 | POST /chats/check-number 557 | ``` 558 | 559 | **Body:** 560 | ```json 561 | { 562 | "sessionId": "mysession", 563 | "phone": "628123456789" 564 | } 565 | ``` 566 | 567 | #### Get Profile Picture 568 | ```http 569 | POST /chats/profile-picture 570 | ``` 571 | 572 | **Body:** 573 | ```json 574 | { 575 | "sessionId": "mysession", 576 | "phone": "628123456789" 577 | } 578 | ``` 579 | 580 | --- 581 | 582 | ### Chat History 583 | 584 | #### Get Chats Overview 585 | ```http 586 | POST /chats/overview 587 | ``` 588 | 589 | **Body:** 590 | ```json 591 | { 592 | "sessionId": "mysession", 593 | "limit": 50, 594 | "offset": 0, 595 | "type": "all" 596 | } 597 | ``` 598 | 599 | | Parameter | Type | Description | 600 | |-----------|------|-------------| 601 | | `sessionId` | string | Required. Session ID | 602 | | `limit` | number | Optional. Max results (default: 50) | 603 | | `offset` | number | Optional. Pagination offset (default: 0) | 604 | | `type` | string | Optional. Filter: `all`, `personal`, `group` | 605 | 606 | #### Get Contacts 607 | ```http 608 | POST /contacts 609 | ``` 610 | 611 | **Body:** 612 | ```json 613 | { 614 | "sessionId": "mysession", 615 | "limit": 100, 616 | "offset": 0, 617 | "search": "john" 618 | } 619 | ``` 620 | 621 | #### Get Chat Messages 622 | ```http 623 | POST /chats/messages 624 | ``` 625 | 626 | **Body:** 627 | ```json 628 | { 629 | "sessionId": "mysession", 630 | "chatId": "628123456789@s.whatsapp.net", 631 | "limit": 50, 632 | "cursor": null 633 | } 634 | ``` 635 | 636 | #### Get Chat Info 637 | ```http 638 | POST /chats/info 639 | ``` 640 | 641 | **Body:** 642 | ```json 643 | { 644 | "sessionId": "mysession", 645 | "chatId": "628123456789@s.whatsapp.net" 646 | } 647 | ``` 648 | 649 | #### Mark Chat as Read 650 | ```http 651 | POST /chats/mark-read 652 | ``` 653 | 654 | **Body:** 655 | ```json 656 | { 657 | "sessionId": "mysession", 658 | "chatId": "628123456789", 659 | "messageId": null 660 | } 661 | ``` 662 | 663 | | Parameter | Type | Description | 664 | |-----------|------|-------------| 665 | | `sessionId` | string | Required. Session ID | 666 | | `chatId` | string | Required. Phone number or group ID | 667 | | `messageId` | string | Optional. Specific message ID to mark as read | 668 | 669 | --- 670 | 671 | ### Group Management 672 | 673 | #### Get All Groups 674 | ```http 675 | POST /groups 676 | ``` 677 | 678 | **Body:** 679 | ```json 680 | { 681 | "sessionId": "mysession" 682 | } 683 | ``` 684 | 685 | **Response:** 686 | ```json 687 | { 688 | "success": true, 689 | "data": { 690 | "count": 5, 691 | "groups": [ 692 | { 693 | "id": "123456789@g.us", 694 | "subject": "My Group", 695 | "participantsCount": 25, 696 | "creation": 1609459200 697 | } 698 | ] 699 | } 700 | } 701 | ``` 702 | 703 | #### Create Group 704 | ```http 705 | POST /groups/create 706 | ``` 707 | 708 | **Body:** 709 | ```json 710 | { 711 | "sessionId": "mysession", 712 | "name": "My New Group", 713 | "participants": ["628123456789", "628987654321"] 714 | } 715 | ``` 716 | 717 | #### Get Group Metadata 718 | ```http 719 | POST /groups/metadata 720 | ``` 721 | 722 | **Body:** 723 | ```json 724 | { 725 | "sessionId": "mysession", 726 | "groupId": "123456789@g.us" 727 | } 728 | ``` 729 | 730 | **Response:** 731 | ```json 732 | { 733 | "success": true, 734 | "data": { 735 | "id": "123456789@g.us", 736 | "subject": "My Group", 737 | "description": "Group description", 738 | "participants": [ 739 | { "id": "628123456789@s.whatsapp.net", "admin": "superadmin" }, 740 | { "id": "628987654321@s.whatsapp.net", "admin": null } 741 | ], 742 | "size": 25 743 | } 744 | } 745 | ``` 746 | 747 | #### Add Participants 748 | ```http 749 | POST /groups/participants/add 750 | ``` 751 | 752 | **Body:** 753 | ```json 754 | { 755 | "sessionId": "mysession", 756 | "groupId": "123456789@g.us", 757 | "participants": ["628111222333", "628444555666"] 758 | } 759 | ``` 760 | 761 | #### Remove Participants 762 | ```http 763 | POST /groups/participants/remove 764 | ``` 765 | 766 | **Body:** 767 | ```json 768 | { 769 | "sessionId": "mysession", 770 | "groupId": "123456789@g.us", 771 | "participants": ["628111222333"] 772 | } 773 | ``` 774 | 775 | #### Promote to Admin 776 | ```http 777 | POST /groups/participants/promote 778 | ``` 779 | 780 | **Body:** 781 | ```json 782 | { 783 | "sessionId": "mysession", 784 | "groupId": "123456789@g.us", 785 | "participants": ["628111222333"] 786 | } 787 | ``` 788 | 789 | #### Demote from Admin 790 | ```http 791 | POST /groups/participants/demote 792 | ``` 793 | 794 | **Body:** 795 | ```json 796 | { 797 | "sessionId": "mysession", 798 | "groupId": "123456789@g.us", 799 | "participants": ["628111222333"] 800 | } 801 | ``` 802 | 803 | #### Update Group Subject (Name) 804 | ```http 805 | POST /groups/subject 806 | ``` 807 | 808 | **Body:** 809 | ```json 810 | { 811 | "sessionId": "mysession", 812 | "groupId": "123456789@g.us", 813 | "subject": "New Group Name" 814 | } 815 | ``` 816 | 817 | #### Update Group Description 818 | ```http 819 | POST /groups/description 820 | ``` 821 | 822 | **Body:** 823 | ```json 824 | { 825 | "sessionId": "mysession", 826 | "groupId": "123456789@g.us", 827 | "description": "This is the new group description" 828 | } 829 | ``` 830 | 831 | #### Update Group Settings 832 | ```http 833 | POST /groups/settings 834 | ``` 835 | 836 | **Body:** 837 | ```json 838 | { 839 | "sessionId": "mysession", 840 | "groupId": "123456789@g.us", 841 | "setting": "announcement" 842 | } 843 | ``` 844 | 845 | | Setting | Description | 846 | |---------|-------------| 847 | | `announcement` | Only admins can send messages | 848 | | `not_announcement` | All participants can send messages | 849 | | `locked` | Only admins can edit group info | 850 | | `unlocked` | All participants can edit group info | 851 | 852 | #### Update Group Picture 853 | ```http 854 | POST /groups/picture 855 | ``` 856 | 857 | **Body:** 858 | ```json 859 | { 860 | "sessionId": "mysession", 861 | "groupId": "123456789@g.us", 862 | "imageUrl": "https://example.com/group-pic.jpg" 863 | } 864 | ``` 865 | 866 | #### Leave Group 867 | ```http 868 | POST /groups/leave 869 | ``` 870 | 871 | **Body:** 872 | ```json 873 | { 874 | "sessionId": "mysession", 875 | "groupId": "123456789@g.us" 876 | } 877 | ``` 878 | 879 | #### Join Group via Invite 880 | ```http 881 | POST /groups/join 882 | ``` 883 | 884 | **Body:** 885 | ```json 886 | { 887 | "sessionId": "mysession", 888 | "inviteCode": "https://chat.whatsapp.com/AbCdEfGhIjKlMn" 889 | } 890 | ``` 891 | 892 | #### Get Invite Code 893 | ```http 894 | POST /groups/invite-code 895 | ``` 896 | 897 | **Body:** 898 | ```json 899 | { 900 | "sessionId": "mysession", 901 | "groupId": "123456789@g.us" 902 | } 903 | ``` 904 | 905 | **Response:** 906 | ```json 907 | { 908 | "success": true, 909 | "data": { 910 | "groupId": "123456789@g.us", 911 | "inviteCode": "AbCdEfGhIjKlMn", 912 | "inviteLink": "https://chat.whatsapp.com/AbCdEfGhIjKlMn" 913 | } 914 | } 915 | ``` 916 | 917 | #### Revoke Invite Code 918 | ```http 919 | POST /groups/revoke-invite 920 | ``` 921 | 922 | **Body:** 923 | ```json 924 | { 925 | "sessionId": "mysession", 926 | "groupId": "123456789@g.us" 927 | } 928 | ``` 929 | 930 | --- 931 | 932 | ## 🔌 WebSocket Events 933 | 934 | Connect to WebSocket server at `ws://localhost:3000` 935 | 936 | ### Connection 937 | 938 | ```javascript 939 | import { io } from 'socket.io-client'; 940 | 941 | const socket = io('http://localhost:3000'); 942 | 943 | // Subscribe to a session 944 | socket.emit('subscribe', 'mysession'); 945 | 946 | // Unsubscribe from a session 947 | socket.emit('unsubscribe', 'mysession'); 948 | ``` 949 | 950 | ### Events 951 | 952 | | Event | Description | Payload | 953 | |-------|-------------|---------| 954 | | `qr` | QR code generated | `{ sessionId, qrCode, timestamp }` | 955 | | `connection.update` | Connection status changed | `{ sessionId, status, phoneNumber?, name?, timestamp }` | 956 | | `message` | New message received | `{ sessionId, message, timestamp }` | 957 | | `message.sent` | Message sent confirmation | `{ sessionId, message, timestamp }` | 958 | | `message.update` | Message status update (read, delivered) | `{ sessionId, update, timestamp }` | 959 | | `message.reaction` | Message reaction added | `{ sessionId, reactions, timestamp }` | 960 | | `message.revoke` | Message deleted/revoked | `{ sessionId, key, participant, timestamp }` | 961 | | `chat.update` | Chat updated | `{ sessionId, chats, timestamp }` | 962 | | `chat.upsert` | New chat created | `{ sessionId, chats, timestamp }` | 963 | | `chat.delete` | Chat deleted | `{ sessionId, chatIds, timestamp }` | 964 | | `contact.update` | Contact updated | `{ sessionId, contacts, timestamp }` | 965 | | `presence.update` | Typing, online status | `{ sessionId, presence, timestamp }` | 966 | | `group.participants` | Group members changed | `{ sessionId, update, timestamp }` | 967 | | `group.update` | Group info changed | `{ sessionId, update, timestamp }` | 968 | | `call` | Incoming call | `{ sessionId, call, timestamp }` | 969 | | `labels` | Labels updated (business) | `{ sessionId, labels, timestamp }` | 970 | | `logged.out` | Session logged out | `{ sessionId, message, timestamp }` | 971 | 972 | ### Example: Listen for Messages 973 | 974 | ```javascript 975 | const socket = io('http://localhost:3000'); 976 | 977 | socket.on('connect', () => { 978 | console.log('Connected to WebSocket'); 979 | socket.emit('subscribe', 'mysession'); 980 | }); 981 | 982 | socket.on('message', (data) => { 983 | console.log('New message:', data.message); 984 | // { 985 | // sessionId: 'mysession', 986 | // message: { 987 | // id: 'ABC123', 988 | // from: '628123456789@s.whatsapp.net', 989 | // text: 'Hello!', 990 | // timestamp: 1234567890, 991 | // ... 992 | // }, 993 | // timestamp: '2024-01-15T10:30:00.000Z' 994 | // } 995 | }); 996 | 997 | socket.on('qr', (data) => { 998 | console.log('Scan QR Code:', data.qrCode); 999 | }); 1000 | 1001 | socket.on('connection.update', (data) => { 1002 | console.log('Connection status:', data.status); 1003 | if (data.status === 'connected') { 1004 | console.log(`Connected as ${data.name} (${data.phoneNumber})`); 1005 | } 1006 | }); 1007 | ``` 1008 | 1009 | ### WebSocket Test Page 1010 | 1011 | Open `http://localhost:3000/ws-test` in your browser for an interactive WebSocket testing interface. 1012 | 1013 | --- 1014 | 1015 | ## 🪝 Webhooks 1016 | 1017 | You can configure multiple webhook URLs to receive events from your WhatsApp session. Each webhook can subscribe to specific events. 1018 | 1019 | ### Setup Multiple Webhooks 1020 | 1021 | Set webhooks when creating or updating a session: 1022 | 1023 | ```bash 1024 | # When creating session with multiple webhooks 1025 | curl -X POST http://localhost:3000/api/whatsapp/sessions/mysession/connect \ 1026 | -H "Content-Type: application/json" \ 1027 | -d '{ 1028 | "metadata": { "userId": "123" }, 1029 | "webhooks": [ 1030 | { "url": "https://primary-server.com/webhook", "events": ["all"] }, 1031 | { "url": "https://analytics.example.com/webhook", "events": ["message"] }, 1032 | { "url": "https://backup.example.com/webhook", "events": ["connection.update"] } 1033 | ] 1034 | }' 1035 | 1036 | # Add a webhook to existing session 1037 | curl -X POST http://localhost:3000/api/whatsapp/sessions/mysession/webhooks \ 1038 | -H "Content-Type: application/json" \ 1039 | -d '{ 1040 | "url": "https://new-webhook.com/endpoint", 1041 | "events": ["message", "connection.update"] 1042 | }' 1043 | 1044 | # Remove a webhook 1045 | curl -X DELETE http://localhost:3000/api/whatsapp/sessions/mysession/webhooks \ 1046 | -H "Content-Type: application/json" \ 1047 | -d '{ 1048 | "url": "https://new-webhook.com/endpoint" 1049 | }' 1050 | 1051 | # Update all webhooks 1052 | curl -X PATCH http://localhost:3000/api/whatsapp/sessions/mysession/config \ 1053 | -H "Content-Type: application/json" \ 1054 | -d '{ 1055 | "webhooks": [ 1056 | { "url": "https://only-this-one.com/webhook", "events": ["all"] } 1057 | ] 1058 | }' 1059 | ``` 1060 | 1061 | ### Webhook Payload 1062 | 1063 | All configured webhook endpoints will receive POST requests with this format: 1064 | 1065 | ```json 1066 | { 1067 | "event": "message", 1068 | "sessionId": "mysession", 1069 | "metadata": { 1070 | "userId": "123", 1071 | "customField": "value" 1072 | }, 1073 | "data": { 1074 | "id": "ABC123", 1075 | "from": "628123456789@s.whatsapp.net", 1076 | "text": "Hello!", 1077 | "timestamp": 1234567890 1078 | }, 1079 | "timestamp": "2024-01-15T10:30:00.000Z" 1080 | } 1081 | ``` 1082 | 1083 | ### Webhook Headers 1084 | 1085 | | Header | Value | 1086 | |--------|-------| 1087 | | `Content-Type` | `application/json` | 1088 | | `X-Webhook-Source` | `chatery-whatsapp-api` | 1089 | | `X-Session-Id` | Session ID | 1090 | | `X-Webhook-Event` | Event name | 1091 | 1092 | ### Available Webhook Events 1093 | 1094 | | Event | Description | 1095 | |-------|-------------| 1096 | | `connection.update` | Connection status changed (connected, disconnected) | 1097 | | `message` | New message received | 1098 | | `message.sent` | Message sent confirmation | 1099 | 1100 | Set `events: ["all"]` to receive all events, or specify individual events per webhook. 1101 | 1102 | ### WebSocket Stats 1103 | 1104 | ```http 1105 | GET /api/websocket/stats 1106 | ``` 1107 | 1108 | **Response:** 1109 | ```json 1110 | { 1111 | "success": true, 1112 | "data": { 1113 | "totalConnections": 5, 1114 | "sessionRooms": { 1115 | "mysession": 2, 1116 | "othersession": 1 1117 | } 1118 | } 1119 | } 1120 | ``` 1121 | 1122 | --- 1123 | 1124 | ## 📁 Project Structure 1125 | 1126 | ``` 1127 | chatery_backend/ 1128 | ├── index.js # Application entry point 1129 | ├── package.json 1130 | ├── .env # Environment variables 1131 | ├── README.md # Documentation 1132 | ├── public/ 1133 | │ ├── dashboard.html # Admin dashboard 1134 | │ ├── websocket-test.html # WebSocket test page 1135 | │ └── media/ # Auto-saved media files 1136 | │ └── {sessionId}/ 1137 | │ └── {chatId}/ 1138 | ├── sessions/ # Session authentication data 1139 | │ └── {sessionId}/ 1140 | │ ├── creds.json 1141 | │ └── store.json 1142 | └── src/ 1143 | ├── routes/ 1144 | │ └── whatsapp.js # API routes 1145 | └── services/ 1146 | ├── websocket/ 1147 | │ └── WebSocketManager.js 1148 | └── whatsapp/ 1149 | ├── index.js 1150 | ├── WhatsAppManager.js 1151 | ├── WhatsAppSession.js 1152 | ├── BaileysStore.js 1153 | └── MessageFormatter.js 1154 | ``` 1155 | 1156 | --- 1157 | 1158 | ## 📝 Examples 1159 | 1160 | ### Node.js Client 1161 | 1162 | ```javascript 1163 | const axios = require('axios'); 1164 | 1165 | const API_URL = 'http://localhost:3000/api/whatsapp'; 1166 | 1167 | // Create session 1168 | async function createSession(sessionId) { 1169 | const response = await axios.post(`${API_URL}/sessions/${sessionId}/connect`); 1170 | return response.data; 1171 | } 1172 | 1173 | // Send message 1174 | async function sendMessage(sessionId, to, message) { 1175 | const response = await axios.post(`${API_URL}/chats/send-text`, { 1176 | sessionId, 1177 | to, 1178 | message 1179 | }); 1180 | return response.data; 1181 | } 1182 | 1183 | // Get all groups 1184 | async function getGroups(sessionId) { 1185 | const response = await axios.post(`${API_URL}/groups`, { sessionId }); 1186 | return response.data; 1187 | } 1188 | ``` 1189 | 1190 | ### Python Client 1191 | 1192 | ```python 1193 | import requests 1194 | 1195 | API_URL = 'http://localhost:3000/api/whatsapp' 1196 | 1197 | # Create session 1198 | def create_session(session_id): 1199 | response = requests.post(f'{API_URL}/sessions/{session_id}/connect') 1200 | return response.json() 1201 | 1202 | # Send message 1203 | def send_message(session_id, to, message): 1204 | response = requests.post(f'{API_URL}/chats/send-text', json={ 1205 | 'sessionId': session_id, 1206 | 'to': to, 1207 | 'message': message 1208 | }) 1209 | return response.json() 1210 | 1211 | # Get all groups 1212 | def get_groups(session_id): 1213 | response = requests.post(f'{API_URL}/groups', json={ 1214 | 'sessionId': session_id 1215 | }) 1216 | return response.json() 1217 | ``` 1218 | 1219 | --- 1220 | 1221 | ## 🤝 Contributing 1222 | 1223 | 1. Fork the repository 1224 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 1225 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 1226 | 4. Push to the branch (`git push origin feature/amazing-feature`) 1227 | 5. Open a Pull Request 1228 | 1229 | --- 1230 | 1231 | ## 📄 License 1232 | 1233 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 1234 | 1235 | --- 1236 | 1237 | ## ☕ Support & Donate 1238 | 1239 | If you find this project helpful, consider supporting the development: 1240 | 1241 |

1242 | 1243 | Saweria 1244 | 1245 | 1246 | PayPal 1247 | 1248 | 1249 |

1250 | 1251 |

1252 | 1253 | GitHub Stars 1254 | 1255 | 1256 | GitHub Forks 1257 | 1258 |

1259 | 1260 | Your support helps me maintain and improve this project! ❤️ 1261 | 1262 | --- 1263 | 1264 | ## 👨‍💻 Author 1265 | 1266 | **Fajri Rinaldi Chan** (Farin Chan) 1267 | 1268 |

1269 | 1270 | GitHub 1271 | 1272 | 1273 | LinkedIn 1274 | 1275 | 1276 | Instagram 1277 | 1278 |

1279 | 1280 | --- 1281 | 1282 | ## 🔗 Quick Links 1283 | 1284 | | Resource | URL | 1285 | |----------|-----| 1286 | | 🎛️ Dashboard | http://localhost:3000/dashboard | 1287 | | 📚 API Base URL | http://localhost:3000/api/whatsapp | 1288 | | 🔌 WebSocket Test | http://localhost:3000/ws-test | 1289 | | 📊 WebSocket Stats | http://localhost:3000/api/websocket/stats | 1290 | | ❤️ Health Check | http://localhost:3000/api/health | 1291 | 1292 | --- 1293 | 1294 | ## ⚠️ Disclaimer 1295 | 1296 | This project is not affiliated with WhatsApp or Meta. Use at your own risk. Make sure to comply with WhatsApp's Terms of Service. 1297 | -------------------------------------------------------------------------------- /src/config/swagger-paths.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * /api/health: 4 | * get: 5 | * tags: [Health] 6 | * summary: Health check 7 | * description: Check if server is running 8 | * security: [] 9 | * responses: 10 | * 200: 11 | * description: Server is healthy 12 | * content: 13 | * application/json: 14 | * schema: 15 | * type: object 16 | * properties: 17 | * success: 18 | * type: boolean 19 | * example: true 20 | * message: 21 | * type: string 22 | * example: Server is running 23 | * timestamp: 24 | * type: string 25 | * format: date-time 26 | */ 27 | 28 | /** 29 | * @swagger 30 | * /api/websocket/stats: 31 | * get: 32 | * tags: [WebSocket] 33 | * summary: Get WebSocket stats 34 | * description: Get current WebSocket connection statistics 35 | * security: [] 36 | * responses: 37 | * 200: 38 | * description: WebSocket statistics 39 | * content: 40 | * application/json: 41 | * schema: 42 | * type: object 43 | * properties: 44 | * success: 45 | * type: boolean 46 | * data: 47 | * type: object 48 | * properties: 49 | * totalConnections: 50 | * type: integer 51 | * rooms: 52 | * type: object 53 | */ 54 | 55 | /** 56 | * @swagger 57 | * /api/dashboard/login: 58 | * post: 59 | * tags: [Dashboard] 60 | * summary: Dashboard login 61 | * description: Authenticate to access the dashboard 62 | * security: [] 63 | * requestBody: 64 | * required: true 65 | * content: 66 | * application/json: 67 | * schema: 68 | * type: object 69 | * required: [username, password] 70 | * properties: 71 | * username: 72 | * type: string 73 | * example: admin 74 | * password: 75 | * type: string 76 | * example: admin123 77 | * responses: 78 | * 200: 79 | * description: Login successful 80 | * 401: 81 | * description: Invalid credentials 82 | */ 83 | 84 | // ==================== SESSIONS ==================== 85 | 86 | /** 87 | * @swagger 88 | * /api/whatsapp/sessions: 89 | * get: 90 | * tags: [Sessions] 91 | * summary: List all sessions 92 | * description: Get list of all WhatsApp sessions 93 | * responses: 94 | * 200: 95 | * description: Sessions list 96 | * content: 97 | * application/json: 98 | * schema: 99 | * type: object 100 | * properties: 101 | * success: 102 | * type: boolean 103 | * data: 104 | * type: array 105 | * items: 106 | * $ref: '#/components/schemas/Session' 107 | */ 108 | 109 | /** 110 | * @swagger 111 | * /api/whatsapp/sessions/{sessionId}/connect: 112 | * post: 113 | * tags: [Sessions] 114 | * summary: Create/Connect session 115 | * description: Create a new session or reconnect existing session 116 | * parameters: 117 | * - in: path 118 | * name: sessionId 119 | * required: true 120 | * schema: 121 | * type: string 122 | * example: mysession 123 | * requestBody: 124 | * content: 125 | * application/json: 126 | * schema: 127 | * type: object 128 | * properties: 129 | * metadata: 130 | * type: object 131 | * description: Custom metadata 132 | * example: { "userId": "123", "plan": "premium" } 133 | * webhooks: 134 | * type: array 135 | * items: 136 | * $ref: '#/components/schemas/Webhook' 137 | * responses: 138 | * 200: 139 | * description: Session created/connected 140 | * 400: 141 | * description: Session already exists 142 | */ 143 | 144 | /** 145 | * @swagger 146 | * /api/whatsapp/sessions/{sessionId}/status: 147 | * get: 148 | * tags: [Sessions] 149 | * summary: Get session status 150 | * description: Get detailed status of a session 151 | * parameters: 152 | * - in: path 153 | * name: sessionId 154 | * required: true 155 | * schema: 156 | * type: string 157 | * responses: 158 | * 200: 159 | * description: Session status 160 | * 404: 161 | * description: Session not found 162 | */ 163 | 164 | /** 165 | * @swagger 166 | * /api/whatsapp/sessions/{sessionId}/qr: 167 | * get: 168 | * tags: [Sessions] 169 | * summary: Get QR code 170 | * description: Get QR code for session authentication (base64) 171 | * parameters: 172 | * - in: path 173 | * name: sessionId 174 | * required: true 175 | * schema: 176 | * type: string 177 | * responses: 178 | * 200: 179 | * description: QR code data 180 | * content: 181 | * application/json: 182 | * schema: 183 | * type: object 184 | * properties: 185 | * success: 186 | * type: boolean 187 | * data: 188 | * type: object 189 | * properties: 190 | * qrCode: 191 | * type: string 192 | * description: Base64 QR code image 193 | */ 194 | 195 | /** 196 | * @swagger 197 | * /api/whatsapp/sessions/{sessionId}/qr/image: 198 | * get: 199 | * tags: [Sessions] 200 | * summary: Get QR code image 201 | * description: Get QR code as PNG image 202 | * parameters: 203 | * - in: path 204 | * name: sessionId 205 | * required: true 206 | * schema: 207 | * type: string 208 | * responses: 209 | * 200: 210 | * description: QR code PNG image 211 | * content: 212 | * image/png: 213 | * schema: 214 | * type: string 215 | * format: binary 216 | */ 217 | 218 | /** 219 | * @swagger 220 | * /api/whatsapp/sessions/{sessionId}/config: 221 | * patch: 222 | * tags: [Sessions] 223 | * summary: Update session config 224 | * description: Update metadata and webhooks for a session 225 | * parameters: 226 | * - in: path 227 | * name: sessionId 228 | * required: true 229 | * schema: 230 | * type: string 231 | * requestBody: 232 | * content: 233 | * application/json: 234 | * schema: 235 | * type: object 236 | * properties: 237 | * metadata: 238 | * type: object 239 | * webhooks: 240 | * type: array 241 | * items: 242 | * $ref: '#/components/schemas/Webhook' 243 | * responses: 244 | * 200: 245 | * description: Config updated 246 | */ 247 | 248 | /** 249 | * @swagger 250 | * /api/whatsapp/sessions/{sessionId}/webhooks: 251 | * post: 252 | * tags: [Sessions] 253 | * summary: Add webhook 254 | * description: Add a new webhook to session 255 | * parameters: 256 | * - in: path 257 | * name: sessionId 258 | * required: true 259 | * schema: 260 | * type: string 261 | * requestBody: 262 | * required: true 263 | * content: 264 | * application/json: 265 | * schema: 266 | * $ref: '#/components/schemas/Webhook' 267 | * responses: 268 | * 200: 269 | * description: Webhook added 270 | * delete: 271 | * tags: [Sessions] 272 | * summary: Remove webhook 273 | * description: Remove a webhook from session 274 | * parameters: 275 | * - in: path 276 | * name: sessionId 277 | * required: true 278 | * schema: 279 | * type: string 280 | * requestBody: 281 | * required: true 282 | * content: 283 | * application/json: 284 | * schema: 285 | * type: object 286 | * required: [url] 287 | * properties: 288 | * url: 289 | * type: string 290 | * responses: 291 | * 200: 292 | * description: Webhook removed 293 | */ 294 | 295 | /** 296 | * @swagger 297 | * /api/whatsapp/sessions/{sessionId}: 298 | * delete: 299 | * tags: [Sessions] 300 | * summary: Delete session 301 | * description: Logout and delete a session 302 | * parameters: 303 | * - in: path 304 | * name: sessionId 305 | * required: true 306 | * schema: 307 | * type: string 308 | * responses: 309 | * 200: 310 | * description: Session deleted 311 | */ 312 | 313 | // ==================== MESSAGING ==================== 314 | 315 | /** 316 | * @swagger 317 | * /api/whatsapp/chats/send-text: 318 | * post: 319 | * tags: [Messaging] 320 | * summary: Send text message 321 | * description: Send a text message to a chat 322 | * requestBody: 323 | * required: true 324 | * content: 325 | * application/json: 326 | * schema: 327 | * type: object 328 | * required: [sessionId, chatId, message] 329 | * properties: 330 | * sessionId: 331 | * type: string 332 | * example: mysession 333 | * chatId: 334 | * type: string 335 | * example: "628123456789" 336 | * description: Phone number or group JID 337 | * message: 338 | * type: string 339 | * example: Hello World! 340 | * typingTime: 341 | * type: integer 342 | * example: 2000 343 | * description: Typing indicator duration (ms) 344 | * responses: 345 | * 200: 346 | * description: Message sent 347 | */ 348 | 349 | /** 350 | * @swagger 351 | * /api/whatsapp/chats/send-image: 352 | * post: 353 | * tags: [Messaging] 354 | * summary: Send image message 355 | * description: Send an image with optional caption 356 | * requestBody: 357 | * required: true 358 | * content: 359 | * application/json: 360 | * schema: 361 | * type: object 362 | * required: [sessionId, chatId, imageUrl] 363 | * properties: 364 | * sessionId: 365 | * type: string 366 | * chatId: 367 | * type: string 368 | * imageUrl: 369 | * type: string 370 | * example: https://example.com/image.jpg 371 | * caption: 372 | * type: string 373 | * typingTime: 374 | * type: integer 375 | * responses: 376 | * 200: 377 | * description: Image sent 378 | */ 379 | 380 | /** 381 | * @swagger 382 | * /api/whatsapp/chats/send-document: 383 | * post: 384 | * tags: [Messaging] 385 | * summary: Send document 386 | * description: Send a document/file 387 | * requestBody: 388 | * required: true 389 | * content: 390 | * application/json: 391 | * schema: 392 | * type: object 393 | * required: [sessionId, chatId, documentUrl, filename] 394 | * properties: 395 | * sessionId: 396 | * type: string 397 | * chatId: 398 | * type: string 399 | * documentUrl: 400 | * type: string 401 | * example: https://example.com/document.pdf 402 | * filename: 403 | * type: string 404 | * example: document.pdf 405 | * mimetype: 406 | * type: string 407 | * example: application/pdf 408 | * typingTime: 409 | * type: integer 410 | * responses: 411 | * 200: 412 | * description: Document sent 413 | */ 414 | 415 | /** 416 | * @swagger 417 | * /api/whatsapp/chats/send-location: 418 | * post: 419 | * tags: [Messaging] 420 | * summary: Send location 421 | * description: Send a location message 422 | * requestBody: 423 | * required: true 424 | * content: 425 | * application/json: 426 | * schema: 427 | * type: object 428 | * required: [sessionId, chatId, latitude, longitude] 429 | * properties: 430 | * sessionId: 431 | * type: string 432 | * chatId: 433 | * type: string 434 | * latitude: 435 | * type: number 436 | * example: -6.2088 437 | * longitude: 438 | * type: number 439 | * example: 106.8456 440 | * name: 441 | * type: string 442 | * example: Jakarta 443 | * typingTime: 444 | * type: integer 445 | * responses: 446 | * 200: 447 | * description: Location sent 448 | */ 449 | 450 | /** 451 | * @swagger 452 | * /api/whatsapp/chats/send-contact: 453 | * post: 454 | * tags: [Messaging] 455 | * summary: Send contact 456 | * description: Send a contact card 457 | * requestBody: 458 | * required: true 459 | * content: 460 | * application/json: 461 | * schema: 462 | * type: object 463 | * required: [sessionId, chatId, contactName, contactPhone] 464 | * properties: 465 | * sessionId: 466 | * type: string 467 | * chatId: 468 | * type: string 469 | * contactName: 470 | * type: string 471 | * example: John Doe 472 | * contactPhone: 473 | * type: string 474 | * example: "628987654321" 475 | * typingTime: 476 | * type: integer 477 | * responses: 478 | * 200: 479 | * description: Contact sent 480 | */ 481 | 482 | /** 483 | * @swagger 484 | * /api/whatsapp/chats/send-button: 485 | * post: 486 | * tags: [Messaging] 487 | * summary: Send button message 488 | * description: Send a message with interactive buttons 489 | * requestBody: 490 | * required: true 491 | * content: 492 | * application/json: 493 | * schema: 494 | * type: object 495 | * required: [sessionId, chatId, text, buttons] 496 | * properties: 497 | * sessionId: 498 | * type: string 499 | * chatId: 500 | * type: string 501 | * text: 502 | * type: string 503 | * example: Choose an option 504 | * footer: 505 | * type: string 506 | * buttons: 507 | * type: array 508 | * items: 509 | * type: object 510 | * properties: 511 | * buttonId: 512 | * type: string 513 | * buttonText: 514 | * type: string 515 | * typingTime: 516 | * type: integer 517 | * responses: 518 | * 200: 519 | * description: Button message sent 520 | */ 521 | 522 | /** 523 | * @swagger 524 | * /api/whatsapp/chats/presence: 525 | * post: 526 | * tags: [Messaging] 527 | * summary: Send presence update 528 | * description: Send typing/recording indicator 529 | * requestBody: 530 | * required: true 531 | * content: 532 | * application/json: 533 | * schema: 534 | * type: object 535 | * required: [sessionId, chatId, presence] 536 | * properties: 537 | * sessionId: 538 | * type: string 539 | * chatId: 540 | * type: string 541 | * presence: 542 | * type: string 543 | * enum: [composing, recording, paused, available, unavailable] 544 | * responses: 545 | * 200: 546 | * description: Presence updated 547 | */ 548 | 549 | /** 550 | * @swagger 551 | * /api/whatsapp/chats/check-number: 552 | * post: 553 | * tags: [Messaging] 554 | * summary: Check WhatsApp number 555 | * description: Check if a phone number is registered on WhatsApp 556 | * requestBody: 557 | * required: true 558 | * content: 559 | * application/json: 560 | * schema: 561 | * type: object 562 | * required: [sessionId, phone] 563 | * properties: 564 | * sessionId: 565 | * type: string 566 | * phone: 567 | * type: string 568 | * example: "628123456789" 569 | * responses: 570 | * 200: 571 | * description: Number check result 572 | * content: 573 | * application/json: 574 | * schema: 575 | * type: object 576 | * properties: 577 | * success: 578 | * type: boolean 579 | * data: 580 | * type: object 581 | * properties: 582 | * exists: 583 | * type: boolean 584 | * jid: 585 | * type: string 586 | */ 587 | 588 | // ==================== CHAT HISTORY ==================== 589 | 590 | /** 591 | * @swagger 592 | * /api/whatsapp/chats/overview: 593 | * post: 594 | * tags: [Chat History] 595 | * summary: Get chat overview 596 | * description: Get list of all chats with last message 597 | * requestBody: 598 | * required: true 599 | * content: 600 | * application/json: 601 | * schema: 602 | * type: object 603 | * required: [sessionId] 604 | * properties: 605 | * sessionId: 606 | * type: string 607 | * limit: 608 | * type: integer 609 | * example: 50 610 | * offset: 611 | * type: integer 612 | * example: 0 613 | * type: 614 | * type: string 615 | * enum: [all, individual, group] 616 | * responses: 617 | * 200: 618 | * description: Chat list 619 | */ 620 | 621 | /** 622 | * @swagger 623 | * /api/whatsapp/chats/messages: 624 | * post: 625 | * tags: [Chat History] 626 | * summary: Get chat messages 627 | * description: Get messages from a specific chat 628 | * requestBody: 629 | * required: true 630 | * content: 631 | * application/json: 632 | * schema: 633 | * type: object 634 | * required: [sessionId, chatId] 635 | * properties: 636 | * sessionId: 637 | * type: string 638 | * chatId: 639 | * type: string 640 | * limit: 641 | * type: integer 642 | * example: 50 643 | * cursor: 644 | * type: string 645 | * description: Pagination cursor 646 | * responses: 647 | * 200: 648 | * description: Messages list 649 | */ 650 | 651 | /** 652 | * @swagger 653 | * /api/whatsapp/chats/info: 654 | * post: 655 | * tags: [Chat History] 656 | * summary: Get chat info 657 | * description: Get detailed information about a chat 658 | * requestBody: 659 | * required: true 660 | * content: 661 | * application/json: 662 | * schema: 663 | * type: object 664 | * required: [sessionId, chatId] 665 | * properties: 666 | * sessionId: 667 | * type: string 668 | * chatId: 669 | * type: string 670 | * responses: 671 | * 200: 672 | * description: Chat information 673 | */ 674 | 675 | /** 676 | * @swagger 677 | * /api/whatsapp/chats/mark-read: 678 | * post: 679 | * tags: [Chat History] 680 | * summary: Mark chat as read 681 | * description: Mark all messages in a chat as read 682 | * requestBody: 683 | * required: true 684 | * content: 685 | * application/json: 686 | * schema: 687 | * type: object 688 | * required: [sessionId, chatId] 689 | * properties: 690 | * sessionId: 691 | * type: string 692 | * chatId: 693 | * type: string 694 | * messageId: 695 | * type: string 696 | * description: Specific message to mark as read 697 | * responses: 698 | * 200: 699 | * description: Chat marked as read 700 | */ 701 | 702 | /** 703 | * @swagger 704 | * /api/whatsapp/contacts: 705 | * post: 706 | * tags: [Chat History] 707 | * summary: Get contacts 708 | * description: Get all contacts 709 | * requestBody: 710 | * required: true 711 | * content: 712 | * application/json: 713 | * schema: 714 | * type: object 715 | * required: [sessionId] 716 | * properties: 717 | * sessionId: 718 | * type: string 719 | * limit: 720 | * type: integer 721 | * offset: 722 | * type: integer 723 | * search: 724 | * type: string 725 | * responses: 726 | * 200: 727 | * description: Contacts list 728 | */ 729 | 730 | /** 731 | * @swagger 732 | * /api/whatsapp/chats/profile-picture: 733 | * post: 734 | * tags: [Chat History] 735 | * summary: Get profile picture 736 | * description: Get profile picture URL of a contact 737 | * requestBody: 738 | * required: true 739 | * content: 740 | * application/json: 741 | * schema: 742 | * type: object 743 | * required: [sessionId, phone] 744 | * properties: 745 | * sessionId: 746 | * type: string 747 | * phone: 748 | * type: string 749 | * responses: 750 | * 200: 751 | * description: Profile picture URL 752 | */ 753 | 754 | /** 755 | * @swagger 756 | * /api/whatsapp/chats/contact-info: 757 | * post: 758 | * tags: [Chat History] 759 | * summary: Get contact info 760 | * description: Get detailed contact information 761 | * requestBody: 762 | * required: true 763 | * content: 764 | * application/json: 765 | * schema: 766 | * type: object 767 | * required: [sessionId, phone] 768 | * properties: 769 | * sessionId: 770 | * type: string 771 | * phone: 772 | * type: string 773 | * responses: 774 | * 200: 775 | * description: Contact information 776 | */ 777 | 778 | // ==================== GROUPS ==================== 779 | 780 | /** 781 | * @swagger 782 | * /api/whatsapp/groups: 783 | * post: 784 | * tags: [Groups] 785 | * summary: List groups 786 | * description: Get all groups for a session 787 | * requestBody: 788 | * required: true 789 | * content: 790 | * application/json: 791 | * schema: 792 | * type: object 793 | * required: [sessionId] 794 | * properties: 795 | * sessionId: 796 | * type: string 797 | * responses: 798 | * 200: 799 | * description: Groups list 800 | */ 801 | 802 | /** 803 | * @swagger 804 | * /api/whatsapp/groups/create: 805 | * post: 806 | * tags: [Groups] 807 | * summary: Create group 808 | * description: Create a new WhatsApp group 809 | * requestBody: 810 | * required: true 811 | * content: 812 | * application/json: 813 | * schema: 814 | * type: object 815 | * required: [sessionId, name, participants] 816 | * properties: 817 | * sessionId: 818 | * type: string 819 | * name: 820 | * type: string 821 | * example: My New Group 822 | * participants: 823 | * type: array 824 | * items: 825 | * type: string 826 | * example: ["628123456789", "628987654321"] 827 | * responses: 828 | * 200: 829 | * description: Group created 830 | */ 831 | 832 | /** 833 | * @swagger 834 | * /api/whatsapp/groups/metadata: 835 | * post: 836 | * tags: [Groups] 837 | * summary: Get group metadata 838 | * description: Get detailed group information 839 | * requestBody: 840 | * required: true 841 | * content: 842 | * application/json: 843 | * schema: 844 | * type: object 845 | * required: [sessionId, groupId] 846 | * properties: 847 | * sessionId: 848 | * type: string 849 | * groupId: 850 | * type: string 851 | * example: "120363123456789@g.us" 852 | * responses: 853 | * 200: 854 | * description: Group metadata 855 | */ 856 | 857 | /** 858 | * @swagger 859 | * /api/whatsapp/groups/participants/add: 860 | * post: 861 | * tags: [Groups] 862 | * summary: Add participants 863 | * description: Add participants to a group 864 | * requestBody: 865 | * required: true 866 | * content: 867 | * application/json: 868 | * schema: 869 | * type: object 870 | * required: [sessionId, groupId, participants] 871 | * properties: 872 | * sessionId: 873 | * type: string 874 | * groupId: 875 | * type: string 876 | * participants: 877 | * type: array 878 | * items: 879 | * type: string 880 | * responses: 881 | * 200: 882 | * description: Participants added 883 | */ 884 | 885 | /** 886 | * @swagger 887 | * /api/whatsapp/groups/participants/remove: 888 | * post: 889 | * tags: [Groups] 890 | * summary: Remove participants 891 | * description: Remove participants from a group 892 | * requestBody: 893 | * required: true 894 | * content: 895 | * application/json: 896 | * schema: 897 | * type: object 898 | * required: [sessionId, groupId, participants] 899 | * properties: 900 | * sessionId: 901 | * type: string 902 | * groupId: 903 | * type: string 904 | * participants: 905 | * type: array 906 | * items: 907 | * type: string 908 | * responses: 909 | * 200: 910 | * description: Participants removed 911 | */ 912 | 913 | /** 914 | * @swagger 915 | * /api/whatsapp/groups/participants/promote: 916 | * post: 917 | * tags: [Groups] 918 | * summary: Promote to admin 919 | * description: Promote participants to group admin 920 | * requestBody: 921 | * required: true 922 | * content: 923 | * application/json: 924 | * schema: 925 | * type: object 926 | * required: [sessionId, groupId, participants] 927 | * properties: 928 | * sessionId: 929 | * type: string 930 | * groupId: 931 | * type: string 932 | * participants: 933 | * type: array 934 | * items: 935 | * type: string 936 | * responses: 937 | * 200: 938 | * description: Participants promoted 939 | */ 940 | 941 | /** 942 | * @swagger 943 | * /api/whatsapp/groups/participants/demote: 944 | * post: 945 | * tags: [Groups] 946 | * summary: Demote from admin 947 | * description: Demote admins to regular participants 948 | * requestBody: 949 | * required: true 950 | * content: 951 | * application/json: 952 | * schema: 953 | * type: object 954 | * required: [sessionId, groupId, participants] 955 | * properties: 956 | * sessionId: 957 | * type: string 958 | * groupId: 959 | * type: string 960 | * participants: 961 | * type: array 962 | * items: 963 | * type: string 964 | * responses: 965 | * 200: 966 | * description: Participants demoted 967 | */ 968 | 969 | /** 970 | * @swagger 971 | * /api/whatsapp/groups/subject: 972 | * post: 973 | * tags: [Groups] 974 | * summary: Update group subject 975 | * description: Change group name/subject 976 | * requestBody: 977 | * required: true 978 | * content: 979 | * application/json: 980 | * schema: 981 | * type: object 982 | * required: [sessionId, groupId, subject] 983 | * properties: 984 | * sessionId: 985 | * type: string 986 | * groupId: 987 | * type: string 988 | * subject: 989 | * type: string 990 | * example: New Group Name 991 | * responses: 992 | * 200: 993 | * description: Subject updated 994 | */ 995 | 996 | /** 997 | * @swagger 998 | * /api/whatsapp/groups/description: 999 | * post: 1000 | * tags: [Groups] 1001 | * summary: Update group description 1002 | * description: Change group description 1003 | * requestBody: 1004 | * required: true 1005 | * content: 1006 | * application/json: 1007 | * schema: 1008 | * type: object 1009 | * required: [sessionId, groupId, description] 1010 | * properties: 1011 | * sessionId: 1012 | * type: string 1013 | * groupId: 1014 | * type: string 1015 | * description: 1016 | * type: string 1017 | * responses: 1018 | * 200: 1019 | * description: Description updated 1020 | */ 1021 | 1022 | /** 1023 | * @swagger 1024 | * /api/whatsapp/groups/settings: 1025 | * post: 1026 | * tags: [Groups] 1027 | * summary: Update group settings 1028 | * description: Change group settings (who can send messages, edit info) 1029 | * requestBody: 1030 | * required: true 1031 | * content: 1032 | * application/json: 1033 | * schema: 1034 | * type: object 1035 | * required: [sessionId, groupId, setting] 1036 | * properties: 1037 | * sessionId: 1038 | * type: string 1039 | * groupId: 1040 | * type: string 1041 | * setting: 1042 | * type: string 1043 | * enum: [announcement, not_announcement, locked, unlocked] 1044 | * responses: 1045 | * 200: 1046 | * description: Settings updated 1047 | */ 1048 | 1049 | /** 1050 | * @swagger 1051 | * /api/whatsapp/groups/picture: 1052 | * post: 1053 | * tags: [Groups] 1054 | * summary: Update group picture 1055 | * description: Change group profile picture 1056 | * requestBody: 1057 | * required: true 1058 | * content: 1059 | * application/json: 1060 | * schema: 1061 | * type: object 1062 | * required: [sessionId, groupId, imageUrl] 1063 | * properties: 1064 | * sessionId: 1065 | * type: string 1066 | * groupId: 1067 | * type: string 1068 | * imageUrl: 1069 | * type: string 1070 | * responses: 1071 | * 200: 1072 | * description: Picture updated 1073 | */ 1074 | 1075 | /** 1076 | * @swagger 1077 | * /api/whatsapp/groups/leave: 1078 | * post: 1079 | * tags: [Groups] 1080 | * summary: Leave group 1081 | * description: Leave a WhatsApp group 1082 | * requestBody: 1083 | * required: true 1084 | * content: 1085 | * application/json: 1086 | * schema: 1087 | * type: object 1088 | * required: [sessionId, groupId] 1089 | * properties: 1090 | * sessionId: 1091 | * type: string 1092 | * groupId: 1093 | * type: string 1094 | * responses: 1095 | * 200: 1096 | * description: Left group 1097 | */ 1098 | 1099 | /** 1100 | * @swagger 1101 | * /api/whatsapp/groups/join: 1102 | * post: 1103 | * tags: [Groups] 1104 | * summary: Join group 1105 | * description: Join a group using invite code 1106 | * requestBody: 1107 | * required: true 1108 | * content: 1109 | * application/json: 1110 | * schema: 1111 | * type: object 1112 | * required: [sessionId, inviteCode] 1113 | * properties: 1114 | * sessionId: 1115 | * type: string 1116 | * inviteCode: 1117 | * type: string 1118 | * example: AbCdEfGhIjK 1119 | * responses: 1120 | * 200: 1121 | * description: Joined group 1122 | */ 1123 | 1124 | /** 1125 | * @swagger 1126 | * /api/whatsapp/groups/invite-code: 1127 | * post: 1128 | * tags: [Groups] 1129 | * summary: Get invite code 1130 | * description: Get group invite code/link 1131 | * requestBody: 1132 | * required: true 1133 | * content: 1134 | * application/json: 1135 | * schema: 1136 | * type: object 1137 | * required: [sessionId, groupId] 1138 | * properties: 1139 | * sessionId: 1140 | * type: string 1141 | * groupId: 1142 | * type: string 1143 | * responses: 1144 | * 200: 1145 | * description: Invite code 1146 | * content: 1147 | * application/json: 1148 | * schema: 1149 | * type: object 1150 | * properties: 1151 | * success: 1152 | * type: boolean 1153 | * data: 1154 | * type: object 1155 | * properties: 1156 | * inviteCode: 1157 | * type: string 1158 | * inviteLink: 1159 | * type: string 1160 | */ 1161 | 1162 | /** 1163 | * @swagger 1164 | * /api/whatsapp/groups/revoke-invite: 1165 | * post: 1166 | * tags: [Groups] 1167 | * summary: Revoke invite code 1168 | * description: Revoke current invite code and generate new one 1169 | * requestBody: 1170 | * required: true 1171 | * content: 1172 | * application/json: 1173 | * schema: 1174 | * type: object 1175 | * required: [sessionId, groupId] 1176 | * properties: 1177 | * sessionId: 1178 | * type: string 1179 | * groupId: 1180 | * type: string 1181 | * responses: 1182 | * 200: 1183 | * description: Invite revoked 1184 | */ 1185 | 1186 | // ==================== WEBSOCKET DOCUMENTATION ==================== 1187 | 1188 | /** 1189 | * @swagger 1190 | * tags: 1191 | * - name: WebSocket 1192 | * description: | 1193 | * ## Real-time WebSocket Events 1194 | * 1195 | * Connect to the WebSocket server for real-time updates. 1196 | * 1197 | * ### Connection 1198 | * ```javascript 1199 | * const socket = io('ws://your-server:3000'); 1200 | * ``` 1201 | * 1202 | * ### Subscribe to Session Events 1203 | * ```javascript 1204 | * // Subscribe to receive events from a specific session 1205 | * socket.emit('subscribe', 'your-session-id'); 1206 | * 1207 | * // Unsubscribe from session events 1208 | * socket.emit('unsubscribe', 'your-session-id'); 1209 | * ``` 1210 | * 1211 | * ### Available Events 1212 | * 1213 | * | Event | Description | Payload | 1214 | * |-------|-------------|---------| 1215 | * | `qr` | QR code generated for authentication | `{ sessionId, qr, qrCode }` | 1216 | * | `connection.update` | Connection status changed | `{ sessionId, status, isConnected }` | 1217 | * | `message` | New incoming message | `{ sessionId, message, chatId, ... }` | 1218 | * | `message.sent` | Message sent confirmation | `{ sessionId, messageId, status }` | 1219 | * | `message.update` | Message status update (delivered/read) | `{ sessionId, messageId, status }` | 1220 | * | `message.revoke` | Message deleted/revoked | `{ sessionId, messageId, chatId }` | 1221 | * | `chat.update` | Chat updated | `{ sessionId, chat }` | 1222 | * | `chat.upsert` | New chat created | `{ sessionId, chat }` | 1223 | * | `chat.delete` | Chat deleted | `{ sessionId, chatId }` | 1224 | * | `contact.update` | Contact updated | `{ sessionId, contact }` | 1225 | * | `presence.update` | Presence status (typing, online) | `{ sessionId, chatId, presence }` | 1226 | * | `group.participants` | Group members update | `{ sessionId, groupId, action, participants }` | 1227 | * | `group.update` | Group info update | `{ sessionId, groupId, update }` | 1228 | * | `call` | Incoming call | `{ sessionId, call }` | 1229 | * | `logged.out` | Session logged out | `{ sessionId, reason }` | 1230 | * 1231 | * ### Example: Listen for Messages 1232 | * ```javascript 1233 | * socket.on('message', (data) => { 1234 | * console.log('New message:', data); 1235 | * // data.sessionId - Which session received the message 1236 | * // data.message - Message content and metadata 1237 | * // data.chatId - Chat/sender ID 1238 | * }); 1239 | * ``` 1240 | * 1241 | * ### Example: Listen for Connection Updates 1242 | * ```javascript 1243 | * socket.on('connection.update', (data) => { 1244 | * console.log('Connection status:', data.status); 1245 | * // data.status: 'connecting', 'connected', 'disconnected' 1246 | * }); 1247 | * ``` 1248 | * 1249 | * ### Example: Listen for QR Code 1250 | * ```javascript 1251 | * socket.on('qr', (data) => { 1252 | * console.log('Scan this QR:', data.qrCode); 1253 | * // data.qrCode - Base64 encoded QR image 1254 | * }); 1255 | * ``` 1256 | */ 1257 | 1258 | /** 1259 | * @swagger 1260 | * components: 1261 | * schemas: 1262 | * WebSocketEvent: 1263 | * type: object 1264 | * description: WebSocket event payload structure 1265 | * properties: 1266 | * sessionId: 1267 | * type: string 1268 | * description: Session that triggered the event 1269 | * example: mysession 1270 | * event: 1271 | * type: string 1272 | * description: Event type 1273 | * example: message 1274 | * data: 1275 | * type: object 1276 | * description: Event-specific data 1277 | * 1278 | * QREvent: 1279 | * type: object 1280 | * description: QR code event payload 1281 | * properties: 1282 | * sessionId: 1283 | * type: string 1284 | * example: mysession 1285 | * qr: 1286 | * type: string 1287 | * description: Raw QR string data 1288 | * qrCode: 1289 | * type: string 1290 | * description: Base64 encoded QR code image (data:image/png;base64,...) 1291 | * 1292 | * ConnectionUpdateEvent: 1293 | * type: object 1294 | * description: Connection status update event 1295 | * properties: 1296 | * sessionId: 1297 | * type: string 1298 | * example: mysession 1299 | * status: 1300 | * type: string 1301 | * enum: [connecting, connected, disconnected] 1302 | * example: connected 1303 | * isConnected: 1304 | * type: boolean 1305 | * example: true 1306 | * phoneNumber: 1307 | * type: string 1308 | * example: "628123456789" 1309 | * name: 1310 | * type: string 1311 | * example: John Doe 1312 | * 1313 | * MessageEvent: 1314 | * type: object 1315 | * description: Incoming message event 1316 | * properties: 1317 | * sessionId: 1318 | * type: string 1319 | * example: mysession 1320 | * message: 1321 | * type: object 1322 | * properties: 1323 | * id: 1324 | * type: string 1325 | * description: Message ID 1326 | * chatId: 1327 | * type: string 1328 | * description: Chat/sender JID 1329 | * fromMe: 1330 | * type: boolean 1331 | * description: Was sent by the session user 1332 | * timestamp: 1333 | * type: integer 1334 | * description: Unix timestamp 1335 | * type: 1336 | * type: string 1337 | * enum: [text, image, video, audio, document, sticker, location, contact] 1338 | * content: 1339 | * type: object 1340 | * description: Message content based on type 1341 | * pushName: 1342 | * type: string 1343 | * description: Sender display name 1344 | * 1345 | * MessageUpdateEvent: 1346 | * type: object 1347 | * description: Message status update event 1348 | * properties: 1349 | * sessionId: 1350 | * type: string 1351 | * messageId: 1352 | * type: string 1353 | * status: 1354 | * type: string 1355 | * enum: [pending, sent, delivered, read, played] 1356 | * chatId: 1357 | * type: string 1358 | * 1359 | * PresenceUpdateEvent: 1360 | * type: object 1361 | * description: Presence/typing indicator event 1362 | * properties: 1363 | * sessionId: 1364 | * type: string 1365 | * chatId: 1366 | * type: string 1367 | * presence: 1368 | * type: string 1369 | * enum: [composing, recording, paused, available, unavailable] 1370 | * lastSeen: 1371 | * type: integer 1372 | * description: Last seen timestamp (if available) 1373 | * 1374 | * GroupParticipantsEvent: 1375 | * type: object 1376 | * description: Group participants update event 1377 | * properties: 1378 | * sessionId: 1379 | * type: string 1380 | * groupId: 1381 | * type: string 1382 | * example: "120363123456789@g.us" 1383 | * action: 1384 | * type: string 1385 | * enum: [add, remove, promote, demote] 1386 | * participants: 1387 | * type: array 1388 | * items: 1389 | * type: string 1390 | * example: ["628123456789@s.whatsapp.net"] 1391 | * actor: 1392 | * type: string 1393 | * description: Who performed the action 1394 | * 1395 | * CallEvent: 1396 | * type: object 1397 | * description: Incoming call event 1398 | * properties: 1399 | * sessionId: 1400 | * type: string 1401 | * call: 1402 | * type: object 1403 | * properties: 1404 | * id: 1405 | * type: string 1406 | * from: 1407 | * type: string 1408 | * isVideo: 1409 | * type: boolean 1410 | * isGroup: 1411 | * type: boolean 1412 | * status: 1413 | * type: string 1414 | * enum: [offer, ringing, timeout, reject, accept] 1415 | */ 1416 | 1417 | module.exports = {}; 1418 | --------------------------------------------------------------------------------