diff --git a/.aiignore b/.aiignore
new file mode 100644
index 0000000..2af745f
--- /dev/null
+++ b/.aiignore
@@ -0,0 +1,4 @@
+node_modules
+.idea
+.git
+package-lock.json
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 644ccb0..d78ba4d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -26,6 +26,7 @@ RUN npm run postinstall || true
# Copy application files
COPY server.js ./
+COPY swagger.yml ./
# ==================== RUNTIME STAGE ====================
FROM node:20-alpine
@@ -55,6 +56,7 @@ COPY --from=builder --chown=whatsapp:whatsapp /app/node_modules ./node_modules
# Copy application files
COPY --chown=whatsapp:whatsapp server.js ./
COPY --chown=whatsapp:whatsapp package*.json ./
+COPY --chown=whatsapp:whatsapp swagger.yml ./
# Create volume directories
RUN mkdir -p /app/data /app/media /app/.wwebjs_cache /app/.wwebjs_auth && \
diff --git a/dev-docker-compose.yml b/dev-docker-compose.yml
index 702ec54..5aab15d 100644
--- a/dev-docker-compose.yml
+++ b/dev-docker-compose.yml
@@ -2,7 +2,7 @@ services:
wapp:
#user: "1000:1000"
build:
- #context: /opt/apps/wapp
+ #context: /opt/00apps/wapp
context: .
dockerfile: Dockerfile
ports:
diff --git a/docker-compose.yml b/docker-compose.yml
index f7c8844..e7b29d0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,7 +2,7 @@ services:
wapp:
user: "1000:1000"
build:
- context: /opt/apps/wapp
+ context: ${WAPP_APP_ROOT}
dockerfile: Dockerfile
ports:
- "3001:3001"
@@ -10,6 +10,7 @@ services:
restart: unless-stopped
networks:
- traefik_net
+ - default
environment:
# Server configuration
- PORT=3001
@@ -61,6 +62,8 @@ networks:
traefik_net:
external: true
name: traefik_net
+ default:
+ driver: bridge
volumes:
whatsapp-data:
diff --git a/fix_path.sh b/fix_path.sh
new file mode 100755
index 0000000..3c9d72b
--- /dev/null
+++ b/fix_path.sh
@@ -0,0 +1,86 @@
+#!/usr/bin/env bash
+set -e
+
+echo "=== Cleaning node_modules ==="
+rm -rf node_modules
+echo "OK"
+
+echo "=== Installing dependencies (ignoring scripts) ==="
+npm install --ignore-scripts
+echo "OK"
+
+echo "=== Locating Injected Store.js ==="
+
+TARGET=$(find node_modules/whatsapp-web.js/src/util/Injected -type f -name "Store.js" | head -n 1)
+
+if [ -z "$TARGET" ]; then
+ echo "ERROR: Cannot find Store.js under Injected/"
+ exit 1
+fi
+
+echo "Found Injected file:"
+echo " $TARGET"
+
+echo "=== Backing up original ==="
+cp "$TARGET" "$TARGET.bak"
+
+echo "=== Applying getContactModel patch ==="
+
+# Patch de getContactModel functie
+sed -i '/getContactModel:/,/return {/c\
+ getContactModel: (contact) => {\n\
+ return {\n\
+ id: contact.id,\n\
+ name: contact.name,\n\
+ type: contact.type,\n\
+\n\
+ isMe: contact.isMe || false,\n\
+ isUser: contact.isUser || false,\n\
+ isGroup: contact.isGroup || false,\n\
+ isBroadcast: contact.isBroadcast || false,\n\
+ isBusiness: contact.isBusiness || false,\n\
+ isEnterprise: contact.isEnterprise || false,\n\
+\n\
+ isContactSyncEnabled: contact.isContactSyncEnabled || false,\n\
+ isContactLocked: contact.isContactLocked || false,\n\
+\n\
+ verifiedLevel: contact.verifiedLevel,\n\
+ verifiedName: contact.verifiedName,\n\
+ pushname: contact.pushname,\n\
+\n\
+ isWAContact: contact.isWAContact || false,\n\
+\n\
+ isMyContact: (\n\
+ contact.isMyContact !== undefined\n\
+ ? contact.isMyContact\n\
+ : contact.type === \"in\" || false\n\
+ ),\n\
+\n\
+ isBlocked: contact.isBlocked || false,\n\
+\n\
+ labels: Array.isArray(contact.labels)\n\
+ ? contact.labels.map(l => l.id)\n\
+ : [],\n\
+\n\
+ displayName: contact.displayName || contact.pushname || contact.name,\n\
+ formattedName: contact.formattedName || contact.name,\n\
+ formattedShortName: contact.formattedShortName || contact.name,\n\
+ shortName: contact.shortName || contact.name,\n\
+ isSaved: contact.isSaved !== undefined ? contact.isSaved : false,\n\
+\n\
+ isMuted: contact.muteExpiration !== undefined\n\
+ ? contact.muteExpiration > 0\n\
+ : false,\n\
+\n\
+ muteExpiration: contact.muteExpiration || 0,\n\
+ disappearingModeDuration: contact.disappearingModeDuration || 0,\n\
+ };\n\
+ },' "$TARGET"
+
+echo "=== Generating patch-package diff ==="
+npx patch-package whatsapp-web.js
+echo "OK"
+
+echo "=== Final npm install ==="
+npm install
+echo "SUCCESS — Patch applied, new diff created 🚀"
diff --git a/output.md b/output.md
new file mode 100644
index 0000000..9a0430f
--- /dev/null
+++ b/output.md
@@ -0,0 +1,1173 @@
+===== package.json =====
+```json
+{
+ "name": "wapp",
+ "version": "1.0.0",
+ "description": "WhatsApp automation server with API and database",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js",
+ "dev": "nodemon server.js",
+ "postinstall": "patch-package"
+ },
+ "dependencies": {
+ "dotenv": "^16.3.1",
+ "express": "^4.18.2",
+ "qrcode": "^1.5.4",
+ "qrcode-terminal": "^0.12.0",
+ "sequelize": "^6.32.1",
+ "socket.io": "^4.7.2",
+ "sqlite3": "^5.1.6",
+ "whatsapp-web.js": "github:pedroslopez/whatsapp-web.js"
+ },
+ "devDependencies": {
+ "nodemon": "^3.0.1",
+ "patch-package": "^8.0.1",
+ "postinstall-postinstall": "^2.1.0"
+ }
+}
+```
+
+===== README.md =====
+```md
+# WhatsApp Web.js Server
+
+Complete WhatsApp automation server with REST API, WebSocket, and SQLite storage.
+
+## Features
+
+✅ **REST API** - Send messages, retrieve contacts/chats/messages
+✅ **WebSocket** - Real-time event streaming
+✅ **SQLite** - Persistent storage for all data
+✅ **Media Storage** - Automatic media file handling
+✅ **Authentication** - API key protection
+✅ **QR Code Web UI** - Easy WhatsApp authentication via browser
+✅ **All Original Commands** - Full command handler support
+
+## Quick Start
+
+### Local Development
+
+```bash
+npm install
+npm start
+```
+
+Visit `http://localhost:3001/qr` to scan the WhatsApp QR code.
+
+### Docker Deployment
+
+```bash
+docker-compose up -d --build
+```
+
+Visit `http://your-server:3001/qr` to authenticate WhatsApp.
+
+## QR Code Authentication - Web-UI
+
+The server provides a web interface for easy WhatsApp authentication:
+
+- **URL**: `http://localhost:3001/qr`
+- **Features**:
+ - Real-time QR code display
+ - Live connection status updates
+ - Auto-refresh on new QR codes
+ - Mobile-friendly design
+
+The QR code is also printed in the console logs for terminal access.```
+
+===== .env.example =====
+```
+# Server Configuration
+PORT=3001
+API_KEY=your-strong-api-key-here
+
+# Database & Storage
+DB_PATH=./data/whatsapp.db
+MEDIA_PATH=./media
+
+# WhatsApp Client
+HEADLESS=false
+REJECT_CALLS=true
+
+# CORS (for WebSocket)
+CORS_ORIGIN=*```
+
+===== docker-compose.yml =====
+```yaml
+services:
+ wapp:
+ user: "1000:1000"
+ build:
+ context: /opt/apps/wapp
+ dockerfile: Dockerfile
+ ports:
+ - "3001:3001"
+ container_name: wapp
+ restart: unless-stopped
+ networks:
+ - traefik_net
+ environment:
+ # Server configuration
+ - PORT=3001
+ - NODE_ENV=production
+
+ # API Security
+ - API_KEY=${API_KEY:-your-secure-api-key-here}
+
+ # Paths (pointing to volume mounts)
+ - DB_PATH=/app/data/whatsapp.db
+ - MEDIA_PATH=/app/media
+
+ # WhatsApp configuration
+ # Set HEADLESS=false to see the browser window (useful for debugging)
+ - HEADLESS=true
+ - REJECT_CALLS=false
+
+ # WebSocket URL for QR page (set this to your public URL when using Traefik - )
+ # Example: - WS_URL=https://wapp.appmodel.nl
+ - WS_URL=https://wapp.appmodel.nl
+
+ # CORS configuration
+ - CORS_ORIGIN=*
+
+ volumes:
+ # Persistent data volumes
+ - whatsapp-data:/app/data
+ - whatsapp-media:/app/media
+ - whatsapp-cache:/app/.wwebjs_cache
+ - whatsapp-auth:/app/.wwebjs_auth
+
+ # Uncomment labels below when ready to use Traefik
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.wapp.rule=Host(`wapp.appmodel.nl`)"
+ - "traefik.http.routers.wapp.entrypoints=websecure"
+ - "traefik.http.routers.wapp.tls=true"
+ - "traefik.http.routers.wapp.tls.certresolver=letsencrypt"
+ - "traefik.http.services.wapp.loadbalancer.server.port=3001"
+
+ healthcheck:
+ test: ["CMD", "node", "-e", "require('http').get('http://localhost:3001/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1); }).on('error', () => process.exit(1));"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 40s
+
+networks:
+ traefik_net:
+ external: true
+ name: traefik_net
+
+volumes:
+ whatsapp-data:
+ driver: local
+ whatsapp-media:
+ driver: local
+ whatsapp-cache:
+ driver: local
+ whatsapp-auth:
+ driver: local
+```
+
+===== .dockerignore =====
+```
+node_modules
+npm-debug.log
+.env
+.git
+.gitignore
+.idea
+.wwebjs_auth
+.wwebjs_cache
+data
+media
+*.md
+test
+markmap.svg
+```
+
+===== server.js =====
+```javascript
+require('dotenv').config()
+const express = require('express')
+const http = require('http')
+const socketIo = require('socket.io')
+const { Sequelize, DataTypes, Op } = require('sequelize')
+const { Client, LocalAuth, Location, Poll, List, Buttons } = require('whatsapp-web.js')
+const path = require('path')
+const fs = require('fs').promises
+const crypto = require('crypto')
+const qrcode = require('qrcode-terminal')
+const QRCode = require('qrcode')
+
+// --- CONFIGURATION ---
+const config = {
+ port : process.env.PORT || 3001,
+ dbPath : process.env.DB_PATH || path.join(__dirname, 'data', 'whatsapp.db'),
+ mediaPath : process.env.MEDIA_PATH || path.join(__dirname, 'media'),
+ headless : process.env.HEADLESS !== 'false',
+ rejectCalls: process.env.REJECT_CALLS !== 'false',
+ apiKey : process.env.API_KEY || 'test-key'
+}
+
+// Ensure directories exist
+async function ensureDirectories() {
+ await fs.mkdir(path.dirname(config.dbPath), { recursive: true })
+ await fs.mkdir(config.mediaPath, { recursive: true })
+}
+
+// Simple logger
+const logger = {
+ info : (msg) => console.log(`[${ new Date().toISOString() }] INFO: ${ msg }`),
+ error: (msg, err) => console.error(`[${ new Date().toISOString() }] ERROR: ${ msg }`, err || ''),
+ warn : (msg) => console.warn(`[${ new Date().toISOString() }] WARN: ${ msg }`)
+}
+
+// --- DATABASE SETUP ---
+const sequelize = new Sequelize({
+ dialect: 'sqlite',
+ storage: config.dbPath,
+ logging: process.env.NODE_ENV === 'development' ? logger.info : false
+})
+
+// Models
+const Contact = sequelize.define('Contact', {
+ id : { type: DataTypes.STRING, primaryKey: true },
+ phoneNumber : { type: DataTypes.STRING, allowNull: true },
+ name : DataTypes.STRING,
+ pushname : DataTypes.STRING,
+ isBlocked : { type: DataTypes.BOOLEAN, defaultValue: false },
+ isMyContact : { type: DataTypes.BOOLEAN, defaultValue: false },
+ labels : { type: DataTypes.JSON, defaultValue: [] },
+ profilePicUrl: DataTypes.STRING,
+ isGroup: { type: DataTypes.BOOLEAN, defaultValue: false },
+ isNewsletter: { type: DataTypes.BOOLEAN, defaultValue: false },
+ isUser: { type: DataTypes.BOOLEAN, defaultValue: true }
+}, { indexes: [{ fields: ['phoneNumber'] }] })
+
+const Chat = sequelize.define('Chat', {
+ id : { type: DataTypes.STRING, primaryKey: true },
+ name : DataTypes.STRING,
+ isGroup : { type: DataTypes.BOOLEAN, defaultValue: false },
+ isMuted : { type: DataTypes.BOOLEAN, defaultValue: false },
+ unreadCount: { type: DataTypes.INTEGER, defaultValue: 0 },
+ timestamp : DataTypes.INTEGER,
+ pinned : { type: DataTypes.BOOLEAN, defaultValue: false },
+ description: DataTypes.TEXT
+}, { indexes: [{ fields: ['isGroup'] }] })
+
+const Message = sequelize.define('Message', {
+ id : { type: DataTypes.STRING, primaryKey: true },
+ from : { type: DataTypes.STRING, allowNull: false },
+ to : { type: DataTypes.STRING, allowNull: false },
+ body : DataTypes.TEXT,
+ type : DataTypes.STRING,
+ timestamp: { type: DataTypes.INTEGER, index: true },
+ hasMedia : { type: DataTypes.BOOLEAN, defaultValue: false },
+ mediaId : DataTypes.STRING,
+ fromMe : { type: DataTypes.BOOLEAN, defaultValue: false },
+ ack : { type: DataTypes.INTEGER, defaultValue: 0 }
+}, { indexes: [{ fields: ['from', 'timestamp'] }] })
+
+const Media = sequelize.define('Media', {
+ id : { type: DataTypes.STRING, primaryKey: true },
+ messageId: { type: DataTypes.STRING, references: { model: Message, key: 'id' } },
+ mimetype : DataTypes.STRING,
+ filename : DataTypes.STRING,
+ filePath : DataTypes.STRING
+})
+
+const Call = sequelize.define('Call', {
+ id : { type: DataTypes.STRING, primaryKey: true },
+ from : DataTypes.STRING,
+ isVideo : { type: DataTypes.BOOLEAN, defaultValue: false },
+ timestamp: DataTypes.INTEGER,
+ rejected : { type: DataTypes.BOOLEAN, defaultValue: false }
+})
+
+Message.hasOne(Media, { foreignKey: 'messageId', as: 'media' })
+
+// --- EXPRESS & SOCKET.IO ---
+const app = express()
+const server = http.createServer(app)
+const io = socketIo(server, {
+ cors: { origin: process.env.CORS_ORIGIN || '*', methods: ['GET', 'POST'] }
+})
+
+app.use(express.json({ limit: '50mb' }))
+
+// Middleware
+function validateApiKey(req, res, next) {
+ const apiKey = req.headers['x-api-key'] || req.query.apiKey
+ if (!apiKey || apiKey !== config.apiKey) {
+ return res.status(401).json({ success: false, error: 'Unauthorized' })
+ }
+ next()
+}
+
+function checkWhatsAppReady(req, res, next) {
+ if (!client.info) return res.status(503).json({ success: false, error: 'Client not ready' })
+ next()
+}
+
+// --- REST API ---
+app.post('/api/send', [validateApiKey, checkWhatsAppReady], async (req, res) => {
+ try {
+ const { phoneNumber, message } = req.body
+ if (!phoneNumber || !message) {
+ return res.status(400).json({ success: false, error: 'Missing phoneNumber or message' })
+ }
+
+ const chatId = phoneNumber.includes('@') ? phoneNumber : `${ phoneNumber }@c.us`
+ const sent = await client.sendMessage(chatId, message)
+
+ await Message.create({
+ id : sent.id._serialized,
+ from : client.info.wid._serialized,
+ to : chatId,
+ body : typeof message === 'string' ? message : JSON.stringify(message),
+ type : sent.type,
+ timestamp: sent.timestamp,
+ fromMe : true,
+ ack : sent.ack
+ })
+
+ io.emit('message_sent', { id: sent.id._serialized, phoneNumber, message })
+ res.json({ success: true, messageId: sent.id._serialized })
+ } catch (error) {
+ logger.error('Error sending message', error)
+ res.status(500).json({ success: false, error: error.message })
+ }
+})
+
+app.get('/api/contacts', [validateApiKey], async (req, res) => {
+ try {
+ const contacts = await Contact.findAll({ order: [['name', 'ASC']], limit: 1000 })
+ res.json({ success: true, data: contacts })
+ } catch (error) {
+ logger.error('Error fetching contacts', error)
+ res.status(500).json({ success: false, error: error.message })
+ }
+})
+
+app.get('/api/chats', [validateApiKey], async (req, res) => {
+ try {
+ const chats = await Chat.findAll({ order: [['timestamp', 'DESC']], limit: 1000 })
+ res.json({ success: true, data: chats })
+ } catch (error) {
+ logger.error('Error fetching chats', error)
+ res.status(500).json({ success: false, error: error.message })
+ }
+})
+
+app.get('/api/messages/:chatId', [validateApiKey], async (req, res) => {
+ try {
+ const { chatId } = req.params
+ const messages = await Message.findAll({
+ where : { [Op.or]: [{ from: chatId }, { to: chatId }] },
+ order : [['timestamp', 'DESC']],
+ limit : 100,
+ include: [{ model: Media, as: 'media' }]
+ })
+ res.json({ success: true, data: messages })
+ } catch (error) {
+ logger.error('Error fetching messages', error)
+ res.status(500).json({ success: false, error: error.message })
+ }
+})
+
+app.get('/api/status', [validateApiKey], async (req, res) => {
+ try {
+ const status = {
+ connected : !!client.info,
+ user : client.info,
+ uptime : process.uptime(),
+ wwebVersion: client.info ? await client.getWWebVersion() : null
+ }
+ res.json({ success: true, data: status })
+ } catch (error) {
+ res.status(500).json({ success: false, error: error.message })
+ }
+})
+
+app.get('/health', (req, res) => res.json({ status: 'OK', timestamp: Date.now() }))
+
+// QR Code storage
+let currentQR = null
+let qrTimestamp = null
+
+// QR Code HTML page
+app.get('/qr', (req, res) => {
+ const html = `
+
+
+
+
+
+ WhatsApp QR Code
+
+
+
+
+
🔗 WhatsApp Connection
+
Scan the QR code with your WhatsApp mobile app
+
+
+ Waiting for QR code...
+
+
+
+
+
+
How to scan:
+
1. Open WhatsApp on your phone
+
2. Tap Menu (⋮) > Linked Devices
+
3. Tap "Link a Device"
+
4. Point your phone at this screen
+
+
+
+
+
+
+
+
+
+ `
+ res.send(html)
+})
+
+// QR Code check endpoint
+app.get('/qr/check', (req, res) => {
+ res.json({
+ hasQR: !!currentQR,
+ connected: !!client.info,
+ timestamp: qrTimestamp
+ })
+})
+
+// QR Code image endpoint
+app.get('/qr/image', async (req, res) => {
+ try {
+ if (!currentQR) {
+ // Generate a placeholder
+ res.setHeader('Content-Type', 'image/png')
+ const placeholder = await QRCode.toBuffer('Waiting for QR code...', {
+ width: 300,
+ color: { dark: '#ccc', light: '#fff' }
+ })
+ return res.send(placeholder)
+ }
+
+ res.setHeader('Content-Type', 'image/png')
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
+ const qrImage = await QRCode.toBuffer(currentQR, {
+ width: 300,
+ margin: 2,
+ color: { dark: '#000', light: '#fff' }
+ })
+ res.send(qrImage)
+ } catch (error) {
+ logger.error('Error generating QR image', error)
+ res.status(500).send('Error generating QR code')
+ }
+})
+
+// --- WEBSOCKET ---
+io.on('connection', (socket) => {
+ logger.info(`WebSocket client connected: ${ socket.id }`)
+ socket.emit('status', { connected: !!client.info })
+
+ socket.on('disconnect', (reason) => {
+ logger.info(`WebSocket client disconnected: ${ socket.id }, reason: ${ reason }`)
+ })
+})
+
+// --- WHATSAPP CLIENT ---
+const client = new Client({
+ authStrategy: new LocalAuth({ clientId: 'whatsapp-server' }),
+ puppeteer : {
+ headless: config.headless,
+ args : ['--no-sandbox', '--disable-setuid-sandbox']
+ }
+})
+
+client.on('qr', (qr) => {
+ logger.info('QR received - scan to authenticate')
+ logger.info('QR Code available at: http://localhost:' + config.port + '/qr')
+ qrcode.generate(qr, { small: true })
+
+ // Store QR for web display
+ currentQR = qr
+ qrTimestamp = Date.now()
+
+ io.emit('qr', { qr, timestamp: qrTimestamp })
+})
+
+client.on('ready', async () => {
+ logger.info('WhatsApp client ready');
+
+ // Clear QR code once connected
+ currentQR = null
+ qrTimestamp = null
+
+ const wwebVersion = await client.getWWebVersion();
+
+ // Sync contacts with proper type detection
+ const contacts = await client.getContacts();
+ let contactsSynced = 0;
+
+ for (const contact of contacts) {
+ try {
+ const id = contact.id?._serialized || contact.id || '';
+ const name = contact.name || contact.pushname || 'Unknown';
+
+ // Determine contact type and extract identifier
+ let phoneNumber = null;
+ let isGroup = false;
+ let isNewsletter = false;
+ let isUser = false;
+
+ if (contact.id && typeof contact.id === 'object') {
+ if (contact.id.server === 'c.us') {
+ // Regular user contact
+ phoneNumber = contact.id.user || contact.number;
+ isUser = true;
+ } else if (contact.id.server === 'g.us') {
+ // Group chat
+ isGroup = true;
+ phoneNumber = contact.id._serialized; // Use group ID
+ } else if (contact.id.server === 'newsletter') {
+ // Newsletter
+ isNewsletter = true;
+ phoneNumber = contact.id._serialized; // Use newsletter ID
+ }
+ }
+
+ // Fallback
+ if (!phoneNumber) {
+ phoneNumber = contact.number || contact.id?._serialized || null;
+ isUser = true;
+ }
+
+ await Contact.upsert({
+ id: id,
+ phoneNumber: phoneNumber,
+ name: name,
+ pushname: contact.pushname || null,
+ isBlocked: contact.isBlocked || false,
+ isMyContact: contact.isMyContact || false,
+ labels: contact.labels || [],
+ profilePicUrl: contact.profilePicUrl || null,
+ isGroup: isGroup,
+ isNewsletter: isNewsletter,
+ isUser: isUser
+ });
+ contactsSynced++;
+ } catch (err) {
+ logger.warn(`Failed to sync contact ${contact.id?._serialized || 'unknown'}: ${err.message}`);
+ }
+ }
+
+ // Sync chats
+ const chats = await client.getChats();
+ let chatsSynced = 0;
+
+ for (const chat of chats) {
+ try {
+ await Chat.upsert({
+ id: chat.id._serialized,
+ name: chat.name,
+ isGroup: chat.isGroup,
+ isMuted: chat.isMuted || false,
+ unreadCount: chat.unreadCount || 0,
+ timestamp: chat.timestamp || 0,
+ pinned: chat.pinned || false,
+ description: chat.description || null
+ });
+ chatsSynced++;
+ } catch (err) {
+ logger.warn(`Failed to sync chat ${chat.id._serialized}: ${err.message}`);
+ }
+ }
+
+ logger.info(`Synced ${contactsSynced}/${contacts.length} contacts and ${chatsSynced}/${chats.length} chats`);
+ io.emit('ready', { version: wwebVersion, user: client.info });
+});
+
+client.on('message', async (msg) => {
+ try {
+ const messageData = {
+ id : msg.id._serialized,
+ from : msg.from,
+ to : msg.to || msg.from,
+ body : msg.body || '',
+ type : msg.type,
+ timestamp: msg.timestamp,
+ hasMedia : msg.hasMedia,
+ fromMe : msg.fromMe,
+ ack : msg.ack || 0
+ }
+
+ if (msg.location) {
+ Object.assign(messageData, {
+ locationLatitude : msg.location.latitude,
+ locationLongitude: msg.location.longitude,
+ locationName : msg.location.name,
+ locationAddress : msg.location.address
+ })
+ }
+
+ await Message.create(messageData)
+
+ if (msg.hasMedia) {
+ const media = await msg.downloadMedia()
+ const mediaId = crypto.randomUUID()
+ const fileExt = media.filename ? path.extname(media.filename) : media.mimetype.split('/')[1] || 'bin'
+ const filename = `${ mediaId }${ fileExt }`
+ const filePath = path.join(config.mediaPath, filename)
+
+ await fs.writeFile(filePath, media.data, 'base64')
+ await Media.create({
+ id : mediaId,
+ messageId: msg.id._serialized,
+ mimetype : media.mimetype,
+ filename : media.filename || filename,
+ filePath
+ })
+ }
+
+ // Update contact info for incoming messages (safely)
+ if (!msg.fromMe && config.syncOnReady) {
+ try {
+ // Only attempt if SYNC_ON_READY is true (you trust the API)
+ const contact = await msg.getContact()
+ await Contact.upsert({
+ id : contact.id._serialized,
+ phoneNumber : contact.number || contact.id.user,
+ name : contact.name || contact.pushname,
+ pushname : contact.pushname,
+ isBlocked : contact.isBlocked || false,
+ isMyContact : contact.isMyContact || false,
+ labels : contact.labels || [],
+ profilePicUrl: contact.profilePicUrl || null
+ })
+ } catch (err) {
+ // Silently fail - this is a known issue with whatsapp-web.js
+ logger.warn(`Failed to update contact: ${ err.message }`)
+ }
+ }
+
+ io.emit('message', {
+ id : msg.id._serialized,
+ from : msg.from,
+ body : msg.body,
+ type : msg.type,
+ timestamp: msg.timestamp,
+ hasMedia : msg.hasMedia,
+ fromMe : msg.fromMe
+ })
+
+ // --- ALL ORIGINAL COMMAND HANDLERS ---
+ if (msg.body === '!ping reply') {
+ await msg.reply('pong')
+ } else if (msg.body === '!ping') {
+ await client.sendMessage(msg.from, 'pong')
+ } else if (msg.body.startsWith('!sendto ')) {
+ const parts = msg.body.split(' ')
+ const number = parts[1].includes('@c.us') ? parts[1] : `${ parts[1] }@c.us`
+ await client.sendMessage(number, parts.slice(2).join(' '))
+ } else if (msg.body.startsWith('!subject ')) {
+ const chat = await msg.getChat()
+ if (chat.isGroup) await chat.setSubject(msg.body.slice(9))
+ } else if (msg.body.startsWith('!desc ')) {
+ const chat = await msg.getChat()
+ if (chat.isGroup) await chat.setDescription(msg.body.slice(6))
+ } else if (msg.body === '!leave') {
+ const chat = await msg.getChat()
+ if (chat.isGroup) await chat.leave()
+ } else if (msg.body.startsWith('!join ')) {
+ try {
+ await client.acceptInvite(msg.body.split(' ')[1])
+ await msg.reply('Joined the group!')
+ } catch (e) {
+ await msg.reply('Invalid invite code.')
+ }
+ } else if (msg.body === '!groupinfo') {
+ const chat = await msg.getChat()
+ if (chat.isGroup) {
+ await msg.reply(`*Group Details*\nName: ${ chat.name }\nDescription: ${ chat.description }\nParticipants: ${ chat.participants.length }`)
+ }
+ } else if (msg.body === '!chats') {
+ const chats = await client.getChats()
+ await client.sendMessage(msg.from, `The bot has ${ chats.length } chats open.`)
+ } else if (msg.body === '!info') {
+ const info = client.info
+ await client.sendMessage(msg.from, `*Connection Info*\nUser: ${ info.pushname }\nNumber: ${ info.wid.user }\nPlatform: ${ info.platform }`)
+ } else if (msg.body === '!mediainfo' && msg.hasMedia) {
+ const media = await msg.downloadMedia()
+ await msg.reply(`*Media Info*\nMime: ${ media.mimetype }\nSize: ${ media.data.length }`)
+ } else if (msg.body === '!location') {
+ await msg.reply(new Location(37.422, -122.084, { name: 'Googleplex' }))
+ } else if (msg.body === '!reaction') {
+ await msg.react('👍')
+ } else if (msg.body === '!sendpoll') {
+ await msg.reply(new Poll('Winter or Summer?', ['Winter', 'Summer']))
+ }
+
+ } catch (error) {
+ logger.error('Error in message handler', error)
+ io.emit('error', { source: 'message_handler', error: error.message })
+ }
+})
+
+client.on('call', async (call) => {
+ logger.info(`Call from ${ call.from }`)
+ await Call.create({
+ id : call.id,
+ from : call.from,
+ isVideo : call.isVideo,
+ timestamp: Math.floor(Date.now() / 1000),
+ rejected : config.rejectCalls
+ })
+
+ io.emit('call', { id: call.id, from: call.from, isVideo: call.isVideo })
+
+ if (config.rejectCalls) await call.reject()
+ await client.sendMessage(call.from, `[${ call.isVideo ? 'Video' : 'Audio' }] Call from ${ call.from } ${ config.rejectCalls ? '(Auto-rejected)' : '' }`)
+})
+
+// Graceful shutdown
+process.on('SIGINT', async () => {
+ logger.info('Shutting down...')
+ await client.destroy()
+ await sequelize.close()
+ server.close(() => process.exit(0))
+})
+
+// Start
+async function start() {
+ await ensureDirectories()
+ await sequelize.authenticate()
+ await sequelize.sync({ force: false })
+
+ server.listen(config.port, () => {
+ logger.info(`🚀 Server running on port ${ config.port }`)
+ logger.info(`🔑 API Key: ${ config.apiKey }`)
+ logger.info(`💾 Database: ${ config.dbPath }`)
+ logger.info(`📱 Initializing WhatsApp...`)
+ })
+
+ client.initialize()
+}
+
+if (require.main === module) {
+ start()
+}
+
+module.exports = { app, server, io, client, sequelize }```
+
+===== markmap.svg =====
+```
+```
+
+===== dev-docker-compose.yml =====
+```yaml
+services:
+ wapp:
+ #user: "1000:1000"
+ build:
+ #context: /opt/apps/wapp
+ context: .
+ dockerfile: Dockerfile
+ ports:
+ - "3001:3001"
+ container_name: wapp
+ restart: unless-stopped
+ networks:
+ - traefik_net
+ environment:
+ # Server configuration
+ - PORT=3001
+ - NODE_ENV=production
+
+ # API Security
+ - API_KEY=${API_KEY:-your-secure-api-key-here}
+
+ # Paths (pointing to volume mounts)
+ - DB_PATH=/app/data/whatsapp.db
+ - MEDIA_PATH=/app/media
+
+ # WhatsApp configuration
+ # Set HEADLESS=false to see the browser window (useful for debugging)
+ - HEADLESS=true
+ - REJECT_CALLS=false
+
+ # WebSocket URL for QR page (set this to your public URL when using Traefik - )
+ # Example: - WS_URL=https://wapp.appmodel.nl
+ - WS_URL=https://wapp.appmodel.nl
+
+ # CORS configuration
+ - CORS_ORIGIN=*
+
+ volumes:
+ # Persistent data volumes
+ - whatsapp-data:/app/data
+ - whatsapp-media:/app/media
+ - whatsapp-cache:/app/.wwebjs_cache
+ - whatsapp-auth:/app/.wwebjs_auth
+
+ # Uncomment labels below when ready to use Traefik
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.wapp.rule=Host(`wapp.appmodel.nl`)"
+ - "traefik.http.routers.wapp.entrypoints=websecure"
+ - "traefik.http.routers.wapp.tls=true"
+ - "traefik.http.routers.wapp.tls.certresolver=letsencrypt"
+ - "traefik.http.services.wapp.loadbalancer.server.port=3001"
+
+ healthcheck:
+ test: ["CMD", "node", "-e", "require('http').get('http://localhost:3001/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1); }).on('error', () => process.exit(1));"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 40s
+
+networks:
+ traefik_net:
+ external: true
+ name: traefik_net
+
+volumes:
+ whatsapp-data:
+ driver: local
+ whatsapp-media:
+ driver: local
+ whatsapp-cache:
+ driver: local
+ whatsapp-auth:
+ driver: local
+```
+
+===== Dockerfile =====
+```
+# ==================== BUILD STAGE ====================
+FROM node:20-alpine AS builder
+WORKDIR /app
+
+# Install chromium dependencies for whatsapp-web.js
+RUN apk add --no-cache \
+ chromium \
+ nss \
+ freetype \
+ harfbuzz \
+ ca-certificates \
+ ttf-freefont \
+ git
+
+# Copy package files
+COPY package*.json ./
+
+# Install dependencies
+RUN npm ci --production=false
+
+# Copy patches directory if it exists
+COPY patches ./patches
+
+# Run postinstall (patch-package)
+RUN npm run postinstall || true
+
+# Copy application files
+COPY server.js ./
+
+# ==================== RUNTIME STAGE ====================
+FROM node:20-alpine
+WORKDIR /app
+
+# Install chromium and runtime dependencies
+RUN apk add --no-cache \
+ chromium \
+ nss \
+ freetype \
+ harfbuzz \
+ ca-certificates \
+ ttf-freefont
+
+# Tell Puppeteer to use installed chromium
+ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
+ PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
+
+#RUN groupadd -r whatsapp && useradd -r -g whatsapp whatsapp
+#DEV Create non-root user
+RUN addgroup -S whatsapp || true && \
+ adduser -D -S -G whatsapp whatsapp || true
+
+# Copy dependencies from builder
+COPY --from=builder --chown=whatsapp:whatsapp /app/node_modules ./node_modules
+
+# Copy application files
+COPY --chown=whatsapp:whatsapp server.js ./
+COPY --chown=whatsapp:whatsapp package*.json ./
+
+# Create volume directories
+RUN mkdir -p /app/data /app/media /app/.wwebjs_cache /app/.wwebjs_auth && \
+ chown -R whatsapp:whatsapp /app
+
+USER whatsapp
+
+EXPOSE 3001
+
+# Health check
+HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
+ CMD node -e "require('http').get('http://localhost:3001/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1); }).on('error', () => process.exit(1));"
+
+CMD ["node", "server.js"]
+```
+
+===== .aiignore =====
+```
+node_modules
+.idea
+.git
+package-lock.json```
+
+===== patches/whatsapp-web.js+1.21.0.patch =====
+```
+diff --git a/node_modules/whatsapp-web.js/src/util/Injected.js b/node_modules/whatsapp-web.js/src/util/Injected.js
+index xxxxxxx..yyyyyyy 100644
+--- a/node_modules/whatsapp-web.js/src/util/Injected.js
++++ b/node_modules/whatsapp-web.js/src/util/Injected.js
+@@ -628,16 +628,28 @@ exports.ExposeStore = () => {
+ return Object.defineProperty(window, 'WWebJS', {
+ get() {
+ return {
+ getContactModel: (contact) => {
++ // Safe method extraction with fallbacks
++ const getContactMethod = (method) => {
++ if (!window.Store.ContactMethods || !window.Store.ContactMethods[method]) {
++ return null;
++ }
++ return window.Store.ContactMethods[method];
++ };
++
+ return {
+ id: contact.id,
+ name: contact.name,
+ type: contact.type,
+- isMe: contact.isMe,
+- isUser: contact.isUser,
+- isGroup: contact.isGroup,
+- isBroadcast: contact.isBroadcast,
+- isBusiness: contact.isBusiness,
+- isEnterprise: contact.isEnterprise,
++ isMe: contact.isMe || false,
++ isUser: contact.isUser || false,
++ isGroup: contact.isGroup || false,
++ isBroadcast: contact.isBroadcast || false,
++ isBusiness: contact.isBusiness || false,
++ isEnterprise: contact.isEnterprise || false,
++ isContactSyncEnabled: contact.isContactSyncEnabled || false,
++ isContactLocked: contact.isContactLocked || false,
+ verifiedLevel: contact.verifiedLevel,
+ verifiedName: contact.verifiedName,
+ pushname: contact.pushname,
+@@ -646,11 +658,23 @@ exports.ExposeStore = () => {
+ isMe: contact.isMe,
+ isUser: contact.isUser,
+ isWAContact: contact.isWAContact,
+- isMyContact: contact.isMyContact,
++ // Fallback for changed API
++ isMyContact: contact.isMyContact !== undefined
++ ? contact.isMyContact
++ : contact.type === 'in' || false,
+ isBlocked: contact.isBlocked,
+- labels: contact.labels ? contact.labels.map(l => l.id) : []
++ labels: contact.labels ? contact.labels.map(l => l.id) : [],
++ // Additional properties
++ displayName: contact.displayName || contact.pushname || contact.name,
++ formattedName: contact.formattedName || contact.name,
++ formattedShortName: contact.formattedShortName || contact.name,
++ shortName: contact.shortName || contact.name,
++ isSaved: contact.isSaved !== undefined ? contact.isSaved : false,
++ isMuted: contact.muteExpiration !== undefined ? contact.muteExpiration > 0 : false,
++ muteExpiration: contact.muteExpiration || 0,
++ disappearingModeDuration: contact.disappearingModeDuration || 0
+ };
+ },
+ });
+ }
+ });
+ }```
+
+===== DIRECTORY TREE =====
+```text
+```
diff --git a/package-lock.json b/package-lock.json
index 9b13d6f..666f74c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "whatsapp-web-server",
+ "name": "wapp",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "whatsapp-web-server",
+ "name": "wapp",
"version": "1.0.0",
"hasInstallScript": true,
"dependencies": {
@@ -16,7 +16,9 @@
"sequelize": "^6.32.1",
"socket.io": "^4.7.2",
"sqlite3": "^5.1.6",
- "whatsapp-web.js": "github:pedroslopez/whatsapp-web.js"
+ "swagger-ui-express": "^5.0.0",
+ "whatsapp-web.js": "github:pedroslopez/whatsapp-web.js",
+ "yamljs": "^0.3.0"
},
"devDependencies": {
"nodemon": "^3.0.1",
@@ -76,6 +78,12 @@
"integrity": "sha512-wtnBAETBVYZ9GvcbgdswRVSLkFkYAGv1KzwBBTeRXvGT9sb9cPllOgFFWXCn9PyARQ0H+Ijz6mmoRrGateUDxQ==",
"license": "MIT"
},
+ "node_modules/@scarf/scarf": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
+ "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
+ "hasInstallScript": true
+ },
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
@@ -348,6 +356,14 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
+ "node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -4023,6 +4039,11 @@
"node": ">= 10"
}
},
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
+ },
"node_modules/sqlite3": {
"version": "5.1.7",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz",
@@ -4126,6 +4147,28 @@
"node": ">=4"
}
},
+ "node_modules/swagger-ui-dist": {
+ "version": "5.31.0",
+ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz",
+ "integrity": "sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==",
+ "dependencies": {
+ "@scarf/scarf": "=1.4.0"
+ }
+ },
+ "node_modules/swagger-ui-express": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
+ "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
+ "dependencies": {
+ "swagger-ui-dist": ">=5.0.0"
+ },
+ "engines": {
+ "node": ">= v0.10.32"
+ },
+ "peerDependencies": {
+ "express": ">=4.0.0 || >=5.0.0-beta"
+ }
+ },
"node_modules/tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
@@ -4606,6 +4649,19 @@
"url": "https://github.com/sponsors/eemeli"
}
},
+ "node_modules/yamljs": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz",
+ "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "glob": "^7.0.5"
+ },
+ "bin": {
+ "json2yaml": "bin/json2yaml",
+ "yaml2json": "bin/yaml2json"
+ }
+ },
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
diff --git a/package.json b/package.json
index 0be3466..af2908f 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,9 @@
"sequelize": "^6.32.1",
"socket.io": "^4.7.2",
"sqlite3": "^5.1.6",
- "whatsapp-web.js": "github:pedroslopez/whatsapp-web.js"
+ "whatsapp-web.js": "github:pedroslopez/whatsapp-web.js",
+ "swagger-ui-express": "^5.0.0",
+ "yamljs": "^0.3.0"
},
"devDependencies": {
"nodemon": "^3.0.1",
diff --git a/patches/whatsapp-web.js+1.21.0.patch b/patches/whatsapp-web.js+1.21.0.patch
deleted file mode 100644
index 4b1ebfc..0000000
--- a/patches/whatsapp-web.js+1.21.0.patch
+++ /dev/null
@@ -1,65 +0,0 @@
-diff --git a/node_modules/whatsapp-web.js/src/util/Injected.js b/node_modules/whatsapp-web.js/src/util/Injected.js
-index xxxxxxx..yyyyyyy 100644
---- a/node_modules/whatsapp-web.js/src/util/Injected.js
-+++ b/node_modules/whatsapp-web.js/src/util/Injected.js
-@@ -628,16 +628,28 @@ exports.ExposeStore = () => {
- return Object.defineProperty(window, 'WWebJS', {
- get() {
- return {
- getContactModel: (contact) => {
-+ // Safe method extraction with fallbacks
-+ const getContactMethod = (method) => {
-+ if (!window.Store.ContactMethods || !window.Store.ContactMethods[method]) {
-+ return null;
-+ }
-+ return window.Store.ContactMethods[method];
-+ };
-+
- return {
- id: contact.id,
- name: contact.name,
- type: contact.type,
-- isMe: contact.isMe,
-- isUser: contact.isUser,
-- isGroup: contact.isGroup,
-- isBroadcast: contact.isBroadcast,
-- isBusiness: contact.isBusiness,
-- isEnterprise: contact.isEnterprise,
-+ isMe: contact.isMe || false,
-+ isUser: contact.isUser || false,
-+ isGroup: contact.isGroup || false,
-+ isBroadcast: contact.isBroadcast || false,
-+ isBusiness: contact.isBusiness || false,
-+ isEnterprise: contact.isEnterprise || false,
-+ isContactSyncEnabled: contact.isContactSyncEnabled || false,
-+ isContactLocked: contact.isContactLocked || false,
- verifiedLevel: contact.verifiedLevel,
- verifiedName: contact.verifiedName,
- pushname: contact.pushname,
-@@ -646,11 +658,23 @@ exports.ExposeStore = () => {
- isMe: contact.isMe,
- isUser: contact.isUser,
- isWAContact: contact.isWAContact,
-- isMyContact: contact.isMyContact,
-+ // Fallback for changed API
-+ isMyContact: contact.isMyContact !== undefined
-+ ? contact.isMyContact
-+ : contact.type === 'in' || false,
- isBlocked: contact.isBlocked,
-- labels: contact.labels ? contact.labels.map(l => l.id) : []
-+ labels: contact.labels ? contact.labels.map(l => l.id) : [],
-+ // Additional properties
-+ displayName: contact.displayName || contact.pushname || contact.name,
-+ formattedName: contact.formattedName || contact.name,
-+ formattedShortName: contact.formattedShortName || contact.name,
-+ shortName: contact.shortName || contact.name,
-+ isSaved: contact.isSaved !== undefined ? contact.isSaved : false,
-+ isMuted: contact.muteExpiration !== undefined ? contact.muteExpiration > 0 : false,
-+ muteExpiration: contact.muteExpiration || 0,
-+ disappearingModeDuration: contact.disappearingModeDuration || 0
- };
- },
- });
- }
- });
- }
\ No newline at end of file
diff --git a/patches/whatsapp-web.js+1.34.2.patch b/patches/whatsapp-web.js+1.34.2.patch
new file mode 100644
index 0000000..b9d83ed
--- /dev/null
+++ b/patches/whatsapp-web.js+1.34.2.patch
@@ -0,0 +1,263 @@
+diff --git a/node_modules/whatsapp-web.js/src/util/Injected/Store.js.bak b/node_modules/whatsapp-web.js/src/util/Injected/Store.js.bak
+new file mode 100644
+index 0000000..08fe745
+--- /dev/null
++++ b/node_modules/whatsapp-web.js/src/util/Injected/Store.js.bak
+@@ -0,0 +1,257 @@
++'use strict';
++
++exports.ExposeStore = () => {
++ /**
++ * Helper function that compares between two WWeb versions. Its purpose is to help the developer to choose the correct code implementation depending on the comparison value and the WWeb version.
++ * @param {string} lOperand The left operand for the WWeb version string to compare with
++ * @param {string} operator The comparison operator
++ * @param {string} rOperand The right operand for the WWeb version string to compare with
++ * @returns {boolean} Boolean value that indicates the result of the comparison
++ */
++ window.compareWwebVersions = (lOperand, operator, rOperand) => {
++ if (!['>', '>=', '<', '<=', '='].includes(operator)) {
++ throw new class _ extends Error {
++ constructor(m) { super(m); this.name = 'CompareWwebVersionsError'; }
++ }('Invalid comparison operator is provided');
++
++ }
++ if (typeof lOperand !== 'string' || typeof rOperand !== 'string') {
++ throw new class _ extends Error {
++ constructor(m) { super(m); this.name = 'CompareWwebVersionsError'; }
++ }('A non-string WWeb version type is provided');
++ }
++
++ lOperand = lOperand.replace(/-beta$/, '');
++ rOperand = rOperand.replace(/-beta$/, '');
++
++ while (lOperand.length !== rOperand.length) {
++ lOperand.length > rOperand.length
++ ? rOperand = rOperand.concat('0')
++ : lOperand = lOperand.concat('0');
++ }
++
++ lOperand = Number(lOperand.replace(/\./g, ''));
++ rOperand = Number(rOperand.replace(/\./g, ''));
++
++ return (
++ operator === '>' ? lOperand > rOperand :
++ operator === '>=' ? lOperand >= rOperand :
++ operator === '<' ? lOperand < rOperand :
++ operator === '<=' ? lOperand <= rOperand :
++ operator === '=' ? lOperand === rOperand :
++ false
++ );
++ };
++
++ window.Store = Object.assign({}, window.require('WAWebCollections'));
++ window.Store.AppState = window.require('WAWebSocketModel').Socket;
++ window.Store.BlockContact = window.require('WAWebBlockContactAction');
++ window.Store.Conn = window.require('WAWebConnModel').Conn;
++ window.Store.Cmd = window.require('WAWebCmd').Cmd;
++ window.Store.DownloadManager = window.require('WAWebDownloadManager').downloadManager;
++ window.Store.GroupQueryAndUpdate = window.require('WAWebGroupQueryJob').queryAndUpdateGroupMetadataById;
++ window.Store.MediaPrep = window.require('WAWebPrepRawMedia');
++ window.Store.MediaObject = window.require('WAWebMediaStorage');
++ window.Store.MediaTypes = window.require('WAWebMmsMediaTypes');
++ window.Store.MediaUpload = window.require('WAWebMediaMmsV4Upload');
++ window.Store.MsgKey = window.require('WAWebMsgKey');
++ window.Store.OpaqueData = window.require('WAWebMediaOpaqueData');
++ window.Store.QueryProduct = window.require('WAWebBizProductCatalogBridge');
++ window.Store.QueryOrder = window.require('WAWebBizOrderBridge');
++ window.Store.SendClear = window.require('WAWebChatClearBridge');
++ window.Store.SendDelete = window.require('WAWebDeleteChatAction');
++ window.Store.SendMessage = window.require('WAWebSendMsgChatAction');
++ window.Store.EditMessage = window.require('WAWebSendMessageEditAction');
++ window.Store.MediaDataUtils = window.require('WAWebMediaDataUtils');
++ window.Store.BlobCache = window.require('WAWebMediaInMemoryBlobCache');
++ window.Store.SendSeen = window.require('WAWebUpdateUnreadChatAction');
++ window.Store.User = window.require('WAWebUserPrefsMeUser');
++ window.Store.ContactMethods = {
++ ...window.require('WAWebContactGetters'),
++ ...window.require('WAWebFrontendContactGetters')
++ };
++ window.Store.UserConstructor = window.require('WAWebWid');
++ window.Store.Validators = window.require('WALinkify');
++ window.Store.WidFactory = window.require('WAWebWidFactory');
++ window.Store.ProfilePic = window.require('WAWebContactProfilePicThumbBridge');
++ window.Store.PresenceUtils = window.require('WAWebPresenceChatAction');
++ window.Store.ChatState = window.require('WAWebChatStateBridge');
++ window.Store.findCommonGroups = window.require('WAWebFindCommonGroupsContactAction').findCommonGroups;
++ window.Store.StatusUtils = window.require('WAWebContactStatusBridge');
++ window.Store.ConversationMsgs = window.require('WAWebChatLoadMessages');
++ window.Store.sendReactionToMsg = window.require('WAWebSendReactionMsgAction').sendReactionToMsg;
++ window.Store.createOrUpdateReactionsModule = window.require('WAWebDBCreateOrUpdateReactions');
++ window.Store.EphemeralFields = window.require('WAWebGetEphemeralFieldsMsgActionsUtils');
++ window.Store.MsgActionChecks = window.require('WAWebMsgActionCapability');
++ window.Store.QuotedMsg = window.require('WAWebQuotedMsgModelUtils');
++ window.Store.LinkPreview = window.require('WAWebLinkPreviewChatAction');
++ window.Store.Socket = window.require('WADeprecatedSendIq');
++ window.Store.SocketWap = window.require('WAWap');
++ window.Store.SearchContext = window.require('WAWebChatMessageSearch');
++ window.Store.DrawerManager = window.require('WAWebDrawerManager').DrawerManager;
++ window.Store.LidUtils = window.require('WAWebApiContact');
++ window.Store.WidToJid = window.require('WAWebWidToJid');
++ window.Store.JidToWid = window.require('WAWebJidToWid');
++ window.Store.getMsgInfo = window.require('WAWebApiMessageInfoStore').queryMsgInfo;
++ window.Store.QueryExist = window.require('WAWebQueryExistsJob').queryWidExists;
++ window.Store.ReplyUtils = window.require('WAWebMsgReply');
++ window.Store.BotSecret = window.require('WAWebBotMessageSecret');
++ window.Store.BotProfiles = window.require('WAWebBotProfileCollection');
++ window.Store.ContactCollection = window.require('WAWebContactCollection').ContactCollection;
++ window.Store.DeviceList = window.require('WAWebApiDeviceList');
++ window.Store.HistorySync = window.require('WAWebSendNonMessageDataRequest');
++ window.Store.AddonReactionTable = window.require('WAWebAddonReactionTableMode').reactionTableMode;
++ window.Store.AddonPollVoteTable = window.require('WAWebAddonPollVoteTableMode').pollVoteTableMode;
++ window.Store.ChatGetters = window.require('WAWebChatGetters');
++ window.Store.UploadUtils = window.require('WAWebUploadManager');
++ window.Store.WAWebStreamModel = window.require('WAWebStreamModel');
++ window.Store.FindOrCreateChat = window.require('WAWebFindChatAction');
++ window.Store.CustomerNoteUtils = window.require('WAWebNoteAction');
++ window.Store.BusinessGatingUtils = window.require('WAWebBizGatingUtils');
++ window.Store.PollsVotesSchema = window.require('WAWebPollsVotesSchema');
++ window.Store.PollsSendVote = window.require('WAWebPollsSendVoteMsgAction');
++
++ window.Store.Settings = {
++ ...window.require('WAWebUserPrefsGeneral'),
++ ...window.require('WAWebUserPrefsNotifications'),
++ setPushname: window.require('WAWebSetPushnameConnAction').setPushname
++ };
++ window.Store.NumberInfo = {
++ ...window.require('WAPhoneUtils'),
++ ...window.require('WAPhoneFindCC')
++ };
++ window.Store.ForwardUtils = {
++ ...window.require('WAWebChatForwardMessage')
++ };
++ window.Store.PinnedMsgUtils = {
++ ...window.require('WAWebPinInChatSchema'),
++ ...window.require('WAWebSendPinMessageAction')
++ };
++ window.Store.ScheduledEventMsgUtils = {
++ ...window.require('WAWebGenerateEventCallLink'),
++ ...window.require('WAWebSendEventEditMsgAction'),
++ ...window.require('WAWebSendEventResponseMsgAction')
++ };
++ window.Store.VCard = {
++ ...window.require('WAWebFrontendVcardUtils'),
++ ...window.require('WAWebVcardParsingUtils'),
++ ...window.require('WAWebVcardGetNameFromParsed')
++ };
++ window.Store.StickerTools = {
++ ...window.require('WAWebImageUtils'),
++ ...window.require('WAWebAddWebpMetadata')
++ };
++ window.Store.GroupUtils = {
++ ...window.require('WAWebGroupCreateJob'),
++ ...window.require('WAWebGroupModifyInfoJob'),
++ ...window.require('WAWebExitGroupAction'),
++ ...window.require('WAWebContactProfilePicThumbBridge'),
++ ...window.require('WAWebSetPropertyGroupAction')
++ };
++ window.Store.GroupParticipants = {
++ ...window.require('WAWebModifyParticipantsGroupAction'),
++ ...window.require('WASmaxGroupsAddParticipantsRPC')
++ };
++ window.Store.GroupInvite = {
++ ...window.require('WAWebGroupInviteJob'),
++ ...window.require('WAWebGroupQueryJob'),
++ ...window.require('WAWebMexFetchGroupInviteCodeJob')
++ };
++ window.Store.GroupInviteV4 = {
++ ...window.require('WAWebGroupInviteV4Job'),
++ ...window.require('WAWebChatSendMessages')
++ };
++ window.Store.MembershipRequestUtils = {
++ ...window.require('WAWebApiMembershipApprovalRequestStore'),
++ ...window.require('WASmaxGroupsMembershipRequestsActionRPC')
++ };
++ window.Store.ChannelUtils = {
++ ...window.require('WAWebLoadNewsletterPreviewChatAction'),
++ ...window.require('WAWebNewsletterMetadataQueryJob'),
++ ...window.require('WAWebNewsletterCreateQueryJob'),
++ ...window.require('WAWebEditNewsletterMetadataAction'),
++ ...window.require('WAWebNewsletterDeleteAction'),
++ ...window.require('WAWebNewsletterSubscribeAction'),
++ ...window.require('WAWebNewsletterUnsubscribeAction'),
++ ...window.require('WAWebNewsletterDirectorySearchAction'),
++ ...window.require('WAWebNewsletterToggleMuteStateJob'),
++ ...window.require('WAWebNewsletterGatingUtils'),
++ ...window.require('WAWebNewsletterModelUtils'),
++ ...window.require('WAWebMexAcceptNewsletterAdminInviteJob'),
++ ...window.require('WAWebMexRevokeNewsletterAdminInviteJob'),
++ ...window.require('WAWebChangeNewsletterOwnerAction'),
++ ...window.require('WAWebDemoteNewsletterAdminAction'),
++ ...window.require('WAWebNewsletterDemoteAdminJob'),
++ countryCodesIso: window.require('WAWebCountriesNativeCountryNames'),
++ currentRegion: window.require('WAWebL10N').getRegion(),
++ };
++ window.Store.SendChannelMessage = {
++ ...window.require('WAWebNewsletterUpdateMsgsRecordsJob'),
++ ...window.require('WAWebMsgDataFromModel'),
++ ...window.require('WAWebNewsletterSendMessageJob'),
++ ...window.require('WAWebNewsletterSendMsgAction'),
++ ...window.require('WAMediaCalculateFilehash')
++ };
++ window.Store.ChannelSubscribers = {
++ ...window.require('WAWebMexFetchNewsletterSubscribersJob'),
++ ...window.require('WAWebNewsletterSubscriberListAction')
++ };
++ window.Store.AddressbookContactUtils = {
++ ...window.require('WAWebSaveContactAction'),
++ ...window.require('WAWebDeleteContactAction')
++ };
++
++ if (!window.Store.Chat._find || !window.Store.Chat.findImpl) {
++ window.Store.Chat._find = e => {
++ const target = window.Store.Chat.get(e);
++ return target ? Promise.resolve(target) : Promise.resolve({
++ id: e
++ });
++ };
++ window.Store.Chat.findImpl = window.Store.Chat._find;
++ }
++
++ /**
++ * Target options object description
++ * @typedef {Object} TargetOptions
++ * @property {string|number} module The target module
++ * @property {string} function The function name to get from a module
++ */
++ /**
++ * Function to modify functions
++ * @param {TargetOptions} target Options specifying the target function to search for modifying
++ * @param {Function} callback Modified function
++ */
++ window.injectToFunction = (target, callback) => {
++ try {
++ let module = window.require(target.module);
++ if (!module) return;
++
++ const path = target.function.split('.');
++ const funcName = path.pop();
++
++ for (const key of path) {
++ if (!module[key]) return;
++ module = module[key];
++ }
++
++ const originalFunction = module[funcName];
++ if (typeof originalFunction !== 'function') return;
++
++ module[funcName] = (...args) => {
++ try {
++ return callback(originalFunction, ...args);
++ } catch {
++ return originalFunction(...args);
++ }
++ };
++
++ } catch {
++ return;
++ }
++ };
++
++ window.injectToFunction({ module: 'WAWebBackendJobsCommon', function: 'mediaTypeFromProtobuf' }, (func, ...args) => { const [proto] = args; return proto.locationMessage ? null : func(...args); });
++
++ window.injectToFunction({ module: 'WAWebE2EProtoUtils', function: 'typeAttributeFromProtobuf' }, (func, ...args) => { const [proto] = args; return proto.locationMessage || proto.groupInviteMessage ? 'text' : func(...args); });
++};
diff --git a/server.js b/server.js
index b7447d4..bc8aad6 100644
--- a/server.js
+++ b/server.js
@@ -1,4 +1,11 @@
require('dotenv').config()
+const swaggerUi = require('swagger-ui-express');
+const YAML = require('yamljs');
+const path = require('path');
+const swaggerPath = path.join(__dirname, 'swagger.yml');
+const swaggerDoc = YAML.load(swaggerPath);
+
+
const express = require('express')
const http = require('http')
const socketIo = require('socket.io')
@@ -105,6 +112,7 @@ const io = socketIo(server, {
})
app.use(express.json({ limit: '50mb' }))
+app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDoc));
// Middleware
function validateApiKey(req, res, next) {
diff --git a/swagger.yml b/swagger.yml
new file mode 100644
index 0000000..8a39e84
--- /dev/null
+++ b/swagger.yml
@@ -0,0 +1,35 @@
+openapi: 3.0.0
+info:
+ title: WAPP API
+ version: 1.0.0
+
+components:
+ securitySchemes:
+ ApiKeyAuth:
+ type: apiKey
+ in: header
+ name: x-api-key
+
+security:
+ - ApiKeyAuth: []
+
+paths:
+ /api/send:
+ post:
+ summary: Send a WhatsApp message
+ security:
+ - ApiKeyAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ phoneNumber:
+ type: string
+ message:
+ type: string
+ responses:
+ '200':
+ description: OK