Spaces:
Runtime error
Runtime error
| const { makeWASocket, useMultiFileAuthState, DisconnectReason, Browsers, getContentType } = require('@whiskeysockets/baileys'); | |
| const { writeFileSync, existsSync, readFileSync, mkdirSync } = require('fs'); | |
| const path = require('path'); | |
| const pino = require('pino'); | |
| const axios = require('axios').default; | |
| const qrcode = require('qrcode-terminal'); | |
| const API_BASE = process.env.API_BASE || 'http://localhost:8000'; | |
| const DEFAULT_SLUG = process.env.DEFAULT_BUSINESS_SLUG || 'sahel'; | |
| const SESSION_DIR = path.join(__dirname, 'session'); | |
| const DEV = process.env.NODE_ENV !== 'production'; | |
| const userSessions = {}; | |
| const userBusinessSlugs = {}; | |
| let reconnectAttempt = 0; | |
| let shutdown = false; | |
| function loadUserData() { | |
| const file = path.join(__dirname, 'users.json'); | |
| if (existsSync(file)) { | |
| try { | |
| const data = JSON.parse(readFileSync(file, 'utf-8')); | |
| Object.assign(userBusinessSlugs, data); | |
| } catch (e) { console.error('Erreur chargement utilisateurs:', e); } | |
| } | |
| } | |
| function saveUserData() { | |
| const file = path.join(__dirname, 'users.json'); | |
| try { | |
| writeFileSync(file, JSON.stringify(userBusinessSlugs, null, 2)); | |
| } catch (e) { console.error('Erreur sauvegarde utilisateurs:', e); } | |
| } | |
| loadUserData(); | |
| async function getBotNumber(sock) { | |
| try { | |
| const id = sock.user?.id; | |
| if (id) return id.replace(/:.*/, ''); | |
| } catch (e) { console.error('Erreur rΓ©cupΓ©ration numΓ©ro bot:', e); } | |
| return 'unknown'; | |
| } | |
| function extractText(msg) { | |
| try { | |
| const contentType = getContentType(msg.message); | |
| if (!contentType) return null; | |
| const content = msg.message[contentType]; | |
| if (contentType === 'conversation') return content.text || content; | |
| if (contentType === 'extendedTextMessage') return content.text || ''; | |
| return null; | |
| } catch (e) { | |
| console.error('Erreur extraction texte:', e); | |
| return null; | |
| } | |
| } | |
| async function handleMessage(sock, msg) { | |
| try { | |
| const key = msg.key; | |
| const remoteJid = key.remoteJid; | |
| const fromMe = key.fromMe; | |
| if (fromMe) return; | |
| if (!remoteJid) return; | |
| if (remoteJid.endsWith('@g.us') || remoteJid.endsWith('@broadcast')) return; | |
| const text = extractText(msg); | |
| if (!text || !text.trim()) return; | |
| const normalizedText = text.toLowerCase().trim(); | |
| const sender = remoteJid.replace('@s.whatsapp.net', ''); | |
| console.log('[whatsapp] π© Message de', sender, ':', text.slice(0, 60)); | |
| if (normalizedText.startsWith('/start') || normalizedText.startsWith('/menu')) { | |
| return sendMenu(sock, remoteJid); | |
| } | |
| if (normalizedText.startsWith('/business')) { | |
| const parts = text.split(/\s+/); | |
| if (parts.length >= 2) { | |
| const slug = parts[1].toLowerCase(); | |
| try { | |
| const res = await axios.get(`${API_BASE}/businesses/${slug}`); | |
| if (res.status === 200) { | |
| userBusinessSlugs[sender] = slug; | |
| saveUserData(); | |
| await sock.sendMessage(remoteJid, { | |
| text: `β ConnectΓ© Γ *${res.data.name || slug}*.\n\nEnvoyez un message pour discuter avec l'assistant IA.` | |
| }); | |
| } | |
| } catch (e) { | |
| console.error('Erreur vΓ©rification commerce:', e); | |
| await sock.sendMessage(remoteJid, { | |
| text: `β Commerce "${slug}" introuvable. VΓ©rifiez le slug.` | |
| }); | |
| } | |
| } else { | |
| await sock.sendMessage(remoteJid, { | |
| text: `Utilisation : /business <slug>\n\nExemple : /business sahel` | |
| }); | |
| } | |
| return; | |
| } | |
| const slug = userBusinessSlugs[sender] || DEFAULT_SLUG; | |
| if (!userSessions[sender]) { | |
| userSessions[sender] = { conversation_id: null }; | |
| } | |
| await sock.sendPresenceUpdate('composing', remoteJid); | |
| try { | |
| const res = await axios.post(`${API_BASE}/businesses/${slug}/chat`, { | |
| question: text, | |
| conversation_id: userSessions[sender].conversation_id | |
| }, { | |
| headers: { 'Content-Type': 'application/json' }, | |
| timeout: 30000 | |
| }); | |
| if (res.status === 200) { | |
| userSessions[sender].conversation_id = res.data.conversation_id; | |
| const answer = res.data.answer || "DΓ©solΓ©, je n'ai pas de rΓ©ponse."; | |
| await sock.sendMessage(remoteJid, { text: answer }); | |
| } | |
| } catch (err) { | |
| const detail = err.response?.data?.detail || err.message; | |
| await sock.sendMessage(remoteJid, { | |
| text: `β οΈ Erreur : ${detail}` | |
| }); | |
| } | |
| } catch (err) { | |
| console.error('β Erreur handleMessage:', err.message); | |
| } | |
| } | |
| async function sendMenu(sock, jid) { | |
| await sock.sendMessage(jid, { | |
| text: `π€ *Sahel.ai β Assistant WhatsApp* | |
| Commandes disponibles : | |
| /business <slug> β Choisir un commerce | |
| /menu ou /start β Afficher ce menu | |
| *Pour commencer :* | |
| 1. Tapez /business <slug> (ex: /business sahel) | |
| 2. Posez vos questions directement | |
| π‘ Pour obtenir le slug de votre commerce, connectez-vous au dashboard.` | |
| }); | |
| } | |
| function getReconnectDelay() { | |
| const delays = [5000, 15000, 30000, 60000]; | |
| const idx = Math.min(reconnectAttempt, delays.length - 1); | |
| return delays[idx] + Math.floor(Math.random() * 5000); | |
| } | |
| async function startBot(pairingPhone) { | |
| if (shutdown) return; | |
| if (!existsSync(SESSION_DIR)) { | |
| mkdirSync(SESSION_DIR, { recursive: true }); | |
| } | |
| const { state, saveCreds } = await useMultiFileAuthState(SESSION_DIR); | |
| const sock = makeWASocket({ | |
| auth: state, | |
| browser: Browsers.windows('Desktop'), | |
| logger: pino({ level: process.env.LOG_LEVEL || 'silent' }), | |
| syncFullHistory: false, | |
| markOnlineOnConnect: true | |
| }); | |
| if (pairingPhone) { | |
| setTimeout(async () => { | |
| try { | |
| const code = await sock.requestPairingCode(pairingPhone); | |
| if (DEV) { | |
| console.log('\nββββββββββββββββββββββββββββββββββββββββββ'); | |
| console.log(' CODE D\'APPARIEMENT'); | |
| console.log(` ${code}`); | |
| console.log('ββββββββββββββββββββββββββββββββββββββββββ\n'); | |
| console.log(' Ouvrez WhatsApp β Appareils liΓ©s β Lier un appareil'); | |
| console.log(' β Associer via un numΓ©ro de tΓ©lΓ©phone'); | |
| console.log(' Entrez le code ci-dessus.\n'); | |
| } | |
| } catch (e) { | |
| console.error('β οΈ Impossible de gΓ©nΓ©rer le code:', e.message); | |
| console.log(' Utilisation du QR code...'); | |
| } | |
| }, 2000); | |
| } | |
| sock.ev.on('connection.update', async ({ connection, lastDisconnect, qr }) => { | |
| if (qr && !pairingPhone) { | |
| reconnectAttempt = 0; | |
| console.log('\nββββββββββββββββββββββββββββββββββββββββββ'); | |
| console.log(' SCANNEZ CE QR CODE AVEC VOTRE WHATSAPP'); | |
| console.log(' 1. Ouvrez WhatsApp sur votre tΓ©lΓ©phone'); | |
| console.log(' 2. Menu β Appareils liΓ©s β Lier un appareil'); | |
| console.log(' 3. Scannez le QR code ci-dessous'); | |
| console.log('ββββββββββββββββββββββββββββββββββββββββββ\n'); | |
| qrcode.generate(qr, { small: true }); | |
| } | |
| if (connection === 'open') { | |
| reconnectAttempt = 0; | |
| const botNumber = await getBotNumber(sock); | |
| console.log('[whatsapp] β WhatsApp bot connectΓ© !'); | |
| if (DEV) { console.log('[whatsapp] π± NumΓ©ro :', botNumber); } | |
| console.log('[whatsapp] π API :', API_BASE); | |
| console.log('[whatsapp] πͺ Slug par dΓ©faut :', DEFAULT_SLUG, '\n'); | |
| } | |
| if (connection === 'close') { | |
| const statusCode = lastDisconnect?.error?.output?.statusCode; | |
| const shouldReconnect = statusCode !== DisconnectReason.loggedOut; | |
| const reason = DisconnectReason[statusCode] || statusCode || 'unknown'; | |
| console.log(`[whatsapp] β οΈ DΓ©connectΓ© (${reason}). Reconnexion : ${shouldReconnect}`); | |
| if (shouldReconnect) { | |
| reconnectAttempt++; | |
| const delay = getReconnectDelay(); | |
| console.log(`[whatsapp] β³ Reconnexion dans ${Math.round(delay / 1000)}s (tentative #${reconnectAttempt})`); | |
| setTimeout(() => startBot(), delay); | |
| } else { | |
| console.log('[whatsapp] β Session expirΓ©e. Supprimez le dossier session/ et relancez.'); | |
| } | |
| } | |
| }); | |
| sock.ev.on('creds.update', saveCreds); | |
| sock.ev.on('messages.upsert', async ({ messages, type }) => { | |
| if (type !== 'notify') return; | |
| for (const msg of messages) { | |
| await handleMessage(sock, msg); | |
| } | |
| }); | |
| } | |
| const phoneArg = process.argv.find(a => a.startsWith('--phone=')); | |
| const pairingPhone = phoneArg ? phoneArg.replace('--phone=', '') : null; | |
| if (pairingPhone && DEV) { | |
| console.log('[whatsapp] π± Mode appairage par numΓ©ro activΓ© pour :', pairingPhone); | |
| } | |
| startBot(pairingPhone).catch(err => { | |
| console.error('Erreur fatale:', err); | |
| process.exit(1); | |
| }); | |
| process.on('SIGINT', () => { | |
| shutdown = true; | |
| console.log('[whatsapp] π ArrΓͺt du bot...'); | |
| process.exit(0); | |
| }); | |