whatsapp-hybrid-gateway/app.js

1121 lines
40 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

// 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 }