diff --git a/bot.py b/bot.py index c561518..d0a0def 100644 --- a/bot.py +++ b/bot.py @@ -2,7 +2,8 @@ import sys import logging import json -from datetime import datetime, timedelta +from datetime import datetime, timedelta +from html import escape from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram.ext import ( ApplicationBuilder, CommandHandler, ContextTypes, @@ -71,9 +72,55 @@ def save_json(file_path, data): REFERALS = load_json(REFERRALS_FILE) ORDERS = load_json(ORDERS_FILE) FEEDBACKS = load_json(FEEDBACKS_FILE) -USER_LOGS = load_json(USER_LOGS_FILE) - -ORDER_TYPES = { +USER_LOGS = load_json(USER_LOGS_FILE) + +def format_contact_link(contact: str) -> str: + if not contact: + return 'не указан' + contact = contact.strip() + if not contact: + return 'не указан' + safe_display = escape(contact) + if contact.startswith('@') and len(contact) > 1: + username = contact[1:].split()[0] + if username and all(ch.isalnum() or ch == '_' for ch in username): + href = escape(f"https://t.me/{username}", quote=True) + display = escape(f"@{username}") + return f'{display}' + lower_contact = contact.lower() + if lower_contact.startswith('http://') or lower_contact.startswith('https://'): + href = escape(contact, quote=True) + return f'{safe_display}' + if '@' in contact and ' ' not in contact and not contact.startswith('@'): + href = escape(f"mailto:{contact}", quote=True) + return f'{safe_display}' + return safe_display + +def build_order_details(uid: str, order: dict) -> str: + order_id = order.get('order_id', 'N/A') + order_name = ORDER_TYPES.get(order.get('type'), {}).get('name', order.get('type', 'Неизвестно')) + user_link = escape(f"tg://user?id={uid}", quote=True) + lines = [ + f"Заказ #{order_id}", + f"Пользователь: {escape(str(uid))}", + f"Тип: {escape(order_name)}", + f"Тема: {escape(order.get('topic', 'Без темы'))}", + f"Срок: {order.get('deadline_days', 'N/A')} дней", + f"Статус: {escape(order.get('status', 'неизвестно'))}", + f"Контакт: {format_contact_link(order.get('contact'))}", + f"Требования: {escape(order.get('requirements', 'Нет'))}", + ] + if order.get('upsells'): + upsells_readable = ', '.join(UPSELL_TITLES.get(code, code) for code in order['upsells']) + lines.append(f"Допы: {escape(upsells_readable)}") + else: + lines.append("Допы: нет") + lines.append(f"Файлов: {len(order.get('attachments') or [])}") + if order.get('created_at'): + lines.append(f"Создан: {escape(order['created_at'])}") + return '
'.join(lines) + +ORDER_TYPES = { 'samostoyatelnye': { 'name': 'Самостоятельные, контрольные, эссе', 'icon': '📝', @@ -81,13 +128,13 @@ def save_json(file_path, data): 'details': 'Объем до 20 страниц. Быстрое выполнение с гарантией качества.', 'examples': ['Эссе по литературе', 'Контрольная по математике', 'Реферат по истории'] }, - 'kursovaya_teoreticheskaya': { - 'name': 'Курсовая теоретическая', - 'icon': '📘', - 'description': 'Глубокий анализ литературы. Получите отличную оценку без стресса! 📈', - 'details': 'Теоретическая основа, анализ источников, структура по ГОСТ.', - 'examples': ['Теория маркетинга', 'Обзор психологии развития'] - }, + 'kursovaya_teoreticheskaya': { + 'name': 'Курсовая теоретическая', + 'icon': '📘', + 'description': 'Глубокий анализ литературы и теории. Получите отличную оценку без стресса! 📈', + 'details': 'Теоретическая основа, аналитика источников и четкая структура по ГОСТ.', + 'examples': ['Теория маркетинга', 'Обзор психологии развития'] + }, 'kursovaya_s_empirikov': { 'name': 'Курсовая с эмпирикой', 'icon': '📊', @@ -109,9 +156,14 @@ def save_json(file_path, data): 'details': 'Научная новизна, методология, публикации.', 'examples': ['Разработка моделей AI', 'Комплексные исследования экологии'] } -} - -FAQ_ITEMS = [ +} + +UPSELL_TITLES = { + 'prez': 'Презентация', + 'speech': 'Речь' +} + +FAQ_ITEMS = [ {'question': 'Как сделать заказ?', 'answer': 'Выберите "Сделать заказ" и следуйте шагам. Можно заказать несколько работ сразу!'}, {'question': 'Как рассчитывается стоимость?', 'answer': 'Зависит от типа, срочности и сложности. Используйте калькулятор для точной цены!'}, {'question': 'Как работает реферальная программа?', 'answer': 'Поделитесь ссылкой — получите 5% от заказов друзей как бонус.'}, @@ -123,7 +175,7 @@ def save_json(file_path, data): current_pricing_mode = 'light' # Состояния -SELECT_MAIN_MENU, SELECT_ORDER_TYPE, VIEW_ORDER_DETAILS, INPUT_TOPIC, SELECT_DEADLINE, INPUT_REQUIREMENTS, ADD_UPSSELL, ADD_ANOTHER_ORDER, CONFIRM_CART, ADMIN_MENU, PROFILE_MENU, SHOW_PRICE_LIST, PRICE_CALCULATOR, SELECT_CALC_DEADLINE, SELECT_CALC_COMPLEXITY, SHOW_FAQ, FAQ_DETAILS, SHOW_ORDERS, LEAVE_FEEDBACK, INPUT_FEEDBACK = range(20) +SELECT_MAIN_MENU, SELECT_ORDER_TYPE, VIEW_ORDER_DETAILS, INPUT_TOPIC, SELECT_DEADLINE, INPUT_REQUIREMENTS, UPLOAD_FILES, INPUT_CONTACT, ADD_UPSSELL, ADD_ANOTHER_ORDER, CONFIRM_CART, ADMIN_MENU, PROFILE_MENU, SHOW_PRICE_LIST, PRICE_CALCULATOR, SELECT_CALC_DEADLINE, SELECT_CALC_COMPLEXITY, SHOW_FAQ, FAQ_DETAILS, SHOW_ORDERS, LEAVE_FEEDBACK, INPUT_FEEDBACK = range(22) # Логирование действий пользователя def log_user_action(user_id, username, action): @@ -132,6 +184,19 @@ def log_user_action(user_id, username, action): save_json(USER_LOGS_FILE, USER_LOGS) logger.info(f"Пользователь {user_id} ({username}): {action}") +async def answer_callback(query): + """Безопасно отвечает на callback-запрос, чтобы избежать ошибок повторного ответа.""" + if not query: + return + try: + await query.answer() + except TelegramError as exc: + error_text = str(exc).lower() + if "query is too old" in error_text or "query id is invalid" in error_text: + logger.debug("Callback уже обработан: %s", exc) + else: + raise + # Расчет цены def calculate_price(order_type_key, days_left, complexity_factor=1.0): if order_type_key not in PRICES: @@ -171,7 +236,12 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): REFERALS.setdefault(str(referrer_id), []).append(user.id) save_json(REFERRALS_FILE, REFERALS) await context.bot.send_message(referrer_id, f"🎉 Новый реферал: {user.first_name}") - welcome = f"👋 Добро пожаловать, {user.first_name}! Заказывайте работы. Уже 5000+ клиентов! 10% скидка на первый заказ 🔥\nПоделитесь ссылкой для бонусов: {ref_link}" + welcome = ( + f"👋 Добро пожаловать, {user.first_name}! Работаем со всеми учебными работами, " + "кроме технических дисциплин с чертежами. Уже 5000+ клиентов и более 6 лет опыта! 10% скидка на первый заказ 🔥\n" + f"Поделитесь ссылкой для бонусов: {ref_link}\n" + "Это ваша реферальная ссылка: если по ней оформляют заказ, вы получаете бонусы!" + ) await main_menu(update, context, welcome) # Главное меню @@ -188,7 +258,7 @@ async def main_menu(update: Update, context: ContextTypes.DEFAULT_TYPE, message= reply_markup = InlineKeyboardMarkup(keyboard) if update.callback_query: query = update.callback_query - await query.answer() + await answer_callback(query) try: await query.edit_message_text(text, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN) except TelegramError as e: @@ -203,7 +273,7 @@ async def main_menu(update: Update, context: ContextTypes.DEFAULT_TYPE, message= # Обработчик главного меню async def main_menu_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query - await query.answer() + await answer_callback(query) data = query.data user = update.effective_user log_user_action(user.id, user.username, f"Выбор в меню: {data}") @@ -224,7 +294,7 @@ async def main_menu_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): # Выбор типа заказа async def select_order_type(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query - await query.answer() + await answer_callback(query) data = query.data if query else None user = update.effective_user log_user_action(user.id, user.username, "Выбор типа заказа") @@ -246,7 +316,7 @@ async def select_order_type(update: Update, context: ContextTypes.DEFAULT_TYPE): # Подробности о типе заказа async def view_order_details(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query - await query.answer() + await answer_callback(query) data = query.data if data.startswith('order_'): key = data[6:] @@ -291,7 +361,7 @@ async def input_topic(update: Update, context: ContextTypes.DEFAULT_TYPE): # Выбор срока async def select_deadline(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query - await query.answer() + await answer_callback(query) data = query.data if data.startswith('deadline_'): days = int(data[9:]) @@ -303,14 +373,98 @@ async def select_deadline(update: Update, context: ContextTypes.DEFAULT_TYPE): return SELECT_DEADLINE # Ввод требований -async def input_requirements(update: Update, context: ContextTypes.DEFAULT_TYPE): - context.user_data['requirements'] = update.message.text - return await add_upsell(update, context) - -async def skip_requirements(update: Update, context: ContextTypes.DEFAULT_TYPE): - context.user_data['requirements'] = 'Нет' - return await add_upsell(update, context) - +async def input_requirements(update: Update, context: ContextTypes.DEFAULT_TYPE): + context.user_data['requirements'] = update.message.text + return await prompt_file_upload(update, context) + +async def skip_requirements(update: Update, context: ContextTypes.DEFAULT_TYPE): + context.user_data['requirements'] = 'Нет' + return await prompt_file_upload(update, context) + +async def prompt_file_upload(update: Update, context: ContextTypes.DEFAULT_TYPE): + context.user_data['current_files'] = [] + context.user_data.pop('current_contact', None) + text = ( + "Прикрепите файлы для заказа (если они есть). Отправьте все документы подряд.\n" + "Когда закончите, нажмите /done. Если файлов нет, нажмите /skip." + ) + if update.message: + await update.message.reply_text(text) + elif update.callback_query: + query = update.callback_query + await answer_callback(query) + await query.edit_message_text(text) + return UPLOAD_FILES + +async def handle_document_upload(update: Update, context: ContextTypes.DEFAULT_TYPE): + document = update.message.document + files = context.user_data.setdefault('current_files', []) + files.append({ + 'type': 'document', + 'file_id': document.file_id, + 'file_name': document.file_name or 'Файл без названия', + 'mime_type': document.mime_type, + }) + await update.message.reply_text( + f"Файл {document.file_name or 'без названия'} сохранен. Отправьте следующий или нажмите /done." + ) + return UPLOAD_FILES + +async def handle_photo_upload(update: Update, context: ContextTypes.DEFAULT_TYPE): + photo = update.message.photo[-1] + files = context.user_data.setdefault('current_files', []) + files.append({ + 'type': 'photo', + 'file_id': photo.file_id, + 'file_unique_id': photo.file_unique_id, + 'caption': update.message.caption, + }) + await update.message.reply_text("Фото сохранено. Отправьте следующее или нажмите /done.") + return UPLOAD_FILES + +async def files_text_reminder(update: Update, context: ContextTypes.DEFAULT_TYPE): + await update.message.reply_text( + "Пожалуйста, прикрепите файл или используйте /done, когда закончите. Если файлов нет, нажмите /skip." + ) + return UPLOAD_FILES + +async def skip_files(update: Update, context: ContextTypes.DEFAULT_TYPE): + context.user_data['current_files'] = [] + await update.message.reply_text("Пропускаем прикрепление файлов.") + return await request_contact(update, context) + +async def finish_files(update: Update, context: ContextTypes.DEFAULT_TYPE): + if context.user_data.get('current_files'): + await update.message.reply_text("Файлы сохранены.") + else: + await update.message.reply_text("Хорошо, продолжаем без файлов.") + return await request_contact(update, context) + +async def request_contact(update: Update, context: ContextTypes.DEFAULT_TYPE): + context.user_data.pop('current_contact', None) + prompt = ( + "Укажите контакт, куда менеджеру написать (Telegram, ВКонтакте, почта). Это обязательное поле." + ) + last_contact = context.user_data.get('last_contact') + if last_contact: + prompt += f"\nРанее вы указывали: {last_contact}. Можно отправить его снова или написать другой." + if update.message: + await update.message.reply_text(prompt) + elif update.callback_query: + query = update.callback_query + await answer_callback(query) + await query.edit_message_text(prompt) + return INPUT_CONTACT + +async def input_contact(update: Update, context: ContextTypes.DEFAULT_TYPE): + contact = update.message.text.strip() + if not contact: + await update.message.reply_text("Контакт обязателен. Пожалуйста, укажите, куда менеджеру написать.") + return INPUT_CONTACT + context.user_data['current_contact'] = contact + context.user_data['last_contact'] = contact + return await add_upsell(update, context) + # Добавление допуслуг async def add_upsell(update: Update, context: ContextTypes.DEFAULT_TYPE): text = "Добавить услуги? (Клиенты, добавляющие, получают 5% скидки на следующий заказ!)" @@ -323,14 +477,14 @@ async def add_upsell(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard)) else: query = update.callback_query - await query.answer() + await answer_callback(query) await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard)) return ADD_UPSSELL # Обработчик допуслуг async def upsell_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query - await query.answer() + await answer_callback(query) data = query.data upsells = context.user_data.setdefault('upsells', set()) added = False @@ -365,27 +519,34 @@ async def process_order(update: Update, context: ContextTypes.DEFAULT_TYPE): return ConversationHandler.END topic = context.user_data.get('topic', 'Без темы') days_left = context.user_data.get('days_left', 14) - requirements = context.user_data.get('requirements', 'Нет') - upsells = list(context.user_data.get('upsells', set())) - price = calculate_price(type_key, days_left) - extra = len(upsells) * 1000 - price += extra - order = { - 'type': type_key, - 'topic': topic, - 'deadline_days': days_left, - 'requirements': requirements, - 'upsells': upsells, - 'price': price, - 'status': 'новый' - } - context.user_data.setdefault('cart', []).append(order) - context.user_data.pop('upsells', None) - context.user_data.pop('requirements', None) - context.user_data.pop('days_left', None) - context.user_data.pop('topic', None) - context.user_data.pop('current_order_type', None) - return await add_another_order(update, context) + requirements = context.user_data.get('requirements', 'Нет') + upsells = list(context.user_data.get('upsells', set())) + attachments = list(context.user_data.get('current_files', [])) + contact = context.user_data.get('current_contact') or context.user_data.get('last_contact', '') + price = calculate_price(type_key, days_left) + extra = len(upsells) * 1000 + price += extra + order = { + 'type': type_key, + 'topic': topic, + 'deadline_days': days_left, + 'requirements': requirements, + 'upsells': upsells, + 'price': price, + 'status': 'новый', + 'attachments': attachments, + 'contact': contact, + 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + context.user_data.setdefault('cart', []).append(order) + context.user_data.pop('upsells', None) + context.user_data.pop('requirements', None) + context.user_data.pop('days_left', None) + context.user_data.pop('topic', None) + context.user_data.pop('current_order_type', None) + context.user_data.pop('current_files', None) + context.user_data.pop('current_contact', None) + return await add_another_order(update, context) # Добавить еще заказ async def add_another_order(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -400,7 +561,7 @@ async def add_another_order(update: Update, context: ContextTypes.DEFAULT_TYPE): async def add_another_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query - await query.answer() + await answer_callback(query) data = query.data if data == 'add_another_yes': return await select_order_type(update, context) @@ -415,12 +576,19 @@ async def confirm_cart(update: Update, context: ContextTypes.DEFAULT_TYPE): if not cart: await query.edit_message_text("Корзина пуста.") return await main_menu(update, context) - text = "Ваша корзина (подтвердите для специального бонуса!):\n" - total = 0 - for i, order in enumerate(cart, 1): - order_name = ORDER_TYPES.get(order['type'], {}).get('name', 'Неизвестно') - text += f"{i}. {order_name} - {order['topic']} - {order['price']} ₽\n" - total += order['price'] + text = "Ваша корзина (подтвердите для специального бонуса!):\n" + total = 0 + for i, order in enumerate(cart, 1): + order_name = ORDER_TYPES.get(order['type'], {}).get('name', 'Неизвестно') + contact = order.get('contact') or context.user_data.get('last_contact', 'Контакт не указан') + attachments_info = '' + if order.get('attachments'): + attachments_info = f" (файлов: {len(order['attachments'])})" + text += ( + f"{i}. {order_name} - {order['topic']} - {order['price']} ₽{attachments_info}\n" + f" Контакт: {contact}\n" + ) + total += order['price'] if len(cart) > 1: discount = total * 0.1 total -= discount @@ -435,23 +603,90 @@ async def confirm_cart(update: Update, context: ContextTypes.DEFAULT_TYPE): async def confirm_cart_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query - await query.answer() + await answer_callback(query) data = query.data - if data == 'place_order': - user_id = str(update.effective_user.id) - user_orders = ORDERS.setdefault(user_id, []) - order_id = len(user_orders) + 1 - for order in context.user_data['cart']: - order['order_id'] = order_id - user_orders.append(order) - order_id += 1 - save_json(ORDERS_FILE, ORDERS) - text = "Заказ оформлен! Менеджер свяжется с вами. Проверьте профиль для статуса." - await query.edit_message_text(text) - if ADMIN_CHAT_ID: - await context.bot.send_message(ADMIN_CHAT_ID, f"Новый заказ от {user_id}: {len(context.user_data['cart'])} позиций") - context.user_data.pop('cart', None) - return await main_menu(update, context, "Спасибо! Хотите заказать еще?") + if data == 'place_order': + user = update.effective_user + user_id = str(user.id) + cart_orders = context.user_data.get('cart', []) + if not cart_orders: + await query.edit_message_text("Корзина пуста.") + return await main_menu(update, context) + user_orders = ORDERS.setdefault(user_id, []) + order_id = len(user_orders) + 1 + for order in cart_orders: + order['order_id'] = order_id + user_orders.append(order) + order_id += 1 + save_json(ORDERS_FILE, ORDERS) + text = ( + "Заказ оформлен! С вами свяжется [администратор](https://t.me/Thisissaymoon) в ближайшее время. " + "Проверьте профиль для статуса." + ) + await query.edit_message_text(text, parse_mode=ParseMode.MARKDOWN) + if ADMIN_CHAT_ID: + user_link = f"https://t.me/{user.username}" if user.username else f"tg://user?id={user.id}" + href_user_link = escape(user_link, quote=True) + user_name = escape(user.full_name or user.username or user_id) + summary_lines = [ + "🆕 Новый заказ", + f"Пользователь: {user_name} (ID: {user.id})", + f"Всего позиций: {len(cart_orders)}" + ] + for order in cart_orders: + order_name = ORDER_TYPES.get(order['type'], {}).get('name', order['type']) + topic = escape(order.get('topic', 'Без темы')) + requirements = escape(order.get('requirements', 'Нет')) + contact_html = format_contact_link(order.get('contact')) + upsells_list = ', '.join(UPSELL_TITLES.get(code, code) for code in order.get('upsells', [])) + upsells_display = escape(upsells_list) if upsells_list else 'Нет' + attachments_count = len(order.get('attachments') or []) + order_summary = [ + f"#{order['order_id']} {escape(order_name)} — {topic}", + f"Срок: {order.get('deadline_days', 'N/A')} дней | Цена: {order.get('price', 0)} ₽", + f"Контакт: {contact_html}", + f"Требования: {requirements if requirements else 'Нет'}", + f"Допы: {upsells_display}", + ] + if attachments_count: + order_summary.append(f"Файлы: {attachments_count} (отправлены отдельно)") + summary_lines.append('
'.join(order_summary)) + admin_message = '

'.join(summary_lines) + await context.bot.send_message( + ADMIN_CHAT_ID, + admin_message, + parse_mode=ParseMode.HTML + ) + for order in cart_orders: + attachments = order.get('attachments') or [] + if not attachments: + continue + order_name = ORDER_TYPES.get(order['type'], {}).get('name', order['type']) + for idx, file_data in enumerate(attachments, 1): + caption_parts = [ + f"Заказ #{order['order_id']} — {escape(order_name)}", + f"Контакт: {format_contact_link(order.get('contact'))}" + ] + if file_data.get('file_name'): + caption_parts.append(f"Файл: {escape(file_data['file_name'])}") + caption_parts.append(f"#{idx} из {len(attachments)}") + caption = '
'.join(caption_parts) + if file_data.get('type') == 'photo': + await context.bot.send_photo( + ADMIN_CHAT_ID, + photo=file_data['file_id'], + caption=caption, + parse_mode=ParseMode.HTML + ) + else: + await context.bot.send_document( + ADMIN_CHAT_ID, + document=file_data['file_id'], + caption=caption, + parse_mode=ParseMode.HTML + ) + context.user_data.pop('cart', None) + return await main_menu(update, context, "Спасибо! Хотите заказать еще?") elif data == 'cancel_cart': context.user_data.pop('cart', None) return await main_menu(update, context, "Корзина отменена. Посмотрите еще?") @@ -460,7 +695,7 @@ async def confirm_cart_handler(update: Update, context: ContextTypes.DEFAULT_TYP # Показ прайс-листа async def show_price_list(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query - await query.answer() + await answer_callback(query) data = query.data if data.startswith('price_detail_'): key = data[13:] @@ -496,7 +731,7 @@ async def show_price_list(update: Update, context: ContextTypes.DEFAULT_TYPE): # Калькулятор цен async def price_calculator(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query - await query.answer() + await answer_callback(query) data = query.data if data.startswith('calc_type_'): key = data[10:] @@ -522,7 +757,7 @@ async def price_calculator(update: Update, context: ContextTypes.DEFAULT_TYPE): async def calc_select_deadline(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query - await query.answer() + await answer_callback(query) data = query.data if data.startswith('calc_dead_'): days = int(data[10:]) @@ -539,7 +774,7 @@ async def calc_select_deadline(update: Update, context: ContextTypes.DEFAULT_TYP async def calc_select_complexity(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query - await query.answer() + await answer_callback(query) data = query.data if data.startswith('calc_comp_'): comp = float(data[10:]) @@ -559,7 +794,7 @@ async def calc_select_complexity(update: Update, context: ContextTypes.DEFAULT_T # Показ профиля async def show_profile(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query - await query.answer() + await answer_callback(query) data = query.data user = update.effective_user user_id = str(user.id) @@ -587,7 +822,7 @@ async def show_profile(update: Update, context: ContextTypes.DEFAULT_TYPE): # Показ заказов async def show_orders(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query - await query.answer() + await answer_callback(query) data = query.data if data == 'profile': return await show_profile(update, context) @@ -616,7 +851,7 @@ async def input_feedback(update: Update, context: ContextTypes.DEFAULT_TYPE): # Показ FAQ async def show_faq(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query - await query.answer() + await answer_callback(query) data = query.data if data.startswith('faq_'): idx = int(data[4:]) @@ -648,7 +883,7 @@ async def show_admin_menu(update: Update, context: ContextTypes.DEFAULT_TYPE): text = "🔐 Админ-панель" if update.callback_query: query = update.callback_query - await query.answer() + await answer_callback(query) await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard)) else: await update.message.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard)) @@ -664,21 +899,106 @@ async def admin_start(update: Update, context: ContextTypes.DEFAULT_TYPE): return await show_admin_menu(update, context) # Обработчик админ-меню -async def admin_menu_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): - query = update.callback_query - await query.answer() - data = query.data - if data == 'admin_menu': - return await show_admin_menu(update, context) - text = "" - keyboard = [[InlineKeyboardButton("Назад", callback_data='admin_menu')]] - if data == 'admin_orders': - text = "📋 Заказы:\n" - for uid, ords in ORDERS.items(): - for ord in ords: - text += f"#{ord['order_id']} от {uid}: {ord['status']}\n" - elif data == 'admin_users': - text = "👥 Пользователи:\n" + "\n".join(f"ID: {uid}" for uid in ORDERS.keys()) +async def admin_menu_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): + query = update.callback_query + data = query.data + shared_routes = { + 'back_to_main': main_menu, + 'make_order': select_order_type, + 'price_list': show_price_list, + 'price_calculator': price_calculator, + 'profile': show_profile, + 'faq': show_faq, + } + if data in shared_routes: + context.user_data.pop('admin_state', None) + return await shared_routes[data](update, context) + if data == 'admin_menu': + return await show_admin_menu(update, context) + await answer_callback(query) + if data == 'admin_orders': + text_lines = [] + buttons = [] + for uid, ords in ORDERS.items(): + for ord_data in ords: + text_lines.append(f"#{ord_data.get('order_id', 'N/A')} от {uid}: {ord_data.get('status', 'новый')}") + buttons.append([ + InlineKeyboardButton( + f"#{ord_data.get('order_id', 'N/A')} ({uid})", + callback_data=f"admin_order|{uid}|{ord_data.get('order_id', 'N/A')}" + ) + ]) + if not text_lines: + text = "Заказы отсутствуют." + else: + text = "📋 Заказы:\n" + "\n".join(text_lines[:20]) + buttons.append([InlineKeyboardButton("Назад", callback_data='admin_menu')]) + await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(buttons)) + return ADMIN_MENU + if data.startswith('admin_order|'): + try: + _, uid, order_id_str = data.split('|', 2) + except ValueError: + await query.edit_message_text("Некорректный идентификатор заказа.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]])) + return ADMIN_MENU + order = next((o for o in ORDERS.get(uid, []) if str(o.get('order_id')) == order_id_str), None) + if not order: + await query.edit_message_text("Заказ не найден.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]])) + return ADMIN_MENU + text = build_order_details(uid, order) + keyboard = [ + [InlineKeyboardButton("Отменить заказ", callback_data=f'admin_cancel|{uid}|{order_id_str}')], + [InlineKeyboardButton("Удалить заказ", callback_data=f'admin_delete|{uid}|{order_id_str}')], + [InlineKeyboardButton("⬅️ К списку", callback_data='admin_orders')] + ] + await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode=ParseMode.HTML) + return ADMIN_MENU + if data.startswith('admin_cancel|'): + try: + _, uid, order_id_str = data.split('|', 2) + except ValueError: + await query.edit_message_text("Некорректный идентификатор заказа.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]])) + return ADMIN_MENU + order = next((o for o in ORDERS.get(uid, []) if str(o.get('order_id')) == order_id_str), None) + if not order: + await query.edit_message_text("Заказ не найден.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]])) + return ADMIN_MENU + order['status'] = 'отменен' + save_json(ORDERS_FILE, ORDERS) + text = f"Статус заказа #{order_id_str} обновлен на 'отменен'." + keyboard = [ + [InlineKeyboardButton("Посмотреть заказ", callback_data=f'admin_order|{uid}|{order_id_str}')], + [InlineKeyboardButton("⬅️ К списку", callback_data='admin_orders')] + ] + await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard)) + return ADMIN_MENU + if data.startswith('admin_delete|'): + try: + _, uid, order_id_str = data.split('|', 2) + except ValueError: + await query.edit_message_text("Некорректный идентификатор заказа.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]])) + return ADMIN_MENU + orders_list = ORDERS.get(uid, []) + new_list = [o for o in orders_list if str(o.get('order_id')) != order_id_str] + if len(new_list) == len(orders_list): + await query.edit_message_text("Заказ не найден.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Назад", callback_data='admin_orders')]])) + return ADMIN_MENU + if new_list: + ORDERS[uid] = new_list + else: + ORDERS.pop(uid, None) + save_json(ORDERS_FILE, ORDERS) + text = f"Заказ #{order_id_str} удален." + keyboard = [ + [InlineKeyboardButton("⬅️ К списку", callback_data='admin_orders')], + [InlineKeyboardButton("Админ-меню", callback_data='admin_menu')] + ] + await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard)) + return ADMIN_MENU + text = "" + keyboard = [[InlineKeyboardButton("Назад", callback_data='admin_menu')]] + if data == 'admin_users': + text = "👥 Пользователи:\n" + "\n".join(f"ID: {uid}" for uid in ORDERS.keys()) elif data == 'admin_logs': text = "📊 Логи (последние 10):\n" for uid, logs in list(USER_LOGS.items())[-10:]: @@ -687,17 +1007,15 @@ async def admin_menu_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) elif data == 'admin_prices': text = f"Текущий режим: {current_pricing_mode}\nВведите новый режим (hard/light):" context.user_data['admin_state'] = 'change_mode' - elif data == 'admin_export': - df = pd.DataFrame([{'user_id': uid, **ord} for uid, ords in ORDERS.items() for ord in ords]) - export_file = os.path.join(DATA_DIR, 'orders_export.csv') - df.to_csv(export_file, index=False) - await context.bot.send_document(ADMIN_CHAT_ID, open(export_file, 'rb')) - os.remove(export_file) - text = "📤 Экспорт отправлен!" - elif data == 'back_to_main': - return await main_menu(update, context) - await query.edit_message_text(text or "Неизвестная команда. Возвращаюсь в админ-меню.", reply_markup=InlineKeyboardMarkup(keyboard)) - return ADMIN_MENU + elif data == 'admin_export': + df = pd.DataFrame([{'user_id': uid, **ord} for uid, ords in ORDERS.items() for ord in ords]) + export_file = os.path.join(DATA_DIR, 'orders_export.csv') + df.to_csv(export_file, index=False) + await context.bot.send_document(ADMIN_CHAT_ID, open(export_file, 'rb')) + os.remove(export_file) + text = "📤 Экспорт отправлен!" + await query.edit_message_text(text or "Неизвестная команда. Возвращаюсь в админ-меню.", reply_markup=InlineKeyboardMarkup(keyboard)) + return ADMIN_MENU # Обработчик сообщений админа async def admin_message(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -720,11 +1038,19 @@ def main(): SELECT_ORDER_TYPE: [CallbackQueryHandler(select_order_type)], VIEW_ORDER_DETAILS: [CallbackQueryHandler(view_order_details)], INPUT_TOPIC: [MessageHandler(filters.TEXT & ~filters.COMMAND, input_topic)], - SELECT_DEADLINE: [CallbackQueryHandler(select_deadline)], - INPUT_REQUIREMENTS: [MessageHandler(filters.TEXT & ~filters.COMMAND, input_requirements), CommandHandler('skip', skip_requirements)], - ADD_UPSSELL: [CallbackQueryHandler(upsell_handler)], - ADD_ANOTHER_ORDER: [CallbackQueryHandler(add_another_handler)], - CONFIRM_CART: [CallbackQueryHandler(confirm_cart_handler)], + SELECT_DEADLINE: [CallbackQueryHandler(select_deadline)], + INPUT_REQUIREMENTS: [MessageHandler(filters.TEXT & ~filters.COMMAND, input_requirements), CommandHandler('skip', skip_requirements)], + UPLOAD_FILES: [ + MessageHandler(filters.Document.ALL, handle_document_upload), + MessageHandler(filters.PHOTO, handle_photo_upload), + MessageHandler(filters.TEXT & ~filters.COMMAND, files_text_reminder), + CommandHandler('skip', skip_files), + CommandHandler('done', finish_files) + ], + INPUT_CONTACT: [MessageHandler(filters.TEXT & ~filters.COMMAND, input_contact)], + ADD_UPSSELL: [CallbackQueryHandler(upsell_handler)], + ADD_ANOTHER_ORDER: [CallbackQueryHandler(add_another_handler)], + CONFIRM_CART: [CallbackQueryHandler(confirm_cart_handler)], ADMIN_MENU: [CallbackQueryHandler(admin_menu_handler), MessageHandler(filters.TEXT & ~filters.COMMAND, admin_message)], PROFILE_MENU: [CallbackQueryHandler(show_profile)], SHOW_PRICE_LIST: [CallbackQueryHandler(show_price_list)], @@ -742,5 +1068,5 @@ def main(): application.add_error_handler(error_handler) application.run_polling(drop_pending_updates=True) -if __name__ == '__main__': - main() \ No newline at end of file +if __name__ == '__main__': + main()