// Load environment variables first require('dotenv').config(); const express = require('express') const multer = require('multer') const fs = require('fs') const path = require('path') const cors = require('cors') const { default: makeWASocket, DisconnectReason, useMultiFileAuthState, downloadMediaMessage } = require('@whiskeysockets/baileys') const qrcode = require('qrcode-terminal') // Configuration from environment variables const config = { // Server configuration port: parseInt(process.env.PORT) || 5000, host: process.env.HOST || '0.0.0.0', nodeEnv: process.env.NODE_ENV || 'development', // Security apiKey: process.env.API_KEY || null, corsOrigin: process.env.CORS_ORIGIN || '*', // File upload limits (MB) maxFileSize: parseInt(process.env.MAX_FILE_SIZE) || 100, // WhatsApp bot configuration botName: process.env.BOT_NAME || 'WhatsApp Gateway + Interactive Bot', sessionDir: process.env.SESSION_DIR || './session', uploadsDir: process.env.UPLOADS_DIR || './uploads', logsDir: process.env.LOGS_DIR || './logs', mediaDir: process.env.MEDIA_DIR || './media', // Bot features enableInteractiveBot: process.env.ENABLE_INTERACTIVE_BOT !== 'false', // Default true enableAPI: process.env.ENABLE_API !== 'false', // Default true // Logging logLevel: process.env.LOG_LEVEL || 'info', logToFile: process.env.LOG_TO_FILE === 'true', // Rate limiting for API rateLimitEnabled: process.env.RATE_LIMIT_ENABLED === 'true', rateLimitMax: parseInt(process.env.RATE_LIMIT_MAX) || 100 } console.log(`šŸš€ Starting ${config.botName}`) console.log(`šŸ“” API Server: ${config.enableAPI ? 'Enabled' : 'Disabled'}`) console.log(`šŸ¤– Interactive Bot: ${config.enableInteractiveBot ? 'Enabled' : 'Disabled'}`) const app = express() // Global variables let sock = null let isConnected = false let qrString = null let isReconnecting = false // Create directories function ensureDirectories() { const dirs = [ config.uploadsDir, config.logsDir, config.sessionDir, config.mediaDir, `${config.mediaDir}/images`, `${config.mediaDir}/audio`, `${config.mediaDir}/documents`, `${config.mediaDir}/stickers`, `${config.mediaDir}/videos` ] dirs.forEach(dir => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }) log(`Created directory: ${dir}`, 'INFO') } }) } // Enhanced logging function log(message, level = 'INFO') { const timestamp = new Date().toISOString() const logMessage = `[${timestamp}] [${level}] ${message}` console.log(logMessage) if (config.logToFile) { try { const logFile = path.join(config.logsDir, 'bot.log') fs.appendFileSync(logFile, logMessage + '\n') } catch (error) { console.error('Logging error:', error.message) } } } // ==================== MIDDLEWARE & SETUP ==================== if (config.enableAPI) { app.use(cors({ origin: config.corsOrigin === '*' ? true : config.corsOrigin.split(','), credentials: true })) app.use(express.json({ limit: `${config.maxFileSize}mb` })) app.use(express.urlencoded({ extended: true, limit: `${config.maxFileSize}mb` })) // API Key authentication middleware function authenticateAPI(req, res, next) { if (!config.apiKey) { return next() } const apiKey = req.headers['x-api-key'] || req.headers['authorization']?.replace('Bearer ', '') if (!apiKey || apiKey !== config.apiKey) { return res.status(401).json({ status: 'error', message: 'Unauthorized: Invalid or missing API key', timestamp: new Date().toISOString() }) } next() } // Apply API key middleware if enabled if (config.apiKey) { app.use('/api', authenticateAPI) log('API key authentication enabled', 'INFO') } } // Multer setup for file uploads const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, config.uploadsDir) }, filename: (req, file, cb) => { const timestamp = Date.now() const randomStr = Math.random().toString(36).substring(2, 8) const ext = path.extname(file.originalname).toLowerCase() const basename = path.basename(file.originalname, ext).replace(/[^a-zA-Z0-9]/g, '_') cb(null, `${timestamp}_${randomStr}_${basename}${ext}`) } }) const upload = multer({ storage: storage, limits: { fileSize: config.maxFileSize * 1024 * 1024, files: 1 } }) // ==================== WHATSAPP CONNECTION ==================== // Function to download media from received messages async function downloadMedia(message) { try { const buffer = await downloadMediaMessage(message, 'buffer', {}) return buffer } catch (error) { log(`āŒ Error downloading media: ${error.message}`) return null } } // Phone number formatting function formatPhoneNumber(phone) { if (!phone || typeof phone !== 'string') { throw new Error('Phone number must be a string') } let cleaned = phone.replace(/\D/g, '') if (cleaned.length < 10 || cleaned.length > 15) { throw new Error('Invalid phone number length') } if (cleaned.startsWith('0')) { cleaned = '62' + cleaned.slice(1) } else if (!cleaned.startsWith('62')) { cleaned = '62' + cleaned } return cleaned + '@s.whatsapp.net' } // File operations function safeDeleteFile(filePath) { try { if (fs.existsSync(filePath)) { fs.unlinkSync(filePath) log(`File deleted: ${path.basename(filePath)}`, 'DEBUG') } } catch (error) { log(`Error deleting file: ${error.message}`, 'ERROR') } } // WhatsApp Bot Connection async function startWhatsAppBot() { if (isReconnecting) { log('Already reconnecting, skipping...', 'WARN') return } try { isReconnecting = true log('Starting WhatsApp Hybrid Bot...') const { state, saveCreds } = await useMultiFileAuthState(config.sessionDir) sock = makeWASocket({ auth: state, browser: [config.botName, 'Chrome', '2.0.0'], syncFullHistory: false, markOnlineOnConnect: false, generateHighQualityLinkPreview: false }) sock.ev.on('connection.update', async (update) => { const { connection, lastDisconnect, qr } = update if (qr) { qrString = qr log('šŸ“± QR Code generated for WhatsApp Gateway') console.log('\n' + '='.repeat(60)) console.log('šŸ“± SCAN QR CODE:') console.log('='.repeat(60)) qrcode.generate(qr, { small: true }) console.log('='.repeat(60)) console.log('āœ… After scanning, both API and Bot will be ready!\n') } if (connection === 'close') { isConnected = false const shouldReconnect = lastDisconnect?.error?.output?.statusCode !== DisconnectReason.loggedOut const reason = lastDisconnect?.error?.output?.statusCode || 'unknown' log(`Connection closed: ${reason}`, 'WARN') if (shouldReconnect) { log('Reconnecting in 5 seconds...') setTimeout(() => { isReconnecting = false startWhatsAppBot() }, 5000) } else { log('Bot logged out, need QR scan', 'WARN') qrString = null isReconnecting = false } } else if (connection === 'open') { isConnected = true qrString = null isReconnecting = false log('āœ… WhatsApp Hybrid System connected successfully!') console.log('\nšŸš€ SYSTEM READY!') if (config.enableAPI) console.log(`šŸ“” API Server: http://${config.host}:${config.port}`) if (config.enableInteractiveBot) console.log(`šŸ¤– Interactive Bot: Online`) console.log('') // Send startup notification const myNumber = sock.user?.id if (myNumber && config.enableInteractiveBot) { await sock.sendMessage(myNumber, { text: `šŸš€ ${config.botName} Online!\n\n` + `${config.enableAPI ? 'šŸ“” API Gateway: Ready\n' : ''}` + `${config.enableInteractiveBot ? 'šŸ¤– Interactive Bot: Ready\n' : ''}` + `šŸ’¬ Sistem siap digunakan!` }) } } }) sock.ev.on('creds.update', saveCreds) // Handle incoming messages for interactive bot if (config.enableInteractiveBot) { sock.ev.on('messages.upsert', async (messageUpdate) => { try { const messages = messageUpdate.messages for (const message of messages) { // Skip messages from bot itself and status updates if (message.key.fromMe || message.key.remoteJid === 'status@broadcast') continue const sender = message.key.remoteJid const senderName = message.pushName || 'Unknown' const isGroup = sender.endsWith('@g.us') // Handle interactive bot messages await handleInteractiveBotMessage(sock, message, sender, senderName, isGroup) } } catch (error) { log(`āŒ Error processing interactive messages: ${error.message}`) } }) } } catch (error) { log(`Error starting bot: ${error.message}`, 'ERROR') isReconnecting = false setTimeout(() => startWhatsAppBot(), 10000) } } // ==================== INTERACTIVE BOT HANDLERS ==================== async function handleInteractiveBotMessage(sock, message, sender, senderName, isGroup) { try { // Get message text and media info const text = message.message?.conversation || message.message?.extendedTextMessage?.text || '' const hasImage = message.message?.imageMessage const hasAudio = message.message?.audioMessage const hasDocument = message.message?.documentMessage const hasSticker = message.message?.stickerMessage const hasVideo = message.message?.videoMessage // Log received messages if (text) log(`šŸ“© Interactive: Text from ${senderName}: ${text}`) if (hasImage) log(`šŸ–¼ļø Interactive: Image from ${senderName}`) if (hasAudio) log(`šŸŽµ Interactive: Audio from ${senderName}`) if (hasDocument) log(`šŸ“„ Interactive: Document from ${senderName}`) if (hasSticker) log(`šŸ˜„ Interactive: Sticker from ${senderName}`) if (hasVideo) log(`šŸŽ¬ Interactive: Video from ${senderName}`) // Handle text commands if (text) { await handleInteractiveTextCommands(sock, sender, text.toLowerCase().trim(), senderName) } // Handle media auto-responses if (hasImage) await handleReceivedImage(sock, message, sender, senderName) if (hasAudio) await handleReceivedAudio(sock, message, sender, senderName) if (hasDocument) await handleReceivedDocument(sock, message, sender, senderName) if (hasSticker) await handleReceivedSticker(sock, message, sender, senderName) if (hasVideo) await handleReceivedVideo(sock, message, sender, senderName) } catch (error) { log(`āŒ Error handling interactive message: ${error.message}`) await sock.sendMessage(sender, { text: 'āš ļø Error processing message.' }) } } async function handleInteractiveTextCommands(sock, sender, message, senderName) { let response = '' switch (message) { case '/help': case '/start': response = `šŸ¤– *${config.botName}*\n\n` + `Halo ${senderName}! Ini adalah hybrid system dengan:\n\n` + `${config.enableAPI ? 'šŸ“” REST API Gateway\n' : ''}` + `${config.enableInteractiveBot ? 'šŸ¤– Interactive Bot\n' : ''}\n` + `šŸ“± Ketik /multimedia untuk fitur multimedia\n` + `ā„¹ļø Ketik /info untuk info sistem` break case '/multimedia': case '/media': response = `šŸŽÆ *Multimedia Bot Commands*\n\n` + `šŸ“± *KIRIM MEDIA:*\n` + `/foto - Kirim foto sample\n` + `/audio - Kirim audio sample\n` + `/dokumen - Kirim dokumen\n` + `/sticker - Kirim sticker\n` + `/video - Kirim video sample\n\n` + `šŸ”„ *AUTO RESPON:*\n` + `Kirim gambar → Bot balas gambar\n` + `Kirim audio → Bot balas audio\n` + `Kirim sticker → Bot balas sticker\n\n` + `✨ *FITUR LAIN:*\n` + `/meme - Random meme\n` + `/quote - Quote inspiratif` break case '/info': const uptime = process.uptime() const uptimeStr = Math.floor(uptime / 3600) + 'h ' + Math.floor((uptime % 3600) / 60) + 'm' response = `ā„¹ļø *System Information*\n\n` + `šŸ–„ļø ${config.botName}\n` + `šŸŒ Environment: ${config.nodeEnv}\n` + `ā±ļø Uptime: ${uptimeStr}\n` + `šŸ’¾ Memory: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB\n\n` + `šŸ“” API Gateway: ${config.enableAPI ? 'Online' : 'Disabled'}\n` + `šŸ¤– Interactive Bot: ${config.enableInteractiveBot ? 'Online' : 'Disabled'}\n` + `${config.enableAPI ? `šŸ”— API URL: http://localhost:${config.port}\n` : ''}` break case '/foto': case '/gambar': await sock.sendMessage(sender, { image: { url: 'https://picsum.photos/800/600' }, caption: `šŸ“ø *Sample Gambar untuk ${senderName}*\n\n` + `šŸŽ² Random image dari Picsum\n` + `šŸ“ Resolusi: 800x600\n` + `ā° Waktu: ${new Date().toLocaleTimeString('id-ID')}\n\n` + `✨ Dari Interactive Bot!` }) log(`šŸ“ø Interactive: Photo sent to ${senderName}`) return case '/audio': case '/suara': await sock.sendMessage(sender, { audio: { url: 'https://www.soundjay.com/misc/sounds/bell-ringing-05.wav' }, mimetype: 'audio/mpeg', ptt: true }) log(`šŸŽµ Interactive: Audio sent to ${senderName}`) return case '/dokumen': const docContent = `${config.botName} Report\n\n` + `User: ${senderName}\n` + `Timestamp: ${new Date().toISOString()}\n` + `System: ${config.nodeEnv}\n\n` + `Features Active:\n` + `- API Gateway: ${config.enableAPI}\n` + `- Interactive Bot: ${config.enableInteractiveBot}\n` + `- Multimedia Support: Yes\n\n` + `Generated by Interactive Bot` fs.writeFileSync(`${config.mediaDir}/documents/report.txt`, docContent) await sock.sendMessage(sender, { document: fs.readFileSync(`${config.mediaDir}/documents/report.txt`), mimetype: 'text/plain', fileName: `System_Report_${Date.now()}.txt`, caption: `šŸ“„ *System Report untuk ${senderName}*\n\nGenerated by Interactive Bot` }) log(`šŸ“„ Interactive: Document sent to ${senderName}`) return case '/sticker': await sock.sendMessage(sender, { sticker: { url: 'https://i.imgur.com/2LnzGOf.jpeg' } }) log(`šŸ˜„ Interactive: Sticker sent to ${senderName}`) return case '/video': await sock.sendMessage(sender, { video: { url: 'https://sample-videos.com/zip/10/3gp/240/SampleVideo_240x160_1mb_3gp.3gp' }, caption: `šŸŽ¬ *Sample Video untuk ${senderName}*\n\nDari Interactive Bot!` }) log(`šŸŽ¬ Interactive: Video sent to ${senderName}`) return case '/meme': const memeTexts = [ "When your hybrid bot works perfectly", "API + Interactive Bot = Perfect", "Running on Ubuntu VPS like a boss", "Multiple features in one system" ] const randomMeme = memeTexts[Math.floor(Math.random() * memeTexts.length)] await sock.sendMessage(sender, { image: { url: 'https://picsum.photos/600/400' }, caption: `šŸ˜‚ *Meme untuk ${senderName}*\n\n"${randomMeme}"` }) return case '/quote': const quotes = [ "Hybrid systems are the future! šŸš€", "One bot, multiple functions, endless possibilities! šŸ’«", "API + Interactive = Perfect combination! ⚔" ] const randomQuote = quotes[Math.floor(Math.random() * quotes.length)] await sock.sendMessage(sender, { image: { url: 'https://picsum.photos/800/500' }, caption: `šŸ’­ *Quote Inspiratif*\n\n"${randomQuote}"\n\n✨ Untuk: ${senderName}` }) return default: if (message.includes('halo') || message.includes('hai')) { response = `šŸ‘‹ Halo ${senderName}! Saya hybrid bot dengan API dan interactive features.\n\nKetik /multimedia untuk fitur lengkap!` } else if (message.includes('bot')) { response = `šŸ¤– Ya, saya ${config.botName}!\n\nšŸ“± /multimedia - Lihat semua fitur` } } if (response) { await new Promise(resolve => setTimeout(resolve, 1000)) await sock.sendMessage(sender, { text: response }) log(`āœ… Interactive: Response sent to ${senderName}`) } } // Media handlers for interactive bot async function handleReceivedImage(sock, message, sender, senderName) { try { const buffer = await downloadMedia(message) if (!buffer) return const filename = `received_${Date.now()}.jpg` fs.writeFileSync(`${config.mediaDir}/images/${filename}`, buffer) await sock.sendMessage(sender, { image: { url: 'https://picsum.photos/700/500' }, caption: `šŸ–¼ļø *Gambar diterima dari ${senderName}!*\n\n` + `āœ… Diproses oleh Interactive Bot\n` + `šŸ’¾ Disimpan sebagai: ${filename}\n` + `šŸ“ Size: ${Math.round(buffer.length / 1024)} KB` }) log(`šŸ–¼ļø Interactive: Image processed for ${senderName}`) } catch (error) { log(`āŒ Interactive: Error handling image: ${error.message}`) } } async function handleReceivedAudio(sock, message, sender, senderName) { try { const buffer = await downloadMedia(message) if (!buffer) return const filename = `received_audio_${Date.now()}.ogg` fs.writeFileSync(`${config.mediaDir}/audio/${filename}`, buffer) await sock.sendMessage(sender, { audio: { url: 'https://www.soundjay.com/misc/sounds/bell-ringing-05.wav' }, mimetype: 'audio/ogg; codecs=opus', ptt: true, }) await sock.sendMessage(sender, { text: `šŸŽµ *Audio diterima dari ${senderName}!*\n\n` + `āœ… Diproses oleh Interactive Bot\n` + `šŸ’¾ Disimpan sebagai: ${filename}\n` + `šŸŽ¤ Auto-reply dengan voice note!` }) log(`šŸŽµ Interactive: Audio processed for ${senderName}`) } catch (error) { log(`āŒ Interactive: Error handling audio: ${error.message}`) } } async function handleReceivedDocument(sock, message, sender, senderName) { try { const docInfo = message.message.documentMessage const fileName = docInfo.fileName || 'unknown_document' const fileSize = Math.round(docInfo.fileLength / 1024) await sock.sendMessage(sender, { text: `šŸ“„ *Dokumen diterima dari ${senderName}!*\n\n` + `šŸ“ Nama file: ${fileName}\n` + `šŸ“Š Ukuran: ${fileSize} KB\n` + `šŸ¤– Diproses oleh Interactive Bot` }) log(`šŸ“„ Interactive: Document processed for ${senderName}`) } catch (error) { log(`āŒ Interactive: Error handling document: ${error.message}`) } } async function handleReceivedSticker(sock, message, sender, senderName) { try { const buffer = await downloadMedia(message) if (!buffer) return const filename = `received_sticker_${Date.now()}.webp` fs.writeFileSync(`${config.mediaDir}/stickers/${filename}`, buffer) await sock.sendMessage(sender, { sticker: { url: 'https://i.imgur.com/2LnzGOf.jpeg' } }) await sock.sendMessage(sender, { text: `šŸ˜„ *Sticker diterima dari ${senderName}!*\n\n` + `āœ… Diproses oleh Interactive Bot\n` + `šŸŽØ Auto-reply dengan sticker!` }) log(`šŸ˜„ Interactive: Sticker processed for ${senderName}`) } catch (error) { log(`āŒ Interactive: Error handling sticker: ${error.message}`) } } async function handleReceivedVideo(sock, message, sender, senderName) { try { const videoInfo = message.message.videoMessage const fileSize = Math.round(videoInfo.fileLength / 1024) await sock.sendMessage(sender, { text: `šŸŽ¬ *Video diterima dari ${senderName}!*\n\n` + `āœ… Diproses oleh Interactive Bot\n` + `šŸ“Š Size: ${fileSize} KB` }) log(`šŸŽ¬ Interactive: Video processed for ${senderName}`) } catch (error) { log(`āŒ Interactive: Error handling video: ${error.message}`) } } // ==================== API ENDPOINTS ==================== if (config.enableAPI) { // Send WhatsApp message with retry (for API) async function sendWhatsAppMessage(phone, content, retries = 3) { if (!isConnected || !sock) { throw new Error('Bot not connected') } const formattedPhone = formatPhoneNumber(phone) for (let attempt = 1; attempt <= retries; attempt++) { try { const result = await sock.sendMessage(formattedPhone, content) return result } catch (error) { log(`API: Send attempt ${attempt} failed: ${error.message}`, 'WARN') if (attempt === retries) { throw error } await new Promise(resolve => setTimeout(resolve, 1000 * attempt)) } } } // Response functions function sendErrorResponse(res, message, statusCode = 400) { const response = { status: "error", message: message, timestamp: new Date().toISOString() } log(`API Error ${statusCode}: ${message}`, 'ERROR') res.status(statusCode).json(response) } function sendSuccessResponse(res, message, data = null) { const response = { status: "success", message: message, ...(data && { data: data }) } res.json(response) } // API Routes // Health check with both API and Interactive Bot status app.get('/', (req, res) => { try { res.json({ service: config.botName, status: "running", bot_connected: isConnected, version: "2.0.0", environment: config.nodeEnv, features: { api_gateway: config.enableAPI ? "āœ… Enabled" : "āŒ Disabled", interactive_bot: config.enableInteractiveBot ? "āœ… Enabled" : "āŒ Disabled", text_messages: "āœ… Working", image_messages: "āœ… Working", document_messages: "āœ… Working", audio_messages: "āœ… Available", video_messages: "āœ… Available", sticker_messages: "āœ… Available" }, endpoints: [ "POST /api/send-message - Send text message", "POST /api/send-image - Send image with caption", "POST /api/send-document - Send document with caption", "POST /api/send-audio - Send audio file", "POST /api/send-video - Send video with caption", "POST /api/send-sticker - Send WebP sticker", "GET /api/status - Bot status" ], uptime: process.uptime(), memory: { used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024) } }) } catch (error) { sendErrorResponse(res, 'Internal server error', 500) } }) // Bot status app.get('/api/status', (req, res) => { try { res.json({ bot_connected: isConnected, api_gateway: config.enableAPI, interactive_bot: config.enableInteractiveBot, environment: config.nodeEnv, uptime: process.uptime(), memory_usage_mb: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), message: "Hybrid system status retrieved" }) } catch (error) { sendErrorResponse(res, 'Error retrieving status', 500) } }) // Send text message via API app.post('/api/send-message', async (req, res) => { try { const { phone, message } = req.body if (!phone) { return sendErrorResponse(res, 'Phone number required') } if (!message) { return sendErrorResponse(res, 'Message text required') } if (!isConnected) { return sendErrorResponse(res, 'Bot not connected', 503) } await sendWhatsAppMessage(phone, { text: message }) log(`API: Text sent to ${phone}: ${message.substring(0, 50)}...`) sendSuccessResponse(res, 'Text message sent successfully', { phone: phone, message: message, type: "text", timestamp: new Date().toISOString().replace('T', ' ').split('.')[0] }) } catch (error) { log(`API: Error sending text: ${error.message}`, 'ERROR') sendErrorResponse(res, `Failed to send message: ${error.message}`, 500) } }) // Send image via API app.post('/api/send-image', upload.single('file'), async (req, res) => { let filePath = null try { const { phone, caption } = req.body const file = req.file if (file) { filePath = file.path } if (!phone) { return sendErrorResponse(res, 'Phone number required') } if (!file) { return sendErrorResponse(res, 'Image file required') } if (!isConnected) { return sendErrorResponse(res, 'Bot not connected', 503) } const imageBuffer = fs.readFileSync(filePath) const messageContent = { image: imageBuffer, ...(caption && { caption: caption }) } await sendWhatsAppMessage(phone, messageContent) const fileSize = Math.round(fs.statSync(filePath).size / 1024 * 100) / 100 log(`API: Image sent to ${phone}: ${file.originalname}`) sendSuccessResponse(res, 'Image sent successfully', { phone: phone, filename: file.originalname, caption: caption || null, type: "image", file_size_kb: fileSize, timestamp: new Date().toISOString().replace('T', ' ').split('.')[0] }) } catch (error) { log(`API: Error sending image: ${error.message}`, 'ERROR') sendErrorResponse(res, `Failed to send image: ${error.message}`, 500) } finally { if (filePath) { safeDeleteFile(filePath) } } }) // Send document via API app.post('/api/send-document', upload.single('file'), async (req, res) => { let filePath = null try { const { phone, caption } = req.body const file = req.file if (file) { filePath = file.path } if (!phone) { return sendErrorResponse(res, 'Phone number required') } if (!file) { return sendErrorResponse(res, 'Document file required') } if (!isConnected) { return sendErrorResponse(res, 'Bot not connected', 503) } const documentBuffer = fs.readFileSync(filePath) const messageContent = { document: documentBuffer, mimetype: file.mimetype || 'application/octet-stream', fileName: file.originalname, ...(caption && { caption: caption }) } await sendWhatsAppMessage(phone, messageContent) const fileSize = Math.round(fs.statSync(filePath).size / 1024 * 100) / 100 log(`API: Document sent to ${phone}: ${file.originalname}`) sendSuccessResponse(res, 'Document sent successfully', { phone: phone, filename: file.originalname, caption: caption || null, type: "document", file_size_kb: fileSize, timestamp: new Date().toISOString().replace('T', ' ').split('.')[0] }) } catch (error) { log(`API: Error sending document: ${error.message}`, 'ERROR') sendErrorResponse(res, `Failed to send document: ${error.message}`, 500) } finally { if (filePath) { safeDeleteFile(filePath) } } }) // Send audio via API app.post('/api/send-audio', upload.single('file'), async (req, res) => { let filePath = null try { const { phone } = req.body const file = req.file if (file) { filePath = file.path } if (!phone) { return sendErrorResponse(res, 'Phone number required') } if (!file) { return sendErrorResponse(res, 'Audio file required') } if (!isConnected) { return sendErrorResponse(res, 'Bot not connected', 503) } const audioBuffer = fs.readFileSync(filePath) const messageContent = { audio: audioBuffer, mimetype: file.mimetype || 'audio/mpeg', ptt: true } await sendWhatsAppMessage(phone, messageContent) const fileSize = Math.round(fs.statSync(filePath).size / 1024 * 100) / 100 log(`API: Audio sent to ${phone}: ${file.originalname}`) sendSuccessResponse(res, 'Audio sent successfully', { phone: phone, filename: file.originalname, type: "audio", file_size_kb: fileSize, timestamp: new Date().toISOString().replace('T', ' ').split('.')[0] }) } catch (error) { log(`API: Error sending audio: ${error.message}`, 'ERROR') sendErrorResponse(res, `Failed to send audio: ${error.message}`, 500) } finally { if (filePath) { safeDeleteFile(filePath) } } }) // Send video via API app.post('/api/send-video', upload.single('file'), async (req, res) => { let filePath = null try { const { phone, caption } = req.body const file = req.file if (file) { filePath = file.path } if (!phone) { return sendErrorResponse(res, 'Phone number required') } if (!file) { return sendErrorResponse(res, 'Video file required') } if (!isConnected) { return sendErrorResponse(res, 'Bot not connected', 503) } const videoBuffer = fs.readFileSync(filePath) const messageContent = { video: videoBuffer, ...(caption && { caption: caption }) } await sendWhatsAppMessage(phone, messageContent) const fileSize = Math.round(fs.statSync(filePath).size / 1024 * 100) / 100 log(`API: Video sent to ${phone}: ${file.originalname}`) sendSuccessResponse(res, 'Video sent successfully', { phone: phone, filename: file.originalname, caption: caption || null, type: "video", file_size_kb: fileSize, timestamp: new Date().toISOString().replace('T', ' ').split('.')[0] }) } catch (error) { log(`API: Error sending video: ${error.message}`, 'ERROR') sendErrorResponse(res, `Failed to send video: ${error.message}`, 500) } finally { if (filePath) { safeDeleteFile(filePath) } } }) // Send sticker via API app.post('/api/send-sticker', upload.single('file'), async (req, res) => { let filePath = null try { const { phone } = req.body const file = req.file if (file) { filePath = file.path } if (!phone) { return sendErrorResponse(res, 'Phone number required') } if (!file) { return sendErrorResponse(res, 'Sticker file required') } if (!isConnected) { return sendErrorResponse(res, 'Bot not connected', 503) } const stickerBuffer = fs.readFileSync(filePath) const messageContent = { sticker: stickerBuffer } await sendWhatsAppMessage(phone, messageContent) const fileSize = Math.round(fs.statSync(filePath).size / 1024 * 100) / 100 log(`API: Sticker sent to ${phone}: ${file.originalname}`) sendSuccessResponse(res, 'Sticker sent successfully', { phone: phone, filename: file.originalname, type: "sticker", file_size_kb: fileSize, timestamp: new Date().toISOString().replace('T', ' ').split('.')[0] }) } catch (error) { log(`API: Error sending sticker: ${error.message}`, 'ERROR') sendErrorResponse(res, `Failed to send sticker: ${error.message}`, 500) } finally { if (filePath) { safeDeleteFile(filePath) } } }) // Error handling middleware app.use((error, req, res, next) => { if (req.file && req.file.path) { safeDeleteFile(req.file.path) } if (error instanceof multer.MulterError) { if (error.code === 'LIMIT_FILE_SIZE') { return sendErrorResponse(res, `File too large. Max size: ${config.maxFileSize}MB`, 400) } } log(`API: Global error handler: ${error.message}`, 'ERROR') sendErrorResponse(res, 'Internal server error', 500) }) // 404 handler app.use((req, res) => { sendErrorResponse(res, 'Endpoint not found', 404) }) } // ==================== SERVER STARTUP ==================== // Graceful shutdown handling function gracefulShutdown(signal) { log(`Received ${signal}, starting graceful shutdown...`, 'INFO') if (server) { server.close(() => { log('HTTP server closed', 'INFO') if (sock) { try { sock.end() log('WhatsApp connection closed', 'INFO') } catch (error) { log(`Error closing WhatsApp connection: ${error.message}`, 'ERROR') } } process.exit(0) }) setTimeout(() => { log('Forced shutdown after timeout', 'ERROR') process.exit(1) }, 10000) } else { process.exit(0) } } // Process error handlers process.on('SIGINT', () => gracefulShutdown('SIGINT')) process.on('SIGTERM', () => gracefulShutdown('SIGTERM')) process.on('uncaughtException', (error) => { log(`Uncaught Exception: ${error.message}`, 'ERROR') console.error(error.stack) process.exit(1) }) process.on('unhandledRejection', (reason, promise) => { log(`Unhandled Rejection at ${promise}: ${reason}`, 'ERROR') }) // Initialize application function initialize() { ensureDirectories() startWhatsAppBot() log('Hybrid system initialized successfully', 'INFO') } // Start server let server = null if (config.enableAPI) { server = app.listen(config.port, config.host, () => { log(`${config.botName} started successfully`, 'INFO') log(`API Server listening on ${config.host}:${config.port}`, 'INFO') log(`Environment: ${config.nodeEnv}`, 'INFO') initialize() }) } else { log(`Starting ${config.botName} (API disabled)`, 'INFO') initialize() } // Export for testing module.exports = { app: config.enableAPI ? app : null, config, sock: () => sock }