"""Обработчики для регистрации пользователей""" from aiogram import Router, F from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup from aiogram.filters import Command, StateFilter from sqlalchemy import select from sqlalchemy.orm import selectinload from src.filters.case_insensitive import CaseInsensitiveCommand from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup import logging from src.core.database import async_session_maker from src.core.registration_services import RegistrationService, AccountService from src.core.services import UserService from src.core.models import Participation, Winner, Lottery logger = logging.getLogger(__name__) router = Router() # Служебные слова, которые нельзя использовать как никнейм FORBIDDEN_NICKNAMES = [ 'привет', 'здравствуйте', 'добрый', 'день', 'вечер', 'утро', 'спасибо', 'пожалуйста', 'извините', 'до свидания', 'пока', 'admin', 'administrator', 'moderator', 'bot', 'system', 'hello', 'hi', 'thanks', 'please', 'sorry', 'goodbye', 'bye' ] def validate_nickname(nickname: str) -> tuple[bool, str]: """ Валидация никнейма Returns: (valid, error_message) """ nickname = nickname.strip() # Проверка длины if len(nickname) < 2: return False, "❌ Никнейм слишком короткий (минимум 2 символа)" if len(nickname) > 20: return False, "❌ Никнейм слишком длинный (максимум 20 символов)" # Проверка на служебные слова nickname_lower = nickname.lower() for forbidden in FORBIDDEN_NICKNAMES: if forbidden in nickname_lower: import random suggestion = f"{nickname[:3]}{random.randint(10, 99)}" return False, f"❌ Это похоже на приветствие или служебное слово.\n\nПридумайте уникальный никнейм (например: {suggestion})" # Проверка на команды if nickname.startswith('/'): return False, "❌ Никнейм не может начинаться с '/'" return True, "" class RegistrationStates(StatesGroup): """Состояния для процесса регистрации""" waiting_for_nickname = State() waiting_for_club_card = State() waiting_for_phone = State() @router.callback_query(F.data == "start_registration") async def start_registration(callback: CallbackQuery, state: FSMContext): """Начать процесс регистрации""" logger.info(f"Получен запрос на регистрацию от пользователя {callback.from_user.id}") text = ( "📝 Регистрация в системе\n\n" "Для участия в розыгрышах необходимо зарегистрироваться.\n\n" "Шаг 1 из 3: Придумайте никнейм\n\n" "🎭 Введите ваш никнейм для чата:\n" "• От 2 до 20 символов\n" "• Может содержать буквы, цифры, пробелы\n" "• Это имя будут видеть другие участники" ) await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data="back_to_main")] ]) ) await state.set_state(RegistrationStates.waiting_for_nickname) @router.message(StateFilter(RegistrationStates.waiting_for_nickname)) async def process_nickname(message: Message, state: FSMContext): """Обработка никнейма""" nickname = message.text.strip() # Валидация никнейма valid, error_msg = validate_nickname(nickname) if not valid: await message.answer( f"{error_msg}\n\n" "Попробуйте другой вариант:" ) return # Сохраняем никнейм await state.update_data(nickname=nickname) await message.answer( f"✅ Отлично! Ваш никнейм: {nickname}\n\n" "Шаг 2 из 3: Клубная карта\n\n" "📝 Введите номер вашей клубной карты:" ) await state.set_state(RegistrationStates.waiting_for_club_card) @router.message(StateFilter(RegistrationStates.waiting_for_club_card)) async def process_club_card(message: Message, state: FSMContext): """Обработка номера клубной карты""" club_card_number = message.text.strip() # Проверяем, не занята ли карта async with async_session_maker() as session: existing_user = await RegistrationService.get_user_by_club_card(session, club_card_number) if existing_user: await message.answer( f"❌ Клубная карта {club_card_number} уже зарегистрирована.\n\n" "Если это ваша карта, обратитесь к администратору." ) await state.clear() return await state.update_data(club_card_number=club_card_number) await message.answer( "Шаг 3 из 3: Телефон\n\n" "📱 Введите ваш номер телефона\n" "(или отправьте '-' чтобы пропустить):" ) await state.set_state(RegistrationStates.waiting_for_phone) @router.message(StateFilter(RegistrationStates.waiting_for_phone)) async def process_phone(message: Message, state: FSMContext): """Обработка номера телефона""" phone_input = message.text.strip() # Проверяем, не отправил ли пользователь просто "-" if phone_input == "-": phone = None else: # Валидируем телефон: не должно быть пустых или некорректных значений if not phone_input: await message.answer( "❌ Неверный номер телефона.\n\n" "Пожалуйста, введите корректный номер или отправьте '-' чтобы пропустить." ) return phone = phone_input data = await state.get_data() club_card_number = data['club_card_number'] nickname = data.get('nickname') try: async with async_session_maker() as session: user = await RegistrationService.register_user( session, telegram_id=message.from_user.id, club_card_number=club_card_number, phone=phone ) # Обновляем никнейм пользователя if nickname: user.nickname = nickname await session.commit() await session.refresh(user) text = ( "✅ Регистрация завершена!\n\n" f"🎭 Никнейм: {user.nickname}\n" f"🎫 Клубная карта: {user.club_card_number}\n" f"🔑 Ваш код верификации: {user.verification_code}\n\n" "⚠️ Сохраните этот код! Он понадобится для подтверждения выигрыша.\n\n" "Теперь вы можете участвовать в розыгрышах!" ) await message.answer(text, parse_mode="HTML") await state.clear() except ValueError as e: await message.answer(f"❌ Ошибка регистрации: {str(e)}") await state.clear() except Exception as e: await message.answer(f"❌ Произошла ошибка: {str(e)}") await state.clear() @router.message(Command("my_code")) async def show_verification_code(message: Message): """Показать код верификации пользователя""" async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, message.from_user.id) if not user or not user.is_registered: await message.answer( "❌ Вы не зарегистрированы в системе.\n\n" "Для регистрации отправьте /start и выберите 'Регистрация'" ) return text = ( "🔑 Ваш код верификации:\n\n" f"{user.verification_code}\n\n" "Этот код используется для подтверждения выигрыша.\n" "Сообщите его администратору при получении приза." ) await message.answer(text, parse_mode="HTML") @router.message(Command("my_accounts")) async def show_user_accounts(message: Message): """Показать логины пользователя с информацией о розыгрышах""" async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, message.from_user.id) if not user or not user.is_registered: await message.answer("❌ Вы не зарегистрированы в системе") return accounts = await AccountService.get_user_accounts(session, user.id) if not accounts: await message.answer( "У вас пока нет привязанных логинов.\n\n" "Логины добавляются администратором." ) return text = f"📱 Ваши логины (Клубная карта: {user.club_card_number})\n\n" for i, account in enumerate(accounts, 1): # Получаем participations для этого account с загруженными данными о lottery participations = await session.execute( select(Participation) .where(Participation.account_id == account.id) .options(selectinload(Participation.lottery)) ) participations = participations.scalars().all() # Определяем статус логина active_participations = [p for p in participations if not p.lottery.is_completed] closed_participations = [p for p in participations if p.lottery.is_completed] # Основная информация о логине status_icon = "✅" if account.is_active and active_participations else "⏸️" text += f"{i}. {status_icon} {account.account_number}\n" if active_participations: text += " 🎲 Активные розыгрыши:\n" for p in active_participations[:5]: # Показываем не более 5 status = "🟢" text += f" {status} {p.lottery.title}\n" if len(active_participations) > 5: text += f" ... и еще {len(active_participations) - 5}\n" if closed_participations: text += " 🏁 Завершенные розыгрыши:\n" for p in closed_participations[:3]: # Показываем не более 3 # Проверяем, выиграл ли в этом розыгрыше winner_result = await session.execute( select(Winner) .where( (Winner.lottery_id == p.lottery_id) & (Winner.account_number == account.account_number) ) ) winner = winner_result.scalar_one_or_none() if winner: text += f" 🏆 {p.lottery.title} - ВЫИГРАЛ! ({winner.place} место)\n" else: text += f" ✗ {p.lottery.title}\n" if len(closed_participations) > 3: text += f" ... и еще {len(closed_participations) - 3} закрытых\n" if not participations: text += " ℹ️ В розыгрышах не участвовал\n" text += "\n" # Добавляем примечание о неактивных логинах if any(not acc.is_active for acc in accounts): text += "⏸️ - Логин не участвует в новых розыгрышах\n" text += "✅ - Логин активен и может участвовать\n\n" text += "💡 Заметка: Логины, участвовавшие в закрытых розыгрышах, не добавляются в новые розыгрыши." await message.answer(text, parse_mode="HTML")