add stuckt

This commit is contained in:
2025-09-22 12:35:41 +05:00
parent 5b9c55a4dc
commit 046b610a34
28 changed files with 937 additions and 3576 deletions

10
app.bot/__init__.py Normal file
View File

@@ -0,0 +1,10 @@
"""
Telegram Bot Module
Модуль содержит все компоненты Telegram бота:
- telegram_bot.py - основной файл бота
- bot_config.py - конфигурация
- api_client.py - HTTP клиент для API
"""
__version__ = "1.0.0"

307
app.bot/api_client.py Normal file
View File

@@ -0,0 +1,307 @@
"""
HTTP клиент для взаимодействия с API
"""
import aiohttp
import asyncio
import logging
from typing import Optional, List, Dict, Any, Union
from dataclasses import dataclass
import json
from bot_config import config
logger = logging.getLogger(__name__)
@dataclass
class APIResponse:
"""Стандартный ответ API"""
success: bool
data: Any = None
error: Optional[str] = None
status_code: int = 200
class APIClient:
"""HTTP клиент для работы с FastAPI"""
def __init__(self, base_url: str = None, timeout: int = 30):
self.base_url = base_url or config.API_BASE_URL
self.timeout = timeout
self.session: Optional[aiohttp.ClientSession] = None
async def __aenter__(self):
"""Асинхронный контекстный менеджер"""
await self.create_session()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Закрытие сессии"""
await self.close_session()
async def create_session(self):
"""Создание HTTP сессии"""
if not self.session or self.session.closed:
timeout = aiohttp.ClientTimeout(total=self.timeout)
self.session = aiohttp.ClientSession(
timeout=timeout,
headers={'Content-Type': 'application/json'}
)
async def close_session(self):
"""Закрытие HTTP сессии"""
if self.session and not self.session.closed:
await self.session.close()
async def _make_request(
self,
method: str,
endpoint: str,
data: Optional[Dict] = None,
params: Optional[Dict] = None,
files: Optional[Dict] = None
) -> APIResponse:
"""Базовый метод для HTTP запросов"""
await self.create_session()
url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}"
try:
# Если есть файлы, не устанавливаем Content-Type
headers = {}
if not files:
headers['Content-Type'] = 'application/json'
kwargs = {
'params': params,
'headers': headers
}
# Данные для запроса
if files:
# Для файлов используем FormData
kwargs['data'] = aiohttp.FormData()
if data:
for key, value in data.items():
kwargs['data'].add_field(key, str(value))
for key, file_data in files.items():
kwargs['data'].add_field(key, file_data)
elif data:
kwargs['data'] = json.dumps(data)
async with self.session.request(method, url, **kwargs) as response:
status_code = response.status
# Обработка ответа
if response.content_type == 'application/json':
response_data = await response.json()
else:
response_data = await response.text()
if 200 <= status_code < 300:
return APIResponse(
success=True,
data=response_data,
status_code=status_code
)
else:
error_msg = response_data.get('detail', f'HTTP {status_code}') if isinstance(response_data, dict) else str(response_data)
logger.error(f"API Error {status_code}: {error_msg}")
return APIResponse(
success=False,
error=error_msg,
status_code=status_code
)
except aiohttp.ClientError as e:
logger.error(f"HTTP Client Error: {e}")
return APIResponse(
success=False,
error=f"Connection error: {str(e)}",
status_code=500
)
except Exception as e:
logger.error(f"Unexpected error: {e}")
return APIResponse(
success=False,
error=f"Unexpected error: {str(e)}",
status_code=500
)
# ===== ПОЛЬЗОВАТЕЛИ =====
async def get_user_by_telegram_id(self, telegram_id: int) -> APIResponse:
"""Получение пользователя по Telegram ID"""
return await self._make_request('GET', f'/users/{telegram_id}')
async def create_user(self, user_data: Dict) -> APIResponse:
"""Создание нового пользователя"""
return await self._make_request('POST', '/users/', data=user_data)
async def update_user(self, telegram_id: int, user_data: Dict) -> APIResponse:
"""Обновление данных пользователя"""
return await self._make_request('PUT', f'/users/{telegram_id}', data=user_data)
async def get_all_users(self, limit: int = 100, offset: int = 0, is_active: Optional[bool] = None) -> APIResponse:
"""Получение списка пользователей"""
params = {'limit': limit, 'offset': offset}
if is_active is not None:
params['is_active'] = is_active
return await self._make_request('GET', '/users/', params=params)
# ===== ТЕСТЫ =====
async def get_tests(self, is_active: Optional[bool] = None, category_id: Optional[int] = None) -> APIResponse:
"""Получение списка тестов"""
params = {}
if is_active is not None:
params['is_active'] = is_active
if category_id is not None:
params['category_id'] = category_id
return await self._make_request('GET', '/tests/', params=params)
async def get_test_by_id(self, test_id: int) -> APIResponse:
"""Получение теста по ID"""
return await self._make_request('GET', f'/tests/{test_id}')
async def create_test(self, test_data: Dict) -> APIResponse:
"""Создание нового теста"""
return await self._make_request('POST', '/tests/', data=test_data)
# ===== ВОПРОСЫ =====
async def get_questions_by_test(self, test_id: int) -> APIResponse:
"""Получение всех вопросов для теста"""
return await self._make_request('GET', f'/questions/test/{test_id}')
async def get_question_by_id(self, question_id: int) -> APIResponse:
"""Получение вопроса по ID"""
return await self._make_request('GET', f'/questions/{question_id}')
async def create_question(self, question_data: Dict) -> APIResponse:
"""Создание нового вопроса"""
return await self._make_request('POST', '/questions/', data=question_data)
# ===== ВАРИАНТЫ ОТВЕТОВ =====
async def get_answer_options_by_question(self, question_id: int) -> APIResponse:
"""Получение вариантов ответов для вопроса"""
return await self._make_request('GET', f'/answer-options/question/{question_id}')
async def create_answer_option(self, option_data: Dict) -> APIResponse:
"""Создание варианта ответа"""
return await self._make_request('POST', '/answer-options/', data=option_data)
# ===== ИЗОБРАЖЕНИЯ =====
async def upload_image(self, file_content: bytes, filename: str, content_type: str, alt_text: Optional[str] = None) -> APIResponse:
"""Загрузка изображения"""
files = {'file': (filename, file_content, content_type)}
data = {}
if alt_text:
data['alt_text'] = alt_text
return await self._make_request('POST', '/images/upload', data=data, files=files)
async def get_image(self, image_id: int) -> APIResponse:
"""Получение изображения по ID"""
return await self._make_request('GET', f'/images/{image_id}')
# ===== НАСТРОЙКИ =====
async def get_all_settings(self) -> APIResponse:
"""Получение всех настроек"""
return await self._make_request('GET', '/settings/')
async def get_setting(self, key: str) -> APIResponse:
"""Получение настройки по ключу"""
return await self._make_request('GET', f'/settings/{key}')
async def create_setting(self, setting_data: Dict) -> APIResponse:
"""Создание новой настройки"""
return await self._make_request('POST', '/settings/', data=setting_data)
async def update_setting(self, key: str, setting_data: Dict) -> APIResponse:
"""Обновление настройки"""
return await self._make_request('PUT', f'/settings/{key}', data=setting_data)
# ===== СООБЩЕНИЯ =====
async def create_message(self, message_data: Dict) -> APIResponse:
"""Создание записи о сообщении"""
return await self._make_request('POST', '/messages/', data=message_data)
async def get_messages(self, user_id: Optional[int] = None, message_type: Optional[str] = None, limit: int = 100) -> APIResponse:
"""Получение списка сообщений"""
params = {'limit': limit}
if user_id:
params['user_id'] = user_id
if message_type:
params['message_type'] = message_type
return await self._make_request('GET', '/messages/', params=params)
# ===== УТИЛИТЫ =====
async def health_check(self) -> bool:
"""Проверка доступности API"""
try:
response = await self._make_request('GET', '/health')
return response.success
except Exception as e:
logger.error(f"Health check failed: {e}")
return False
# Глобальный экземпляр клиента
api_client = APIClient()
# Вспомогательные функции для удобства использования
async def ensure_user_exists(telegram_id: int, user_data: Dict) -> Optional[Dict]:
"""Убедиться, что пользователь существует, создать если нет"""
async with api_client:
# Попробуем получить пользователя
response = await api_client.get_user_by_telegram_id(telegram_id)
if response.success:
return response.data
# Если пользователь не найден, создаем нового
if response.status_code == 404:
create_response = await api_client.create_user(user_data)
if create_response.success:
logger.info(f"Создан новый пользователь: {telegram_id}")
return create_response.data
else:
logger.error(f"Ошибка создания пользователя: {create_response.error}")
return None
else:
logger.error(f"Ошибка получения пользователя: {response.error}")
return None
async def get_available_tests() -> List[Dict]:
"""Получить доступные тесты"""
async with api_client:
response = await api_client.get_tests(is_active=True)
if response.success:
return response.data
else:
logger.error(f"Ошибка получения тестов: {response.error}")
return []
async def get_test_questions_with_options(test_id: int) -> List[Dict]:
"""Получить вопросы теста с вариантами ответов"""
async with api_client:
# Получаем вопросы
questions_response = await api_client.get_questions_by_test(test_id)
if not questions_response.success:
logger.error(f"Ошибка получения вопросов: {questions_response.error}")
return []
questions = questions_response.data
# Для каждого вопроса получаем варианты ответов
for question in questions:
options_response = await api_client.get_answer_options_by_question(question['id'])
if options_response.success:
question['options'] = options_response.data
else:
question['options'] = []
logger.error(f"Ошибка получения вариантов для вопроса {question['id']}: {options_response.error}")
return questions

58
app.bot/bot_config.py Normal file
View File

@@ -0,0 +1,58 @@
"""
Конфигурация для Telegram бота
"""
import os
from dotenv import load_dotenv
# Загружаем переменные окружения
load_dotenv()
class BotConfig:
"""Конфигурация бота"""
# Telegram Bot
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
# Database
DB_HOST = os.getenv("DB_HOST", "localhost")
DB_PORT = os.getenv("DB_PORT", "5432")
DB_NAME = os.getenv("DB_NAME", "botdb")
DB_USER = os.getenv("DB_USER", "botuser")
DB_PASSWORD = os.getenv("DB_PASSWORD")
# Application
APP_ENV = os.getenv("APP_ENV", "development")
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
DEBUG = os.getenv("DEBUG", "False").lower() == "true"
# API Settings
API_HOST = os.getenv("API_HOST", "localhost")
API_PORT = int(os.getenv("API_PORT", "8000"))
API_BASE_URL = os.getenv("API_BASE_URL", f"http://{API_HOST}:{API_PORT}")
API_TIMEOUT = int(os.getenv("API_TIMEOUT", "30"))
@classmethod
def validate(cls):
"""Проверка обязательных конфигурационных параметров"""
if not cls.TELEGRAM_BOT_TOKEN:
raise ValueError("TELEGRAM_BOT_TOKEN не найден в переменных окружения")
if not cls.DB_PASSWORD:
raise ValueError("DB_PASSWORD не найден в переменных окружения")
return True
@classmethod
def get_database_url(cls):
"""Получение URL для подключения к базе данных"""
return f"postgresql://{cls.DB_USER}:{cls.DB_PASSWORD}@{cls.DB_HOST}:{cls.DB_PORT}/{cls.DB_NAME}"
# Создаем экземпляр конфигурации
config = BotConfig()
# Проверяем конфигурацию только если не в Docker (где переменные могут быть не доступны на момент импорта)
if os.getenv("DOCKER_BUILD") != "true":
try:
config.validate()
except ValueError as e:
print(f"Предупреждение конфигурации: {e}")

60
app.bot/main.py Normal file
View File

@@ -0,0 +1,60 @@
"""
Точка входа для Telegram бота
Бот работает только через API, без прямого доступа к БД
"""
import asyncio
import logging
import sys
import os
# Добавляем текущую директорию в PYTHONPATH
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from bot_config import config
def setup_logging():
"""Настройка системы логирования"""
logging.basicConfig(
level=getattr(logging, config.LOG_LEVEL),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler('bot.log') if config.APP_ENV == 'production' else logging.NullHandler()
]
)
async def main():
"""Основная функция запуска бота"""
# Настраиваем логирование
setup_logging()
logger = logging.getLogger(__name__)
try:
# Проверяем конфигурацию
config.validate()
logger.info(f"Конфигурация проверена. Режим: {config.APP_ENV}")
logger.info(f"API URL: {config.API_BASE_URL}")
# Проверяем доступность API
from api_client import api_client
async with api_client:
api_available = await api_client.health_check()
if not api_available:
logger.error("API недоступен! Проверьте, что API сервер запущен.")
return
logger.info("API доступен, запускаем бота...")
# Запускаем бота
import telegram_bot
await telegram_bot.main()
except KeyboardInterrupt:
logger.info("Получен сигнал остановки")
except Exception as e:
logger.error(f"Критическая ошибка: {e}")
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())

469
app.bot/telegram_bot.py Normal file
View File

@@ -0,0 +1,469 @@
"""
Современный Telegram бот, работающий только через API
БЕЗ прямого доступа к базе данных
"""
import asyncio
import io
from datetime import datetime
from aiogram import Bot, Dispatcher, types
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove, BufferedInputFile
from aiogram.filters.state import StateFilter
import logging
from bot_config import config
from api_client import api_client
# Настройка логирования
logging.basicConfig(
level=getattr(logging, config.LOG_LEVEL),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Инициализация бота
bot = Bot(token=config.TELEGRAM_BOT_TOKEN)
dp = Dispatcher(storage=MemoryStorage())
# ===== FSM States =====
class Form(StatesGroup):
consent = State()
fio = State()
dob = State()
phone = State()
city = State()
test_in_progress = State()
# ===== /start command =====
@dp.message(Command("start"))
async def start(message: types.Message, state: FSMContext):
"""Обработка команды /start"""
try:
# Создаем или получаем пользователя через API
user_data = {
"telegram_id": message.from_user.id,
"username": message.from_user.username,
"first_name": message.from_user.first_name,
"last_name": message.from_user.last_name,
"language_code": message.from_user.language_code or 'ru'
}
async with api_client:
# Попробуем получить пользователя
response = await api_client.get_user_by_telegram_id(message.from_user.id)
if response.success:
user = response.data
logger.info(f"Найден существующий пользователь: {user['telegram_id']}")
else:
# Создаем нового пользователя
create_response = await api_client.create_user(user_data)
if create_response.success:
user = create_response.data
logger.info(f"Создан новый пользователь: {user['telegram_id']}")
else:
logger.error(f"Ошибка создания пользователя: {create_response.error}")
await message.answer("Произошла ошибка при регистрации. Попробуйте позже.")
return
# Сохраняем данные пользователя в состояние
await state.update_data(user_id=user['id'], telegram_id=user['telegram_id'])
welcome_text = f"Добро пожаловать, {user.get('first_name', 'пользователь')}!\n\n"
welcome_text += "Этот бот поможет вам пройти тестирование.\n"
welcome_text += "Для продолжения необходимо дать согласие на обработку персональных данных.\n\n"
welcome_text += "Даете ли вы согласие на обработку персональных данных?"
keyboard = ReplyKeyboardMarkup(
keyboard=[
[KeyboardButton(text="Да, согласен")],
[KeyboardButton(text="Нет, не согласен")]
],
resize_keyboard=True,
one_time_keyboard=True
)
await message.answer(welcome_text, reply_markup=keyboard)
await state.set_state(Form.consent)
except Exception as e:
logger.error(f"Ошибка в start handler: {e}")
await message.answer("Произошла ошибка. Попробуйте позже.")
# ===== Согласие на обработку данных =====
@dp.message(Form.consent)
async def process_consent(message: types.Message, state: FSMContext):
"""Обработка согласия на обработку данных"""
if message.text == "Да, согласен":
await message.answer(
"Спасибо! Теперь введите ваше ФИО:",
reply_markup=ReplyKeyboardRemove()
)
await state.set_state(Form.fio)
elif message.text == "Нет, не согласен":
await message.answer(
"К сожалению, без согласия на обработку персональных данных использование бота невозможно.",
reply_markup=ReplyKeyboardRemove()
)
await state.clear()
else:
await message.answer("Пожалуйста, выберите один из предложенных вариантов.")
# ===== Сбор ФИО =====
@dp.message(Form.fio)
async def process_fio(message: types.Message, state: FSMContext):
"""Обработка ввода ФИО"""
if len(message.text.strip()) < 5:
await message.answer("Пожалуйста, введите полное ФИО (минимум 5 символов).")
return
await state.update_data(fio=message.text.strip())
await message.answer("Введите вашу дату рождения в формате ДД.ММ.ГГГГ:")
await state.set_state(Form.dob)
# ===== Дата рождения =====
@dp.message(Form.dob)
async def process_dob(message: types.Message, state: FSMContext):
"""Обработка ввода даты рождения"""
try:
# Проверяем формат даты
dob = datetime.strptime(message.text.strip(), "%d.%m.%Y")
# Проверяем возраст (должен быть от 16 до 80 лет)
age = (datetime.now() - dob).days // 365
if age < 16 or age > 80:
await message.answer("Возраст должен быть от 16 до 80 лет. Проверьте правильность даты.")
return
await state.update_data(date_of_birth=message.text.strip())
await message.answer("Введите ваш номер телефона:")
await state.set_state(Form.phone)
except ValueError:
await message.answer("Неверный формат даты. Используйте формат ДД.ММ.ГГГГ (например, 15.05.1990):")
# ===== Телефон =====
@dp.message(Form.phone)
async def process_phone(message: types.Message, state: FSMContext):
"""Обработка ввода телефона"""
phone = message.text.strip()
# Простая проверка телефона
if len(phone) < 10 or not any(char.isdigit() for char in phone):
await message.answer("Пожалуйста, введите корректный номер телефона.")
return
await state.update_data(phone=phone)
await message.answer("Введите ваш город:")
await state.set_state(Form.city)
# ===== Город =====
@dp.message(Form.city)
async def process_city(message: types.Message, state: FSMContext):
"""Обработка ввода города"""
city = message.text.strip()
if len(city) < 2:
await message.answer("Пожалуйста, введите название города (минимум 2 символа).")
return
# Получаем все собранные данные
data = await state.get_data()
telegram_id = data['telegram_id']
# Обновляем данные пользователя через API
update_data = {
"first_name": data.get('fio', '').split()[0] if data.get('fio') else None,
"last_name": ' '.join(data.get('fio', '').split()[1:]) if len(data.get('fio', '').split()) > 1 else None,
# Можно добавить дополнительные поля в модель User для хранения ФИО, телефона, города и даты рождения
}
async with api_client:
response = await api_client.update_user(telegram_id, update_data)
if not response.success:
logger.error(f"Ошибка обновления пользователя: {response.error}")
await message.answer(
f"Спасибо! Ваши данные сохранены:\n"
f"ФИО: {data['fio']}\n"
f"Дата рождения: {data['date_of_birth']}\n"
f"Телефон: {data['phone']}\n"
f"Город: {city}\n\n"
f"Теперь вы можете приступить к тестированию!"
)
# Показываем доступные тесты
await show_available_tests(message, state)
# ===== Показать доступные тесты =====
async def show_available_tests(message: types.Message, state: FSMContext):
"""Показать список доступных тестов"""
try:
async with api_client:
response = await api_client.get_tests(is_active=True)
if not response.success:
logger.error(f"Ошибка получения тестов: {response.error}")
await message.answer("Ошибка загрузки тестов. Попробуйте позже.")
return
tests = response.data
if not tests:
await message.answer("На данный момент нет доступных тестов.")
return
# Создаем клавиатуру с тестами
keyboard_buttons = []
for test in tests:
keyboard_buttons.append([KeyboardButton(text=test['title'])])
keyboard = ReplyKeyboardMarkup(
keyboard=keyboard_buttons,
resize_keyboard=True,
one_time_keyboard=True
)
await message.answer(
"Выберите тест для прохождения:",
reply_markup=keyboard
)
await state.clear() # Очищаем предыдущее состояние
except Exception as e:
logger.error(f"Ошибка в show_available_tests: {e}")
await message.answer("Произошла ошибка при загрузке тестов.")
# ===== Выбор теста =====
@dp.message()
async def handle_test_selection(message: types.Message, state: FSMContext):
"""Обработка выбора теста"""
try:
async with api_client:
# Получаем все тесты и ищем по названию
response = await api_client.get_tests(is_active=True)
if not response.success:
await message.answer("Ошибка загрузки тестов.")
return
selected_test = None
for test in response.data:
if test['title'] == message.text:
selected_test = test
break
if not selected_test:
await message.answer(
"Тест не найден. Используйте /start для просмотра доступных тестов.",
reply_markup=ReplyKeyboardRemove()
)
return
# Получаем вопросы теста
questions_response = await api_client.get_questions_by_test(selected_test['id'])
if not questions_response.success:
await message.answer("Ошибка загрузки вопросов теста.")
return
questions = questions_response.data
if not questions:
await message.answer("В этом тесте пока нет вопросов.")
return
# Загружаем варианты ответов для всех вопросов
for question in questions:
options_response = await api_client.get_answer_options_by_question(question['id'])
if options_response.success:
question['options'] = options_response.data
else:
question['options'] = []
# Сохраняем данные теста в состояние
await state.update_data(
test=selected_test,
questions=questions,
current_question=0,
answers=[]
)
await message.answer(
f"Вы выбрали тест: {selected_test['title']}\n"
f"Описание: {selected_test.get('description', 'Описание отсутствует')}\n"
f"Количество вопросов: {len(questions)}\n\n"
f"Готовы начать?",
reply_markup=ReplyKeyboardMarkup(
keyboard=[[KeyboardButton(text="Начать тест")]],
resize_keyboard=True,
one_time_keyboard=True
)
)
await state.set_state(Form.test_in_progress)
except Exception as e:
logger.error(f"Ошибка в handle_test_selection: {e}")
await message.answer("Произошла ошибка при выборе теста.")
# ===== Прохождение теста =====
@dp.message(Form.test_in_progress)
async def handle_test_progress(message: types.Message, state: FSMContext):
"""Обработка прохождения теста"""
try:
data = await state.get_data()
if message.text == "Начать тест":
# Показываем первый вопрос
await show_question(message, state, 0)
return
# Обрабатываем ответ на вопрос
questions = data.get('questions', [])
current_question_idx = data.get('current_question', 0)
answers = data.get('answers', [])
if current_question_idx >= len(questions):
await message.answer("Тест уже завершен.")
return
current_question = questions[current_question_idx]
# Находим выбранный вариант ответа
selected_option = None
for option in current_question.get('options', []):
if option['option_text'] == message.text:
selected_option = option
break
if not selected_option:
await message.answer("Пожалуйста, выберите один из предложенных вариантов ответа.")
return
# Сохраняем ответ
answers.append({
'question_id': current_question['id'],
'option_id': selected_option['id'],
'is_correct': selected_option.get('is_correct', False)
})
# Переходим к следующему вопросу
next_question_idx = current_question_idx + 1
await state.update_data(
answers=answers,
current_question=next_question_idx
)
if next_question_idx >= len(questions):
# Тест завершен
await finish_test(message, state)
else:
# Показываем следующий вопрос
await show_question(message, state, next_question_idx)
except Exception as e:
logger.error(f"Ошибка в handle_test_progress: {e}")
await message.answer("Произошла ошибка при обработке ответа.")
async def show_question(message: types.Message, state: FSMContext, question_idx: int):
"""Показать вопрос"""
try:
data = await state.get_data()
questions = data.get('questions', [])
if question_idx >= len(questions):
return
question = questions[question_idx]
# Формируем текст вопроса
question_text = f"Вопрос {question_idx + 1} из {len(questions)}:\n\n"
question_text += question['question_text']
# Создаем клавиатуру с вариантами ответов
keyboard_buttons = []
for option in question.get('options', []):
keyboard_buttons.append([KeyboardButton(text=option['option_text'])])
keyboard = ReplyKeyboardMarkup(
keyboard=keyboard_buttons,
resize_keyboard=True,
one_time_keyboard=True
)
await message.answer(question_text, reply_markup=keyboard)
except Exception as e:
logger.error(f"Ошибка в show_question: {e}")
await message.answer("Ошибка при показе вопроса.")
async def finish_test(message: types.Message, state: FSMContext):
"""Завершить тест и показать результаты"""
try:
data = await state.get_data()
answers = data.get('answers', [])
test = data.get('test', {})
# Подсчитываем результаты
correct_answers = sum(1 for answer in answers if answer.get('is_correct', False))
total_questions = len(answers)
score_percentage = (correct_answers / total_questions * 100) if total_questions > 0 else 0
result_text = f"🎉 Тест завершен!\n\n"
result_text += f"Тест: {test.get('title', 'Неизвестный тест')}\n"
result_text += f"Правильных ответов: {correct_answers} из {total_questions}\n"
result_text += f"Результат: {score_percentage:.1f}%\n\n"
if score_percentage >= 80:
result_text += "Отличный результат! 🏆"
elif score_percentage >= 60:
result_text += "Хороший результат! 👍"
elif score_percentage >= 40:
result_text += "Удовлетворительный результат. 👌"
else:
result_text += "Рекомендуем повторить материал. 📚"
await message.answer(
result_text,
reply_markup=ReplyKeyboardMarkup(
keyboard=[[KeyboardButton(text="/start")]],
resize_keyboard=True,
one_time_keyboard=True
)
)
await state.clear()
except Exception as e:
logger.error(f"Ошибка в finish_test: {e}")
await message.answer("Ошибка при завершении теста.")
# ===== Основная функция =====
async def main():
"""Основная функция запуска бота"""
logger.info("Запуск Telegram бота...")
try:
# Проверяем доступность API
async with api_client:
if not await api_client.health_check():
logger.error("API недоступен! Убедитесь, что API сервер запущен.")
return
logger.info("API доступен, бот готов к работе")
# Запускаем бота
await dp.start_polling(bot)
except Exception as e:
logger.error(f"Ошибка при запуске бота: {e}")
finally:
await bot.session.close()
if __name__ == "__main__":
asyncio.run(main())