add stuckt
This commit is contained in:
10
app.bot/__init__.py
Normal file
10
app.bot/__init__.py
Normal 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
307
app.bot/api_client.py
Normal 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
58
app.bot/bot_config.py
Normal 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
60
app.bot/main.py
Normal 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
469
app.bot/telegram_bot.py
Normal 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())
|
||||
Reference in New Issue
Block a user