diff --git a/.env.example b/.env.example
index 8e0258b..82580e9 100644
--- a/.env.example
+++ b/.env.example
@@ -1,6 +1,6 @@
# Server Configuration
PORT=3001
-API_KEY=your-strong-api-key-here
+WAPP_API_KEY=your-strong-api-key-here
# Database & Storage
DB_PATH=./data/whatsapp.db
diff --git a/dev-docker-compose.yml b/dev-docker-compose.yml
index 5aab15d..93a01b5 100644
--- a/dev-docker-compose.yml
+++ b/dev-docker-compose.yml
@@ -17,7 +17,7 @@ services:
- NODE_ENV=production
# API Security
- - API_KEY=${API_KEY:-your-secure-api-key-here}
+ - WAPP_API_KEY=${WAPP_API_KEY:-your-secure-api-key-here}
# Paths (pointing to volume mounts)
- DB_PATH=/app/data/whatsapp.db
diff --git a/docker-compose.yml b/docker-compose.yml
index 00b43bd..d083e13 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -17,7 +17,7 @@ services:
- NODE_ENV=production
# API Security
- - API_KEY=${API_KEY:-your-secure-api-key-here}
+ - WAPP_API_KEY=${WAPP_API_KEY:-your-secure-api-key-here}
# Paths (pointing to volume mounts)
- DB_PATH=/app/data/whatsapp.db
diff --git a/output.md b/output.md
deleted file mode 100644
index 9a0430f..0000000
--- a/output.md
+++ /dev/null
@@ -1,1173 +0,0 @@
-===== 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/server.js b/server.js
index 3a5d5a5..fd22ee0 100644
--- a/server.js
+++ b/server.js
@@ -21,7 +21,7 @@ const config = {
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'
+ apiKey : process.env.WAPP_API_KEY || 'your-secure-api-key-here'
}
// Ensure directories exist