Solusi WhatsApp Center Unlimited Device dengan Baileys & Chatwoot

  1. Tekno 
  2. 3 hari yang lalu

whatsapp-unlimited-device Banyak bisnis modern membutuhkan WhatsApp Center yang mampu menangani ribuan percakapan sekaligus dengan banyak device. Keterbatasan aplikasi WhatsApp resmi membuat perusahaan kesulitan ketika ingin menampung interaksi customer service yang masif.

Solusinya? Gunakan Baileys sebagai WhatsApp Web API library dan integrasikan dengan Chatwoot sebagai platform omnichannel customer service.


Kenapa Butuh WhatsApp Center Unlimited Device?

  1. Skalabilitas Tinggi
    Bisnis dengan ratusan bahkan ribuan pelanggan membutuhkan sistem multi-device agar pesan tidak menumpuk pada satu ponsel.

  2. Integrasi ke Customer Service
    Dengan integrasi ke Chatwoot, tim bisa langsung membalas pesan WhatsApp bersama-sama dalam satu dashboard.

  3. Efisiensi Operasional
    Tidak perlu lagi menyalin pesan dari WhatsApp ke sistem CRM atau ticketing, karena semua sudah otomatis sinkron.


Apa Itu Baileys?

Baileys adalah library Node.js open-source untuk berinteraksi dengan WhatsApp Web API. Dengan Baileys, Anda bisa:

  • Menghubungkan banyak nomor WhatsApp tanpa batasan perangkat.
  • Mengirim & menerima pesan teks, gambar, video, file, hingga voice note.
  • Mengelola sesi dengan sistem autentikasi QR Code.
  • Membuat bot WhatsApp custom sesuai kebutuhan bisnis.

Apa Itu Chatwoot?

Chatwoot adalah platform customer engagement open-source yang mendukung berbagai kanal komunikasi, seperti:

  • WhatsApp
  • Facebook Messenger
  • Telegram
  • Instagram
  • Email & Live Chat

Integrasi WhatsApp ke Chatwoot memungkinkan semua tim CS Anda menjawab pesan secara kolaboratif dalam satu dashboard.


Cara Integrasi WhatsApp Unlimited Device

1. Siapkan Server

Gunakan server Linux/Ubuntu dengan Node.js minimal versi 18 untuk menjalankan Baileys. Pastikan juga tersedia Redis atau database untuk menyimpan sesi.

2. Deploy Baileys

Clone repositori Baileys:

git clone https://github.com/WhiskeySockets/Baileys
cd Baileys
npm install

Jalankan koneksi WhatsApp menggunakan skrip bot sederhana.

3. Hubungkan dengan Chatwoot

  • Gunakan API Private Chatwoot (/api/v1/...) untuk membuat kontak, percakapan, dan pesan.
  • Relay semua pesan dari Baileys → Chatwoot.
  • Sebaliknya, pesan dari Chatwoot → dikirim ke WhatsApp melalui Baileys.

4. Kelola Multi Device

Untuk unlimited device, jalankan multi-instance Baileys dengan manajemen sesi. Setiap nomor WhatsApp akan memiliki session file terpisah yang bisa dikelola oleh server.


Keunggulan Solusi Ini

Unlimited Device – Tidak terikat pada keterbatasan WhatsApp Business API resmi. ✅ Open Source – Gratis, fleksibel, dan bisa dikembangkan sesuai kebutuhan. ✅ Terintegrasi CRM – Semua percakapan otomatis masuk ke Chatwoot. ✅ Scalable – Bisa menambah nomor baru tanpa konfigurasi ulang yang rumit.


Studi Kasus Penggunaan

  • E-commerce: Satu dashboard untuk ribuan pertanyaan pelanggan setiap hari.
  • Fintech/Banking: Notifikasi transaksi & customer service real-time.
  • PPOB & Server Pulsa: Balasan otomatis status transaksi via WhatsApp.
  • Perusahaan Layanan Publik: Satu nomor WhatsApp resmi bisa digunakan banyak agen.

