const baileys = require('@whiskeysockets/baileys'); const makeWASocket = baileys.default || baileys.makeWASocket || baileys; const { useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion } = baileys; const pino = require('pino'); const axios = require('axios'); const fs = require('fs'); const path = require('path'); const sessions = new Map(); // Store active sockets in memory const retryCounters = new Map(); // Track reconnection attempts per session const MAX_RETRIES = 5; // Maximum reconnection attempts before giving up // Local folder for saving auth keys const SESSIONS_DIR = path.join(__dirname, 'sessions'); if (!fs.existsSync(SESSIONS_DIR)) { fs.mkdirSync(SESSIONS_DIR, { recursive: true }); } async function sendWebhook(webhook_url, payload) { try { console.log(`[Webhook] Sending to ${webhook_url} | state=${payload.state}`); const response = await axios.post(webhook_url, payload, { headers: { 'Content-Type': 'application/json', 'X-Webhook-Secret': process.env.WEBHOOK_SECRET || '' }, timeout: 10000 }); console.log(`[Webhook] ✅ Success | HTTP ${response.status}`); } catch (err) { if (err.response) { console.error(`[Webhook] ❌ HTTP ${err.response.status} | ${JSON.stringify(err.response.data)}`); } else { console.error(`[Webhook] ❌ Network Error: ${err.message}`); } } } async function startSession(session_key, webhook_url) { // Return existing socket if it's already active if (sessions.has(session_key)) { console.log(`[Session] ${session_key} already active, reusing`); return sessions.get(session_key); } console.log(`[Session] Starting ${session_key} → webhook: ${webhook_url}`); const sessionFolder = path.join(SESSIONS_DIR, session_key); const { state, saveCreds } = await useMultiFileAuthState(sessionFolder); // Fetch the latest WhatsApp Web version to avoid 405 rejection let version; try { const versionInfo = await fetchLatestBaileysVersion(); version = versionInfo.version; console.log(`[Baileys] Using WA version: ${version}`); } catch (e) { console.warn(`[Baileys] Could not fetch version, using default`); } const socketConfig = { auth: state, printQRInTerminal: false, logger: pino({ level: 'silent' }), browser: ['Nabeh Gateway', 'Chrome', '120.0.0'] }; if (version) socketConfig.version = version; const sock = makeWASocket(socketConfig); console.log(`[Session] Socket created for ${session_key}`); sessions.set(session_key, sock); sock.ev.on('creds.update', saveCreds); // Listen for incoming messages sock.ev.on('messages.upsert', async (m) => { console.log(`[Upsert] Event received type=${m.type} messagesCount=${m.messages?.length}`); if (m.messages && m.messages.length > 0) { console.log(`[Upsert] First message keys:`, Object.keys(m.messages[0])); console.log(`[Upsert] First message key:`, JSON.stringify(m.messages[0].key)); console.log(`[Upsert] First message structure:`, JSON.stringify(m.messages[0].message)); } if (m.type !== 'notify') return; for (const msg of m.messages) { // Ignore messages sent by ourselves if (msg.key.fromMe) continue; const remoteJid = msg.key.remoteJid; if (!remoteJid) continue; // Only process individual chats (ignore groups and broadcasts) const isGroup = remoteJid.endsWith('@g.us'); const isBroadcast = remoteJid.endsWith('@broadcast'); if (isGroup || isBroadcast) continue; // Extract text body const body = msg.message?.conversation || msg.message?.extendedTextMessage?.text || msg.message?.imageMessage?.caption || msg.message?.videoMessage?.caption || ''; // For now, only process messages that have text content if (!body) continue; // Extract sender phone number (handle LID privacy scheme) let senderPhone = ''; if (msg.key.senderPn && msg.key.senderPn.endsWith('@s.whatsapp.net')) { senderPhone = msg.key.senderPn.split('@')[0]; } else if (remoteJid.endsWith('@s.whatsapp.net')) { senderPhone = remoteJid.split('@')[0]; } else if (remoteJid.endsWith('@lid')) { senderPhone = remoteJid.split('@')[0]; } if (!senderPhone) continue; const senderName = msg.pushName || ''; console.log(`[Message] Received from ${senderPhone} (JID: ${remoteJid}): ${body}`); await sendWebhook(webhook_url, { session_key, state: 'message_received', message: { id: msg.key.id, phone: senderPhone, name: senderName, body: body, timestamp: msg.messageTimestamp } }); } }); sock.ev.on('connection.update', async (update) => { const { connection, lastDisconnect, qr } = update; if (qr) { console.log(`[QR] Generated for ${session_key}`); // Reset retry counter on QR generation (session is alive) retryCounters.set(session_key, 0); await sendWebhook(webhook_url, { session_key, state: 'waiting_qr', qr_code: qr }); } if (connection === 'close') { const statusCode = lastDisconnect?.error?.output?.statusCode; const shouldReconnect = statusCode !== DisconnectReason.loggedOut; const retries = (retryCounters.get(session_key) || 0) + 1; retryCounters.set(session_key, retries); console.log(`[Connection] ${session_key} closed | code=${statusCode} | retry=${retries}/${MAX_RETRIES} | shouldReconnect=${shouldReconnect}`); if (shouldReconnect && retries <= MAX_RETRIES) { // Try reconnecting with exponential backoff sessions.delete(session_key); const delay = Math.min(retries * 3000, 15000); // 3s, 6s, 9s, 12s, 15s console.log(`[Connection] Reconnecting ${session_key} in ${delay}ms...`); setTimeout(() => startSession(session_key, webhook_url), delay); } else { // Either logged out, banned, or max retries exceeded console.log(`[Connection] ${session_key} permanently closed. Cleaning up.`); await cleanupSession(session_key); await sendWebhook(webhook_url, { session_key, state: 'disconnected' }); } } else if (connection === 'open') { console.log(`[Connection] ✅ ${session_key} connected successfully!`); retryCounters.set(session_key, 0); // Reset on successful connection // Parse phone number from the JID (e.g. 9665XXXXXXX@s.whatsapp.net) const phone = sock.user.id.split(':')[0]; await sendWebhook(webhook_url, { session_key, state: 'connected', phone: phone }); } }); return sock; } /** * Cleanup session: remove from memory and delete auth files */ async function cleanupSession(session_key) { const sock = sessions.get(session_key); if (sock) { try { sock.end(); } catch (e) { } // Gracefully close socket without logout sessions.delete(session_key); } retryCounters.delete(session_key); // Wipe the auth directory so a fresh session can be created next time const sessionFolder = path.join(SESSIONS_DIR, session_key); if (fs.existsSync(sessionFolder)) { fs.rmSync(sessionFolder, { recursive: true, force: true }); console.log(`[Cleanup] Deleted session folder for ${session_key}`); } } /** * Disconnect session: logout from WhatsApp and cleanup */ async function disconnectSession(session_key) { const sock = sessions.get(session_key); if (sock) { try { sock.logout(); } catch (e) { } // best effort logout sessions.delete(session_key); } retryCounters.delete(session_key); // Completely wipe the auth directory const sessionFolder = path.join(SESSIONS_DIR, session_key); if (fs.existsSync(sessionFolder)) { fs.rmSync(sessionFolder, { recursive: true, force: true }); console.log(`[Disconnect] Deleted session folder for ${session_key}`); } } module.exports = { startSession, disconnectSession };