294 lines
7.5 KiB
JavaScript
294 lines
7.5 KiB
JavaScript
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;
|
|
|
|
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}]`);
|
|
for (const m of msgs) {
|
|
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`);
|
|
} catch (err) {
|
|
log('ERROR', `Flush failed: ${err.message}`);
|
|
messageQueue = batch.concat(messageQueue);
|
|
msgCounter = messageQueue.length;
|
|
scheduleFlush();
|
|
}
|
|
}
|
|
|
|
function enqueue(group, sender, text) {
|
|
msgCounter++;
|
|
messageQueue.push({ group, sender, text });
|
|
|
|
scheduleFlush();
|
|
log('QUEUE', `Queue #${msgCounter} - Message from ${sender}, flushed at ${flushTime()}`);
|
|
|
|
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']
|
|
}
|
|
});
|
|
|
|
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;
|
|
if (message.type !== 'chat') 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 body = message.body || (message.hasMedia ? '[Media]' : '');
|
|
if (!body) return;
|
|
|
|
enqueue(chat.name, sender, body);
|
|
} catch (err) {
|
|
log('ERROR', `Message handler: ${err.message}`);
|
|
}
|
|
});
|
|
|
|
client.initialize();
|
|
|
|
} finally {
|
|
starting = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 🔥 FIX: detect detached frame / WhatsApp Web crashes
|
|
*/
|
|
function shouldRestart(err) {
|
|
const msg = (err && err.message) || String(err);
|
|
return msg.includes('detached Frame') ||
|
|
msg.includes('Execution context was destroyed') ||
|
|
msg.includes('Target closed') ||
|
|
msg.includes('Session closed') ||
|
|
msg.includes('Navigation failed') ||
|
|
msg.includes('Protocol error');
|
|
}
|
|
|
|
process.on('uncaughtException', (err) => {
|
|
if (shouldRestart(err)) {
|
|
log('WARN', `Recoverable: ${err.message}. Restarting...`);
|
|
startClient();
|
|
return;
|
|
}
|
|
|
|
log('FATAL', err.message);
|
|
process.exit(1);
|
|
});
|
|
|
|
process.on('unhandledRejection', (err) => {
|
|
if (shouldRestart(err)) {
|
|
log('WARN', `Recoverable: ${err.message}. Restarting...`);
|
|
startClient();
|
|
return;
|
|
}
|
|
|
|
log('FATAL', `Unhandled rejection: ${err.message}`);
|
|
process.exit(1);
|
|
});
|
|
|
|
startClient(); |