Cheia de refolosire a embeddings-urilor era doar textul chunk-ului. La schimbarea
lui EMBED_MODEL indexul ar fi ramas un amestec de vectori din doua modele, iar
cautarea ar fi dat rezultate aiurea fara nici un mesaj de eroare. Acum fiecare
intrare poarta modelul cu care a fost calculata si se refolosesc doar cele cu
modelul curent; intrarile vechi, fara camp, se recalculeaza o singura data.
Indexul de pe container a fost stampilat manual cu `nomic-embed-text` (singurul
folosit pana acum), deci nu s-au recalculat cele 169 de chunk-uri.
`GET /groups` listeaza grupurile contului cu JID, nume si numar de participanti.
JID-ul unui grup ("120363...@g.us") nu se vede nicaieri in WhatsApp, iar el e
singurul mod de a scrie SUPPORT_JID pentru un grup de suport.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
468 lines
18 KiB
JavaScript
468 lines
18 KiB
JavaScript
// Punte WhatsApp (Baileys) pentru Maria — implicit doar self-chat (vezi
|
|
// TEST_MODE_SELF_CHAT_ONLY). Sesiunea WhatsApp (auth/) NU intra in git —
|
|
// contine chei de sesiune, e in ~/.maria-bridge/whatsapp-auth/ (vezi config.py).
|
|
//
|
|
// Asocierea se poate face in doua feluri, ambele expuse in dashboard:
|
|
// - cod QR (GET /status -> qr, sau pagina GET /qr)
|
|
// - cod de 8 caractere legat de numar (POST /pair {phone}), pentru cand
|
|
// telefonul e in mana si camera nu poate scana ecranul serverului.
|
|
// Imaginile primite (capturi cu erori) se descarca in ~/.maria-bridge/media/ si
|
|
// se pun in coada cu `media`; consumer-ul le trece prin OCR inainte de RAG.
|
|
|
|
const express = require('express');
|
|
const pino = require('pino');
|
|
const QRCode = require('qrcode');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
let makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion,
|
|
downloadMediaMessage, Browsers;
|
|
|
|
const PORT = parseInt(process.env.BRIDGE_PORT || '8099', 10);
|
|
const HOST = process.env.BRIDGE_HOST || '127.0.0.1';
|
|
const AUTH_DIR = process.env.MARIA_BRIDGE_DIR
|
|
? path.join(process.env.MARIA_BRIDGE_DIR, 'whatsapp-auth')
|
|
: path.join(__dirname, 'auth');
|
|
const MEDIA_DIR = process.env.MARIA_BRIDGE_DIR
|
|
? path.join(process.env.MARIA_BRIDGE_DIR, 'media')
|
|
: path.join(__dirname, 'media');
|
|
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
const TEST_MODE_SELF_CHAT_ONLY = (process.env.TEST_MODE_SELF_CHAT_ONLY || 'true') !== 'false';
|
|
// WhatsApp invalideaza codul de asociere in cateva minute. Il tinem afisat doar
|
|
// atat: un cod expirat afisat in dashboard trimite omul sa tasteze degeaba.
|
|
const PAIRING_CODE_TTL_MS = 3 * 60 * 1000;
|
|
const MAX_MEDIA_MB = parseFloat(process.env.MAX_MEDIA_MB || '8');
|
|
// Fisierele descarcate sunt temporare: consumer-ul le sterge dupa OCR. Curatarea
|
|
// de la pornire prinde ce a ramas dupa un restart in mijlocul procesarii.
|
|
const MEDIA_TTL_MS = 24 * 60 * 60 * 1000;
|
|
|
|
const logger = pino({ level: 'warn' });
|
|
|
|
let sock = null;
|
|
let connected = false;
|
|
let phoneNumber = null;
|
|
let ownJid = null;
|
|
let ownLid = null; // acelasi cont, adresat in formatul nou LID
|
|
let currentQR = null;
|
|
let pairing = null; // { code, phone, at } — vezi PAIRING_CODE_TTL_MS
|
|
let reconnectAttempts = 0;
|
|
let messageQueue = [];
|
|
// Acelasi mesaj din self-chat soseste si pe `<numar>@s.whatsapp.net` si pe
|
|
// `<lid>@lid` — doua livrari, acelasi `key.id`. Fara filtrul asta, consumer-ul
|
|
// raspundea de doua ori la fiecare intrebare.
|
|
let seenIds = [];
|
|
const SEEN_IDS_MAX = 400;
|
|
let shuttingDown = false;
|
|
|
|
function alreadySeen(id) {
|
|
if (!id) return false;
|
|
if (seenIds.includes(id)) return true;
|
|
seenIds.push(id);
|
|
if (seenIds.length > SEEN_IDS_MAX) seenIds = seenIds.slice(-SEEN_IDS_MAX);
|
|
return false;
|
|
}
|
|
|
|
function activePairing() {
|
|
if (!pairing) return null;
|
|
if (Date.now() - pairing.at > PAIRING_CODE_TTL_MS) return null;
|
|
return pairing;
|
|
}
|
|
|
|
function pruneMediaDir() {
|
|
try {
|
|
fs.mkdirSync(MEDIA_DIR, { recursive: true });
|
|
const now = Date.now();
|
|
for (const name of fs.readdirSync(MEDIA_DIR)) {
|
|
const p = path.join(MEDIA_DIR, name);
|
|
try {
|
|
if (now - fs.statSync(p).mtimeMs > MEDIA_TTL_MS) fs.unlinkSync(p);
|
|
} catch (_) { /* fisier disparut intre listare si stat */ }
|
|
}
|
|
} catch (err) {
|
|
console.error('[whatsapp] Nu pot curata directorul media:', err.message);
|
|
}
|
|
}
|
|
|
|
// Mesajele reale sosesc uneori impachetate (mesaje efemere, "vezi o data").
|
|
// Fara despachetare, o captura de ecran trimisa cu dispariție automata pare
|
|
// un mesaj fara continut si se arunca tacut.
|
|
function unwrap(message) {
|
|
let m = message;
|
|
for (let i = 0; i < 5 && m; i++) {
|
|
const inner = m.ephemeralMessage?.message
|
|
|| m.viewOnceMessage?.message
|
|
|| m.viewOnceMessageV2?.message
|
|
|| m.viewOnceMessageV2Extension?.message
|
|
|| m.documentWithCaptionMessage?.message;
|
|
if (!inner) break;
|
|
m = inner;
|
|
}
|
|
return m || {};
|
|
}
|
|
|
|
function extForMime(mimetype) {
|
|
const map = {
|
|
'image/jpeg': '.jpg', 'image/jpg': '.jpg', 'image/png': '.png',
|
|
'image/webp': '.webp', 'image/gif': '.gif', 'image/bmp': '.bmp',
|
|
};
|
|
return map[(mimetype || '').split(';')[0].toLowerCase()] || '.bin';
|
|
}
|
|
|
|
/** Nodul de imagine al unui mesaj, fie el `imageMessage` sau un document cu mimetype de imagine. */
|
|
function imageNode(message) {
|
|
const m = unwrap(message);
|
|
if (m.imageMessage) return { node: m.imageMessage, kind: 'image' };
|
|
const doc = m.documentMessage;
|
|
if (doc && (doc.mimetype || '').startsWith('image/')) return { node: doc, kind: 'document' };
|
|
return null;
|
|
}
|
|
|
|
async function saveImage(msg, node) {
|
|
const bytes = Number(node.fileLength || 0);
|
|
if (bytes > MAX_MEDIA_MB * 1024 * 1024) {
|
|
throw new Error(`imagine prea mare (${(bytes / 1048576).toFixed(1)} MB > ${MAX_MEDIA_MB} MB)`);
|
|
}
|
|
const buffer = await downloadMediaMessage(
|
|
msg, 'buffer', {},
|
|
{ logger, reuploadRequest: sock.updateMediaMessage },
|
|
);
|
|
fs.mkdirSync(MEDIA_DIR, { recursive: true });
|
|
const safeId = String(msg.key.id || Date.now()).replace(/[^A-Za-z0-9_-]/g, '');
|
|
const file = path.join(MEDIA_DIR, `${Date.now()}_${safeId}${extForMime(node.mimetype)}`);
|
|
fs.writeFileSync(file, buffer, { mode: 0o600 });
|
|
return { path: file, mimetype: node.mimetype || null, bytes: buffer.length };
|
|
}
|
|
|
|
async function startConnection() {
|
|
if (!makeWASocket) {
|
|
const baileys = await import('@whiskeysockets/baileys');
|
|
makeWASocket = baileys.default;
|
|
useMultiFileAuthState = baileys.useMultiFileAuthState;
|
|
DisconnectReason = baileys.DisconnectReason;
|
|
fetchLatestBaileysVersion = baileys.fetchLatestBaileysVersion;
|
|
downloadMediaMessage = baileys.downloadMediaMessage;
|
|
Browsers = baileys.Browsers;
|
|
}
|
|
|
|
const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR);
|
|
const { version } = await fetchLatestBaileysVersion();
|
|
|
|
sock = makeWASocket({
|
|
version,
|
|
auth: state,
|
|
logger,
|
|
printQRInTerminal: false,
|
|
// Asocierea prin cod de 8 caractere e refuzata de WhatsApp daca descriptorul
|
|
// de browser nu e unul cunoscut; `Browsers.ubuntu('Chrome')` e cel folosit de
|
|
// exemplele Baileys. Nu afecteaza asocierea prin QR.
|
|
browser: Browsers.ubuntu('Chrome'),
|
|
defaultQueryTimeoutMs: 60000,
|
|
});
|
|
|
|
sock.ev.on('creds.update', saveCreds);
|
|
|
|
sock.ev.on('connection.update', async (update) => {
|
|
const { connection, lastDisconnect, qr } = update;
|
|
|
|
if (qr) {
|
|
try {
|
|
currentQR = await QRCode.toDataURL(qr);
|
|
console.log('[whatsapp] QR code generated — scan with WhatsApp to link');
|
|
} catch (err) {
|
|
console.error('[whatsapp] Failed to generate QR code:', err.message);
|
|
}
|
|
}
|
|
|
|
if (connection === 'open') {
|
|
connected = true;
|
|
currentQR = null;
|
|
pairing = null;
|
|
reconnectAttempts = 0;
|
|
phoneNumber = sock.user?.id?.split(':')[0] || sock.user?.id?.split('@')[0] || null;
|
|
ownJid = phoneNumber ? `${phoneNumber}@s.whatsapp.net` : null;
|
|
// WhatsApp adreseaza conturile si prin LID (Linked ID), nu doar prin numar. Pe
|
|
// conturile noi, self-chat-ul soseste ca `<lid>@lid`, deci comparatia doar cu
|
|
// `<numar>@s.whatsapp.net` arunca toate mesajele. Sufixul de dispozitiv (":13")
|
|
// apare in credentiale dar nu si in remoteJid-ul mesajelor — il taiem.
|
|
const rawLid = sock.user?.lid || sock.authState?.creds?.me?.lid || null;
|
|
ownLid = rawLid ? `${rawLid.split(':')[0].split('@')[0]}@lid` : null;
|
|
console.log(`[whatsapp] Connected as ${phoneNumber} (lid: ${ownLid || 'n/a'}, self-chat-only mode: ${TEST_MODE_SELF_CHAT_ONLY})`);
|
|
}
|
|
|
|
if (connection === 'close') {
|
|
connected = false;
|
|
phoneNumber = null;
|
|
ownLid = null;
|
|
const statusCode = lastDisconnect?.error?.output?.statusCode;
|
|
const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
|
|
|
|
console.log(`[whatsapp] Disconnected (status: ${statusCode})`);
|
|
|
|
if (shouldReconnect && !shuttingDown) {
|
|
// Dupa introducerea codului de asociere WhatsApp cere explicit un restart
|
|
// (515). Nu e o eroare si nu trebuie sa consume din bugetul de reincercari,
|
|
// altfel o asociere reusita poate opri puntea la a cincea incercare.
|
|
const isRestartRequired = statusCode === DisconnectReason.restartRequired;
|
|
if (isRestartRequired) {
|
|
console.log('[whatsapp] Restart cerut de server (asociere finalizata?) — reconectare imediata');
|
|
setTimeout(startConnection, 500);
|
|
} else if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
reconnectAttempts++;
|
|
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
|
|
console.log(`[whatsapp] Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
|
|
setTimeout(startConnection, delay);
|
|
} else {
|
|
console.error(`[whatsapp] Max reconnect attempts reached (${MAX_RECONNECT_ATTEMPTS})`);
|
|
}
|
|
} else if (statusCode === DisconnectReason.loggedOut) {
|
|
console.log('[whatsapp] Logged out — delete auth dir and restart to re-link');
|
|
}
|
|
}
|
|
});
|
|
|
|
sock.ev.on('messages.upsert', async ({ messages, type }) => {
|
|
if (type !== 'notify') return;
|
|
|
|
for (const msg of messages) {
|
|
if (msg.key.remoteJid === 'status@broadcast') continue;
|
|
const isGroup = msg.key.remoteJid.endsWith('@g.us');
|
|
const isSelfChat = (ownJid && msg.key.remoteJid === ownJid)
|
|
|| (ownLid && msg.key.remoteJid === ownLid);
|
|
// Mesajele aruncate se logheaza. Fara asta, un JID care nu se potriveste cu
|
|
// ownJid (ex. formatul @lid) face bridge-ul sa taca fara nicio urma in log.
|
|
if (TEST_MODE_SELF_CHAT_ONLY && !isSelfChat) {
|
|
console.log(`[whatsapp] ignorat (nu e self-chat): remoteJid=${msg.key.remoteJid} ownJid=${ownJid} ownLid=${ownLid}`);
|
|
continue;
|
|
}
|
|
if (msg.key.fromMe && !isGroup && !isSelfChat) {
|
|
console.log(`[whatsapp] ignorat (fromMe, nu e self-chat): remoteJid=${msg.key.remoteJid}`);
|
|
continue;
|
|
}
|
|
|
|
if (alreadySeen(msg.key.id)) {
|
|
console.log(`[whatsapp] ignorat (duplicat ${msg.key.id}): remoteJid=${msg.key.remoteJid}`);
|
|
continue;
|
|
}
|
|
|
|
const inner = unwrap(msg.message);
|
|
const img = imageNode(msg.message);
|
|
let text = inner.conversation || inner.extendedTextMessage?.text || '';
|
|
let media = null;
|
|
|
|
if (img) {
|
|
// Legenda imaginii e adesea tot ce spune omul („nu merge asta").
|
|
text = img.node.caption || text || '';
|
|
try {
|
|
media = await saveImage(msg, img.node);
|
|
console.log(`[whatsapp] Imagine primita de la ${msg.key.remoteJid}: ${media.path} (${media.bytes} octeti, ${media.mimetype})`);
|
|
} catch (err) {
|
|
console.error(`[whatsapp] Descarcarea imaginii a esuat (${msg.key.remoteJid}): ${err.message}`);
|
|
media = { error: err.message };
|
|
}
|
|
}
|
|
|
|
if (!text && !media) {
|
|
console.log(`[whatsapp] ignorat (fara text): remoteJid=${msg.key.remoteJid} tipuri=${Object.keys(inner || {}).join(',')}`);
|
|
continue;
|
|
}
|
|
|
|
messageQueue.push({
|
|
from: msg.key.remoteJid,
|
|
participant: msg.key.participant || null,
|
|
pushName: msg.pushName || null,
|
|
text,
|
|
media,
|
|
timestamp: msg.messageTimestamp,
|
|
id: msg.key.id,
|
|
isGroup,
|
|
fromMe: msg.key.fromMe || false,
|
|
});
|
|
|
|
console.log(`[whatsapp] Message from ${msg.pushName || 'unknown'} in ${msg.key.remoteJid}: ${media ? '[imagine] ' : ''}${text.substring(0, 80)}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
const app = express();
|
|
app.use(express.json({ limit: '50mb' }));
|
|
|
|
app.get('/status', (_req, res) => {
|
|
const p = activePairing();
|
|
res.json({
|
|
connected,
|
|
phone: phoneNumber,
|
|
selfChatOnly: TEST_MODE_SELF_CHAT_ONLY,
|
|
lid: ownLid,
|
|
qr: connected ? null : currentQR,
|
|
pairing: connected || !p ? null : {
|
|
code: p.code,
|
|
phone: p.phone,
|
|
expires_in: Math.max(0, Math.round((PAIRING_CODE_TTL_MS - (Date.now() - p.at)) / 1000)),
|
|
},
|
|
});
|
|
});
|
|
|
|
app.post('/pair', async (req, res) => {
|
|
if (connected) {
|
|
return res.status(409).json({ ok: false, error: 'deja conectat' });
|
|
}
|
|
const { phone } = req.body || {};
|
|
const digits = String(phone || '').replace(/\D/g, '');
|
|
// Numarul trebuie dat in format international, fara `+` si fara `00`: 40723197939.
|
|
if (digits.length < 8 || digits.length > 15) {
|
|
return res.status(400).json({ ok: false, error: 'numar invalid — dă-l international, fara "+" (ex: 40723197939)' });
|
|
}
|
|
if (!sock) {
|
|
return res.status(503).json({ ok: false, error: 'puntea inca porneste, reincearca in cateva secunde' });
|
|
}
|
|
if (sock.authState?.creds?.registered) {
|
|
return res.status(409).json({ ok: false, error: 'sesiunea e deja inregistrata — sterge whatsapp-auth/ si reporneste puntea' });
|
|
}
|
|
try {
|
|
const code = await sock.requestPairingCode(digits);
|
|
pairing = { code, phone: digits, at: Date.now() };
|
|
console.log(`[whatsapp] Cod de asociere pentru ${digits}: ${code}`);
|
|
res.json({ ok: true, code, phone: digits, expires_in: PAIRING_CODE_TTL_MS / 1000 });
|
|
} catch (err) {
|
|
console.error('[whatsapp] Cererea codului de asociere a esuat:', err.message);
|
|
res.status(500).json({ ok: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
app.get('/pair-code', (_req, res) => {
|
|
if (connected) return res.json({ error: 'already connected' });
|
|
const p = activePairing();
|
|
if (!p) return res.json({ error: 'niciun cod valabil — cere unul cu POST /pair' });
|
|
res.json({ code: p.code, phone: p.phone });
|
|
});
|
|
|
|
app.get('/qr', (_req, res) => {
|
|
if (connected) {
|
|
return res.json({ error: 'already connected' });
|
|
}
|
|
if (!currentQR) {
|
|
return res.json({ error: 'no QR code available yet' });
|
|
}
|
|
const html = `<!DOCTYPE html>
|
|
<html><head><title>WhatsApp QR</title>
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<style>body{display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#111;flex-direction:column;font-family:sans-serif;color:#fff}
|
|
img{width:400px;height:400px;border-radius:12px}p{margin-top:16px;opacity:.6}</style></head>
|
|
<body><img src="${currentQR}" alt="QR Code"/><p>Scan with WhatsApp → Linked Devices</p></body></html>`;
|
|
res.type('html').send(html);
|
|
});
|
|
|
|
app.post('/send', async (req, res) => {
|
|
const { to, text } = req.body || {};
|
|
|
|
if (!to || !text) {
|
|
return res.status(400).json({ ok: false, error: 'missing "to" or "text" in body' });
|
|
}
|
|
if (!connected || !sock) {
|
|
return res.status(503).json({ ok: false, error: 'not connected to WhatsApp' });
|
|
}
|
|
|
|
try {
|
|
const result = await sock.sendMessage(to, { text });
|
|
res.json({ ok: true, id: result.key.id });
|
|
} catch (err) {
|
|
console.error('[whatsapp] Send failed:', err.message);
|
|
res.status(500).json({ ok: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
// Retrimiterea unei capturi primite catre altcineva (escaladare la suport).
|
|
// Accepta doar cai din MEDIA_DIR: puntea nu are motiv sa trimita orice fisier de
|
|
// pe disc, iar cine ajunge la API-ul ei nu trebuie sa poata cere /etc/passwd.
|
|
app.post('/send-image', async (req, res) => {
|
|
const { to, path: filePath, caption } = req.body || {};
|
|
|
|
if (!to || !filePath) {
|
|
return res.status(400).json({ ok: false, error: 'missing "to" or "path" in body' });
|
|
}
|
|
if (!connected || !sock) {
|
|
return res.status(503).json({ ok: false, error: 'not connected to WhatsApp' });
|
|
}
|
|
const resolved = path.resolve(filePath);
|
|
if (resolved !== path.join(MEDIA_DIR, path.basename(resolved))) {
|
|
return res.status(400).json({ ok: false, error: 'calea trebuie sa fie un fisier din MEDIA_DIR' });
|
|
}
|
|
try {
|
|
const buffer = fs.readFileSync(resolved);
|
|
const result = await sock.sendMessage(to, { image: buffer, caption: caption || undefined });
|
|
res.json({ ok: true, id: result.key.id });
|
|
} catch (err) {
|
|
console.error('[whatsapp] Send image failed:', err.message);
|
|
res.status(500).json({ ok: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
app.post('/react', async (req, res) => {
|
|
const { to, id, emoji, fromMe, participant } = req.body || {};
|
|
|
|
if (!to || !id || emoji == null) {
|
|
return res.status(400).json({ ok: false, error: 'missing "to", "id", or "emoji" in body' });
|
|
}
|
|
if (!connected || !sock) {
|
|
return res.status(503).json({ ok: false, error: 'not connected to WhatsApp' });
|
|
}
|
|
|
|
try {
|
|
const key = { remoteJid: to, id, fromMe: fromMe || false };
|
|
if (participant) key.participant = participant;
|
|
await sock.sendMessage(to, { react: { text: emoji, key } });
|
|
res.json({ ok: true });
|
|
} catch (err) {
|
|
console.error('[whatsapp] React failed:', err.message);
|
|
res.status(500).json({ ok: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
// Grupurile din care face parte contul, cu JID-ul lor. WhatsApp nu arata nicaieri
|
|
// JID-ul unui grup ("120363...@g.us"), iar el e singurul mod de a-l scrie in
|
|
// SUPPORT_JID: de aici se citeste, o data, cand se leaga escaladarile de un grup.
|
|
app.get('/groups', async (_req, res) => {
|
|
if (!connected || !sock) {
|
|
return res.status(503).json({ ok: false, error: 'not connected to WhatsApp' });
|
|
}
|
|
try {
|
|
const all = await sock.groupFetchAllParticipating();
|
|
const groups = Object.values(all)
|
|
.map((g) => ({ jid: g.id, subject: g.subject, participants: g.participants?.length || 0 }))
|
|
.sort((a, b) => (a.subject || '').localeCompare(b.subject || ''));
|
|
res.json({ ok: true, groups });
|
|
} catch (err) {
|
|
console.error('[whatsapp] Group fetch failed:', err.message);
|
|
res.status(500).json({ ok: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
app.get('/messages', (_req, res) => {
|
|
const messages = messageQueue.splice(0);
|
|
res.json({ messages });
|
|
});
|
|
|
|
const server = app.listen(PORT, HOST, () => {
|
|
console.log(`[whatsapp] Bridge API listening on http://${HOST}:${PORT}`);
|
|
pruneMediaDir();
|
|
startConnection().catch((err) => {
|
|
console.error('[whatsapp] Failed to start connection:', err.message);
|
|
});
|
|
});
|
|
|
|
function shutdown(signal) {
|
|
console.log(`[whatsapp] Received ${signal}, shutting down...`);
|
|
shuttingDown = true;
|
|
if (sock) {
|
|
sock.end(undefined);
|
|
}
|
|
server.close(() => {
|
|
console.log('[whatsapp] HTTP server closed');
|
|
process.exit(0);
|
|
});
|
|
setTimeout(() => process.exit(1), 5000);
|
|
}
|
|
|
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
process.on('SIGINT', () => shutdown('SIGINT'));
|