#!/usr/bin/env python3 """ Исправленная версия бота с правильным HTML форматированием """ import asyncio import logging import os import random import sys from aiogram import Bot, Dispatcher, F from aiogram.filters import Command, StateFilter from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.storage.memory import MemoryStorage from aiogram.types import (CallbackQuery, InaccessibleMessage, InlineKeyboardButton, InlineKeyboardMarkup, Message) from aiogram.utils.keyboard import InlineKeyboardBuilder # Добавляем путь к проекту project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, project_root) from config.config import config from src.database.database import DatabaseManager from src.services.csv_service import CSVQuizLoader, QuizGenerator # Настройка логирования logging.basicConfig(level=logging.INFO) class QuizStates(StatesGroup): choosing_mode = State() choosing_category = State() choosing_level = State() in_quiz = State() class QuizBot: def __init__(self): self.bot = Bot(token=config.bot_token) self.dp = Dispatcher(storage=MemoryStorage()) self.db = DatabaseManager(config.database_path) self.csv_loader = CSVQuizLoader(config.csv_data_path) # Регистрируем обработчики self.setup_handlers() def setup_handlers(self): """Регистрация всех обработчиков""" # Команды self.dp.message(Command("start"))(self.start_command) self.dp.message(Command("help"))(self.help_command) self.dp.message(Command("stats"))(self.stats_command) self.dp.message(Command("stop"))(self.stop_command) # Callback обработчики self.dp.callback_query(F.data == "guest_mode")(self.guest_mode_handler) self.dp.callback_query(F.data == "test_mode")(self.test_mode_handler) self.dp.callback_query(F.data.startswith("category_"))(self.category_handler) self.dp.callback_query(F.data.startswith("level_"))(self.level_handler) self.dp.callback_query(F.data.startswith("answer_"))(self.answer_handler) self.dp.callback_query(F.data == "next_question")(self.next_question) self.dp.callback_query(F.data == "stats")(self.stats_callback_handler) self.dp.callback_query(F.data == "back_to_menu")(self.back_to_menu) async def start_command(self, message: Message, state: FSMContext): """Обработка команды /start""" user = message.from_user # Регистрируем пользователя await self.db.register_user( user_id=user.id, username=user.username, first_name=user.first_name, last_name=user.last_name, language_code=user.language_code or "ru", ) await state.set_state(QuizStates.choosing_mode) keyboard = InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text="🎯 Гостевой режим (QUIZ)", callback_data="guest_mode" ) ], [ InlineKeyboardButton( text="📚 Тестирование по материалам", callback_data="test_mode" ) ], [InlineKeyboardButton(text="📊 Моя статистика", callback_data="stats")], ] ) await message.answer( f"👋 Добро пожаловать в Quiz Bot, {user.first_name}!\n\n" "🎯 Гостевой режим - быстрая викторина для развлечения\n" "📚 Тестирование - серьезное изучение материалов с результатами\n\n" "Выберите режим работы:", reply_markup=keyboard, parse_mode="HTML", ) async def help_command(self, message: Message): """Обработка команды /help""" help_text = """🤖 Команды бота: /start - Главное меню /help - Справка /stats - Ваша статистика /stop - Остановить текущий тест 🎯 Гостевой режим: • Быстрые викторины • Показ правильных ответов • Развлекательная атмосфера • 5 случайных вопросов 📚 Режим тестирования: • Серьезное тестирование знаний • Без показа правильных ответов • Рандомные варианты ответов • 10 вопросов, детальная статистика 📊 Доступные категории: • Корейский язык (уровни 1-5) • Более 120 уникальных вопросов""" await message.answer(help_text, parse_mode="HTML") async def stats_command(self, message: Message): """Обработка команды /stats""" user_stats = await self.db.get_user_stats(message.from_user.id) if not user_stats or user_stats["total_questions"] == 0: await message.answer("📊 У вас пока нет статистики. Пройдите первый тест!") return accuracy = ( (user_stats["correct_answers"] / user_stats["total_questions"]) * 100 if user_stats["total_questions"] > 0 else 0 ) stats_text = f"""📊 Ваша статистика: ❓ Всего вопросов: {user_stats['total_questions']} ✅ Правильных ответов: {user_stats['correct_answers']} 📈 Точность: {accuracy:.1f}% 🎯 Завершенных сессий: {user_stats['sessions_completed'] or 0} 🏆 Лучший результат: {user_stats['best_score'] or 0:.1f}% 📊 Средний балл: {user_stats['average_score'] or 0:.1f}%""" keyboard = InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text="🏠 Главное меню", callback_data="back_to_menu" ) ] ] ) await message.answer(stats_text, reply_markup=keyboard, parse_mode="HTML") async def stop_command(self, message: Message): """Остановка текущего теста""" session = await self.db.get_active_session(message.from_user.id) if session: await self.db.finish_session(message.from_user.id, 0) await message.answer("❌ Текущий тест остановлен.") else: await message.answer("❌ У вас нет активного теста.") keyboard = InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text="🏠 Главное меню", callback_data="back_to_menu" ) ] ] ) await message.answer("Что бы вы хотели сделать дальше?", reply_markup=keyboard) async def guest_mode_handler(self, callback: CallbackQuery, state: FSMContext): """Обработка выбора гостевого режима""" await state.update_data(mode="guest") await state.set_state(QuizStates.choosing_category) keyboard = InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text="🇰🇷 Корейский язык", callback_data="category_korean" ) ], [InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_menu")], ] ) await callback.message.edit_text( "🎯 Гостевой режим\n\nВыберите категорию для викторины:", reply_markup=keyboard, parse_mode="HTML", ) await callback.answer() async def test_mode_handler(self, callback: CallbackQuery, state: FSMContext): """Обработка выбора режима тестирования""" await state.update_data(mode="test") await state.set_state(QuizStates.choosing_category) keyboard = InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text="🇰🇷 Корейский язык", callback_data="category_korean" ) ], [InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_menu")], ] ) await callback.message.edit_text( "📚 Режим тестирования\n\nВыберите категорию для серьезного изучения:", reply_markup=keyboard, parse_mode="HTML", ) await callback.answer() async def category_handler(self, callback: CallbackQuery, state: FSMContext): """Обработка выбора категории""" category = callback.data.split("_")[1] await state.update_data(category=category) keyboard = InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text="🥉 Уровень 1 (начальный)", callback_data="level_1" ) ], [ InlineKeyboardButton( text="🥈 Уровень 2 (базовый)", callback_data="level_2" ) ], [ InlineKeyboardButton( text="🥇 Уровень 3 (средний)", callback_data="level_3" ) ], [ InlineKeyboardButton( text="🏆 Уровень 4 (продвинутый)", callback_data="level_4" ) ], [ InlineKeyboardButton( text="💎 Уровень 5 (эксперт)", callback_data="level_5" ) ], [InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_menu")], ] ) await callback.message.edit_text( f"🇰🇷 Корейский язык\n\nВыберите уровень сложности:", reply_markup=keyboard, parse_mode="HTML", ) await callback.answer() async def level_handler(self, callback: CallbackQuery, state: FSMContext): """Обработка выбора уровня""" level = int(callback.data.split("_")[1]) data = await state.get_data() # Загружаем вопросы filename = f"{data['category']}_level_{level}.csv" questions = await self.csv_loader.load_questions_from_csv(filename) if not questions: await callback.message.edit_text( "❌ Вопросы для этого уровня пока недоступны.", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text="🔙 Назад", callback_data="back_to_menu" ) ] ] ), ) await callback.answer() return # Определяем количество вопросов questions_count = 5 if data["mode"] == "guest" else 10 # Берем случайные вопросы selected_questions = random.sample( questions, min(questions_count, len(questions)) ) # Создаем тестовую запись в БД test_id = await self.db.add_test( name=f"{data['category'].title()} Level {level}", description=f"Тест по {data['category']} языку, уровень {level}", level=level, category=data["category"], csv_file=filename, ) # Начинаем сессию await self.db.start_session( user_id=callback.from_user.id, test_id=test_id or 1, questions=selected_questions, mode=data["mode"], ) await state.set_state(QuizStates.in_quiz) await self.show_question_safe(callback, callback.from_user.id, 0) await callback.answer() def shuffle_answers(self, question_data: dict) -> dict: """Перемешивает варианты ответов и обновляет правильный ответ""" options = [ question_data["option1"], question_data["option2"], question_data["option3"], question_data["option4"], ] correct_answer_text = options[question_data["correct_answer"] - 1] # Перемешиваем варианты random.shuffle(options) # Находим новую позицию правильного ответа new_correct_position = options.index(correct_answer_text) + 1 # Обновляем данные вопроса shuffled_question = question_data.copy() shuffled_question["option1"] = options[0] shuffled_question["option2"] = options[1] shuffled_question["option3"] = options[2] shuffled_question["option4"] = options[3] shuffled_question["correct_answer"] = new_correct_position return shuffled_question async def show_question_safe( self, callback: CallbackQuery, user_id: int, question_index: int ): """Безопасный показ вопроса через callback""" session = await self.db.get_active_session(user_id) if not session or question_index >= len(session["questions_data"]): return question = session["questions_data"][question_index] # Перемешиваем варианты ответов только в тестовом режиме if session["mode"] == "test": question = self.shuffle_answers(question) session["questions_data"][question_index] = question await self.db.update_session_questions(user_id, session["questions_data"]) total_questions = len(session["questions_data"]) # Создаем клавиатуру с ответами keyboard_builder = InlineKeyboardBuilder() for i in range(1, 5): keyboard_builder.add( InlineKeyboardButton( text=f"{i}. {question[f'option{i}']}", callback_data=f"answer_{i}" ) ) keyboard_builder.adjust(1) question_text = ( f"❓ Вопрос {question_index + 1}/{total_questions}\n\n" f"{question['question']}" ) # Безопасная отправка сообщения if callback.message and not isinstance(callback.message, InaccessibleMessage): try: await callback.message.edit_text( question_text, reply_markup=keyboard_builder.as_markup(), parse_mode="HTML", ) except Exception: await self.bot.send_message( callback.from_user.id, question_text, reply_markup=keyboard_builder.as_markup(), parse_mode="HTML", ) else: await self.bot.send_message( callback.from_user.id, question_text, reply_markup=keyboard_builder.as_markup(), parse_mode="HTML", ) async def answer_handler(self, callback: CallbackQuery, state: FSMContext): """Обработка ответа на вопрос""" answer = int(callback.data.split("_")[1]) user_id = callback.from_user.id session = await self.db.get_active_session(user_id) if not session: await callback.answer("❌ Сессия не найдена") return current_q_index = session["current_question"] question = session["questions_data"][current_q_index] is_correct = answer == question["correct_answer"] mode = session["mode"] # Обновляем счетчик правильных ответов if is_correct: session["correct_count"] += 1 # Обновляем прогресс в базе await self.db.update_session_progress( user_id, current_q_index + 1, session["correct_count"] ) # Проверяем, есть ли еще вопросы if current_q_index + 1 >= len(session["questions_data"]): # Тест завершен score = (session["correct_count"] / len(session["questions_data"])) * 100 await self.db.finish_session(user_id, score) keyboard = InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text="🏠 Главное меню", callback_data="back_to_menu" ) ], [ InlineKeyboardButton( text="📊 Моя статистика", callback_data="stats" ) ], ] ) # Разный текст для разных режимов if mode == "test": final_text = ( f"🎉 Тест завершен!\n\n" f"📊 Результат: {session['correct_count']}/{len(session['questions_data'])}\n" f"📈 Точность: {score:.1f}%\n" f"🏆 Оценка: {self.get_grade(score)}\n\n" f"💡 Результат сохранен в вашей статистике" ) else: result_text = ( "✅ Правильно!" if is_correct else f"❌ Неправильно. Правильный ответ: {question['correct_answer']}" ) final_text = ( f"{result_text}\n\n" f"🎉 Викторина завершена!\n\n" f"📊 Результат: {session['correct_count']}/{len(session['questions_data'])}\n" f"📈 Точность: {score:.1f}%\n" f"🏆 Оценка: {self.get_grade(score)}" ) # Безопасная отправка сообщения if callback.message and not isinstance( callback.message, InaccessibleMessage ): try: await callback.message.edit_text( final_text, reply_markup=keyboard, parse_mode="HTML" ) except Exception: await self.bot.send_message( callback.from_user.id, final_text, reply_markup=keyboard, parse_mode="HTML", ) else: await self.bot.send_message( callback.from_user.id, final_text, reply_markup=keyboard, parse_mode="HTML", ) else: # Есть еще вопросы if mode == "test": # В тестовом режиме сразу переходим к следующему вопросу await self.show_question_safe( callback, callback.from_user.id, current_q_index + 1 ) else: # В гостевом режиме показываем результат и кнопку "Следующий" result_text = ( "✅ Правильно!" if is_correct else f"❌ Неправильно. Правильный ответ: {question['correct_answer']}" ) keyboard = InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text="➡️ Следующий вопрос", callback_data="next_question" ) ] ] ) # Безопасная отправка сообщения if callback.message and not isinstance( callback.message, InaccessibleMessage ): try: await callback.message.edit_text( result_text, reply_markup=keyboard ) except Exception: await self.bot.send_message( callback.from_user.id, result_text, reply_markup=keyboard ) else: await self.bot.send_message( callback.from_user.id, result_text, reply_markup=keyboard ) await callback.answer() async def next_question(self, callback: CallbackQuery): """Переход к следующему вопросу""" session = await self.db.get_active_session(callback.from_user.id) if session: await self.show_question_safe( callback, callback.from_user.id, session["current_question"] ) await callback.answer() async def stats_callback_handler(self, callback: CallbackQuery): """Обработчик кнопки статистики через callback""" user_stats = await self.db.get_user_stats(callback.from_user.id) if not user_stats or user_stats["total_questions"] == 0: stats_text = "📊 У вас пока нет статистики. Пройдите первый тест!" else: accuracy = ( (user_stats["correct_answers"] / user_stats["total_questions"]) * 100 if user_stats["total_questions"] > 0 else 0 ) # Получаем дополнительную статистику recent_results = await self.db.get_recent_results(callback.from_user.id, 3) category_stats = await self.db.get_category_stats(callback.from_user.id) best_score = user_stats["best_score"] or 0 avg_score = user_stats["average_score"] or 0 stats_text = f"""📊 Ваша статистика: 📈 Общие показатели: ❓ Всего вопросов: {user_stats['total_questions']} ✅ Правильных ответов: {user_stats['correct_answers']} 🎯 Общая точность: {accuracy:.1f}% 🎪 Завершенных сессий: {user_stats['sessions_completed'] or 0} 🏆 Лучший результат: {best_score:.1f}% 📊 Средний балл: {avg_score:.1f}% 🎮 По режимам: 🎯 Гостевые викторины: {user_stats['guest_sessions'] or 0} 📚 Серьезные тесты: {user_stats['test_sessions'] or 0}""" # Добавляем статистику по категориям if category_stats: stats_text += "\n\n🏷️ По категориям:" for cat_stat in category_stats[:2]: cat_accuracy = ( (cat_stat["correct_answers"] / cat_stat["total_questions"]) * 100 if cat_stat["total_questions"] > 0 else 0 ) stats_text += f"\n📖 {cat_stat['category']}: {cat_stat['attempts']} попыток, {cat_accuracy:.1f}% точность" # Добавляем последние результаты if recent_results: stats_text += "\n\n📈 Последние результаты:" for result in recent_results: mode_emoji = "🎯" if result["mode"] == "guest" else "📚" stats_text += f"\n{mode_emoji} {result['score']:.1f}% ({result['correct_answers']}/{result['questions_asked']})" keyboard = InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text="🏠 Главное меню", callback_data="back_to_menu" ) ], [ InlineKeyboardButton( text="🔄 Обновить статистику", callback_data="stats" ) ], ] ) # Безопасная отправка сообщения if callback.message and not isinstance(callback.message, InaccessibleMessage): try: await callback.message.edit_text( stats_text, reply_markup=keyboard, parse_mode="HTML" ) except Exception: await self.bot.send_message( callback.from_user.id, stats_text, reply_markup=keyboard, parse_mode="HTML", ) else: await self.bot.send_message( callback.from_user.id, stats_text, reply_markup=keyboard, parse_mode="HTML", ) await callback.answer() async def back_to_menu(self, callback: CallbackQuery, state: FSMContext): """Возврат в главное меню""" await state.clear() user = callback.from_user # Регистрируем пользователя (если еще не зарегистрирован) await self.db.register_user( user_id=user.id, username=user.username, first_name=user.first_name, last_name=user.last_name, language_code=user.language_code or "ru", ) await state.set_state(QuizStates.choosing_mode) keyboard = InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text="🎯 Гостевой режим (QUIZ)", callback_data="guest_mode" ) ], [ InlineKeyboardButton( text="📚 Тестирование по материалам", callback_data="test_mode" ) ], [InlineKeyboardButton(text="📊 Моя статистика", callback_data="stats")], ] ) text = ( f"👋 Добро пожаловать в Quiz Bot, {user.first_name}!\n\n" "🎯 Гостевой режим - быстрая викторина для развлечения\n" "📚 Тестирование - серьезное изучение материалов с результатами\n\n" "Выберите режим работы:" ) if callback.message and not isinstance(callback.message, InaccessibleMessage): try: await callback.message.edit_text( text, reply_markup=keyboard, parse_mode="HTML" ) except Exception: await self.bot.send_message( callback.from_user.id, text, reply_markup=keyboard, parse_mode="HTML", ) else: await self.bot.send_message( callback.from_user.id, text, reply_markup=keyboard, parse_mode="HTML" ) await callback.answer() def get_grade(self, score: float) -> str: """Получение оценки по проценту правильных ответов""" if score >= 90: return "Отлично! 🌟" elif score >= 70: return "Хорошо! 👍" elif score >= 50: return "Удовлетворительно 📚" else: return "Нужно подтянуть знания 📖" async def start(self): """Запуск бота""" # Проверяем токен if not config.bot_token or config.bot_token in [ "your_bot_token_here", "test_token_for_demo_purposes", ]: print("❌ Ошибка: не настроен BOT_TOKEN в файле .env") return False # Инициализируем базу данных await self.db.init_database() print("✅ Bot starting...") print(f"🗄️ Database: {config.database_path}") print(f"📁 CSV files: {config.csv_data_path}") try: await self.dp.start_polling(self.bot) except Exception as e: logging.error(f"Error starting bot: {e}") return False async def main(): """Главная функция""" bot = QuizBot() await bot.start() if __name__ == "__main__": asyncio.run(main())