388 lines
14 KiB
JavaScript
388 lines
14 KiB
JavaScript
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.agents-runtime.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;
|
|
|
|
// Forward the message to the new LangChain/Gemini orchestrator
|
|
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...');
|
|
|
|
// Watchdog Timer to prevent infinite Chromium loops (126% CPU bug)
|
|
const WATCHDOG_TIMEOUT_MS = 90000; // 90 seconds
|
|
let watchdog = setTimeout(() => {
|
|
console.error(`[WhatsApp Bot] 🚨 Watchdog triggered! Client failed to reach 'qr' or 'ready' state within ${WATCHDOG_TIMEOUT_MS / 1000} seconds. Exiting to allow Kubernetes restart.`);
|
|
process.exit(1);
|
|
}, WATCHDOG_TIMEOUT_MS);
|
|
|
|
// Clear the watchdog if it successfully generates a QR code or becomes ready
|
|
client.on('qr', () => { clearTimeout(watchdog); });
|
|
client.on('ready', () => { clearTimeout(watchdog); });
|
|
|
|
client.initialize();
|