Kesimpulan

Dengan menggabungkan Baileys dan Chatwoot, Anda bisa membangun WhatsApp Center unlimited device yang powerful, aman, dan sesuai kebutuhan bisnis modern.

Bagi perusahaan dengan trafik pesan tinggi, solusi ini jauh lebih fleksibel dibandingkan API resmi yang terbatas pada harga dan jumlah perangkat.


🚀 Ingin mencoba? Silakan mulai dengan Baileys GitHub lalu integrasikan ke Chatwoot agar tim customer service Anda lebih produktif.

Kode script Whatsapp Unlimited Device

Dibawah ini ada contoh script untuk integrasi chatwoot dan whatsapp baileys. Namun masih ada file dan folder yang harus di tambahkan seperti .env dan folder “session-wa1”

require('dotenv').config();
const { default: makeWASocket, useMultiFileAuthState, DisconnectReason, delay, downloadContentFromMessage } = require('@whiskeysockets/baileys');
const express = require('express');
const axios = require('axios');
const fs = require('fs');
const path = require('path');
const mime = require('mime-types');
const FormData = require('form-data');
const qrcode = require('qrcode-terminal');

const recentlySentMessageIds = new Set();
const messageHistory = new Map(); // Store message history for replies
const chatwootToWhatsAppMapping = new Map(); // Map Chatwoot message ID to WhatsApp message ID

// Config from .env
const {
    CHATWOOT_BASE_URL,
    CHATWOOT_API_TOKEN,
    CHATWOOT_ACCOUNT_ID = 1,
} = process.env;

const PORT = 3000;
const CHATWOOT_INBOX_ID = 5;
const SESSION_NAME = 'session-wa1'; // Nama sesi untuk bot ini

const CLEANED_CHATWOOT_BASE_URL = CHATWOOT_BASE_URL ? CHATWOOT_BASE_URL.replace(/\/$/, "") : "";

// Log final config values being used
console.log('Environment Variables Loaded:', {
    CHATWOOT_BASE_URL: CLEANED_CHATWOOT_BASE_URL ? "******" : "MISSING",
    CHATWOOT_API_TOKEN: CHATWOOT_API_TOKEN ? "******" : "MISSING",
    CHATWOOT_ACCOUNT_ID,
    CHATWOOT_INBOX_ID: CHATWOOT_INBOX_ID || "MISSING",
    NODE_ENV: process.env.NODE_ENV || "development"
});

// Validate essential variables
if (!CLEANED_CHATWOOT_BASE_URL || !CHATWOOT_API_TOKEN || !CHATWOOT_INBOX_ID) {
    console.error('❌ FATAL ERROR: Missing one or more required environment variables: CHATWOOT_BASE_URL, CHATWOOT_API_TOKEN, CHATWOOT_INBOX_ID');
    process.exit(1);
}

// Initialize Express
const app = express();
app.use(express.json());

const contactCache = new Map();
const conversationCache = new Map();
let sock;

async function streamToBuffer(stream) {
    const chunks = [];
    for await (const chunk of stream) {
        chunks.push(chunk);
    }
    return Buffer.concat(chunks);
}

// Function to store message for potential replies
function storeMessageForReply(waId, messageId, content, chatwootMessageId = null) {
    const key = `${waId}_${messageId}`;
    messageHistory.set(key, {
        content: content,
        timestamp: Date.now(),
        chatwootMessageId: chatwootMessageId
    });
    
    // Store mapping if we have Chatwoot message ID
    if (chatwootMessageId) {
        chatwootToWhatsAppMapping.set(chatwootMessageId, messageId);
    }
    
    // Clean old messages (keep only last 50 messages per conversation)
    const conversationMessages = Array.from(messageHistory.keys())
        .filter(k => k.startsWith(waId + '_'))
        .sort((a, b) => messageHistory.get(b).timestamp - messageHistory.get(a).timestamp)
        .slice(50);
    
    conversationMessages.forEach(k => messageHistory.delete(k));
}

