===== 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 ```