617 lines
21 KiB
JavaScript
617 lines
21 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;
|
|
|
|
let startTime = Date.now();
|
|
let totalForwarded = 0;
|
|
let smsReplies = 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 prodBatch = batch.filter((m) => !m.isTest);
|
|
const testBatch = batch.filter((m) => m.isTest);
|
|
|
|
async function doFlush(subset, recipient) {
|
|
if (subset.length === 0) return;
|
|
const text = formatBatch(subset);
|
|
try {
|
|
await sendSMS(text, recipient);
|
|
log('INFO', `Flushed ${subset.length} messages to ${recipient}`);
|
|
for (const m of subset) {
|
|
userStats[m.sender] = (userStats[m.sender] || 0) + 1;
|
|
groupStats[m.group] = (groupStats[m.group] || 0) + 1;
|
|
totalForwarded++;
|
|
}
|
|
} catch (err) {
|
|
log('ERROR', `Flush failed for ${recipient}: ${err.message}`);
|
|
messageQueue = subset.concat(messageQueue);
|
|
msgCounter = messageQueue.length;
|
|
}
|
|
}
|
|
|
|
await doFlush(prodBatch, config.smsGateway.recipientNumber);
|
|
const testRecipient = config.test.smsRecipient || config.smsGateway.recipientNumber;
|
|
await doFlush(testBatch, testRecipient);
|
|
|
|
if (messageQueue.length > 0) scheduleFlush();
|
|
}
|
|
|
|
function enqueue(group, sender, text, showSender = true, isTest = false) {
|
|
msgCounter++;
|
|
messageQueue.push({ group, sender, text, showSender, isTest });
|
|
|
|
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();
|
|
|
|
const allGroups = [...new Set([...config.groupNames, ...config.test.groupNames])];
|
|
log('INIT', `Groups to monitor: ${allGroups.join(', ')}`);
|
|
if (config.test.groupNames.length) log('INIT', `Test groups: ${config.test.groupNames.join(', ')} → SMS to ${config.test.smsRecipient || config.smsGateway.recipientNumber}`);
|
|
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;
|
|
const allGroups = [...new Set([...config.groupNames, ...config.test.groupNames])];
|
|
log('READY', 'WhatsApp connected successfully');
|
|
log('READY', `Monitoring ${allGroups.length} group(s): ${allGroups.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;
|
|
const isTest = config.test.groupNames.length && config.test.groupNames.includes(chat.name);
|
|
if (!config.groupNames.includes(chat.name) && !isTest) 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, isTest);
|
|
} 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]) =>
|
|
`<tr><td>${name}</td><td>${count}</td></tr>`
|
|
).join('');
|
|
|
|
const groupRows = Object.entries(groupStats)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.map(([name, count]) =>
|
|
`<tr><td>${name}</td><td>${count}</td></tr>`
|
|
).join('');
|
|
|
|
const connected = clientState === 'CONNECTED';
|
|
|
|
const api = JSON.stringify({
|
|
uptime, uptimeStr, connected,
|
|
totalForwarded, smsReplies, queued: messageQueue.length, flushTime: flushTime(),
|
|
userStats, groupStats,
|
|
});
|
|
|
|
return `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>OmegaBaSMS</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: #0f172a; color: #e2e8f0; min-height: 100vh; padding: 2rem;
|
|
}
|
|
h1 {
|
|
font-size: 1.75rem; font-weight: 700; margin-bottom: 0.25rem;
|
|
background: linear-gradient(135deg, #22d3ee, #3b82f6);
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
|
}
|
|
.subtitle { color: #64748b; margin-bottom: 2rem; font-size: 0.9rem; }
|
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
|
.card {
|
|
background: #1e293b; border-radius: 12px; padding: 1.25rem; border: 1px solid #334155;
|
|
}
|
|
.card .label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; margin-bottom: 0.5rem; }
|
|
.card .value { font-size: 1.75rem; font-weight: 700; }
|
|
.card .value.green { color: #22c55e; }
|
|
.card .value.blue { color: #3b82f6; }
|
|
.card .value.yellow { color: #eab308; }
|
|
.card .value.red { color: #ef4444; }
|
|
.status-dot {
|
|
display: inline-block; width: 10px; height: 10px; border-radius: 50%;
|
|
margin-right: 0.5rem;
|
|
}
|
|
.status-dot.on { background: #22c55e; box-shadow: 0 0 8px #22c55e88; }
|
|
.status-dot.off { background: #ef4444; box-shadow: 0 0 8px #ef444488; }
|
|
.tables { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; }
|
|
@media (max-width: 640px) { .tables { grid-template-columns: 1fr; } }
|
|
h2 { font-size: 1rem; font-weight: 600; margin-bottom: 0.75rem; color: #94a3b8; }
|
|
table { width: 100%; border-collapse: collapse; }
|
|
th { text-align: left; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; padding: 0.5rem 0.75rem; border-bottom: 1px solid #334155; }
|
|
td { padding: 0.5rem 0.75rem; border-bottom: 1px solid #1e293b; font-size: 0.9rem; }
|
|
tr:last-child td { border-bottom: none; }
|
|
td:last-child { text-align: right; font-weight: 600; }
|
|
.bar-bg { background: #334155; border-radius: 4px; height: 6px; overflow: hidden; margin-top: 4px; }
|
|
.bar-fill { height: 100%; border-radius: 4px; background: linear-gradient(90deg, #3b82f6, #22d3ee); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<h1>OmegaBaSMS</h1>
|
|
<p class="subtitle">WhatsApp group messages forwarded via SMS</p>
|
|
<div class="grid">
|
|
<div class="card"><div class="label">Status</div><div class="value" id="status">—</div></div>
|
|
<div class="card"><div class="label">Uptime</div><div class="value blue" id="uptime">—</div></div>
|
|
<div class="card"><div class="label">Forwarded</div><div class="value green" id="forwarded">—</div></div>
|
|
<div class="card"><div class="label">SMS Replies</div><div class="value blue" id="smsReplies">—</div></div>
|
|
<div class="card"><div class="label">Queued</div><div class="value yellow" id="queued">—</div><div style="font-size:0.75rem;color:#64748b;margin-top:0.25rem" id="flushTime"></div></div>
|
|
</div>
|
|
<div class="tables">
|
|
<div><h2>By Sender</h2><table><thead><tr><th>Name</th><th>Messages</th></tr></thead><tbody id="users"></tbody></table></div>
|
|
<div><h2>By Group</h2><table><thead><tr><th>Group</th><th>Messages</th></tr></thead><tbody id="groups"></tbody></table></div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
async function poll() {
|
|
try {
|
|
const r = await fetch('/api/stats');
|
|
const d = await r.json();
|
|
document.getElementById('status').innerHTML = '<span class="status-dot ' + (d.connected ? 'on' : 'off') + '"></span>' + (d.connected ? 'Connected' : 'Disconnected');
|
|
document.getElementById('uptime').textContent = d.uptimeStr;
|
|
document.getElementById('forwarded').textContent = d.totalForwarded;
|
|
document.getElementById('smsReplies').textContent = d.smsReplies;
|
|
document.getElementById('queued').textContent = d.queued;
|
|
document.getElementById('flushTime').textContent = d.queued > 0 ? 'flushed at ' + d.flushTime : '';
|
|
document.getElementById('users').innerHTML = Object.entries(d.userStats).sort((a,b) => b[1]-a[1]).map(([n,c]) => '<tr><td>' + n + '</td><td>' + c + '</td></tr>').join('') || '<tr><td colspan="2" style="color:#64748b;">No messages yet</td></tr>';
|
|
document.getElementById('groups').innerHTML = Object.entries(d.groupStats).sort((a,b) => b[1]-a[1]).map(([n,c]) => '<tr><td>' + n + '</td><td>' + c + '</td></tr>').join('') || '<tr><td colspan="2" style="color:#64748b;">No messages yet</td></tr>';
|
|
} catch(e) {}
|
|
}
|
|
setInterval(poll, 3000);
|
|
poll();
|
|
</script>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
async function getClientState() {
|
|
if (!client || restarting) return 'DISCONNECTED';
|
|
try {
|
|
return await client.getState();
|
|
} catch {
|
|
return 'DISCONNECTED';
|
|
}
|
|
}
|
|
|
|
const server = http.createServer(async (req, res) => {
|
|
if (req.url === '/liveness') {
|
|
const state = await getClientState();
|
|
if (state === 'CONNECTED') {
|
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
res.end('OK');
|
|
} else {
|
|
res.writeHead(503, { 'Content-Type': 'text/plain' });
|
|
res.end('NOT_CONNECTED');
|
|
}
|
|
return;
|
|
}
|
|
if (req.url === '/api/stats') {
|
|
const clientState = await getClientState();
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
uptime: Math.floor((Date.now() - startTime) / 1000),
|
|
uptimeStr: (() => {
|
|
const t = Math.floor((Date.now() - startTime) / 1000);
|
|
return Math.floor(t/3600)+'h '+Math.floor((t%3600)/60)+'m '+t%60+'s';
|
|
})(),
|
|
connected: clientState === 'CONNECTED',
|
|
clientState,
|
|
totalForwarded,
|
|
smsReplies,
|
|
queued: messageQueue.length,
|
|
flushTime: flushTime(),
|
|
userStats,
|
|
groupStats,
|
|
}));
|
|
return;
|
|
}
|
|
if (req.url.startsWith('/api/webhook/sms') && req.method === 'POST') {
|
|
const urlObj = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
const token = urlObj.searchParams.get('token');
|
|
if (config.smsWebhookToken && token !== config.smsWebhookToken) {
|
|
res.writeHead(403);
|
|
res.end('Forbidden');
|
|
return;
|
|
}
|
|
|
|
let raw = '';
|
|
req.on('data', (chunk) => (raw += chunk));
|
|
await new Promise((r) => req.on('end', r));
|
|
|
|
let data;
|
|
try {
|
|
data = JSON.parse(raw);
|
|
} catch {
|
|
res.writeHead(400);
|
|
res.end('Invalid JSON');
|
|
return;
|
|
}
|
|
|
|
const from = data.from || 'Unknown';
|
|
const msg = (data.message || '').trim();
|
|
if (!msg) {
|
|
res.writeHead(400);
|
|
res.end('Empty message');
|
|
return;
|
|
}
|
|
|
|
const colonIdx = msg.indexOf(':');
|
|
if (colonIdx === -1) {
|
|
res.writeHead(400);
|
|
res.end('Format: <group name>: <message>');
|
|
return;
|
|
}
|
|
|
|
const groupName = msg.slice(0, colonIdx).trim();
|
|
const replyText = msg.slice(colonIdx + 1).trim();
|
|
if (!replyText) {
|
|
res.writeHead(400);
|
|
res.end('Empty reply');
|
|
return;
|
|
}
|
|
|
|
const isTest = config.test.smsFrom && from === config.test.smsFrom;
|
|
const candidates = isTest && config.test.groupNames.length ? config.test.groupNames : config.groupNames;
|
|
const matchedGroup = candidates.find((g) => g.toLowerCase() === groupName.toLowerCase());
|
|
if (!matchedGroup) {
|
|
res.writeHead(404);
|
|
res.end(`Group "${groupName}" not found`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const chats = await client.getChats();
|
|
const chat = chats.find((c) => c.isGroup && c.name === matchedGroup);
|
|
if (!chat) {
|
|
res.writeHead(404);
|
|
res.end(`Chat "${matchedGroup}" not found on WhatsApp`);
|
|
return;
|
|
}
|
|
await chat.sendMessage(`[${from}]\n ${replyText}`);
|
|
smsReplies++;
|
|
log('SMS', `Reply sent to "${matchedGroup}" from ${from}: ${replyText}`);
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ success: true, group: matchedGroup }));
|
|
} catch (err) {
|
|
log('ERROR', `SMS reply failed: ${err.message}`);
|
|
res.writeHead(500);
|
|
res.end(`Failed: ${err.message}`);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const clientState = await getClientState();
|
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
res.end(renderDashboard(clientState));
|
|
});
|
|
const PORT = process.env.PORT || 3000;
|
|
server.listen(PORT, () => log('INIT', `Dashboard on http://0.0.0.0:${PORT}`));
|
|
|
|
/** Recoverable error detection */
|
|
function shouldRestart(err) {
|
|
const msg = (err && err.message) || String(err);
|
|
return msg.includes('detached') ||
|
|
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(); |