// Function to find message for reply
function findMessageForReply(waId, content) {
    const conversationMessages = Array.from(messageHistory.entries())
        .filter(([key, value]) => key.startsWith(waId + '_'))
        .sort((a, b) => b[1].timestamp - a[1].timestamp);
    
    // Find the most recent message that contains the content
    for (const [key, value] of conversationMessages) {
        // Try multiple matching strategies
        const contentLower = content.toLowerCase().trim();
        const storedContentLower = value.content.toLowerCase().trim();
        
        if (value.content.includes(content) || 
            content.includes(value.content) ||
            contentLower === storedContentLower ||
            storedContentLower.includes(contentLower)) {
            const messageId = key.split('_')[1];
            return messageId;
        }
    }
    
    return null;
}

// Function to find message by Chatwoot message ID
function findMessageByChatwootId(chatwootMessageId) {
    if (chatwootToWhatsAppMapping.has(chatwootMessageId)) {
        const whatsappMessageId = chatwootToWhatsAppMapping.get(chatwootMessageId);
        return whatsappMessageId;
    }
    
    return null;
}

// Chatwoot API Functions
async function getOrCreateContact({ name, phone, wa_id, isGroup = false }) {
    try {
        const cacheKey = wa_id;
        if (contactCache.has(cacheKey)) return contactCache.get(cacheKey);

        const cleanedPhone = phone.replace(/\D/g, '');
        console.log(`🔍 Searching contact for ${name} (${isGroup ? 'Group' : cleanedPhone})...`);
        
        const searchParam = isGroup ? wa_id : cleanedPhone;
        const searchUrl = `${CLEANED_CHATWOOT_BASE_URL}/api/v1/accounts/${CHATWOOT_ACCOUNT_ID}/contacts/search?q=${searchParam}`;
        
        const searchRes = await axios.get(searchUrl, {
            headers: { api_access_token: CHATWOOT_API_TOKEN },
            timeout: 5000
        });

        const existingContact = searchRes.data.payload?.find(c => c.identifier === wa_id);

        if (existingContact) {
            console.log(`✅ Found existing contact: ${existingContact.id}`);
            contactCache.set(cacheKey, existingContact.id);
            return existingContact.id;
        }

        console.log(`🆕 Creating new contact for ${name}`);
        
        // --- MODIFIKASI DI SINI ---
        const contactName = isGroup ? name : `Pelanggan Sales1 ${name}`;
        // ---------------------------

        const payload = {
            name: contactName, // Gunakan nama yang sudah dimodifikasi
            identifier: wa_id,
            custom_attributes: { whatsapp_id: wa_id }
        };

        if (isGroup) {
            payload.email = `${cleanedPhone}@g.us.local`;
        } else {
            payload.phone_number = `+${cleanedPhone}`;
        }

        const createRes = await axios.post(
            `${CLEANED_CHATWOOT_BASE_URL}/api/v1/accounts/${CHATWOOT_ACCOUNT_ID}/contacts`,
            payload,
            { headers: { api_access_token: CHATWOOT_API_TOKEN }, timeout: 5000 }
        );

        const contactId = createRes.data.payload.contact.id;
        console.log(`🆔 New contact created: ${contactId} with name "${contactName}"`);
        contactCache.set(cacheKey, contactId);
        return contactId;

    } catch (err) {
        console.error('❌ Contact Error:', {
            message: err.message,
            url: err.config?.url,
            status: err.response?.status,
            data: err.response?.data
        });
        return null;
    }
}

