/** * BookingWA — Servizio WhatsApp Baileys per Farmacia Ianni * Espone REST: GET /status, GET /qr, POST /send, POST /disconnect * Sessione persistente in /data/auth per sopravvivere ai restart. */ const express = require('express'); const { default: makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion } = require('@whiskeysockets/baileys'); const qrcode = require('qrcode'); const pino = require('pino'); const app = express(); app.use(express.json()); const PORT = process.env.PORT || 3100; const AUTH_DIR = '/data/auth'; const logger = pino({ level: 'warn' }); let sock = null; let currentQR = null; let qrGeneratedAt = null; let qrAttempt = 0; let connectionState = 'disconnected'; let phoneNumber = null; let retryCount = 0; const MAX_RETRIES = 5; const QR_TTL_SEC = 20; async function startSocket() { const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR); const { version } = await fetchLatestBaileysVersion(); sock = makeWASocket({ version, auth: state, logger, printQRInTerminal: false, browser: ['Farmacia Ianni', 'Chrome', '120.0'], connectTimeoutMs: 30000, defaultQueryTimeoutMs: 30000, generateHighQualityLinkPreview: false, syncFullHistory: false, qrTimeout: 60000, }); sock.ev.on('creds.update', saveCreds); sock.ev.on('connection.update', async (update) => { const { connection, lastDisconnect, qr } = update; if (qr) { currentQR = await qrcode.toDataURL(qr, { width: 280, margin: 2 }); qrGeneratedAt = Date.now(); qrAttempt++; connectionState = 'connecting'; console.log(`[WA] QR #${qrAttempt} generato — scade tra ${QR_TTL_SEC}s`); } if (connection === 'close') { currentQR = null; qrGeneratedAt = null; const statusCode = lastDisconnect?.error?.output?.statusCode; const shouldReconnect = statusCode !== DisconnectReason.loggedOut; if (statusCode === DisconnectReason.loggedOut) { connectionState = 'disconnected'; phoneNumber = null; retryCount = 0; qrAttempt = 0; console.log('[WA] Disconnesso (logout). Serve nuovo QR.'); const fs = require('fs'); fs.rmSync(AUTH_DIR, { recursive: true, force: true }); fs.mkdirSync(AUTH_DIR, { recursive: true }); } else if (shouldReconnect && retryCount < MAX_RETRIES) { retryCount++; const delay = Math.min(retryCount * 2000, 15000); console.log(`[WA] Riconnessione ${retryCount}/${MAX_RETRIES} in ${delay}ms...`); connectionState = 'connecting'; setTimeout(startSocket, delay); } else { connectionState = 'disconnected'; console.log('[WA] Connessione persa, max retry raggiunto.'); } } if (connection === 'open') { connectionState = 'connected'; currentQR = null; qrGeneratedAt = null; retryCount = 0; qrAttempt = 0; phoneNumber = sock.user?.id?.split(':')[0] || sock.user?.id; console.log(`[WA] Connesso come ${phoneNumber}`); } }); } // ── API ── app.get('/status', (req, res) => { res.json({ connected: connectionState === 'connected', state: connectionState, phone: phoneNumber, message: connectionState === 'connected' ? `Connesso come ${phoneNumber}` : connectionState === 'connecting' ? 'In attesa di scansione QR...' : 'Disconnesso' }); }); app.get('/qr', (req, res) => { if (connectionState === 'connected') { return res.json({ qr: null, expired: false, message: 'Già connesso' }); } if (currentQR && qrGeneratedAt) { const elapsed = (Date.now() - qrGeneratedAt) / 1000; const remaining = Math.max(0, Math.round(QR_TTL_SEC - elapsed)); if (remaining <= 0) { return res.json({ qr: null, expired: true, attempt: qrAttempt, message: 'QR scaduto — in attesa del prossimo...' }); } return res.json({ qr: currentQR, expired: false, expires_in: remaining, ttl: QR_TTL_SEC, attempt: qrAttempt, }); } // Se non c'è QR e non siamo connessi, avvia socket if (connectionState === 'disconnected') { startSocket().catch(console.error); return res.json({ qr: null, expired: false, message: 'Avvio connessione... riprova tra 5 secondi' }); } res.json({ qr: null, expired: false, message: 'QR in generazione...' }); }); app.post('/send', async (req, res) => { const { to, text } = req.body; if (!to || !text) return res.status(400).json({ error: 'Servono "to" e "text"' }); if (connectionState !== 'connected' || !sock) return res.status(503).json({ error: 'WhatsApp non connesso' }); try { let jid = to.replace(/[^0-9]/g, ''); if (!jid.endsWith('@s.whatsapp.net')) jid = jid + '@s.whatsapp.net'; await sock.sendMessage(jid, { text }); console.log(`[WA] Messaggio inviato a ${to}`); res.json({ ok: true, to, message: 'Inviato' }); } catch (e) { console.error('[WA] Errore invio:', e.message); res.status(500).json({ error: e.message }); } }); app.post('/disconnect', async (req, res) => { try { if (sock) { await sock.logout(); sock = null; } connectionState = 'disconnected'; phoneNumber = null; currentQR = null; qrGeneratedAt = null; qrAttempt = 0; res.json({ ok: true, message: 'Disconnesso' }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.get('/health', (req, res) => { res.json({ status: 'ok', service: 'booking-wa', state: connectionState }); }); // ── START ── app.listen(PORT, '0.0.0.0', () => { console.log(`[WA] Servizio avviato su :${PORT}`); const fs = require('fs'); if (fs.existsSync(AUTH_DIR) && fs.readdirSync(AUTH_DIR).length > 0) { console.log('[WA] Sessione trovata, riconnessione...'); startSocket().catch(console.error); } else { console.log('[WA] Nessuna sessione. Chiama GET /qr per iniziare.'); fs.mkdirSync(AUTH_DIR, { recursive: true }); } });