const { Telegraf, session, Scenes, Markup } = require('telegraf'); console.log("🚀 Bot Version 2.0: FINAL DEPLOY (Cache Bust)"); const config = require('./config'); const connectDB = require('./database/db'); const userController = require('./controllers/userController'); const User = require('./models/User'); const Order = require('./models/Order'); const https = require('https'); const dns = require('dns'); const path = require('path'); // --- API & Express Integration --- const express = require('express'); const cors = require('cors'); const bodyParser = require('body-parser'); const apiRoutes = require('./api/routes'); const app = express(); app.use(cors()); app.use(bodyParser.json()); // Inject Bot into Request // Inject Bot into Request (Moved below) // const bot = require('../main'); // CIRCULAR DEPENDENCY & ERROR // app.use((req, res, next) => { ... }); app.use('/api', apiRoutes); app.use('/public', express.static(path.join(__dirname, '../public'))); // Serve Static Files (APK, etc.) app.get('/', (req, res) => res.send('Bot & API are running!')); // Health Check for Hugging Face const PORT = process.env.PORT || 7860; // Use 7860 for HF Spaces app.listen(PORT, () => { console.log(`🌍 API Server running on port ${PORT}`); }); // -------------------------------- if (!config.BOT_TOKEN) { console.error('BOT_TOKEN is missing in .env'); process.exit(1); } // Connect to Database connectDB(); const telegramAgent = require('./utils/telegramAgent'); const bot = new Telegraf(config.BOT_TOKEN, { telegram: { agent: telegramAgent } }); // Inject Bot into Request (Fixed) app.use((req, res, next) => { req.bot = bot; next(); }); module.exports = bot; // Export bot instance // Middleware // Middleware: Check Block Status const checkUserStatus = async (ctx, next) => { if (ctx.from) { const user = await User.findOne({ id: ctx.from.id }); if (user && user.isBlocked) { return ctx.reply("🚫 Siz bloklangansiz.\n\nAdmin bilan bog'laning.", { parse_mode: 'HTML' }); } } return next(); }; bot.use(checkUserStatus); bot.use(session()); // Global i18n & User Middleware bot.use(async (ctx, next) => { try { if (ctx.from) { const User = require('./models/User'); const locales = require('./locales'); // Optimistic check: maybe session has language? (For speed) // But for correctness, let's fetch DB or rely on cached user if possible. // For now, fast fetch. const user = await User.findOne({ id: ctx.from.id }); const lang = (user && user.language) ? user.language : 'uz'; ctx.i18n = locales[lang] || locales.uz; ctx.dbUser = user; } } catch (e) { console.error("Middleware Error:", e); const locales = require('./locales'); ctx.i18n = locales.uz; // Fallback } return next(); }); // Scenes const stage = require('./scenes/index'); bot.use(stage.middleware()); const adminController = require('./controllers/adminController'); // Admin Commands bot.command('admin', (ctx) => { if (!config.ADMIN_IDS.includes(ctx.from.id.toString())) return; adminController.showDashboard(ctx); }); // Admin Callbacks bot.action('admin_dashboard', (ctx) => adminController.showDashboard(ctx)); // Admin Callbacks bot.action('admin_dashboard', (ctx) => adminController.showDashboard(ctx)); bot.action('admin_api', (ctx) => adminController.showApiMenu(ctx)); // --- CONTACT HANDLER (For Phone Login) --- bot.on('contact', async (ctx) => { try { const contact = ctx.message.contact; // Check if contact belongs to the user if (contact.user_id !== ctx.from.id) { return ctx.reply("🔒 Iltimos, o'zingizning raqamingizni yuboring (tugmani bosing)."); } let user = await User.findOne({ id: ctx.from.id }); if (!user) { user = new User({ id: ctx.from.id, first_name: ctx.from.first_name, username: ctx.from.username, phone: contact.phone_number.replace('+', '') }); await user.save(); } else { user.phone = contact.phone_number.replace('+', ''); await user.save(); } ctx.reply(`✅ Raqam tasdiqlandi!\n\nEndi ilovaga kirib, ${user.phone} raqamini kiritsangiz, men sizga kod yuboraman.`, { parse_mode: 'HTML' }); } catch (e) { console.error("Contact Error:", e); ctx.reply("Xatolik yuz berdi."); } }); bot.action('admin_api_generate', (ctx) => adminController.generateApiKey(ctx)); // NEW bot.action('admin_users', (ctx) => adminController.showUsers(ctx)); bot.action(/admin_user_manage_(.+)/, (ctx) => adminController.manageUser(ctx, ctx.match[1])); bot.action(/user_block_(.+)/, (ctx) => adminController.toggleBlockUser(ctx, ctx.match[1], true)); bot.action(/user_unblock_(.+)/, (ctx) => adminController.toggleBlockUser(ctx, ctx.match[1], false)); bot.action('admin_flash_sale', (ctx) => ctx.scene.enter('admin_flash_sale')); bot.action('admin_inventory', (ctx) => adminController.startMassPriceUpdate(ctx)); // Reuse Inventory button for Mass Update temporarily or create new scene // Mass Update Text Handler bot.on('text', async (ctx, next) => { // Check for Mass Update State if (ctx.session && ctx.session.isMassUpdating) { const handled = await adminController.handleMassUpdate(ctx); if (handled) return; } // Check for Name Change State if (ctx.session && ctx.session.isChangingName) { const handled = await userController.handleNameChange(ctx); if (handled) return; } // Check for Search State if (ctx.session && ctx.session.isSearching) { const handled = await userController.handleSearch(ctx); if (handled) return; } // Default flow return next(); }); bot.action('admin_delete_product', (ctx) => adminController.showDeleteProductList(ctx)); // Ensure this exists bot.action('admin_edit_product_list', (ctx) => adminController.showEditProductList(ctx)); bot.action('admin_excel_export', (ctx) => adminController.exportOrders(ctx)); bot.action('admin_stats', (ctx) => adminController.showStats(ctx)); bot.action('admin_orders_new', (ctx) => adminController.showNewOrders(ctx)); bot.action('admin_add_product', (ctx) => { ctx.deleteMessage(); // Remove dashboard to clear screen ctx.scene.enter('ADD_PRODUCT_SCENE'); }); bot.action('admin_broadcast', (ctx) => { ctx.deleteMessage(); ctx.scene.enter('BROADCAST_SCENE'); }); bot.action('admin_settings', (ctx) => { ctx.deleteMessage(); ctx.scene.enter('SETTINGS_SCENE'); }); bot.action(/edit_prod_(.+)/, (ctx) => { const prodId = parseInt(ctx.match[1]); ctx.scene.enter('EDIT_PRODUCT_SCENE', { prodId: prodId }); }); bot.action(/admin_order_(.+)/, (ctx) => adminController.viewOrder(ctx, ctx.match[1])); bot.action(/order_accept_(.+)/, (ctx) => adminController.acceptOrder(ctx, ctx.match[1])); bot.action(/order_reject_(.+)/, (ctx) => adminController.rejectOrder(ctx, ctx.match[1])); bot.action(/order_ship_(.+)/, (ctx) => adminController.shipOrder(ctx, ctx.match[1])); // NEW bot.action(/order_deliver_(.+)/, (ctx) => adminController.deliverOrder(ctx, ctx.match[1])); // NEW bot.action(/gen_invoice_(.+)/, (ctx) => adminController.showInvoice(ctx, ctx.match[1])); bot.action(/delete_prod_(.+)/, (ctx) => adminController.deleteProduct(ctx, ctx.match[1])); // User Actions bot.action(/user_cancel_order_(.+)/, (ctx) => userController.cancelUserOrder(ctx, ctx.match[1])); // User Commands bot.start(async (ctx) => { try { const user = ctx.from; let dbUser = await User.findOne({ id: user.id }); // Handle Referral const payload = ctx.startPayload; if (!dbUser) { // NEW USER: Create with null language to force selection dbUser = new User({ id: user.id, first_name: user.first_name, username: user.username, language: null // Explicitly null }); await dbUser.save(); // If referral exists, handle it if (payload && payload !== user.id.toString()) { userController.handleReferral(ctx, payload); } } // FORCE LANGUAGE SELECTION IF NULL if (!dbUser.language) { return ctx.reply("🇺🇿 Iltimos, tilni tanlang:\n🇷🇺 Пожалуйста, выберите язык:\n🇬🇧 Please select a language:", Markup.inlineKeyboard([ [Markup.button.callback("🇺🇿 O'zbekcha", "set_lang_uz")], [Markup.button.callback("🇷🇺 Русский", "set_lang_ru")], [Markup.button.callback("🇬🇧 English", "set_lang_en")] ])); } // If language exists, proceed to menu // Attach locale using the user's language const locales = require('./locales'); ctx.i18n = locales[dbUser.language] || locales.uz; userController.start(ctx); } catch (err) { console.error(err); } }); // Middleware: Global Checks (Blocking, Language, etc.) bot.use(async (ctx, next) => { try { if (!ctx.from) return next(); const User = require('./models/User'); const locales = require('./locales'); // 1. Find or Create User (Lightweight check) let user = await User.findOne({ id: ctx.from.id }); // If user completely doesn't exist, we usually handle it in /start, // but for safety in other updates, we might ignore or let /start handle it. // However, if we want strict logic, we can create them here or wait for /start. // 2. Blocked Check if (user && user.isBlocked) { return ctx.reply("🚫 Siz bloklangansiz.\n\nAdmin bilan bog'laning.", { parse_mode: 'HTML' }); } // 3. Language Check (The "Maximal Logic") // If user exists but has no language, FORCE selection. // Exception: logic shouldn't block the CALLBACK events using to SET the language. const isLanguageSetAction = ctx.callbackQuery && ['set_lang_uz', 'set_lang_ru', 'set_lang_en'].includes(ctx.callbackQuery.data); const isStart = ctx.message && ctx.message.text && ctx.message.text.startsWith('/start'); if (user && !user.language && !isLanguageSetAction && !isStart) { return ctx.reply("🇺🇿 Iltimos, oldin tilni tanlang:\n🇷🇺 Пожалуйста, сначала выберите язык:\n🇬🇧 Please select a language first:", Markup.inlineKeyboard([ [Markup.button.callback("🇺🇿 O'zbekcha", "set_lang_uz")], [Markup.button.callback("🇷🇺 Русский", "set_lang_ru")], [Markup.button.callback("🇬🇧 English", "set_lang_en")] ])); } // 4. Attach i18n // If user has language, use it. If not, default to 'uz' (but /start will force selection) const lang = (user && user.language) ? user.language : 'uz'; ctx.i18n = locales[lang] || locales.uz; return next(); } catch (err) { console.error("Middleware Error:", err); return next(); } }); // Language Selection Handlers const setLanguage = async (ctx, lang) => { try { await ctx.answerCbQuery(); // Stop loading animation // 1. Update Database await User.updateOne({ id: ctx.from.id }, { language: lang }); // 2. Update Context Locale Immediately const locales = require('./locales'); ctx.i18n = locales[lang]; // 3. Clean up previous valid/invalid messages try { await ctx.deleteMessage(); } catch (e) { // Message might be too old or already deleted } // 4. Send Confirmation in NEW Language await ctx.reply(`✅ ${ctx.i18n.language_set}`, { parse_mode: 'HTML' }); // 5. Refresh Main Menu with NEW Language Buttons // We pass a flag or text to start to indicate a refresh, // but userController.start(ctx) usually handles simple menu generation. // Let's ensure it sends the FULL welcome text again or just the menu. // For "Maximal Logic", resetting to the main menu with a fresh "Welcome back" or just the menu is best. userController.start(ctx); } catch (e) { console.error("Language Change Error:", e); ctx.reply("⚠️ Error changing language. Please try again or /start"); } }; bot.action('set_lang_uz', (ctx) => setLanguage(ctx, 'uz')); bot.action('set_lang_ru', (ctx) => setLanguage(ctx, 'ru')); bot.action('set_lang_en', (ctx) => setLanguage(ctx, 'en')); // Settings: Change Language Action bot.action('change_lang', (ctx) => { ctx.reply("🇺🇿 Iltimos, tilni tanlang:\n🇷🇺 Пожалуйста, выберите язык:\n🇬🇧 Please select a language:", Markup.inlineKeyboard([ [Markup.button.callback("🇺🇿 O'zbekcha", "set_lang_uz")], [Markup.button.callback("🇷🇺 Русский", "set_lang_ru")], [Markup.button.callback("🇬🇧 English", "set_lang_en")] ])); }); // User Interactions // User Interactions // Match Helper to get keywords from all languages const match = (key) => { const locales = require('./locales'); const triggers = Object.values(locales).map(l => l[key]).filter(v => v); // Filter out undefined/null return triggers.length > 0 ? triggers : [new RegExp(`^${key}$`)]; // Fallback if empty (shouldn't happen) }; bot.hears(match('btn_catalog'), (ctx) => userController.showCategories(ctx)); // Syntax Fix Applied bot.hears(match('btn_cart'), (ctx) => userController.showCart(ctx)); bot.hears(match('btn_orders'), (ctx) => userController.showOrderHistory(ctx)); // Fixed Name bot.hears(match('btn_contact'), (ctx) => userController.showContact(ctx)); // Use Controller bot.hears(match('btn_search'), (ctx) => ctx.reply(ctx.i18n.search_prompt)); // Language Switch Handler bot.hears(match('btn_lang'), (ctx) => { ctx.reply("🇺🇿 Iltimos, tilni tanlang:\n🇷🇺 Пожалуйста, выберите язык:\n🇬🇧 Please select a language:", Markup.inlineKeyboard([ [Markup.button.callback("🇺🇿 O'zbekcha", "set_lang_uz")], [Markup.button.callback("🇷🇺 Русский", "set_lang_ru")], [Markup.button.callback("🇬🇧 English", "set_lang_en")] ])); }); bot.hears("👨‍💼 Admin Panel", (ctx) => { if (!config.ADMIN_IDS.includes(ctx.from.id.toString())) return; adminController.showDashboard(ctx); }); bot.hears(match('btn_contact'), (ctx) => { ctx.reply(ctx.i18n.contact_info, Markup.inlineKeyboard([ [Markup.button.url("📦 Mahsulotlar bo'yicha Admin", "https://t.me/isfandiyor_3")], [Markup.button.url("🤖 Bot bo'yicha Admin", "https://t.me/IBROHM_7")] ])); }); bot.hears(match('btn_channel'), (ctx) => { ctx.reply(ctx.i18n.btn_channel, Markup.inlineKeyboard([ [Markup.button.url("📢 Kanalga o'tish", "https://t.me/Pc_Store_Market")] ])); }); bot.hears(match('btn_group'), (ctx) => { ctx.reply(ctx.i18n.btn_group, Markup.inlineKeyboard([ [Markup.button.url("👥 Guruhga o'tish", "https://t.me/Pc_Store_Market1")] ])); }); bot.hears(match('btn_invite'), (ctx) => userController.inviteFriends(ctx)); bot.hears(match('btn_settings'), (ctx) => userController.settings(ctx)); bot.hears(match('btn_back'), (ctx) => userController.start(ctx)); // Language Change Button Handler bot.hears(match('btn_lang'), (ctx) => { ctx.reply("🇺🇿 Iltimos, tilni tanlang:\n🇷🇺 Пожалуйста, выберите язык:\n🇬🇧 Please select a language:", Markup.inlineKeyboard([ [Markup.button.callback("🇺🇿 O'zbekcha", "set_lang_uz")], [Markup.button.callback("🇷🇺 Русский", "set_lang_ru")], [Markup.button.callback("🇬🇧 English", "set_lang_en")] ])); }); // Handle /start with Payload bot.start((ctx) => { // Check for payload const payload = ctx.startPayload; // Telegraf extracts this automatically "start 123" -> "123" // Register User logic here (or ensure it's done) - Middlewares usually trigger first, ensuring user exists. // Register User logic here // Pass payload to controller if (payload) { if (payload.startsWith('login_')) { // --- NEW: LOGIN HANDLER --- const token = payload.replace('login_', ''); const LoginRequest = require('./models/LoginRequest'); // Update the login request with user info LoginRequest.findOneAndUpdate( { token: token }, { userId: user.id.toString(), firstName: user.first_name, username: user.username, status: 'approved' }, { upsert: true } // Create if doesn't exist (though App should have created it, or we rely on token uniqueness) ).then(() => { ctx.reply(`✅ Muvaffaqiyatli kirish!\n\nIlovaga qaytishingiz mumkin.`, { parse_mode: 'HTML' }); }).catch(err => { console.error("Login Error:", err); ctx.reply("⚠️ Xatolik yuz berdi. Qaytadan urunib ko'ring."); }); return; // Stop here, don't show welcome menu } userController.handleReferral(ctx, payload); } const user = ctx.from; const welcomeText = `👋 Assalomu alaykum, ${user.first_name}!\n\n💻 PC Store botiga xush kelibsiz!\nSifati va halolligi kafolatlangan texnikalar makoni.\n\n👇 Quyidagi bo'limlardan birini tanlang:`; userController.start(ctx, welcomeText); }); bot.hears("🔎 Qidiruv", (ctx) => userController.startSearch(ctx)); bot.hears("📦 Buyurtmalarim", (ctx) => userController.showOrderHistory(ctx)); bot.hears("⚙️ Sozlamalar", (ctx) => userController.settings(ctx)); bot.action("set_name", (ctx) => { ctx.deleteMessage(); userController.changeName(ctx); }); // Handle Category Selection (Expanded for Condition Suffix) bot.action(/cat_(.+)/, async (ctx) => { // Expected format: cat_123 or cat_123_new or cat_123_used const fullMatch = ctx.match[1]; let catId = fullMatch; let condition = null; if (fullMatch.endsWith('_new')) { catId = fullMatch.replace('_new', ''); condition = 'new'; } else if (fullMatch.endsWith('_used')) { catId = fullMatch.replace('_used', ''); condition = 'used'; } else if (fullMatch.endsWith('_all')) { catId = fullMatch.replace('_all', ''); condition = 'all'; } const Category = require('./models/Category'); const userController = require('./controllers/userController'); // Check if subcategories exist const subCount = await Category.countDocuments({ parent: catId }); if (subCount > 0) { // Show Subcategories (passing condition filter) return userController.showSubCategories(ctx, catId, condition); } else { // Show Products (Leaf Category) -> Skip asking if condition is already set if (condition) { return userController.browseCategory(ctx, `cat_${catId}`, 0, 0, condition); } // Fallback for old buttons -> Smart Logic in showProducts return userController.showProducts(ctx, `cat_${catId}`); } }); // Carousel Navigation // Condition Filter Selection bot.action(/cond_(.+)_(.+)/, (ctx) => { const catId = "cat_" + ctx.match[1]; const condition = ctx.match[2]; userController.filterByCondition(ctx, catId, condition); }); // Carousel Navigation (New) bot.action(/br_(.+)/, (ctx) => { // pattern: br_catId_prodIndex_mediaIndex_condition // We split manually because condition is optional and captured groups are tricky const text = ctx.match[1]; const parts = text.split('_'); // Safety check if (parts.length < 3) return ctx.answerCbQuery("Error"); const catId = "cat_" + parts[0]; const prodIndex = parseInt(parts[1]); const mediaIndex = parseInt(parts[2]); const condition = parts[3] || 'all'; // Default to all if missing userController.browseCategory(ctx, catId, prodIndex, mediaIndex, condition); }); // Backward compatibility or redirect bot.action(/browse_(.+)_(.+)/, (ctx) => { const catId = "cat_" + ctx.match[1]; const index = parseInt(ctx.match[2]); userController.browseCategory(ctx, catId, index, 0); }); bot.action(/rate_ask_(.+)/, (ctx) => userController.askRating(ctx, ctx.match[1])); bot.action(/rate_save_(.+)_(.+)/, (ctx) => userController.saveRating(ctx, ctx.match[1], ctx.match[2])); bot.action("back_to_cats", (ctx) => { ctx.deleteMessage(); userController.showCategories(ctx); }); bot.action("noop", (ctx) => ctx.answerCbQuery()); // Do nothing for page counter bot.action(/prod_(.+)/, (ctx) => { ctx.answerCbQuery(); userController.showProduct(ctx, ctx.match[1]); }); bot.action(/add_cart_(.+)/, (ctx) => { userController.addToCart(ctx, ctx.match[1]); }); bot.action("clear_cart", (ctx) => userController.clearCart(ctx)); bot.action(/cart_incr_(.+)/, (ctx) => userController.cartAction(ctx, 'incr', ctx.match[1])); bot.action(/cart_decr_(.+)/, (ctx) => userController.cartAction(ctx, 'decr', ctx.match[1])); bot.action(/cart_del_(.+)/, (ctx) => userController.cartAction(ctx, 'del', ctx.match[1])); bot.action(/remove_item_(.+)/, (ctx) => userController.removeItem(ctx, parseInt(ctx.match[1]))); bot.action("checkout", (ctx) => { ctx.answerCbQuery(); ctx.deleteMessage(); // Clear cart message ctx.scene.enter('CHECKOUT_SCENE'); }); bot.action("my_orders_list", (ctx) => { ctx.deleteMessage(); userController.showOrderHistory(ctx); }); bot.action("back_to_menu", (ctx) => { ctx.deleteMessage(); ctx.reply("Asosiy menyu:", Markup.keyboard(userController.getMenuButtons(ctx)).resize()); // Wait, getMenuButtons is not exported or defined nicely. userController.start(ctx) sends the menu. // Let's just call userController.start(ctx); userController.start(ctx); }); bot.action(/my_order_(.+)/, (ctx) => userController.showOrderDetails(ctx, ctx.match[1])); bot.action(/cancel_order_(.+)/, (ctx) => userController.cancelOrder(ctx, ctx.match[1])); // New Action // Handle Text (Search & Settings) // This MUST be the last text handler bot.on('text', async (ctx, next) => { // 1. Check Search let isHandled = await userController.handleSearch(ctx); if (isHandled) return; // 2. Check Name Change isHandled = await userController.handleNameChange(ctx); if (isHandled) return; return next(); }); // Launch with Retry Logic and DNS Check // const dns = require('dns'); // Already imported at the top // Launch Strategy (Polling vs Webhook) // Launch Strategy (Polling vs Webhook) const launchBot = async () => { try { const PORT = process.env.PORT || 7860; // Render/HF default // Force Webhook if explicitly requested or if on Render/Railway const USE_WEBHOOK = process.env.USE_WEBHOOK === 'true' || process.env.RENDER_EXTERNAL_URL || process.env.RAILWAY_PUBLIC_DOMAIN; const DOMAIN = process.env.RENDER_EXTERNAL_URL || process.env.RAILWAY_PUBLIC_DOMAIN || process.env.WEBHOOK_DOMAIN; if (USE_WEBHOOK && DOMAIN) { // WEBHOOK MODE (Production) console.log(`🚀 Starting in WEBHOOK mode on port ${PORT}`); console.log(`🔗 Domain: ${DOMAIN}`); // Delete any existing webhook first (optional, but good for restart) // await bot.telegram.deleteWebhook(); await bot.launch({ webhook: { domain: DOMAIN, port: PORT }, dropPendingUpdates: true }); console.log(`✅ Webhook Active: ${DOMAIN}/telegraf/`); } else { // POLLING MODE (Local / Dev) console.log(`🚀 Starting in POLLING mode`); // Clear any stuck webhooks await bot.telegram.deleteWebhook({ drop_pending_updates: true }); console.log('🔄 Webhook tozalandi (Polling uchun)...'); await bot.launch({ dropPendingUpdates: true }); console.log('✅ Bot (Polling) ishga tushdi!'); } } catch (err) { console.error('❌ Botni ishga tushirishda xatolik:', err.message); if (err.description && err.description.includes('Conflict')) { console.warn("⚠️ MUAZZAM DIQQAT: Bot allaqachon boshqa joyda ishlamoqda!"); console.warn("Agar bu server bo'lsa, demak lokal (kompyuterda) botni o'chirish kerak."); console.warn("Yechim: Bitta botni to'xtating."); // Increase delay significantly to break the loop console.log('⏳ Conflict: 30 soniya kuting...'); setTimeout(launchBot, 30000); return; } // Retry logic for Polling stability const jitter = Math.floor(Math.random() * 5000); // 0-5s random delay const delay = 10000 + jitter; console.log(`⏳ ${delay / 1000} soniyadan keyin qayta urunib ko'ramiz (Jitter: ${jitter}ms)...`); setTimeout(launchBot, delay); } }; launchBot(); // Enable graceful stop process.once('SIGINT', () => bot.stop('SIGINT')); process.once('SIGTERM', () => bot.stop('SIGTERM')); module.exports = bot;