require('dotenv').config(); const express = require('express'); const { Client, LocalAuth } = require('whatsapp-web.js'); const qrcode = require('qrcode-terminal'); const http = require('http'); const { Server } = require('socket.io'); const path = require('path'); const app = express(); const server = http.createServer(app); const io = new Server(server, { path: '/proxy/whatsapp/socket.io', cors: { origin: "*" } }); const PORT = process.env.PORT || 5001; // Feature Flags const ENABLE_INCOMING_MESSAGES = process.env.ENABLE_INCOMING_MESSAGES === 'true'; const ENABLE_AUTO_REPLY = process.env.ENABLE_AUTO_REPLY === 'true'; app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); // Initialize WhatsApp Web Client with LocalAuth to persist session const client = new Client({ authStrategy: new LocalAuth({ dataPath: './.wwebjs_auth' }), puppeteer: { headless: true, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--disable-software-rasterizer', '--disable-extensions', '--no-zygote', '--single-process' ], protocolTimeout: 120000, timeout: 120000 }, webVersionCache: { type: 'none' } }); let isReady = false; let currentQr = null; client.on('qr', (qr) => { currentQr = qr; console.log('\n[WhatsApp Bot] QR code generated. Scan via Dashboard or Terminal.'); qrcode.generate(qr, { small: true }); io.emit('qr', qr); }); client.on('ready', () => { console.log('\n[WhatsApp Bot] Client is ready and authenticated!'); isReady = true; currentQr = null; io.emit('ready', { ready: true }); }); client.on('auth_failure', (msg) => { console.error('[WhatsApp Bot] Authentication failure:', msg); io.emit('error', 'Authentication failure: ' + msg); }); client.on('disconnected', (reason) => { console.log('[WhatsApp Bot] Client was logged out:', reason); isReady = false; currentQr = null; io.emit('disconnected', reason); }); // Incoming messages handler client.on('message', async msg => { if (ENABLE_INCOMING_MESSAGES) { console.log(`[WhatsApp Bot] Received message from ${msg.from}: ${msg.body}`); io.emit('incoming_message', { from: msg.from, body: msg.body }); } if (ENABLE_AUTO_REPLY && !msg.fromMe) { if (msg.body.toLowerCase() === '!ping') { msg.reply('pong'); } } }); // Real-time connections io.on('connection', (socket) => { socket.emit('status', { ready: isReady, authenticated: client.info ? true : false, qr: currentQr }); }); // ─── REST API ───────────────────────────────────────────────────────────── app.get('/api/status', (req, res) => { res.json({ service: 'whatsapp-gateway', ready: isReady, authenticated: client.info ? true : false }); }); // ─── App Tracking ─────────────────────────────────────────────────────────── const connectedApps = {}; function trackApp(req) { const appName = req.body.appName || req.headers['x-app-name'] || 'Unknown App'; const ip = req.ip || req.connection.remoteAddress; if (!connectedApps[appName]) { connectedApps[appName] = { name: appName, firstSeen: new Date().toISOString(), lastSeen: new Date().toISOString(), messageCount: 0, lastIp: ip }; } connectedApps[appName].lastSeen = new Date().toISOString(); connectedApps[appName].messageCount += 1; connectedApps[appName].lastIp = ip; } app.get('/api/apps', (req, res) => { res.json({ success: true, apps: Object.values(connectedApps) }); }); const handleSendMessage = async (req, res) => { trackApp(req); // Track the app sending the request if (!isReady) { return res.status(503).json({ success: false, error: 'WhatsApp Web client is not ready. Please scan the QR code in the dashboard.' }); } const { to, message } = req.body; if (!to || !message) { return res.status(400).json({ success: false, error: "Missing 'to' or 'message' fields." }); } try { let targetJid = to; if (to.includes('chat.whatsapp.com') || /^[A-Za-z0-9]{20,24}$/.test(to)) { let inviteCode = to; if (to.includes('chat.whatsapp.com/')) { inviteCode = to.split('chat.whatsapp.com/')[1].split('?')[0].trim(); } try { targetJid = await client.acceptInvite(inviteCode); } catch (err) { // If the bot is already in the group or invite code is invalid, it will fail here. // It might already be joined. } } if (!targetJid.includes('@') && !targetJid.includes('-')) { let cleanPhone = targetJid.replace('whatsapp:', '').replace(/[+\s()-]/g, '').trim(); targetJid = `${cleanPhone}@c.us`; } else if (!targetJid.includes('@')) { targetJid = `${targetJid}@g.us`; } const response = await client.sendMessage(targetJid, message); res.json({ success: true, messageId: response.id._serialized, recipient: targetJid }); } catch (error) { res.status(500).json({ success: false, error: error.message || error }); } }; app.post('/api/send-message', handleSendMessage); // Legacy endpoint app.post('/send-message', handleSendMessage); app.get('/api/groups', async (req, res) => { if (!isReady) return res.status(503).json({ error: 'Not ready' }); try { const chats = await client.getChats(); const groups = chats.filter(c => c.isGroup); const groupData = groups.map(g => ({ id: g.id._serialized, name: g.name, participants: g.participants ? g.participants.length : 0 })); res.json({ success: true, groups: groupData }); } catch (err) { res.status(500).json({ error: err.message }); } }); app.post('/api/groups/invite', async (req, res) => { if (!isReady) return res.status(503).json({ error: 'Not ready' }); const { groupId } = req.body; try { const code = await client.groupGetInviteCode(groupId); res.json({ success: true, inviteLink: `https://chat.whatsapp.com/${code}` }); } catch (err) { res.status(500).json({ error: err.message }); } }); app.post('/api/groups/create', async (req, res) => { if (!isReady) return res.status(503).json({ error: 'Not ready' }); const { title, initialParticipants = [] } = req.body; // Needs standard WhatsApp JIDs try { const result = await client.createGroup(title, initialParticipants); res.json({ success: true, groupId: result.gid._serialized }); } catch (err) { res.status(500).json({ error: err.message }); } }); server.listen(PORT, '0.0.0.0', () => { console.log(`[WhatsApp Gateway] Server running at http://0.0.0.0:${PORT}`); }); console.log('[WhatsApp Bot] Initializing client...'); client.initialize();