OmegaBaSMS/index.js
2026-05-28 09:38:42 +03:00

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();