whatsapp/index.js
2025-05-07 11:43:01 +02:00

614 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const express = require('express');
const qrcode = require('qrcode');
const path = require('path');
const fs = require('fs');
const NodeCache = require('node-cache');
const mime = require('mime-types');
const fetch = require('node-fetch'); // en haut si pas encore ajouté
const {
default: makeWASocket,
useMultiFileAuthState,
DisconnectReason,
fetchLatestBaileysVersion,
proto,
generateWAMessageFromContent,
prepareWAMessageMedia,
generateWAMessageContent
} = require('@fizzxydev/baileys-pro');
const app = express();
const http = require('http');
const WebSocket = require('ws');
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
function broadcastToClients(data) {
const message = JSON.stringify(data);
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
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" };
}
app.post('/webhook/status', (req, res) => {
console.log('📩 Webhook reçu :', req.body);
res.sendStatus(200);
});
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',
});
}
}
});
sock.ev.on('messages.update', async (updates) => {
broadcastToClients({
messageId,
status,
jid
});
});
};
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('/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);
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('/testViewOnce', 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' // Ton image publique
},
viewOnce: true,
caption: 'Voici une image à affichage unique 📷'
});
res.json({ success: true });
} catch (e) {
console.error('❌ Erreur testViewOnce :', e);
res.status(500).json({ error: e.message });
}
});
app.post('/testHeaderImage', 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 {
// 1. Charger l'image locale
const imagePath = path.join(__dirname, 'public/logo-merlo-cs-FR.jpg');
const imageBuffer = fs.readFileSync(imagePath);
const generate = async (type, url) => {
const generated = await generateWAMessageContent({
[type]: { url }
}, {
upload: sock.waUploadToServer
})
return generated[`${type}Message`]
}
// 3. Créer le message interactif avec image dans le header
const msg = generateWAMessageFromContent(jid.remoteJid, {
viewOnceMessage: {
message: {
messageContextInfo: {
deviceListMetadata: {},
deviceListMetadataVersion: 2
},
interactiveMessage: proto.Message.InteractiveMessage.create({
body: proto.Message.InteractiveMessage.Body.create({
text: "body text (optional)"
}),
footer: proto.Message.InteractiveMessage.Footer.create({
text: "footer text (optional)"
}),
header: proto.Message.InteractiveMessage.Header.create({
title: "some title",
hasMediaAttachment: false, // false if you don't want to send media with it
//imageMessage: generate("image", "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/TEIDE.JPG/500px-TEIDE.JPG"),
//videoMessage: generate("video", "url/path to video"), // if it's an video
}),
nativeFlowMessage: proto.Message.InteractiveMessage.NativeFlowMessage.create({
buttons: [{
name: "quick_reply",
buttonParamsJson: JSON.stringify({
display_text: "button 1", // <-- displayed text
id: ".menu" // <-- this is the id or you may call it command 🤷‍♂️
}) // REMEMBER TO USE "JSON.stringify()" BECAUSE "buttonParamsJson" ONLY ACCEPTING STIRNG JSON, NOT AN OBJECT
},{
name: "cta_url",
buttonParamsJson: JSON.stringify({
display_text: "subscribe my Youtube!",
url: "https://youtube.com/@fannmods",
merchant_url: "https://youtube.com"
})
}]
})
})
}
}
}, {})
console.log(msg.message)
// 4. Envoyer le message
await sock.relayMessage(jid, msg.message, { messageId: msg.key.id });
res.json({ success: true });
} catch (e) {
console.error('❌ Erreur testHeaderImage :', 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 derreur :', err);
res.status(500).json({ error: err.message });
});
app.listen(3001, () => console.log('🚀 Serveur Baileys démarré sur http://localhost:3001'));