from aiogram import Bot, Dispatcher, Router, F from aiogram.types import ( Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, BotCommand ) 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 sqlalchemy.ext.asyncio import AsyncSession import asyncio import logging import signal import sys from src.core.config import BOT_TOKEN, ADMIN_IDS from src.core.database import async_session_maker, init_db from src.core.services import UserService, LotteryService, ParticipationService from src.handlers.admin_panel import admin_router from src.handlers.account_handlers import account_router from src.utils.async_decorators import ( async_user_action, admin_async_action, db_operation, TaskManagerMiddleware, shutdown_task_manager, format_task_stats, TaskPriority ) from src.utils.account_utils import validate_account_number, format_account_number from src.display.winner_display import format_winner_display # Настройка логирования logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Состояния для FSM class CreateLotteryStates(StatesGroup): waiting_for_title = State() waiting_for_description = State() waiting_for_prizes = State() class SetWinnerStates(StatesGroup): waiting_for_lottery_id = State() waiting_for_place = State() waiting_for_user_id = State() class AccountStates(StatesGroup): waiting_for_account_number = State() # Инициализация бота bot = Bot(token=BOT_TOKEN) storage = MemoryStorage() dp = Dispatcher(storage=storage) router = Router() # Подключаем middleware для управления задачами dp.message.middleware(TaskManagerMiddleware()) dp.callback_query.middleware(TaskManagerMiddleware()) def is_admin(user_id: int) -> bool: """Проверка, является ли пользователь администратором""" return user_id in ADMIN_IDS def get_main_keyboard(is_admin_user: bool = False) -> InlineKeyboardMarkup: """Главная клавиатура""" buttons = [ [InlineKeyboardButton(text="🎲 Активные розыгрыши", callback_data="list_lotteries")], [InlineKeyboardButton(text="📝 Мои участия", callback_data="my_participations")], [InlineKeyboardButton(text="💳 Мой счёт", callback_data="my_account")] ] if is_admin_user: buttons.extend([ [InlineKeyboardButton(text="🔧 Админ-панель", callback_data="admin_panel")], [InlineKeyboardButton(text="➕ Создать розыгрыш", callback_data="create_lottery")], [InlineKeyboardButton(text="👑 Установить победителя", callback_data="set_winner")], [InlineKeyboardButton(text="📊 Статистика задач", callback_data="task_stats")] ]) return InlineKeyboardMarkup(inline_keyboard=buttons) @router.message(Command("start")) async def cmd_start(message: Message): """Обработчик команды /start""" async with async_session_maker() as session: user = await UserService.get_or_create_user( session, 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 ) # Устанавливаем права администратора, если пользователь в списке if message.from_user.id in ADMIN_IDS: await UserService.set_admin(session, message.from_user.id, True) is_admin_user = is_admin(message.from_user.id) welcome_text = f"Добро пожаловать, {message.from_user.first_name}! 🎉\n\n" welcome_text += "Это бот для проведения розыгрышей.\n\n" welcome_text += "Выберите действие из меню ниже:" if is_admin_user: welcome_text += "\n\n👑 У вас есть права администратора!" await message.answer( welcome_text, reply_markup=get_main_keyboard(is_admin_user) ) @router.callback_query(F.data == "list_lotteries") async def show_active_lotteries(callback: CallbackQuery): """Показать активные розыгрыши""" async with async_session_maker() as session: lotteries = await LotteryService.get_active_lotteries(session) if not lotteries: await callback.message.edit_text( "🔍 Активных розыгрышей нет", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")] ]) ) return text = "🎲 Активные розыгрыши:\n\n" buttons = [] for lottery in lotteries: async with async_session_maker() as session: participants_count = await ParticipationService.get_participants_count( session, lottery.id ) text += f"🎯 {lottery.title}\n" text += f"👥 Участников: {participants_count}\n" text += f"📅 Создан: {lottery.created_at.strftime('%d.%m.%Y %H:%M')}\n\n" buttons.append([ InlineKeyboardButton( text=f"🎲 {lottery.title}", callback_data=f"lottery_{lottery.id}" ) ]) buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]) await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons) ) @router.callback_query(F.data.startswith("lottery_")) async def show_lottery_details(callback: CallbackQuery): """Показать детали розыгрыша""" lottery_id = int(callback.data.split("_")[1]) async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) if not lottery: await callback.answer("Розыгрыш не найден", show_alert=True) return participants_count = await ParticipationService.get_participants_count(session, lottery_id) # Проверяем, участвует ли пользователь is_participating = any( p.user_id == user.id for p in lottery.participations ) if user else False text = f"🎯 {lottery.title}\n\n" text += f"📋 Описание: {lottery.description or 'Не указано'}\n\n" if lottery.prizes: text += "🏆 Призы:\n" for i, prize in enumerate(lottery.prizes, 1): text += f"{i}. {prize}\n" text += "\n" text += f"👥 Участников: {participants_count}\n" text += f"📅 Создан: {lottery.created_at.strftime('%d.%m.%Y %H:%M')}\n" if lottery.is_completed: text += "\n✅ Розыгрыш завершен" # Показываем победителей async with async_session_maker() as session: winners = await LotteryService.get_winners(session, lottery_id) if winners: text += "\n\n🏆 Победители:\n" for winner in winners: # Используем новую систему отображения winner_display = format_winner_display(winner.user, lottery, show_sensitive_data=False) text += f"{winner.place}. {winner_display}\n" else: text += f"\n🟢 Статус: {'Активен' if lottery.is_active else 'Неактивен'}" if is_participating: text += "\n✅ Вы участвуете в розыгрыше" buttons = [] if not lottery.is_completed and lottery.is_active and not is_participating: buttons.append([ InlineKeyboardButton( text="🎫 Участвовать", callback_data=f"join_{lottery_id}" ) ]) if is_admin(callback.from_user.id) and not lottery.is_completed: buttons.append([ InlineKeyboardButton( text="🎲 Провести розыгрыш", callback_data=f"conduct_{lottery_id}" ) ]) buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="list_lotteries")]) await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons) ) @router.callback_query(F.data.startswith("join_")) async def join_lottery(callback: CallbackQuery): """Присоединиться к розыгрышу""" lottery_id = int(callback.data.split("_")[1]) async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) if not user: await callback.answer("Ошибка получения данных пользователя", show_alert=True) return success = await LotteryService.add_participant(session, lottery_id, user.id) if success: await callback.answer("✅ Вы успешно присоединились к розыгрышу!", show_alert=True) else: await callback.answer("❌ Вы уже участвуете в этом розыгрыше", show_alert=True) # Обновляем информацию о розыгрыше await show_lottery_details(callback) @router.callback_query(F.data.startswith("conduct_")) async def conduct_lottery(callback: CallbackQuery): """Провести розыгрыш""" if not is_admin(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return lottery_id = int(callback.data.split("_")[1]) async with async_session_maker() as session: results = await LotteryService.conduct_draw(session, lottery_id) if not results: await callback.answer("❌ Не удалось провести розыгрыш", show_alert=True) return text = "🎉 Розыгрыш завершен!\n\n🏆 Победители:\n\n" for place, winner_info in results.items(): user = winner_info['user'] prize = winner_info['prize'] # Используем новую систему отображения winner_display = format_winner_display(user, lottery, show_sensitive_data=False) text += f"{place}. {winner_display}\n" text += f" 🎁 {prize}\n\n" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔙 К розыгрышам", callback_data="list_lotteries")] ]) ) # Создание розыгрыша @router.callback_query(F.data == "create_lottery") async def start_create_lottery(callback: CallbackQuery, state: FSMContext): """Начать создание розыгрыша""" if not is_admin(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return await callback.message.edit_text( "📝 Создание нового розыгрыша\n\n" "Введите название розыгрыша:", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data="back_to_main")] ]) ) await state.set_state(CreateLotteryStates.waiting_for_title) @router.message(StateFilter(CreateLotteryStates.waiting_for_title)) async def process_lottery_title(message: Message, state: FSMContext): """Обработка названия розыгрыша""" await state.update_data(title=message.text) await message.answer( "📋 Введите описание розыгрыша (или отправьте '-' для пропуска):" ) await state.set_state(CreateLotteryStates.waiting_for_description) @router.message(StateFilter(CreateLotteryStates.waiting_for_description)) async def process_lottery_description(message: Message, state: FSMContext): """Обработка описания розыгрыша""" description = None if message.text == "-" else message.text await state.update_data(description=description) await message.answer( "🏆 Введите призы через новую строку:\n\n" "Пример:\n" "1000 рублей\n" "iPhone 15\n" "Подарочный сертификат" ) await state.set_state(CreateLotteryStates.waiting_for_prizes) @router.message(StateFilter(CreateLotteryStates.waiting_for_prizes)) async def process_lottery_prizes(message: Message, state: FSMContext): """Обработка призов розыгрыша""" prizes = [prize.strip() for prize in message.text.split('\n') if prize.strip()] async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, message.from_user.id) data = await state.get_data() lottery = await LotteryService.create_lottery( session, title=data['title'], description=data['description'], prizes=prizes, creator_id=user.id ) await state.clear() text = f"✅ Розыгрыш успешно создан!\n\n" text += f"🎯 Название: {lottery.title}\n" text += f"📋 Описание: {lottery.description or 'Не указано'}\n\n" text += f"🏆 Призы:\n" for i, prize in enumerate(prizes, 1): text += f"{i}. {prize}\n" await message.answer( text, reply_markup=get_main_keyboard(is_admin(message.from_user.id)) ) # Установка ручного победителя @router.callback_query(F.data == "set_winner") async def start_set_winner(callback: CallbackQuery, state: FSMContext): """Начать установку ручного победителя""" if not is_admin(callback.from_user.id): await callback.answer("❌ Недостаточно прав", show_alert=True) return async with async_session_maker() as session: lotteries = await LotteryService.get_active_lotteries(session) if not lotteries: await callback.message.edit_text( "❌ Нет активных розыгрышей", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")] ]) ) return text = "👑 Установка ручного победителя\n\n" text += "Выберите розыгрыш:\n\n" buttons = [] for lottery in lotteries: text += f"🎯 {lottery.title} (ID: {lottery.id})\n" buttons.append([ InlineKeyboardButton( text=f"{lottery.title}", callback_data=f"setwinner_{lottery.id}" ) ]) buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]) await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons) ) @router.callback_query(F.data.startswith("setwinner_")) async def select_winner_place(callback: CallbackQuery, state: FSMContext): """Выбор места для ручного победителя""" lottery_id = int(callback.data.split("_")[1]) async with async_session_maker() as session: lottery = await LotteryService.get_lottery(session, lottery_id) if not lottery: await callback.answer("Розыгрыш не найден", show_alert=True) return await state.update_data(lottery_id=lottery_id) num_prizes = len(lottery.prizes) if lottery.prizes else 3 text = f"👑 Установка ручного победителя для розыгрыша:\n" text += f"🎯 {lottery.title}\n\n" text += f"Введите номер места (1-{num_prizes}):" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data="set_winner")] ]) ) await state.set_state(SetWinnerStates.waiting_for_place) @router.message(StateFilter(SetWinnerStates.waiting_for_place)) async def process_winner_place(message: Message, state: FSMContext): """Обработка места победителя""" try: place = int(message.text) if place < 1: raise ValueError except ValueError: await message.answer("❌ Введите корректный номер места (положительное число)") return await state.update_data(place=place) await message.answer( f"👑 Установка ручного победителя на {place} место\n\n" "Введите Telegram ID пользователя:" ) await state.set_state(SetWinnerStates.waiting_for_user_id) @router.message(StateFilter(SetWinnerStates.waiting_for_user_id)) async def process_winner_user_id(message: Message, state: FSMContext): """Обработка ID пользователя-победителя""" try: telegram_id = int(message.text) except ValueError: await message.answer("❌ Введите корректный Telegram ID (число)") return data = await state.get_data() async with async_session_maker() as session: success = await LotteryService.set_manual_winner( session, data['lottery_id'], data['place'], telegram_id ) await state.clear() if success: await message.answer( f"✅ Ручной победитель установлен!\n\n" f"🏆 Место: {data['place']}\n" f"👤 Telegram ID: {telegram_id}", reply_markup=get_main_keyboard(is_admin(message.from_user.id)) ) else: await message.answer( "❌ Не удалось установить ручного победителя.\n" "Проверьте, что пользователь существует в системе.", reply_markup=get_main_keyboard(is_admin(message.from_user.id)) ) @router.callback_query(F.data == "my_participations") async def show_my_participations(callback: CallbackQuery): """Показать участие пользователя в розыгрышах""" async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) if not user: await callback.answer("Ошибка получения данных пользователя", show_alert=True) return participations = await ParticipationService.get_user_participations(session, user.id) if not participations: await callback.message.edit_text( "📝 Вы пока не участвуете в розыгрышах", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")] ]) ) return text = "📝 Ваши участия в розыгрышах:\n\n" for participation in participations: lottery = participation.lottery status = "✅ Завершен" if lottery.is_completed else "🟢 Активен" text += f"🎯 {lottery.title}\n" text += f"📊 Статус: {status}\n" text += f"📅 Участие с: {participation.created_at.strftime('%d.%m.%Y %H:%M')}\n\n" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")] ]) ) # Хэндлеры для работы с номерами счетов @router.callback_query(F.data == "my_account") @db_operation() async def show_my_account(callback: CallbackQuery): """Показать информацию о счёте пользователя""" async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) if not user: await callback.answer("Пользователь не найден", show_alert=True) return text = "💳 **Ваш клиентский счёт**\n\n" if user.account_number: # Показываем маскированный номер для безопасности from src.utils.account_utils import mask_account_number masked = mask_account_number(user.account_number, show_last_digits=6) text += f"📋 Номер счёта: `{masked}`\n" text += f"✅ Статус: Активен\n\n" text += "ℹ️ Счёт используется для идентификации в розыгрышах" else: text += "❌ Счёт не привязан\n\n" text += "Привяжите счёт для участия в розыгрышах" buttons = [] if user.account_number: buttons.append([InlineKeyboardButton(text="🔄 Изменить счёт", callback_data="change_account")]) else: buttons.append([InlineKeyboardButton(text="➕ Привязать счёт", callback_data="add_account")]) buttons.append([InlineKeyboardButton(text="🔙 Главное меню", callback_data="back_to_main")]) await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="Markdown" ) @router.callback_query(F.data.in_(["add_account", "change_account"])) @db_operation() async def start_account_setup(callback: CallbackQuery, state: FSMContext): """Начало процесса привязки/изменения счёта""" await state.set_state(AccountStates.waiting_for_account_number) action = "привязки" if callback.data == "add_account" else "изменения" text = f"💳 **Процедура {action} счёта**\n\n" text += "Введите номер вашего клиентского счёта в формате:\n" text += "`12-34-56-78-90-12-34`\n\n" text += "📝 **Требования:**\n" text += "• Ровно 14 цифр\n" text += "• Разделены дефисами через каждые 2 цифры\n" text += "• Номер должен быть уникальным\n\n" text += "✉️ Отправьте номер счёта в ответном сообщении" await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data="my_account")] ]), parse_mode="Markdown" ) @router.message(StateFilter(AccountStates.waiting_for_account_number)) @db_operation() async def process_account_number(message: Message, state: FSMContext): """Обработка введённого номера счёта""" account_input = message.text.strip() # Форматируем и валидируем номер formatted_number = format_account_number(account_input) if not formatted_number: await message.answer( "❌ **Некорректный формат номера счёта**\n\n" "Номер должен содержать ровно 14 цифр.\n" "Пример правильного формата: `12-34-56-78-90-12-34`\n\n" "Попробуйте ещё раз:", parse_mode="Markdown" ) return async with async_session_maker() as session: # Проверяем уникальность existing_user = await UserService.get_user_by_account(session, formatted_number) if existing_user and existing_user.telegram_id != message.from_user.id: await message.answer( "❌ **Номер счёта уже используется**\n\n" "Данный номер счёта уже привязан к другому пользователю.\n" "Убедитесь, что вы вводите правильный номер.\n\n" "Попробуйте ещё раз:" ) return # Обновляем номер счёта success = await UserService.set_account_number( session, message.from_user.id, formatted_number ) if success: await state.clear() await message.answer( f"✅ **Счёт успешно привязан!**\n\n" f"💳 Номер счёта: `{formatted_number}`\n\n" f"Теперь вы можете участвовать в розыгрышах.\n" f"Ваш номер счёта будет использоваться для идентификации.", parse_mode="Markdown", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🏠 Главное меню", callback_data="back_to_main")] ]) ) else: await message.answer( "❌ **Ошибка привязки счёта**\n\n" "Произошла ошибка при сохранении номера счёта.\n" "Попробуйте ещё раз или обратитесь к администратору.", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔙 Назад", callback_data="my_account")] ]) ) @router.callback_query(F.data == "task_stats") @admin_async_action() async def show_task_stats(callback: CallbackQuery): """Показать статистику задач (только для админов)""" if not is_admin(callback.from_user.id): await callback.answer("Доступ запрещён", show_alert=True) return stats_text = await format_task_stats() await callback.message.edit_text( stats_text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔄 Обновить", callback_data="task_stats")], [InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")] ]), parse_mode="Markdown" ) @router.callback_query(F.data == "back_to_main") async def back_to_main(callback: CallbackQuery, state: FSMContext): """Вернуться в главное меню""" await state.clear() is_admin_user = is_admin(callback.from_user.id) await callback.message.edit_text( "🏠 Главное меню\n\nВыберите действие:", reply_markup=get_main_keyboard(is_admin_user) ) async def set_commands(): """Установка команд бота""" commands = [ BotCommand(command="start", description="🚀 Запустить бота"), ] await bot.set_my_commands(commands) async def main(): """Главная функция""" # Инициализация базы данных await init_db() # Установка команд await set_commands() # Подключение роутеров dp.include_router(account_router) # Роутер для работы со счетами (приоритетный) dp.include_router(router) dp.include_router(admin_router) # Обработка сигналов для graceful shutdown def signal_handler(): logger.info("Получен сигнал завершения, остановка бота...") asyncio.create_task(shutdown_task_manager()) # Настройка обработчиков сигналов if sys.platform != "win32": for sig in (signal.SIGTERM, signal.SIGINT): asyncio.get_event_loop().add_signal_handler(sig, signal_handler) # Запуск бота logger.info("Бот запущен") try: await dp.start_polling(bot) finally: # Остановка менеджера задач при завершении await shutdown_task_manager() if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: logger.info("Бот остановлен пользователем") except Exception as e: logger.error(f"Критическая ошибка: {e}") finally: logger.info("Завершение работы")