This commit is contained in:
julien 2025-05-07 09:13:36 +02:00
commit 7f0ba5a600
3 changed files with 513 additions and 193 deletions

558
index.js
View File

@ -1,9 +1,16 @@
const { default: makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion, proto, generateWAMessageFromContent, generateWAMessageContent ,prepareWAMessageMedia} = require('@fizzxydev/baileys-pro');
const express = require('express');
const { Boom } = require('@hapi/boom');
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());
@ -13,6 +20,15 @@ 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');
@ -20,23 +36,21 @@ const initBaileys = async () => {
sock = makeWASocket({
version,
auth: state,
printQRInTerminal: false
markOnlineOnConnect: false,
getMessage: async (key) => await getMessageFromStore(key),
cachedGroupMetadata: async (jid) => groupCache.get(jid)
});
sock.ev.on('connection.update', async (update) => {
const { connection, lastDisconnect, qr } = update;
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;
if (shouldReconnect) {
console.log('🔁 Reconnexion...');
initBaileys();
} else {
console.log('❌ Déconnecté.');
}
console.log(shouldReconnect ? '🔁 Reconnexion...' : '❌ Déconnecté.');
if (shouldReconnect) initBaileys();
} else if (connection === 'open') {
console.log('✅ Connecté à WhatsApp');
isConnected = true;
@ -44,9 +58,56 @@ const initBaileys = async () => {
});
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) => {
@ -67,53 +128,145 @@ app.post('/sendText', async (req, res) => {
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é' });
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 = {
message: {
interactiveMessage: proto.Message.InteractiveMessage.create({
body: { text: "Bienvenue sur notre service !" },
footer: { text: "Choisis une action ci-dessous" },
header: {
title: "Menu principal",
hasMediaAttachment: false
viewOnceMessage: {
message: {
messageContextInfo: {
deviceListMetadata: {},
deviceListMetadataVersion: 2
},
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",
merchant_url: "https://canguidev.fr"
})
},
{
name: "cta_call",
buttonParamsJson: JSON.stringify({
display_text: "📞 Appeler le support",
id: "+33612345678"
})
}
]
}
})
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(`${phone}@s.whatsapp.net`, content, {});
await sock.relayMessage(`${phone}@s.whatsapp.net`, msg.message, { messageId: msg.key.id });
const msg = generateWAMessageFromContent(jid, content, {});
await sock.relayMessage(jid, msg.message, { messageId: msg.key.id });
res.json({ success: true });
} catch (e) {
@ -121,6 +274,8 @@ app.post('/sendButtons', async (req, res) => {
res.status(500).json({ error: e.message });
}
});
// Votre route POST
app.post('/sendInteractiveImage', async (req, res) => {
const { phone, caption, title, subtitle, footer } = req.body;
@ -133,17 +288,13 @@ app.post('/sendInteractiveImage', async (req, res) => {
const imageUrl = `${BASE_URL}/static/logo-merlo-cs-FR.jpg`;
// Construire le Header correct
const header = proto.Message.InteractiveMessage.Header.create({
title, // 'Igna' ou envoyé par le client
subtitle, // 'test' ou envoyé par le client
hasMediaAttachment: true, // ← impératif pour afficher un media
imageMessage: {
url: imageUrl, // URL publique de l'image
mimetype: 'image/jpeg' // type MIME
},
media: 'imageMessage' // ← sélectionne le bon union field
});
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'
@ -205,44 +356,44 @@ app.post('/sendInteractiveImage', async (req, res) => {
}
});
// 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é' });
app.post('/sendInteractiveImage2', 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);
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 });
// }
// });
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) {
@ -315,7 +466,117 @@ app.post('/sendProductMessage', async (req, res) => {
return res.status(500).json({ error: e.message });
}
});
<<<<<<< HEAD
app.post('/testViewOnce', async (req, res) => {
=======
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) => {
>>>>>>> 15e5852440e045e5e8529d63206d79288ac62dab
const { phone } = req.body;
if (!sock || !isConnected) {
@ -326,6 +587,7 @@ app.post('/testViewOnce', async (req, res) => {
try {
await sock.sendMessage(jid, {
<<<<<<< HEAD
image: {
url: 'https://wa.canguidev.fr/static/logo-merlo-cs-FR.jpg' // Ton image publique
},
@ -336,6 +598,112 @@ app.post('/testViewOnce', async (req, res) => {
res.json({ success: true });
} catch (e) {
console.error('❌ Erreur testViewOnce :', e);
=======
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 });
}
});
app.post('/testButtons3', async (req, res) => {
const { phone } = req.body;
if (!sock || !isConnected) {
return res.status(400).json({ error: 'Non connecté' });
}
const jid = `${phone}@s.whatsapp.net`;
let msg = generateWAMessageFromContent(m.chat, {
viewOnceMessage: {
message: {
"messageContextInfo": {
"deviceListMetadata": {},
"deviceListMetadataVersion": 2
},
interactiveMessage: proto.Message.InteractiveMessage.create({
body: proto.Message.InteractiveMessage.Body.create({
text: "Fizzxy Dev"
}),
footer: proto.Message.InteractiveMessage.Footer.create({
text: "Bot"
}),
header: proto.Message.InteractiveMessage.Header.create({
title: "Igna",
subtitle: "test",
hasMediaAttachment: false
}),
nativeFlowMessage: proto.Message.InteractiveMessage.NativeFlowMessage.create({
buttons: [
{
"name": "single_select",
"buttonParamsJson": "{\"title\":\"title\",\"sections\":[{\".menu\":\".play dj webito\",\"highlight_label\":\"label\",\"rows\":[{\"header\":\"header\",\"title\":\"title\",\"description\":\"description\",\"id\":\"id\"},{\"header\":\"header\",\"title\":\"title\",\"description\":\"description\",\"id\":\"id\"}]}]}"
},
{
"name": "cta_reply",
"buttonParamsJson": "{\"display_text\":\"quick_reply\",\"id\":\"message\"}"
},
{
"name": "cta_url",
"buttonParamsJson": "{\"display_text\":\"url\",\"url\":\"https://www.google.com\",\"merchant_url\":\"https://www.google.com\"}"
},
{
"name": "cta_call",
"buttonParamsJson": "{\"display_text\":\"call\",\"id\":\"message\"}"
},
{
"name": "cta_copy",
"buttonParamsJson": "{\"display_text\":\"copy\",\"id\":\"123456789\",\"copy_code\":\"message\"}"
},
{
"name": "cta_reminder",
"buttonParamsJson": "{\"display_text\":\"Recordatorio\",\"id\":\"message\"}"
},
{
"name": "cta_cancel_reminder",
"buttonParamsJson": "{\"display_text\":\"cta_cancel_reminder\",\"id\":\"message\"}"
},
{
"name": "address_message",
"buttonParamsJson": "{\"display_text\":\"address_message\",\"id\":\"message\"}"
},
{
"name": "send_location",
"buttonParamsJson": ""
}
],
})
})
}
}
}, {})
try {
await sock.relayMessage(msg.key.remoteJid, msg.message, { messageId: msg.key.id })
res.json({ success: true });
} catch (e) {
console.error('❌ Erreur testButtons :', e);
>>>>>>> 15e5852440e045e5e8529d63206d79288ac62dab
res.status(500).json({ error: e.message });
}
});

View File

@ -12,6 +12,8 @@
"axios": "^1.4.0",
"express": "^4.18.4",
"qrcode": "^1.5.1",
"sharp": "^0.33.0"
"sharp": "^0.33.0",
"node-cache": "^5.1.2"
}
}

View File

@ -2,116 +2,66 @@
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Connexion WhatsApp & Envoi Interactif</title>
<title>Connexion WhatsApp</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: auto; padding: 20px; }
h1, h2 { text-align: center; }
#qrcode { text-align: center; margin-bottom: 20px; }
form { display: flex; flex-direction: column; gap: 10px; }
label { display: flex; flex-direction: column; font-weight: bold; }
input { padding: 8px; font-size: 1rem; }
button { padding: 10px; font-size: 1rem; cursor: pointer; }
#result { margin-top: 15px; font-weight: bold; }
hr { margin: 30px 0; }
body {
font-family: Arial, sans-serif;
text-align: center;
background: #f2f2f2;
padding: 40px;
}
#qr-container {
margin-top: 40px;
}
#qr {
max-width: 300px;
margin: auto;
display: block;
}
#status {
margin-top: 20px;
font-size: 18px;
color: #444;
}
</style>
</head>
<body>
<h1>Scanne le QR Code WhatsApp</h1>
<div id="qrcode">Chargement...</div>
<h1>Connexion WhatsApp</h1>
<p>Scannez le QR Code avec votre application WhatsApp</p>
<hr>
<h2>Envoyer du message interactif avec image</h2>
<form id="interactiveForm">
<label>
Téléphone (sans +, ex: 33612345678):
<input type="text" name="phone" required placeholder="33612345678">
</label>
<label>
Caption:
<input type="text" name="caption" placeholder="Description par défaut">
</label>
<label>
Title:
<input type="text" name="title" placeholder="Titre par défaut">
</label>
<label>
Subtitle:
<input type="text" name="subtitle" placeholder="Sous-titre">
</label>
<label>
Footer:
<input type="text" name="footer" placeholder="Pied de page">
</label>
<label>
URL Proposition:
<input type="url" name="propositionUrl" placeholder="https://exemple.com/prop.pdf" required>
</label>
<label>
URL Spec Machine:
<input type="url" name="specUrl" placeholder="https://exemple.com/spec.pdf" required>
</label>
<button type="submit">Envoyer le message</button>
</form>
<div id="result"></div>
<div id="qr-container">
<img id="qr" src="" alt="QR Code WhatsApp" />
<div id="status">Chargement du QR code...</div>
</div>
<script>
// Vérification du QR code et statut de connexion
async function checkQR() {
try {
const res = await fetch('/api/qrcode');
const data = await res.json();
console.log('Réponse /api/qrcode :', data);
async function fetchQRCode() {
try {
const res = await fetch('/api/qrcode');
const data = await res.json();
const qrContainer = document.getElementById('qrcode');
if (data.connected) {
qrContainer.innerHTML = '✅ Connecté à WhatsApp !';
} else if (data.qr) {
// Si data.qr est une chaîne (Data-URL)
if (typeof data.qr === 'string') {
qrContainer.innerHTML = `<img src="${data.qr}" alt="QR Code WhatsApp" width="200" height="200"/>`;
}
// Sinon, si c'est un objet { data: […], mime: 'image/png' }
else if (data.qr.data) {
const uint8Array = new Uint8Array(data.qr.data);
const base64 = btoa(String.fromCharCode(...uint8Array));
const src = `data:${data.qr.mime};base64,${base64}`;
qrContainer.innerHTML = `<img src="${src}" alt="QR Code WhatsApp" width="200" height="200"/>`;
const qrImg = document.getElementById('qr');
const status = document.getElementById('status');
if (data.connected) {
qrImg.style.display = 'none';
status.textContent = "✅ Connecté à WhatsApp";
} else if (data.qr) {
qrImg.src = data.qr;
qrImg.style.display = 'block';
status.textContent = "QR code prêt à être scanné";
} else {
qrContainer.textContent = 'QR reçu, mais format non pris en charge.';
status.textContent = "⏳ En attente de génération du QR code...";
}
} else {
qrContainer.textContent = 'Pas encore de QR disponible.';
} catch (error) {
console.error("Erreur de récupération QR:", error);
document.getElementById('status').textContent = "❌ Impossible de charger le QR Code";
}
} catch (err) {
console.error('Erreur checkQR:', err);
}
setTimeout(checkQR, 3000);
}
checkQR();
// Envoi du formulaire interactif
document.getElementById('interactiveForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const body = {};
formData.forEach((value, key) => body[key] = value);
const res = await fetch('/sendInteractiveImage', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await res.json();
const resultDiv = document.getElementById('result');
if (data.success) {
resultDiv.textContent = '✅ Message envoyé avec succès';
resultDiv.style.color = 'green';
} else {
resultDiv.textContent = '❌ Erreur : ' + (data.error || 'Inconnue');
resultDiv.style.color = 'red';
}
});
// Rafraîchit toutes les 5 secondes
fetchQRCode();
setInterval(fetchQRCode, 5000);
</script>
</body>
</html>