From 92f145e6b30e0707e09bc474b0ae0a3c553f5e7e Mon Sep 17 00:00:00 2001 From: mike Date: Fri, 12 Dec 2025 09:12:03 +0100 Subject: [PATCH] add file --- .aiignore | 4 + Dockerfile | 2 + dev-docker-compose.yml | 2 +- docker-compose.yml | 5 +- fix_path.sh | 86 ++ output.md | 1173 ++++++++++++++++++++++++++ package-lock.json | 62 +- package.json | 4 +- patches/whatsapp-web.js+1.21.0.patch | 65 -- patches/whatsapp-web.js+1.34.2.patch | 263 ++++++ server.js | 8 + swagger.yml | 35 + 12 files changed, 1638 insertions(+), 71 deletions(-) create mode 100644 .aiignore create mode 100755 fix_path.sh create mode 100644 output.md delete mode 100644 patches/whatsapp-web.js+1.21.0.patch create mode 100644 patches/whatsapp-web.js+1.34.2.patch create mode 100644 swagger.yml 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 ===== +``` +
Features
npm install
Installation
WhatsApp Web.js Server
``` + +===== 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