""" Новая модульная версия main.py с применением SOLID принципов """ import asyncio import logging from contextlib import asynccontextmanager from aiogram import Bot, Dispatcher, Router, F from aiogram.types import Message, CallbackQuery from aiogram.filters import Command from aiogram.fsm.storage.memory import MemoryStorage, SimpleEventIsolation from aiogram.fsm.storage.redis import RedisStorage, DefaultKeyBuilder from aiogram.client.session.aiohttp import AiohttpSession from aiogram.fsm.context import FSMContext from src.filters.case_insensitive import CaseInsensitiveCommand from src.core.config import BOT_TOKEN, REDIS_URL, FSM_TTL_SECONDS, LOG_LEVEL from src.core.access import is_admin, is_staff from src.middlewares.access import AccessMiddleware from src.core.database import async_session_maker from src.core.scheduler import bot_scheduler from src.container import container from src.interfaces.base import IBotController from src.middlewares.activity import ActivityMiddleware from src.handlers.admin_panel import admin_router from src.handlers.registration_handlers import router as registration_router from src.handlers.admin_account_handlers import router as admin_account_router from src.handlers.redraw_handlers import router as redraw_router from src.handlers.chat_handlers import router as chat_router from src.handlers.admin_chat_handlers import router as admin_chat_router from src.handlers.account_handlers import account_router from src.handlers.message_management import message_admin_router from src.handlers.p2p_chat import router as p2p_chat_router from src.handlers.help_handlers import router as help_router from src.handlers.admin_emoji_handlers import router as admin_emoji_router # Настройка логирования logging.basicConfig( level=LOG_LEVEL, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # Создание бота и диспетчера bot = Bot(token=BOT_TOKEN, session=AiohttpSession(timeout=30, limit=100)) if REDIS_URL: storage = RedisStorage.from_url( REDIS_URL, key_builder=DefaultKeyBuilder(with_bot_id=True), state_ttl=FSM_TTL_SECONDS, data_ttl=FSM_TTL_SECONDS, ) isolation = storage.create_isolation(lock_kwargs={"timeout": 3600}) else: storage = MemoryStorage() isolation = SimpleEventIsolation() dp = Dispatcher(storage=storage, events_isolation=isolation) router = Router() @router.message(CaseInsensitiveCommand("cancel")) async def cancel_dialog(message: Message, state: FSMContext): await state.clear() await message.answer("Операция отменена. /start — главное меню, /cashier — касса.") # Middleware для логирования всех callback'ов @dp.callback_query.middleware() async def log_callback_middleware(handler, event, data): """Middleware для логирования всех callback запросов""" logger.warning(f"🔔 MIDDLEWARE CALLBACK: data='{event.data}', user_id={event.from_user.id}") result = await handler(event, data) logger.warning(f"🔔 MIDDLEWARE CALLBACK HANDLED: data='{event.data}', result={result}") return result @asynccontextmanager async def get_controller(): """Контекстный менеджер для получения контроллера с БД сессией""" async with async_session_maker() as session: scoped_container = container.create_scoped_container(session) controller = scoped_container.get(IBotController) yield controller # === COMMAND HANDLERS === @router.message(CaseInsensitiveCommand("start")) async def cmd_start(message: Message, state: FSMContext): """Обработчик команды /start (регистронезависимо)""" await state.clear() async with get_controller() as controller: await controller.handle_start(message) # === TEXT BUTTON HANDLERS === @router.message(F.text == "🎰 Розыгрыши") async def btn_lotteries(message: Message): """Обработчик кнопки 'Розыгрыши'""" from src.core.database import async_session_maker from src.repositories.implementations import LotteryRepository, ParticipationRepository from src.components.ui import MessageFormatterImpl from src.components.ui import KeyboardBuilderImpl from src.core.services import UserService from src.core.config import ADMIN_IDS async with async_session_maker() as session: lottery_repo = LotteryRepository(session) participation_repo = ParticipationRepository(session) lotteries = await lottery_repo.get_active() if not lotteries: await message.answer("❌ Нет активных розыгрышей") return text = "🎲 **Активные розыгрыши:**\n\n" formatter = MessageFormatterImpl() for lottery in lotteries: participants_count = await participation_repo.get_count_by_lottery(lottery.id) lottery_info = formatter.format_lottery_info(lottery, participants_count) await message.answer(lottery_info, parse_mode="HTML") # Получаем информацию о регистрации пользователя 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 ) keyboard_builder = KeyboardBuilderImpl() keyboard = keyboard_builder.get_main_keyboard( is_admin=await is_admin(message.from_user.id), is_registered=user.is_registered ) await message.answer( text, reply_markup=keyboard, parse_mode="Markdown" ) @router.message(F.text == "💬 Чат") async def btn_chat(message: Message, state: FSMContext): """Обработчик кнопки 'Чат'""" from src.handlers.chat_handlers import enter_chat await enter_chat(message, state) @router.message(F.text == "📝 Регистрация") async def btn_registration(message: Message, state: FSMContext): from src.handlers.registration_handlers import begin_registration await begin_registration(message, state) @router.message(CaseInsensitiveCommand("register")) async def cmd_register(message: Message, state: FSMContext): """Обработчик команды /register (регистронезависимо)""" await btn_registration(message, state) @router.message(F.text.lower().in_(["регистрация", "регистр", "register"])) async def text_registration(message: Message, state: FSMContext): """Обработчик текста для регистрации""" await btn_registration(message, state) @router.message(F.text == "🔑 Мой код") async def btn_my_code(message: Message): """Обработчик кнопки 'Мой код'""" from src.handlers.registration_handlers import show_verification_code await show_verification_code(message) @router.message(F.text.in_({"📱 Мои логины", "💳 Мои логины", "� Мои логины"})) async def btn_my_accounts(message: Message): """Обработчик кнопки 'Мои логины'""" from src.handlers.registration_handlers import show_user_accounts await show_user_accounts(message) @router.message(F.text == "❓ Справка") async def btn_help(message: Message): """Обработчик кнопки 'Справка'""" from src.handlers.help_handlers import show_help_main await show_help_main(message) @router.message(F.text == "⚙️ Админ панель") async def btn_admin(message: Message): """Обработчик кнопки 'Админ панель'""" await cmd_admin(message) @router.message(F.text == "🚪 Выйти из чата") async def btn_exit_chat(message: Message, state: FSMContext): """Обработчик кнопки 'Выйти из чата'""" from src.handlers.chat_handlers import exit_chat await exit_chat(message, state) @router.message(F.text == "🏠 Главная") async def btn_main_menu(message: Message, state: FSMContext): """Обработчик кнопки 'Главная'""" await cmd_start(message, state) @router.message(CaseInsensitiveCommand("admin")) async def cmd_admin(message: Message): """Обработчик команды /admin (регистронезависимо) - перенаправляет в admin_panel""" from src.core.config import ADMIN_IDS from src.core.database import async_session_maker from src.core.models import User from sqlalchemy import select # Проверяем, является ли пользователь главным администратором из .env user_id = message.from_user.id is_super_admin = user_id in ADMIN_IDS # Проверяем, является ли пользователь назначенным администратором is_assigned_admin = False if not is_super_admin: async with async_session_maker() as session: user = await session.execute( select(User).where(User.telegram_id == user_id) ) user = user.scalar_one_or_none() is_assigned_admin = user and user.is_admin # Если не администратор ни того, ни другого типа if not (is_super_admin or is_assigned_admin): await message.answer("❌ Недостаточно прав для доступа к админ панели") return # Отправляем сообщение с кнопкой админ панели from src.components.ui import KeyboardBuilderImpl kb = KeyboardBuilderImpl() keyboard = kb.get_admin_keyboard() text = "⚙️ **Панель администратора**\n\n" text += "Выберите раздел для управления:" await message.answer(text, reply_markup=keyboard, parse_mode="Markdown") # === CALLBACK HANDLERS === @router.callback_query(F.data == "active_lotteries") async def active_lotteries_handler(callback: CallbackQuery): """Обработчик показа активных розыгрышей""" async with get_controller() as controller: await controller.handle_active_lotteries(callback) @router.callback_query(F.data == "back_to_main") async def back_to_main_handler(callback: CallbackQuery, state: FSMContext): """Обработчик возврата в главное меню""" await state.clear() await callback.answer() async with get_controller() as controller: await controller.handle_start(callback.message, actor=callback.from_user) # Функции обрабатываются в: # - admin_panel.py: создание розыгрышей, управление пользователями, счетами, чатом, статистика # - registration_handlers.py: регистрация пользователей # - admin_account_handlers.py: управление счетами # - admin_chat_handlers.py: управление чатом # - chat_handlers.py: пользовательский чат # === FALLBACK HANDLERS === # Обработка неизвестных callback и сообщений происходит в соответствующих роутерах def configure_dispatcher(): """Главная функция запуска бота""" logger.info("Запуск бота...") from src.middlewares.runtime import PrivateDialogMiddleware, ErrorMiddleware for observer in (dp.message, dp.callback_query): observer.outer_middleware(PrivateDialogMiddleware()) observer.outer_middleware(ErrorMiddleware()) # Подключаем middleware для отслеживания активности dp.message.middleware(ActivityMiddleware()) dp.callback_query.middleware(ActivityMiddleware()) # Enforce access on every selected handler, including stale FSM callbacks. from src.middlewares.access import ChatAccessMiddleware p2p_chat_router.message.middleware(ChatAccessMiddleware()) p2p_chat_router.callback_query.middleware(ChatAccessMiddleware()) for protected in (admin_router, admin_chat_router, redraw_router, message_admin_router, admin_emoji_router): protected.message.middleware(AccessMiddleware()) protected.callback_query.middleware(AccessMiddleware( public_handlers={"confirm_winner_callback"} if protected is redraw_router else (), )) for protected in (admin_account_router,): protected.message.middleware(AccessMiddleware(allow_cashier=True)) protected.callback_query.middleware(AccessMiddleware( allow_cashier=True, public_handlers={"view_my_accounts_callback"}, )) # Подключаем роутеры в правильном порядке # 1. Основной роутер main.py с базовыми командами (/start, /help, /admin) dp.include_router(router) from src.handlers.cashier_handlers import cashier_router dp.include_router(cashier_router) # 2. Специфичные роутеры dp.include_router(message_admin_router) # Управление сообщениями администратором dp.include_router(admin_emoji_router) # Управление кастомными эмодзи dp.include_router(admin_router) # Админ панель - самая высокая специфичность dp.include_router(registration_router) # Регистрация dp.include_router(admin_account_router) # Админские команды счетов dp.include_router(admin_chat_router) # Админские команды чата dp.include_router(redraw_router) # Повторные розыгрыши dp.include_router(p2p_chat_router) # P2P чат между пользователями dp.include_router(help_router) # Справка и помощь # 3. Chat router для broadcast (обрабатывает обычные сообщения) dp.include_router(chat_router) # Пользовательский чат (broadcast всем) - РАНЬШЕ account_router # 4. Account router для обнаружения счетов (обрабатывает сообщения со счетами от админов) dp.include_router(account_router) # Обнаружение счетов для админов - ПОСЛЕ chat_router async def main(): configure_dispatcher() # Запускаем планировщик задач bot_scheduler.start() logger.info("Планировщик задач запущен") # Запускаем polling try: from src.core.health import HEARTBEAT, heartbeat, start_watchdog HEARTBEAT.unlink(missing_ok=True) watchdog = start_watchdog() if REDIS_URL: await storage.redis.ping() from sqlalchemy import select from src.core.models import User async with async_session_maker() as session: await session.execute(select(User).limit(0)) await bot.get_me() health_task = asyncio.create_task(heartbeat(), name="database-heartbeat") logger.info("Бот запущен") polling_task = asyncio.create_task(dp.start_polling(bot)) completed, _ = await asyncio.wait({health_task, polling_task}, return_when=asyncio.FIRST_COMPLETED) for task in completed: await task except Exception: logger.exception("Runtime supervision failed; stopping the application") raise finally: from src.core.database import close_db from contextlib import suppress for task_name in ("polling_task", "health_task"): task = locals().get(task_name) if task is not None: task.cancel() with suppress(asyncio.CancelledError, Exception): await task HEARTBEAT.unlink(missing_ok=True) # Останавливаем планировщик bot_scheduler.shutdown() await bot.session.close() await storage.close() await close_db() if "watchdog" in locals(): watchdog.set() if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: logger.info("Бот остановлен пользователем") except Exception as e: logger.exception("Критическая ошибка") raise