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.post('/api/whatsapp/logout', async (req, res) => { try { console.log('[WhatsApp Bot] Logging out...'); if (client.info) { await client.logout(); } res.json({ success: true, message: 'Logged out successfully. Pod restarting...' }); // Wait briefly to allow the response to be sent, then exit so Kubernetes restarts the pod cleanly setTimeout(() => process.exit(0), 1000); } catch (err) { console.error('Logout error:', err); res.status(500).json({ success: false, error: err.message }); // Still exit to force a clean slate setTimeout(() => process.exit(1), 1000); } }); // ─── 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 { message } = req.body; let to = req.body.to; if (!to || !message) { return res.status(400).json({ success: false, error: "Missing 'to' or 'message' fields." }); } if (to === 'default') { const settings = loadSettings(); if (!settings.defaultDestination) { return res.status(400).json({ success: false, error: 'No default destination configured. Please configure it in the dashboard.' }); } to = settings.defaultDestination; } 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', (req, res) => { if (!isReady) return res.status(503).json({ error: 'Not ready' }); res.json({ success: true, groups: loadConfiguredGroups() }); }); app.post('/api/groups/add-by-link', async (req, res) => { if (!isReady) return res.status(503).json({ error: 'Not ready' }); const { link } = req.body; if (!link) return res.status(400).json({ error: 'Link is required' }); try { let inviteCode = link; if (link.includes('chat.whatsapp.com/')) { inviteCode = link.split('chat.whatsapp.com/')[1].split('?')[0].trim(); } let groupId, groupName; try { // Attempt to accept invite (this will fail if already in the group) groupId = await client.acceptInvite(inviteCode); // If acceptInvite returns the ID, we try to get info to get the name const info = await client.getInviteInfo(inviteCode); groupName = info.subject || 'Unknown Group'; } catch (e) { // If it failed, it might be because we're already in it. // Let's just fetch the info! const info = await client.getInviteInfo(inviteCode); groupId = info.id._serialized || info.id; groupName = info.subject || 'Unknown Group'; } const newGroup = { id: groupId, name: groupName, participants: 'Unknown' // We don't fetch participant count from invite info to save time }; const groups = loadConfiguredGroups(); if (!groups.find(g => g.id === groupId)) { groups.push(newGroup); saveConfiguredGroups(groups); } res.json({ success: true, group: newGroup }); } catch (err) { console.error('[Add Group Error]:', err); res.status(500).json({ error: err.message || 'Failed to process invite link' }); } }); // ─── Settings ───────────────────────────────────────────────────────────── const fs = require('fs'); const SETTINGS_FILE = path.join(__dirname, 'settings.json'); const GROUPS_FILE = path.join(__dirname, 'configured_groups.json'); function loadSettings() { if (fs.existsSync(SETTINGS_FILE)) { return JSON.parse(fs.readFileSync(SETTINGS_FILE)); } return {}; } function saveSettings(settings) { fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2)); } function loadConfiguredGroups() { if (fs.existsSync(GROUPS_FILE)) { return JSON.parse(fs.readFileSync(GROUPS_FILE)); } return []; } function saveConfiguredGroups(groups) { fs.writeFileSync(GROUPS_FILE, JSON.stringify(groups, null, 2)); } app.get('/api/settings', (req, res) => { res.json({ success: true, settings: loadSettings() }); }); app.post('/api/settings/default-destination', (req, res) => { const { destinationId } = req.body; if (!destinationId) return res.status(400).json({ success: false, error: 'destinationId is required' }); const settings = loadSettings(); settings.defaultDestination = destinationId; saveSettings(settings); res.json({ success: true, message: 'Default destination updated.' }); }); 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();