1121 lines
40 KiB
JavaScript
1121 lines
40 KiB
JavaScript
// 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 } |