Files
booking-service/wa-service/server.js

195 lines
5.9 KiB
JavaScript

/**
* 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 });
}
});