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 function removeSingletonLock() { try { const authPath = require('path').join(__dirname, '.wwebjs_auth'); require('child_process').execSync(`find ${authPath} -name "SingletonLock" -delete`); require('child_process').execSync(`find ${authPath} -name "SingletonCookie" -delete`); require('child_process').execSync(`find ${authPath} -name "SingletonSocket" -delete`); console.log('[WhatsApp Bot] Cleaned up Chromium locks recursively.'); } catch (err) { console.error('[WhatsApp Bot] Error removing Chromium locks:', err); } } removeSingletonLock(); 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); }); const AGENTS_API_URL = process.env.AGENTS_API_URL || 'http://agents-api.ai-agents.svc.cluster.local:8000/webhook'; // Incoming messages handler client.on('message_create', async msg => { if (!msg.fromMe && 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'); } } // Forward @bot messages to agents-runtime orchestrator const text = msg.body.trim(); if (text.toLowerCase().startsWith('@bot')) { console.log(`[WhatsApp Bot] Bot command detected: ${text}`); // Target Jid is where the bot should reply. const targetJid = msg.fromMe ? msg.to : msg.from; try { const data = JSON.stringify({ text: text, sender: msg.author || msg.from, targetJid: targetJid }); const url = new URL(AGENTS_API_URL); const req = require('http').request({ hostname: url.hostname, port: url.port || 80, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': data.length } }, (res) => { let body = ''; res.on('data', chunk => body += chunk); res.on('end', () => console.log(`[WhatsApp Bot] Forwarded to agents-runtime. Status: ${res.statusCode}, Body: ${body}`)); }); req.on('error', (err) => console.error('[WhatsApp Bot] Failed to forward message:', err)); req.write(data); req.end(); } catch (err) { console.error('[WhatsApp Bot] Failed to execute http.request:', err); } } }); // 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, qr: currentQr }); }); 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); // Mark the chat as unread so it shows up as a notification/badge on the user's phone // We add a 3-second delay because if we do it immediately, the phone might override it! setTimeout(async () => { try { const chat = await client.getChatById(targetJid); await chat.markUnread(); console.log(`[WhatsApp Bot] Marked chat ${targetJid} as unread after delay.`); } catch (err) { console.error('[WhatsApp Bot] Failed to mark chat as unread:', err); } }, 3000); 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, '.wwebjs_auth', 'settings.json'); const GROUPS_FILE = path.join(__dirname, '.wwebjs_auth', '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();