const { Client, LocalAuth } = require('whatsapp-web.js'); const qrcode = require('qrcode'); const qrcodeTerminal = require('qrcode-terminal'); const fs = require('fs'); const path = require('path'); const config = require('./config'); const { sendSMS } = require('./sms'); const { sendTelegramPhoto } = require('./telegram'); let messageQueue = []; let flushTimer = null; let flushTimerStart = null; let client = null; let restartDelay = 1000; let starting = false; let restarting = false; let msgCounter = 0; let startTime = Date.now(); let totalForwarded = 0; let userStats = {}; let groupStats = {}; function ts() { return new Date().toLocaleString('he-IL', { hour12: false }); } function log(level, msg) { console.log(`[${ts()}] [${level}] ${msg}`); } function flushTime() { if (!flushTimerStart) return '--:--'; const t = new Date(Date.now() + Math.max(0, config.batch.intervalMs - (Date.now() - flushTimerStart))); return t.toLocaleTimeString('he-IL', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); } function formatBatch(queue) { const groups = {}; for (const m of queue) { if (!groups[m.group]) groups[m.group] = []; groups[m.group].push(m); } const parts = []; for (const [group, msgs] of Object.entries(groups)) { parts.push(`[đĨ ${group} đĨ]`); let prevSender = ''; for (const m of msgs) { if (m.showSender === false) { parts.push(''); parts.push(m.text); } else { if (prevSender && prevSender !== m.sender) parts.push(''); prevSender = m.sender; parts.push(`${m.sender}: ${m.text}`); } } } return parts.join('\n'); } function queueSize() { return messageQueue.reduce( (sum, m) => sum + m.group.length + m.sender.length + m.text.length + 6, 0 ); } function scheduleFlush() { if (flushTimer) clearTimeout(flushTimer); if (!flushTimerStart) flushTimerStart = Date.now(); flushTimer = setTimeout(flushQueue, config.batch.intervalMs); } async function flushQueue() { if (restarting) return; if (messageQueue.length === 0) return; flushTimer = null; flushTimerStart = null; const batch = messageQueue; messageQueue = []; msgCounter = 0; const text = formatBatch(batch); try { await sendSMS(text); log('INFO', `Flushed ${batch.length} messages`); for (const m of batch) { userStats[m.sender] = (userStats[m.sender] || 0) + 1; groupStats[m.group] = (groupStats[m.group] || 0) + 1; totalForwarded++; } } catch (err) { log('ERROR', `Flush failed: ${err.message}`); messageQueue = batch.concat(messageQueue); msgCounter = messageQueue.length; scheduleFlush(); } } function enqueue(group, sender, text, showSender = true) { msgCounter++; messageQueue.push({ group, sender, text, showSender }); scheduleFlush(); log('QUEUE', `Queue #${msgCounter} - Message from ${sender}, flushed at ${flushTime()}`); console.log(formatBatch(messageQueue)) if (queueSize() >= config.batch.maxChars) { clearTimeout(flushTimer); flushTimer = null; flushTimerStart = null; flushQueue(); } } async function killClient() { if (!client) return; try { client.removeAllListeners(); } catch {} try { await client.destroy(); } catch {} /** * IMPORTANT: * Give Chromium time to release Windows file locks */ await new Promise(r => setTimeout(r, 3000)); client = null; } let keepAliveTimer = null; function startKeepAlive() { if (keepAliveTimer) clearInterval(keepAliveTimer); const ka = config.keepAlive; if (!ka || !ka.url) return; log('INIT', `Keep-alive ping every ${ka.intervalMs / 1000}s to ${ka.url}`); const ping = () => { fetch(ka.url).catch(() => {}); }; ping(); keepAliveTimer = setInterval(ping, ka.intervalMs); } async function startClient() { if (starting) { log('WARN', 'Already starting â skipping duplicate call'); return; } starting = true; try { restarting = true; log('INIT', 'Starting OmegaBaSMS...'); await killClient(); log('INIT', `Groups to monitor: ${config.groupNames.join(', ')}`); log('INIT', `Batch interval: ${config.batch.intervalMs / 1000}s / max ${config.batch.maxChars} chars`); log('INIT', `Forwarding SMS to: ${config.smsGateway.recipientNumber}`); if (config.telegram.botToken) { log('INIT', 'Telegram notifications enabled'); } // startKeepAlive(); log('INIT', 'Launching WhatsApp Web...'); client = new Client({ authStrategy: new LocalAuth(), puppeteer: { headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'], protocolTimeout: 120000 // 2 minutes instead of default 30s } }); client.on('qr', async (qr) => { log('QR', 'New QR code received â scan with WhatsApp on your phone'); qrcodeTerminal.generate(qr, { small: true }); if (config.telegram.botToken) { try { const buf = await qrcode.toBuffer(qr, { width: 400 }); await sendTelegramPhoto(buf, 'WhatsApp re-auth needed - scan this QR\nhttps://wa.me/settings/linked_devices'); log('QR', 'QR photo sent to Telegram'); } catch (err) { log('ERROR', `Failed to send QR photo: ${err.message}`); } } }); client.on('ready', () => { restarting = false; restartDelay = 1000; log('READY', 'WhatsApp connected successfully'); log('READY', `Monitoring ${config.groupNames.length} group(s): ${config.groupNames.join(', ')}`); log('READY', `Forwarding to ${config.smsGateway.recipientNumber}`); }); client.on('auth_failure', (msg) => { log('ERROR', `Auth failure: ${msg}`); }); client.on('disconnected', async (reason) => { if (starting) return; log('WARN', `Disconnected: ${reason}. Restarting in ${restartDelay / 1000}s...`); restarting = true; if (reason === 'LOGOUT') { const authDir = path.join(__dirname, '.wwebjs_auth'); if (fs.existsSync(authDir)) { try { fs.rmSync(authDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 500 }); log('WARN', 'Cleared old session data'); } catch (err) { log('ERROR', `Failed clearing auth data: ${err.message}`); } } } await new Promise((r) => setTimeout(r, restartDelay)); restartDelay = Math.min(restartDelay * 2, 30000); startClient(); }); client.on('message_create', async (message) => { try { if (restarting) return; const allowed = ['chat', 'image', 'video', 'ptt', 'audio', 'document', 'sticker']; if (!allowed.includes(message.type)) return; const chat = await message.getChat(); if (!chat.isGroup) return; if (!config.groupNames.includes(chat.name)) return; if (message.fromMe && !config.includeOwnMessages) return; const contact = await message.getContact(); const sender = message.fromMe ? config.ownName : (contact.name || contact.pushname || contact.shortName || contact.number || 'Unknown'); const typeLabel = { image: 'đˇ Image', video: 'đĨ Video', ptt: 'đ¤ Voice', audio: 'đĩ Audio', document: 'đ Document', sticker: 'đŧī¸ Sticker' }[message.type]; let body = message.body || ''; let isReply = false; if (message.hasQuotedMsg) { try { const quoted = await message.getQuotedMessage(); if (quoted) { const qContact = await quoted.getContact(); const qName = qContact.name || qContact.pushname || qContact.shortName || 'Unknown'; let qText = quoted.body || ({ image: 'đˇ Image', video: 'đĨ Video', ptt: 'đ¤ Voice', audio: 'đĩ Audio', document: 'đ Document', sticker: 'đŧī¸ Sticker' })[quoted.type] || '[media]'; if (qText.length > 50) qText = qText.slice(0, 50) + '...'; body = `âŠī¸ ${qName} ××ר: ${qText} âŠī¸\n${sender}: ${body}`; isReply = true; } } catch (_) {} } if (message.isForwarded) { let fwdFrom = ''; if (message.author) { try { const fwdContact = await client.getContactById(message.author); fwdFrom = fwdContact.name || fwdContact.pushname || fwdContact.shortName || ''; } catch (_) {} } body = `⊠×××ĸ×ר${fwdFrom ? ' ×' + fwdFrom : ''}âŠ\n` + body; } if (message.hasMedia && config.appsScriptUrl && message.type !== 'sticker') { try { const media = await message.downloadMedia(); if (media) { const { uploadMedia } = require('./uploader'); const ext = media.mimeType ? media.mimeType.split('/')[1] || '' : ''; const link = await uploadMedia(media.data, media.mimeType, ext, media.filename); if (link) body = (body ? `${body}\n[${typeLabel}]\n[${link}]` : `[${typeLabel}]\n[${link}]`); } } catch (err) { log('ERROR', `Media upload: ${err.message}`); } } if (!body && typeLabel) body = typeLabel; if (!body) return; if (message.mentionedIds && message.mentionedIds.length > 0) { for (const id of message.mentionedIds) { const num = id.split('@')[0]; if (!body.includes(`@${num}`)) continue; try { const c = await client.getContactById(id); const name = c.name || c.pushname || c.shortName || num; body = body.replaceAll(`@${num}`, `@${name}`); } catch (_) {} } } enqueue(chat.name, sender, body, !isReply && !message.isForwarded); } catch (err) { log('ERROR', `Message handler: ${err.message}`); } }); client.initialize(); } finally { starting = false; } } const http = require('http'); function renderDashboard(clientState) { const uptime = Math.floor((Date.now() - startTime) / 1000); const h = Math.floor(uptime / 3600); const m = Math.floor((uptime % 3600) / 60); const s = uptime % 60; const uptimeStr = `${h}h ${m}m ${s}s`; const userRows = Object.entries(userStats) .sort((a, b) => b[1] - a[1]) .map(([name, count]) => `
WhatsApp group messages forwarded via SMS
| Name | Messages |
|---|
| Group | Messages |
|---|