async function getOrCreateConversation(contactId, inboxId) {
    try {
        const cacheKey = `${contactId}-${inboxId}`;
        if (conversationCache.has(cacheKey)) return conversationCache.get(cacheKey);

        const searchUrl = `${CLEANED_CHATWOOT_BASE_URL}/api/v1/accounts/${CHATWOOT_ACCOUNT_ID}/contacts/${contactId}/conversations`;
        const searchRes = await axios.get(searchUrl, {
            headers: { api_access_token: CHATWOOT_API_TOKEN },
        });

        const existingConversation = searchRes.data.payload.find(c => c.inbox_id == inboxId);

        if (existingConversation) {
            console.log(`✅ Found existing conversation: ${existingConversation.id}`);
            conversationCache.set(cacheKey, existingConversation.id);
            return existingConversation.id;
        }

        console.log(`🆕 Creating new conversation for contact ${contactId} in inbox ${inboxId}`);
        const createRes = await axios.post(
            `${CLEANED_CHATWOOT_BASE_URL}/api/v1/accounts/${CHATWOOT_ACCOUNT_ID}/conversations`,
            {
                source_id: `wa-${contactId}`,
                inbox_id: inboxId,
                contact_id: contactId,
            },
            { headers: { api_access_token: CHATWOOT_API_TOKEN } }
        );

        const conversationId = createRes.data.id;
        console.log(`🆔 New conversation created: ${conversationId}`);
        conversationCache.set(cacheKey, conversationId);
        return conversationId;

    } catch (err) {
        console.error('❌ Conversation Error:', {
            message: err.message,
            url: err.config?.url,
            status: err.response?.status,
            data: err.response?.data
        });
        return null;
    }
}

async function sendTextMessageToChatwoot(conversationId, content, originalMessageId, messageType = 'incoming') {
    try {
        const payload = {
            content: content,
            message_type: messageType,
            private: false,
            content_attributes: {
                whatsapp_message_id: originalMessageId,
                source: 'wabot' 
            }
        };

        const response = await axios.post(
            `${CLEANED_CHATWOOT_BASE_URL}/api/v1/accounts/${CHATWOOT_ACCOUNT_ID}/conversations/${conversationId}/messages`,
            payload,
            { headers: { api_access_token: CHATWOOT_API_TOKEN } }
        );
        return response.data;
    } catch (err) {
        console.error('❌ Error in sendTextMessageToChatwoot:', err.response?.data || err.message);
        return null;
    }
}


