const express = require('express'); const qrcode = require('qrcode'); const path = require('path'); const fs = require('fs'); const NodeCache = require('node-cache'); const { default: makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion, proto, generateWAMessageFromContent } = require('@fizzxydev/baileys-pro'); const app = express(); app.use(express.json()); app.use(express.static('public')); let sock; let qrData = null; let isConnected = false; // 💾 Group Metadata Cache (5 minutes) const groupCache = new NodeCache({ stdTTL: 300, useClones: false }); // Simule une récupération de message depuis un store pour getMessage async function getMessageFromStore(key) { // À adapter selon ta logique réelle return { conversation: "Message temporaire pour retry" }; } const initBaileys = async () => { const { version } = await fetchLatestBaileysVersion(); const { state, saveCreds } = await useMultiFileAuthState('auth'); sock = makeWASocket({ version, auth: state, markOnlineOnConnect: false, getMessage: async (key) => await getMessageFromStore(key), cachedGroupMetadata: async (jid) => groupCache.get(jid) }); sock.ev.on('connection.update', async ({ connection, lastDisconnect, qr }) => { if (qr) { qrData = await qrcode.toDataURL(qr); isConnected = false; } if (connection === 'close') { const shouldReconnect = lastDisconnect?.error?.output?.statusCode !== DisconnectReason.loggedOut; console.log(shouldReconnect ? '🔁 Reconnexion...' : '❌ Déconnecté.'); if (shouldReconnect) initBaileys(); } else if (connection === 'open') { console.log('✅ Connecté à WhatsApp'); isConnected = true; } }); sock.ev.on('creds.update', saveCreds); // 📌 Caching des groupes sock.ev.on('groups.update', async ([event]) => { const metadata = await sock.groupMetadata(event.id); groupCache.set(event.id, metadata); }); sock.ev.on('group-participants.update', async (event) => { const metadata = await sock.groupMetadata(event.id); groupCache.set(event.id, metadata); }); // (Facultatif) Gestion des messages reçus sock.ev.on('messages.upsert', async ({ messages }) => { const msg = messages[0]; if (!msg.key.fromMe && msg.message?.conversation) { console.log('💬 Message reçu de', msg.key.remoteJid, ':', msg.message.conversation); } if (!msg.message || !msg.key.fromMe) { const jid = msg.key.remoteJid; const buttonId = msg.message.buttonsResponseMessage?.selectedButtonId; if (buttonId === 'doc_1') { await sock.sendMessage(jid, { document: { url: 'https://merlo-ch.com/uploads/proposition/f_p_250505_0000136_00008_EB00001909.pdf', }, mimetype: 'application/pdf', fileName: 'Document_1.pdf', caption: 'Voici votre *Document 1* 📄', footer: '© Fizzxy Dev', }); } else if (buttonId === 'doc_2') { await sock.sendMessage(jid, { document: { url: 'https://merlo-ch.com/uploads/proposition/d_p_250505_0000136_00008_EB00001909.pdf', }, mimetype: 'application/pdf', fileName: 'Document_2.pdf', caption: 'Voici votre *Document 2* 📄', footer: '© Fizzxy Dev', }); } } }); }; initBaileys(); app.use('/static', express.static(path.join(__dirname, 'public'))); app.get('/login', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'login.html')); }); app.get('/api/qrcode', (req, res) => { res.json({ qr: qrData, connected: isConnected }); }); app.post('/sendText', async (req, res) => { const { phone, message } = req.body; if (!sock || !isConnected) return res.status(400).json({ error: 'Non connecté' }); try { await sock.sendMessage(`${phone}@s.whatsapp.net`, { text: message }); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/testButtons', async (req, res) => { const { phone } = req.body; if (!sock || !isConnected) { return res.status(400).json({ error: 'Non connecté' }); } const jid = `${phone}@s.whatsapp.net`; try { await sock.sendMessage(jid, { image: { url : "https://wa.canguidev.fr/static/logo-merlo-cs-FR.jpg" }, // Can buffer text: "Hello Wolrd !;", caption: "Description Of Messages", //Additional information footer: "© Fizzxy Dev", media:true, image: { url: 'https://wa.canguidev.fr/static/logo-merlo-cs-FR.jpg' }, buttons: [ { buttonId: '.tes', buttonText: { displayText: 'TESTING BOT' }, type: 1 }, { buttonId: ' ', buttonText: { displayText: 'PRIVATE SCRIPT' }, type: 1 }, { buttonId: 'action', buttonText: { displayText: 'ini pesan interactiveMeta' }, type: 4, nativeFlowInfo: { name: 'single_select', paramsJson: JSON.stringify({ title: 'message', sections: [ { title: 'FizzxyDev - 2025', highlight_label: '😜', rows: [ { header: 'HEADER', title: 'TITLE', description: 'DESCRIPTION', id: 'YOUR ID 1' }, { header: 'HEADER', title: 'TITLE', description: 'DESCRIPTION', id: 'YOUR ID 2' } ] } ] }) } } ], headerType: 1, viewOnce: true }, { quoted: null // ou tu peux injecter un msg pour reply ici }); res.json({ success: true }); } catch (e) { console.error('❌ Erreur testButtons :', e); res.status(500).json({ error: e.message }); } }); app.post('/sendButtons', async (req, res) => { const { phone } = req.body; if (!sock || !isConnected) { return res.status(400).json({ error: 'Non connecté' }); } const BASE_URL = process.env.BASE_URL || 'https://wa.canguidev.fr'; const imageUrl = `${BASE_URL}/static/logo-merlo-cs-FR.jpg`; const jid = `${phone}@s.whatsapp.net`; try { const content = { viewOnceMessage: { message: { messageContextInfo: { deviceListMetadata: {}, deviceListMetadataVersion: 2 }, interactiveMessage: proto.Message.InteractiveMessage.create({ body: { text: "Bienvenue sur notre service !" }, footer: { text: "Choisis une action ci-dessous" }, header: { hasMediaAttachment: true, imageMessage: { url: imageUrl } }, nativeFlowMessage: { buttons: [ { name: "cta_reply", buttonParamsJson: JSON.stringify({ display_text: "📩 Contacter support", id: "support_action" }) }, { name: "cta_url", buttonParamsJson: JSON.stringify({ display_text: "🌐 Voir notre site", url: "https://canguidev.fr" }) }, { name: "cta_call", buttonParamsJson: JSON.stringify({ display_text: "📞 Appeler le support", id: "+33612345678" }) } ] } }) } } }; const msg = generateWAMessageFromContent(jid, content, {}); await sock.relayMessage(jid, msg.message, { messageId: msg.key.id }); res.json({ success: true }); } catch (e) { console.error('❌ Erreur bouton actif :', e); res.status(500).json({ error: e.message }); } }); // Votre route POST app.post('/sendInteractiveImage', async (req, res) => { const { phone, caption, title, subtitle, footer } = req.body; if (!sock || !isConnected) { return res.status(400).json({ error: 'Non connecté à WhatsApp' }); } try { const BASE_URL = process.env.BASE_URL || 'https://wa.canguidev.fr'; const imageUrl = `${BASE_URL}/static/logo-merlo-cs-FR.jpg`; // Construire le Header correct const header = proto.Message.InteractiveMessage.create({ // ici on injecte l'image dans le header sans besoin de type explicite header: proto.Message.InteractiveMessage.Header.create({ title: 'Igna', subtitle: 'test' }), }) // Body et Footer const body = proto.Message.InteractiveMessage.Body.create({ text: caption || 'Description par défaut' }); const foot = proto.Message.InteractiveMessage.Footer.create({ text: footer || 'Pied de page' }); // Vos boutons URL const nativeFlow = proto.Message.InteractiveMessage.NativeFlowMessage.create({ buttons: [ { name: 'cta_url', buttonParamsJson: JSON.stringify({ display_text: '📄 Voir proposition', url: 'https://merlo-ch.com/uploads/proposition/f_p_250505_0000136_00008_EB00001909.pdf' }) }, { name: 'cta_url', buttonParamsJson: JSON.stringify({ display_text: '🔧 Spécifications', url: 'https://merlo-ch.com/uploads/proposition/d_p_250505_0000136_00008_EB00001909.pdf' }) } ] }); // Construire l'InteractiveMessage complet const interactiveMsg = proto.Message.InteractiveMessage.create({ header, body, footer: foot, nativeFlowMessage: nativeFlow }); // Envelopper (ici dans viewOnceMessage, comme dans votre exemple) const raw = { viewOnceMessage: { message: { messageContextInfo: { deviceListMetadata: {}, deviceListMetadataVersion: 2 }, interactiveMessage: interactiveMsg } } }; // Générer et relayer const jid = `${phone}@s.whatsapp.net`; const msg = generateWAMessageFromContent(jid, raw, {}); await sock.relayMessage(jid, msg.message, { messageId: msg.key.id }); return res.json({ success: true }); } catch (e) { console.error('❌ Erreur /sendInteractiveImage :', e); return res.status(500).json({ error: e.message }); } }); // app.post('/sendInteractiveImage', async (req, res) => { // const { phone, caption, title, subtitle, footer } = req.body; // if (!sock || !isConnected) return res.status(400).json({ error: 'Non connecté' }); // try { // const imagePath = path.join(__dirname, 'public', 'logo-merlo-cs-FR.jpg'); // const resizedBuffer = fs.readFileSync(imagePath); // await sock.sendMessage(`${phone}@s.whatsapp.net`, { // image: resizedBuffer, // caption: caption || 'Description par défaut', // title: title || 'Titre par défaut', // subtitle: subtitle || 'Sous-titre', // footer: footer || 'Pied de page', // media: true, // interactiveButtons: [ // { // name: 'cta_url', // buttonParamsJson: JSON.stringify({ // display_text: 'Proposition', // url: 'https://merlo-ch.com/uploads/proposition/f_p_250505_0000136_00008_EB00001909.pdf' // }) // }, // { // name: 'cta_url', // buttonParamsJson: JSON.stringify({ // display_text: 'Spec machine', // url: 'https://merlo-ch.com/uploads/proposition/d_p_250505_0000136_00008_EB00001909.pdf' // }) // } // ] // }); // res.json({ success: true }); // } catch (e) { // console.error('❌ Erreur interactive image :', e); // res.status(500).json({ error: e.message }); // } // }); app.post('/sendProductMessage', async (req, res) => { // On vérifie sock et isConnected, pas client if (!sock || !isConnected) { return res.status(400).json({ error: 'Non connecté à WhatsApp' }); } const { phone, productImageUrl, productImageCount, productTitle, productDescription, priceAmount1000, currencyCode, retailerId, productUrl, businessOwnerJid, caption, messageTitle, footer, interactiveButtons = [], quoted } = req.body; try { const jid = `${phone}@s.whatsapp.net`; // Build product payload exactly comme avant const productPayload = { productImage: { url: productImageUrl }, productImageCount, title: productTitle, // ici productTitle description: productDescription, priceAmount1000: priceAmount1000 * 1000, // ajustez si besoin currencyCode, retailerId, url: productUrl }; // Transformer les boutons const buttons = interactiveButtons.map(btn => { const params = {}; if (btn.id) params.id = btn.id; if (btn.url) params.url = btn.url; if (btn.display_text) params.display_text = btn.display_text; return { name: btn.name, buttonParamsJson: JSON.stringify(params) }; }); // **On utilise sock.sendMessage** et non client.sendMessage await sock.sendMessage( jid, { product: productPayload, businessOwnerJid, caption, title: messageTitle, footer, media: true, interactiveButtons: buttons }, quoted ? { quoted } : {} ); return res.json({ success: true }); } catch (e) { console.error('❌ Erreur /sendProductMessage :', e); return res.status(500).json({ error: e.message }); } }); app.post('/testProductMessage', async (req, res) => { const { phone } = req.body; if (!sock || !isConnected) { return res.status(400).json({ error: 'Non connecté à WhatsApp' }); } const jid = `${phone}@s.whatsapp.net`; try { await sock.sendMessage( jid, { product: { productImage: { url: "https://wa.canguidev.fr/static/logo-merlo-cs-FR.jpg" }, // image du produit productImageCount: 1, title: "Tracteur Merlo TF38.10", description: "Charge maximale 3.8T, hauteur 10m", priceAmount1000: 49500 * 1000, // 49 500 € currencyCode: "EUR", retailerId: "MERLO-FR-001", url: "https://example.com/product/tf38-10" }, businessOwnerJid: "1234@s.whatsapp.net", caption: "🛒 Découvrez notre nouveau modèle Merlo TF38.10", title: "Offre Spéciale Merlo", footer: "MERLO France • Offre valable jusqu'au 30/06", media: true, interactiveButtons: [ { name: "quick_reply", buttonParamsJson: JSON.stringify({ display_text: "📩 Demander un devis", id: "request_quote" }) }, { name: "cta_url", buttonParamsJson: JSON.stringify({ display_text: "🌐 Voir la fiche produit", url: "https://example.com/product/tf38-10" }) } ] }, { quoted: null // ou remplace par un message existant si tu veux répondre à un msg } ); res.json({ success: true }); } catch (e) { console.error('❌ Erreur envoi produit :', e); res.status(500).json({ error: e.message }); } }); app.post('/testInteractiveImage', async (req, res) => { const { phone } = req.body; if (!sock || !isConnected) { return res.status(400).json({ error: 'Non connecté à WhatsApp' }); } const content = { viewOnceMessage: { message: { messageContextInfo: { deviceListMetadata: {}, deviceListMetadataVersion: 2 }, interactiveMessage: proto.Message.InteractiveMessage.create({ header: { hasMediaAttachment: true, imageMessage: { url: "https://canguidev.fr/static/logo-merlo-cs-FR.jpg" } }, body: { text: "Bienvenue chez Merlo France 🇫🇷\nChoisissez une action ci-dessous." }, footer: { text: "MERLO - Support & Documentation" }, nativeFlowMessage: { buttons: [ { name: "cta_reply", buttonParamsJson: JSON.stringify({ display_text: "📩 Contacter support", id: "support_action" }) }, { name: "cta_url", buttonParamsJson: JSON.stringify({ display_text: "🌐 Voir la fiche produit", url: "https://example.com/product" }) } ] } }) } } }; const jid = `${phone}@s.whatsapp.net`; const msg = generateWAMessageFromContent(jid, content, {}); await sock.relayMessage(jid, msg.message, { messageId: msg.key.id }); }); app.post('/testButtons2', async (req, res) => { const { phone } = req.body; if (!sock || !isConnected) { return res.status(400).json({ error: 'Non connecté' }); } const jid = `${phone}@s.whatsapp.net`; try { await sock.sendMessage(jid, { text: '📚 *Veuillez choisir le document à télécharger :*', footer: '© Fizzxy Dev', buttons: [ { buttonId: 'doc_1', buttonText: { displayText: 'Télécharger Document 1' }, type: 1, }, { buttonId: 'doc_2', buttonText: { displayText: 'Télécharger Document 2' }, type: 1, } ], headerType: 1, // Texte seulement }); res.json({ success: true }); } catch (e) { console.error('❌ Erreur testButtons :', e); res.status(500).json({ error: e.message }); } }); // 5) 404 et gestion des erreurs app.use((req, res) => res.status(404).json({ error: 'Ressource introuvable' })); app.use((err, req, res, next) => { console.error('Middleware d’erreur :', err); res.status(500).json({ error: err.message }); }); app.listen(3001, () => console.log('🚀 Serveur Baileys démarré sur http://localhost:3001'));