Spaces:
Paused
Paused
| 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("π« <b>Siz bloklangansiz.</b>\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(`β <b>Raqam tasdiqlandi!</b>\n\nEndi ilovaga kirib, <b>${user.phone}</b> 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("π« <b>Siz bloklangansiz.</b>\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(`β <b>Muvaffaqiyatli kirish!</b>\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 = `π <b>Assalomu alaykum, ${user.first_name}!</b>\n\nπ» <b>PC Store</b> botiga xush kelibsiz!\nSifati va halolligi kafolatlangan texnikalar makoni.\n\nπ <i>Quyidagi bo'limlardan birini tanlang:</i>`; | |
| 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/<token>`); | |
| } 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; | |