UNPKG

tes-wc-bot

Version:

🌐 Advanced Telegram Bot for Cloudflare Wildcard Domain Management with Telegram Notifications

506 lines (421 loc) 16.8 kB
const { Telegraf, Markup } = require('telegraf'); // Import Markup const { readConfig } = require('./utils/fileUtils'); const { BOT_TOKEN } = require('./config/default'); // Import handlers for required commands only const { handleAddCf, handleCfConfig, handleUpdateCf, handleDeleteCf, } = require('./handlers/configHandlers'); const { handleNew, handleListDomain, handleSearchDomain, handleDelSub, handleMySub, } = require('./handlers/domainHandlers'); const { handleSetupWildcard } = require('./handlers/cloudflareHandlers'); const { handleUnknownCommand, handleBroadcast, handleTestNotification, } = require('./handlers/generalHandlers'); // Initialize bot const bot = new Telegraf(BOT_TOKEN); // Advanced rate limiting with queue system for multiple users const userQueues = new Map(); const MAX_CONCURRENT_REQUESTS = 100; // Max concurrent requests const USER_RATE_LIMIT = 5; // Max requests per user per second const REQUEST_TIMEOUT = 30000; // 30 seconds timeout class RequestQueue { constructor() { this.queue = []; this.processing = false; this.lastRequestTime = 0; this.requestCount = 0; this.resetTime = Date.now(); } async add(request) { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Request timeout')); }, REQUEST_TIMEOUT); this.queue.push({ request, resolve: (result) => { clearTimeout(timeout); resolve(result); }, reject: (error) => { clearTimeout(timeout); reject(error); }, }); this.process(); }); } async process() { if (this.processing || this.queue.length === 0) return; this.processing = true; while (this.queue.length > 0) { const now = Date.now(); // Reset rate limit counter every second if (now - this.resetTime > 1000) { this.requestCount = 0; this.resetTime = now; } // Check rate limit if (this.requestCount >= USER_RATE_LIMIT) { await new Promise((resolve) => setTimeout(resolve, 100)); continue; } const { request, resolve, reject } = this.queue.shift(); this.requestCount++; try { const result = await request(); resolve(result); } catch (error) { reject(error); } // Small delay only if needed (not for every request) if (this.queue.length > 10) { await new Promise((resolve) => setTimeout(resolve, 50)); } } this.processing = false; } } // Global request counter for monitoring let totalActiveRequests = 0; // Performance monitoring const botStats = { totalRequests: 0, successfulRequests: 0, errorRequests: 0, activeUsers: new Set(), startTime: Date.now(), // Performance metrics averageResponseTime: 0, totalResponseTime: 0, maxConcurrentRequests: 0, log() { const uptime = Math.floor((Date.now() - this.startTime) / 1000); const avgResponseTime = this.totalRequests > 0 ? Math.round(this.totalResponseTime / this.totalRequests) : 0; console.log( `📊 Bot Stats: ${this.totalRequests} requests | ${this.activeUsers.size} users | ${totalActiveRequests} active | ${avgResponseTime}ms avg | ${uptime}s uptime` ); }, }; // Log stats every 5 minutes setInterval(() => { if (botStats.totalRequests > 0) { botStats.log(); } }, 300000); // Smart rate limiting middleware bot.use(async (ctx, next) => { const userId = ctx.from?.id; if (!userId) return next(); const requestStart = Date.now(); botStats.totalRequests++; botStats.activeUsers.add(userId); // Update max concurrent requests if (totalActiveRequests > botStats.maxConcurrentRequests) { botStats.maxConcurrentRequests = totalActiveRequests; } // Check global load if (totalActiveRequests >= MAX_CONCURRENT_REQUESTS) { botStats.errorRequests++; // Silent rejection - don't spam user with error messages return; } // Get or create user queue if (!userQueues.has(userId)) { userQueues.set(userId, new RequestQueue()); } const userQueue = userQueues.get(userId); try { totalActiveRequests++; await userQueue.add(async () => { return await next(); }); // Calculate response time const responseTime = Date.now() - requestStart; botStats.totalResponseTime += responseTime; botStats.successfulRequests++; } catch (error) { botStats.errorRequests++; // Silent error handling - log but don't spam user console.error(`❌ Request error for user ${userId}:`, error.message); } finally { totalActiveRequests--; // Cleanup inactive queues periodically if (Math.random() < 0.01) { // 1% chance to cleanup bot.cleanupQueues(); } } }); // Cleanup function for inactive queues bot.cleanupQueues = function () { const now = Date.now(); for (const [userId, queue] of userQueues.entries()) { if (queue.queue.length === 0 && !queue.processing && now - queue.lastRequestTime > 300000) { // 5 minutes inactive userQueues.delete(userId); } } }; // Optimized error handling middleware bot.catch((err, ctx) => { // Log error for monitoring but don't spam users console.error('❌ Bot error:', { error: err.message, userId: ctx.from?.id, command: ctx.message?.text?.split(' ')[0], timestamp: new Date().toISOString(), }); // Only respond to critical errors, not rate limit or timeout errors if ( !err.message.includes('429') && !err.message.includes('timeout') && !err.message.includes('ETELEGRAM') && ctx.chat?.type === 'private' ) { // Silent error response - minimal and only once ctx.reply( '❌ **Terjadi kendala sementara**\n\n' + '🔄 Silakan coba lagi dalam beberapa detik\n' + '💡 Jika masalah berlanjut, gunakan `/start` untuk refresh', { parse_mode: 'Markdown' } ).catch(() => {}); // Silent catch to prevent error loops } }); // Middleware untuk log user interaction (simplified) bot.use(async (ctx, next) => { const user = ctx.from; const chatType = ctx.chat?.type; // Restrict to private chats only if (chatType !== 'private') { return; } // Simplified logging - only log commands, not every message if (user && ctx.message?.text?.startsWith('/')) { const command = ctx.message.text.split(' ')[0]; console.log(`👤 ${user.first_name} | ${command}`); } return next(); }); // --- Callback Query Handlers --- // Ini adalah bagian baru untuk menangani klik tombol inline keyboard // Anda perlu menambahkan handler untuk setiap 'callback_data' yang Anda definisikan bot.action('addcf_command', handleAddCf); bot.action('cfconfig_command', handleCfConfig); bot.action('updatecf_command', handleUpdateCf); bot.action('deletecf_command', handleDeleteCf); bot.action('listdomain_command', handleListDomain); bot.action('new_command', handleNew); bot.action('mysub_command', handleMySub); bot.action('searchdomain_command', handleSearchDomain); bot.action('delsub_command', handleDelSub); bot.action('setupwildcard_command', handleSetupWildcard); // Untuk instruksi setup wildcard // Anda mungkin perlu menambahkan handler untuk admin commands jika ingin tombolnya muncul untuk admin // bot.action('broadcast_command', handleBroadcast); // bot.action('testnotif_command', handleTestNotification); // Configuration Commands bot.command('start', async (ctx) => { const userId = String(ctx.from.id); const configData = readConfig(); const isRegistered = !!configData[userId]; let welcomeMessage = '**SELAMAT DATANG!**\n\n'; welcomeMessage += 'Bot untuk mengelola wildcard domain dengan mudah!\n\n'; let keyboard; if (!isRegistered) { welcomeMessage += '**MULAI DENGAN REGISTRASI:**\n'; welcomeMessage += '```\n/addcf <global_api_key> <email>\n```\n\n'; welcomeMessage += '🔑 **Cara mendapat Global API Key:**\n'; welcomeMessage += '1️⃣ Buka [Cloudflare Dashboard](https://dash.cloudflare.com)\n'; welcomeMessage += '2️⃣ Klik profil → **My Profile** → **API Tokens**\n'; welcomeMessage += '3️⃣ Di **Global API Key**, klik **"View"** dan copy\n\n'; welcomeMessage += '💡 **Setelah registrasi, semua fitur akan terbuka!**\n\n'; keyboard = Markup.inlineKeyboard([ [Markup.button.callback('➕ Daftar Cloudflare', 'addcf_command')], [Markup.button.callback('📊 Lihat Konfigurasi', 'cfconfig_command')] ]); } else { welcomeMessage += '✅ **Anda sudah terdaftar! Siap digunakan** 🎯\n\n'; welcomeMessage += 'Pilih kategori di bawah ini untuk melihat perintah:\n'; keyboard = Markup.inlineKeyboard([ [Markup.button.callback('🔧 Konfigurasi', 'menu_config')], [Markup.button.callback('🌐 Domain', 'menu_domain')], [Markup.button.callback('⚙️ Lainnya', 'menu_other')] // Untuk hal seperti analitik atau admin ]); } return ctx.reply(welcomeMessage, { parse_mode: 'Markdown', disable_web_page_preview: true, reply_markup: keyboard }); }); // --- Menu Handlers for Inline Keyboard --- bot.action('menu_config', async (ctx) => { const configMenuMessage = '🔧 **Menu Konfigurasi:**\n\n' + 'Pilih aksi yang ingin Anda lakukan:\n'; const configKeyboard = Markup.inlineKeyboard([ [Markup.button.callback('📊 Lihat Konfigurasi', 'cfconfig_command')], [Markup.button.callback('✏️ Update Konfigurasi', 'updatecf_command')], [Markup.button.callback('🗑️ Hapus Konfigurasi', 'deletecf_command')], [Markup.button.callback('⬅️ Kembali ke Menu Utama', 'start_command')] ]); await ctx.editMessageText(configMenuMessage, { parse_mode: 'Markdown', reply_markup: configKeyboard }); }); bot.action('menu_domain', async (ctx) => { const domainMenuMessage = '🌐 **Menu Domain Management:**\n\n' + 'Pilih perintah terkait domain:\n' + '• Untuk menambahkan subdomain baru, gunakan: `/new <subdomain>`\n' + '• Untuk setup wildcard, gunakan: `/setupwildcard <domain>`\n'; // Tambah instruksi manual untuk command dengan argumen const domainKeyboard = Markup.inlineKeyboard([ [Markup.button.callback('📋 Daftar Domain Tersedia', 'listdomain_command')], [Markup.button.callback('➕ Tambah Subdomain (Manual)', 'new_command')], // ini akan hanya memicu handler, argumennya tetap manual [Markup.button.callback('🔍 Cari Domain', 'searchdomain_command')], [Markup.button.callback('✨ Subdomain Saya', 'mysub_command')], [Markup.button.callback('🗑️ Hapus Subdomain', 'delsub_command')], [Markup.button.callback('⬅️ Kembali ke Menu Utama', 'start_command')] ]); await ctx.editMessageText(domainMenuMessage, { parse_mode: 'Markdown', reply_markup: domainKeyboard }); }); bot.action('menu_other', async (ctx) => { const otherMenuMessage = '⚙️ **Menu Lainnya:**\n\n' + 'Pilih opsi di bawah:\n'; const otherKeyboard = Markup.inlineKeyboard([ [Markup.button.callback('📊 Lihat Statistik Bot', 'stats_command')], // asumsi ada handler untuk admin [Markup.button.callback('📢 Broadcast Pesan (Admin)', 'broadcast_command')], // asumsi ada handler untuk admin [Markup.button.callback('⬅️ Kembali ke Menu Utama', 'start_command')] ]); await ctx.editMessageText(otherMenuMessage, { parse_mode: 'Markdown', reply_markup: otherKeyboard }); }); // Fallback for 'start_command' callback, simply calls the start command handler bot.action('start_command', async (ctx) => { // Telegraf action handlers automatically answer the callback query // We just need to re-invoke the start command logic await ctx.deleteMessage().catch(() => {}); // Optional: delete the previous message to clean up chat await bot.telegram.sendMessage(ctx.chat.id, '🔄 Kembali ke Menu Utama...'); await ctx.telegram.sendChatAction(ctx.chat.id, 'typing'); // Show typing status // Call the start command directly, or redirect to it ctx.message = { text: '/start', from: ctx.from, chat: ctx.chat }; // Recreate a message object for the command handler await bot.handleUpdate({ update_id: 0, message: ctx.message }); // Simulate update }); // Existing Command Handlers (tetap sama) bot.command('addcf', handleAddCf); bot.command('cfconfig', handleCfConfig); bot.command('updatecf', handleUpdateCf); bot.command('deletecf', handleDeleteCf); // Domain Management Commands bot.command('listdomain', handleListDomain); bot.command('new', handleNew); bot.command('mysub', handleMySub); bot.command('searchdomain', handleSearchDomain); bot.command('delsub', handleDelSub); // Wildcard Setup Command bot.command('setupwildcard', handleSetupWildcard); // Admin Commands bot.command('broadcast', handleBroadcast); bot.command('testnotif', handleTestNotification); // Handle confirmation for delete config bot.hears(/^CONFIRM DELETE$/i, async (ctx) => { const userId = String(ctx.from.id); const configData = readConfig(); if (!configData[userId]) { return ctx.reply( '⚠️ *Tidak ada konfigurasi untuk dihapus*\n\n' + '📝 Anda belum terdaftar di sistem.', { parse_mode: 'Markdown' } ); } try { delete configData[userId]; require('./utils/fileUtils').writeConfig(configData); return ctx.reply( '✅ *Konfigurasi berhasil dihapus!*\n\n' + '🗑️ Semua data Cloudflare Anda telah dihapus dari sistem.\n' + '📝 Gunakan /addcf untuk mendaftar kembali.', { parse_mode: 'Markdown' } ); } catch (error) { console.error('❌ Error deleting config:', error); return ctx.reply( '❌ *Gagal menghapus konfigurasi*\n\n' + '🔄 Silakan coba lagi atau hubungi admin.', { parse_mode: 'Markdown' } ); } }); // Handle unknown commands and messages (completely silent) bot.on('text', (ctx) => { const text = ctx.message.text; // Only process commands (starts with /) if (text.startsWith('/')) { const command = text.split(' ')[0]; const knownCommands = [ '/start', '/addcf', '/cfconfig', '/updatecf', '/deletecf', '/listdomain', '/new', '/mysub', '/searchdomain', '/delsub', '/setupwildcard', '/broadcast', '/testnotif', ]; // Unknown commands are silently ignored - no response if (!knownCommands.includes(command)) { return; // Silent ignore } } // Non-command text is silently ignored }); // Handle non-text messages silently (no response) bot.on( ['photo', 'sticker', 'document', 'video', 'voice', 'audio', 'location', 'contact'], (ctx) => { // Silently ignore all non-text messages return; } ); // Graceful shutdown process.once('SIGINT', () => { console.log('🛑 Received SIGINT, shutting down gracefully...'); bot.stop('SIGINT'); }); process.once('SIGTERM', () => { console.log('🛑 Received SIGTERM, shutting down gracefully...'); bot.stop('SIGTERM'); }); // Start bot console.log('🚀 Tes WC Bot Starting...'); bot.launch() .then(() => { console.log('✅ Bot Online | Users: 0 | Load: 0%'); console.log('📋 Commands: 10 active | Ready for requests'); // Initial stats botStats.log(); }) .catch((err) => { console.error('❌ Failed to start:', err.message); process.exit(1); }); // Export bot for testing purposes module.exports = bot;