Files
new_lottery_bot/main.py
Trevor1985 d5a9c13b68
All checks were successful
continuous-integration/drone/push Build is passing
Recover stalled database connections and skip unreachable chat recipients
2026-09-13 20:21:20 +09:00

385 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Новая модульная версия 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_({"📱 Мои логины", "💳 Мои логины", "<EFBFBD> Мои логины"}))
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