#!/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())