async function handleWhatsAppMessage(msg) {
    try {
        if (msg.key.remoteJid === 'status@broadcast') {
            return;
        }
        if (!msg.message) return;
        
        const messageTypeForChatwoot = msg.key.fromMe ? 'outgoing' : 'incoming';
        const remoteJid = msg.key.remoteJid;
        const isGroup = remoteJid.endsWith('@g.us');
        let contactId, conversationId, senderDisplayName;
        
        const conversationJid = remoteJid; 
        
        if (isGroup) {
            const groupMeta = await sock.groupMetadata(conversationJid);
            contactId = await getOrCreateContact({
                name: groupMeta.subject,
                phone: remoteJid.replace(/@.+/, ''),
                wa_id: conversationJid,
                isGroup: true
            });
            senderDisplayName = msg.pushName || (msg.key.participant || '').replace(/@.+/, '');
        } else {
            const phone = conversationJid.replace(/@.+/, '');
            senderDisplayName = msg.pushName || phone;
            contactId = await getOrCreateContact({
                name: senderDisplayName,
                phone: phone,
                wa_id: conversationJid,
                isGroup: false
            });
        }
        
        if (!contactId) return console.error(`❌ Failed to get/create contact. Aborting.`);

        conversationId = await getOrCreateConversation(contactId, CHATWOOT_INBOX_ID);
        if (!conversationId) return console.error(`❌ Failed to get/create conversation. Aborting.`);

        const messageType = Object.keys(msg.message)[0];
        if (['protocolMessage', 'senderKeyDistributionMessage'].includes(messageType)) {
             return;
        }

        let sentMessage;
        const isMedia = ['imageMessage', 'videoMessage', 'documentMessage'].includes(messageType);
        
        // Handle quoted messages with context
        let textContent = '';
        let quotedContext = '';
        
        if (msg.message?.conversation) {
            textContent = msg.message.conversation;
            // Check for quote in messageContextInfo
            if (msg.message.messageContextInfo && msg.message.messageContextInfo.quotedMessage) {
                const quotedMsg = msg.message.messageContextInfo.quotedMessage;
                const quotedText = quotedMsg.conversation || quotedMsg.extendedTextMessage?.text || '';
                quotedContext = `\n\n> ${quotedText}`;
            }
        } else if (msg.message?.extendedTextMessage) {
            const extendedText = msg.message.extendedTextMessage;
            textContent = extendedText.text || '';
            
            // Handle quoted message (reply)
            if (extendedText.contextInfo && extendedText.contextInfo.quotedMessage) {
                const quotedMsg = extendedText.contextInfo.quotedMessage;
                const quotedText = quotedMsg.conversation || quotedMsg.extendedTextMessage?.text || '';
                
                // Format quoted context for Chatwoot - use native reply format
                quotedContext = `\n\n> ${quotedText}`;
            }
        } else if (msg.message?.imageMessage) {
            // Handle image with quote
            const imageMsg = msg.message.imageMessage;
            textContent = imageMsg.caption || '';
            
            if (imageMsg.contextInfo && imageMsg.contextInfo.quotedMessage) {
                const quotedMsg = imageMsg.contextInfo.quotedMessage;
                const quotedText = quotedMsg.conversation || quotedMsg.extendedTextMessage?.text || '';
                quotedContext = `\n\n> ${quotedText}`;
            }
        } else if (msg.message?.videoMessage) {
            // Handle video with quote
            const videoMsg = msg.message.videoMessage;
            textContent = videoMsg.caption || '';
            
            if (videoMsg.contextInfo && videoMsg.contextInfo.quotedMessage) {
                const quotedMsg = videoMsg.contextInfo.quotedMessage;
                const quotedText = quotedMsg.conversation || quotedMsg.extendedTextMessage?.text || '';
                quotedContext = `\n\n> ${quotedText}`;
            }
        }
        
        if (isMedia) {
            const media = msg.message[messageType];
            const stream = await downloadContentFromMessage(media, media.mimetype.split('/')[0]);
            const buffer = await streamToBuffer(stream);
            
            let finalContent = isGroup ? `💬 [${senderDisplayName}]: ${media.caption || ''}` : (media.caption || '');
            // Add quoted context if present
            finalContent += quotedContext;

            const formData = new FormData();
            formData.append('content', finalContent);
            formData.append('message_type', messageTypeForChatwoot);
            formData.append('private', 'false');
            formData.append('content_attributes', JSON.stringify({ 
                source: 'wabot',
                whatsapp_message_id: msg.key.id
            }));
            formData.append('attachments[]', buffer, {
                contentType: media.mimetype,
                filename: media.fileName || `upload.${mime.extension(media.mimetype) || 'bin'}`
            });

            const url = `${CLEANED_CHATWOOT_BASE_URL}/api/v1/accounts/${CHATWOOT_ACCOUNT_ID}/conversations/${conversationId}/messages`;
            try {
                const response = await axios.post(url, formData, {
                    headers: { 'api_access_token': CHATWOOT_API_TOKEN, ...formData.getHeaders() }
                });
                sentMessage = response.data;
                console.log(`✅ Media message from ${senderDisplayName} forwarded to Chatwoot`);
                
                // Store media message for potential replies
                const mediaContent = media.caption || `[${messageType}]`;
                storeMessageForReply(remoteJid, msg.key.id, mediaContent, sentMessage?.id);
            } catch (err) {
                console.error('❌ Error processing media:', err.message);
                // Try to send as text message if media fails
                if (textContent) {
                    let finalContent = isGroup ? `💬 [${senderDisplayName}]: ${textContent}` : textContent;
                    finalContent += quotedContext; // Add quoted context
                    sentMessage = await sendTextMessageToChatwoot(conversationId, finalContent, msg.key.id, messageTypeForChatwoot);
                    console.log(`✅ Text fallback from ${senderDisplayName} forwarded to Chatwoot`);
                    
                    // Store fallback message for potential replies
                    storeMessageForReply(remoteJid, msg.key.id, textContent, sentMessage?.id);
                }
            }

        } else if (textContent) {
            let finalContent = isGroup ? `💬 [${senderDisplayName}]: ${textContent}` : textContent;
            finalContent += quotedContext; // Add quoted context
            
            sentMessage = await sendTextMessageToChatwoot(conversationId, finalContent, msg.key.id, messageTypeForChatwoot);
            console.log(`✅ Text message from ${senderDisplayName} forwarded to Chatwoot`);
            
            // Store message for potential replies
            storeMessageForReply(remoteJid, msg.key.id, textContent, sentMessage?.id);
        }
        
        if (sentMessage && msg.key.fromMe) {
            recentlySentMessageIds.add(sentMessage.id);
            setTimeout(() => {
                recentlySentMessageIds.delete(sentMessage.id);
            }, 10000);
        }

    } catch (err) {
        console.error('❌ Error in handleWhatsAppMessage:', err);
    }
}

