tes-wc-bot
Version:
🌐 Advanced Telegram Bot for Cloudflare Wildcard Domain Management with Telegram Notifications
506 lines (421 loc) • 16.8 kB
JavaScript
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;