From 7d7a82a8d3be538126dc7dbbae7a0eebccf0cf0c Mon Sep 17 00:00:00 2001 From: andri Date: Tue, 23 Sep 2025 19:46:54 +0000 Subject: [PATCH] Add app.js --- app.js | 1121 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1121 insertions(+) create mode 100644 app.js diff --git a/app.js b/app.js new file mode 100644 index 0000000..9431d90 --- /dev/null +++ b/app.js @@ -0,0 +1,1121 @@ +// 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 } \ No newline at end of file