app.post('/webhook', async (req, res) => {
    try {
        const message = req.body;

        if (recentlySentMessageIds.has(message.id)) {
            console.log(`[Webhook] Ignoring echo for message ID ${message.id}.`);
            return res.sendStatus(200);
        }

        if (message.event !== 'message_created' || message.message_type !== 'outgoing' || !message.sender) {
            return res.sendStatus(200);
        }
        
        const contact = message.conversation.meta.sender;
        if (!contact) return res.sendStatus(200);
        
        const waId = contact.identifier || `${contact.phone_number.replace(/^\+/, '')}@s.whatsapp.net`;
        
        if (!waId.endsWith('@g.us')) {
            const [result] = await sock.onWhatsApp(waId);
            if (!result?.exists) {
                console.warn(`⚠️ Contact ${waId} is not on WhatsApp. Cannot send message.`);
                return res.status(200);
            }
        }
        
        // Check if this is a reply message
        const isReply = message.in_reply_to || message.content_attributes?.in_reply_to;

        if (message.content) {
            
            // Check if message contains quote format - try multiple user-friendly patterns
            let match = null;
            let quotedText = '';
            let actualMessage = '';
            
            // Pattern 1: "> quote text" format (simple)
            match = message.content.match(/^(.+?)\n> (.+?)$/s);
            if (match) {
                actualMessage = match[1].trim();
                quotedText = match[2].trim();
            }
            
            // Pattern 2: "@quote text" format (simple)
            if (!match) {
                match = message.content.match(/^(.+?)\n@(.+?)$/s);
                if (match) {
                    actualMessage = match[1].trim();
                    quotedText = match[2].trim();
                }
            }
            
            // Pattern 3: "reply: quote text" format
            if (!match) {
                match = message.content.match(/^(.+?)\nreply: (.+?)$/s);
                if (match) {
                    actualMessage = match[1].trim();
                    quotedText = match[2].trim();
                }
            }
            
            // Pattern 4: Markdown quote format (native Chatwoot style)
            if (!match) {
                match = message.content.match(/^(.+?)\n\n> (.+?)$/s);
                if (match) {
                    actualMessage = match[1].trim();
                    quotedText = match[2].trim();
                }
            }
            
            if (match && quotedText && actualMessage) {
                
                // Send as regular text with contextInfo
                try {
                    await sock.sendMessage(waId, {
                        text: actualMessage,
                        contextInfo: {
                            quotedMessage: {
                                conversation: quotedText
                            }
                        }
                    });
                    console.log(`📤 ✅ Sent quoted message to ${waId}`);
                } catch (error) {
                    console.error('❌ Error sending quoted message:', error);
                    // Fallback to regular text
                    await sock.sendMessage(waId, { text: actualMessage });
                }
            } else if (isReply) {
                // Handle automatic reply from Chatwoot
                console.log('💬 Processing reply from Chatwoot');
                
                // Try to find the message to reply to
                let quotedMessageId = null;
                
                if (message.content_attributes?.in_reply_to) {
                    quotedMessageId = findMessageByChatwootId(message.content_attributes.in_reply_to);
                }
                
                if (!quotedMessageId) {
                    quotedMessageId = findMessageForReply(waId, message.content);
                }
                
                if (quotedMessageId) {
                    try {
                        // Try format 1: text with contextInfo
                        await sock.sendMessage(waId, {
                            text: message.content,
                            contextInfo: {
                                stanzaId: quotedMessageId,
                                participant: waId
                            }
                        });
                        console.log(`📤 ✅ Sent reply to ${waId}`);
                    } catch (error) {
                        console.error('❌ Error sending reply:', error);
                        // Fallback to regular text
                        await sock.sendMessage(waId, { text: message.content });
                    }
                } else {
                    await sock.sendMessage(waId, { text: message.content });
                }
            } else {
                await sock.sendMessage(waId, { text: message.content });
                console.log(`📤 Sent message to ${waId}`);
            }
        }

        if (message.attachments && message.attachments.length > 0) {
            for (const attachment of message.attachments) {
                const response = await axios.get(attachment.data_url, { responseType: 'arraybuffer' });
                const buffer = Buffer.from(response.data, 'binary');
                const fileType = mime.lookup(attachment.file_name || attachment.data_url);

                // Use the full content (including quoted info) as caption
                const caption = message.content || '';

                if (fileType.startsWith('image/')) {
                    await sock.sendMessage(waId, { image: buffer, caption: caption });
                } else if (fileType.startsWith('video/')) {
                    await sock.sendMessage(waId, { video: buffer, caption: caption });
                } else {
                    await sock.sendMessage(waId, {
                        document: buffer,
                        fileName: attachment.file_name || 'file',
                        mimetype: fileType
                    });
                }
            }
        }
        console.log(`📤 Sent message from Chatwoot to ${waId}`);
        res.sendStatus(200);
    } catch (err) {
        console.error('❌ Error in webhook handler:', err);
        res.status(500).send('Internal Server Error');
    }
});

