v1.3.0 — pannello admin completo, auth localStorage, Baileys WA, customers, calendario, paginazione, dashboard 7gg
This commit is contained in:
13
wa-service/Dockerfile
Normal file
13
wa-service/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM node:20-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json .
|
||||
RUN npm install --omit=dev
|
||||
COPY server.js .
|
||||
|
||||
RUN mkdir -p /data/auth
|
||||
|
||||
EXPOSE 3100
|
||||
CMD ["node", "server.js"]
|
||||
12
wa-service/package.json
Normal file
12
wa-service/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "booking-wa",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": { "start": "node server.js" },
|
||||
"dependencies": {
|
||||
"@whiskeysockets/baileys": "^6.7.16",
|
||||
"express": "^4.21.0",
|
||||
"pino": "^9.6.0",
|
||||
"qrcode": "^1.5.4"
|
||||
}
|
||||
}
|
||||
194
wa-service/server.js
Normal file
194
wa-service/server.js
Normal file
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* 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 });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user