async function startWhatsApp() {
    console.log(`Starting WhatsApp with session: ${SESSION_NAME}`);
    const { state, saveCreds } = await useMultiFileAuthState(SESSION_NAME);

    sock = makeWASocket({
        auth: state,
        printQRInTerminal: false,
        getMessage: async (key) => ({})
    });

    sock.ev.on('creds.update', saveCreds);

    sock.ev.on('connection.update', (update) => {
        const { connection, lastDisconnect, qr } = update;

        if(qr) {
            console.log('QR Code diterima, silakan pindai:');
            qrcode.generate(qr, { small: true });
        }
        
        if (connection === 'close') {
            const shouldReconnect = (lastDisconnect.error?.output?.statusCode !== DisconnectReason.loggedOut);
            console.log('Koneksi ditutup karena:', lastDisconnect.error, ', menyambungkan kembali:', shouldReconnect);
            if (shouldReconnect) {
                setTimeout(startWhatsApp, 5000);
            }
        } else if (connection === 'open') {
            console.log('✅ Terhubung ke WhatsApp');
        }
    });

    sock.ev.on('messages.upsert', async ({ messages, type }) => {
        if (type !== 'notify') return;
        for (const msg of messages) {
            await handleWhatsAppMessage(msg);
        }
    });
}

// Start the server
app.listen(PORT, () => {
    console.log(`🚀 Server running on port ${PORT}`);
    startWhatsApp();
});

Terimakasih semoga lancar dan sukses selalu.

Mau jualan Pulsa, Kuota, Token DLL terlengkap dengan Jaminan Harga Termurah & Pasti Untung kunjungi: Agen Pulsa Termurah bisa buat usaha atau untuk kebutuhan pribadi.

WhatsApp Center Baileys Chatwoot Integrasi Customer Service