Stabilize staff concurrency and premium emoji; enable verified Drone deployment
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
138
main.py
138
main.py
@@ -9,12 +9,16 @@ 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
|
||||
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
|
||||
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
|
||||
@@ -34,18 +38,32 @@ from src.handlers.admin_emoji_handlers import router as admin_emoji_router
|
||||
|
||||
# Настройка логирования
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
level=LOG_LEVEL,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Создание бота и диспетчера
|
||||
bot = Bot(token=BOT_TOKEN)
|
||||
storage = MemoryStorage()
|
||||
dp = Dispatcher(storage=storage)
|
||||
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):
|
||||
@@ -68,8 +86,9 @@ async def get_controller():
|
||||
# === COMMAND HANDLERS ===
|
||||
|
||||
@router.message(CaseInsensitiveCommand("start"))
|
||||
async def cmd_start(message: Message):
|
||||
async def cmd_start(message: Message, state: FSMContext):
|
||||
"""Обработчик команды /start (регистронезависимо)"""
|
||||
await state.clear()
|
||||
async with get_controller() as controller:
|
||||
await controller.handle_start(message)
|
||||
|
||||
@@ -81,7 +100,7 @@ async def btn_lotteries(message: Message):
|
||||
"""Обработчик кнопки 'Розыгрыши'"""
|
||||
from src.core.database import async_session_maker
|
||||
from src.repositories.implementations import LotteryRepository, ParticipationRepository
|
||||
from src.display.message_formatter import MessageFormatterImpl
|
||||
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
|
||||
@@ -101,11 +120,11 @@ async def btn_lotteries(message: Message):
|
||||
for lottery in lotteries:
|
||||
participants_count = await participation_repo.get_count_by_lottery(lottery.id)
|
||||
lottery_info = formatter.format_lottery_info(lottery, participants_count)
|
||||
text += lottery_info + "\n" + "="*30 + "\n\n"
|
||||
await message.answer(lottery_info, parse_mode="HTML")
|
||||
|
||||
# Получаем информацию о регистрации пользователя
|
||||
user_service = UserService(session)
|
||||
user = await user_service.get_or_create_user(
|
||||
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,
|
||||
@@ -114,7 +133,7 @@ async def btn_lotteries(message: Message):
|
||||
|
||||
keyboard_builder = KeyboardBuilderImpl()
|
||||
keyboard = keyboard_builder.get_main_keyboard(
|
||||
is_admin=message.from_user.id in ADMIN_IDS,
|
||||
is_admin=await is_admin(message.from_user.id),
|
||||
is_registered=user.is_registered
|
||||
)
|
||||
|
||||
@@ -134,30 +153,9 @@ async def btn_chat(message: Message, state: FSMContext):
|
||||
|
||||
@router.message(F.text == "📝 Регистрация")
|
||||
async def btn_registration(message: Message, state: FSMContext):
|
||||
"""Обработчик кнопки 'Регистрация'"""
|
||||
from src.handlers.registration_handlers import RegistrationStates
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from src.handlers.registration_handlers import begin_registration
|
||||
await begin_registration(message, state)
|
||||
|
||||
logger.info(f"User {message.from_user.id} pressed Registration button")
|
||||
|
||||
text = (
|
||||
"📝 Регистрация в системе\n\n"
|
||||
"Для участия в розыгрышах необходимо зарегистрироваться.\n\n"
|
||||
"Шаг 1 из 3: Придумайте никнейм\n\n"
|
||||
"🎭 Введите ваш никнейм для чата:\n"
|
||||
"• От 2 до 20 символов\n"
|
||||
"• Может содержать буквы, цифры, пробелы\n"
|
||||
"• Это имя будут видеть другие участники"
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="back_to_main")]
|
||||
])
|
||||
)
|
||||
|
||||
await state.set_state(RegistrationStates.waiting_for_nickname)
|
||||
|
||||
|
||||
@router.message(CaseInsensitiveCommand("register"))
|
||||
@@ -179,7 +177,7 @@ async def btn_my_code(message: Message):
|
||||
await show_verification_code(message)
|
||||
|
||||
|
||||
@router.message(F.text == "<EFBFBD> Мои логины")
|
||||
@router.message(F.text.in_({"📱 Мои логины", "💳 Мои логины", "<EFBFBD> Мои логины"}))
|
||||
async def btn_my_accounts(message: Message):
|
||||
"""Обработчик кнопки 'Мои логины'"""
|
||||
from src.handlers.registration_handlers import show_user_accounts
|
||||
@@ -207,9 +205,9 @@ async def btn_exit_chat(message: Message, state: FSMContext):
|
||||
|
||||
|
||||
@router.message(F.text == "🏠 Главная")
|
||||
async def btn_main_menu(message: Message):
|
||||
async def btn_main_menu(message: Message, state: FSMContext):
|
||||
"""Обработчик кнопки 'Главная'"""
|
||||
await cmd_start(message)
|
||||
await cmd_start(message, state)
|
||||
|
||||
|
||||
@router.message(CaseInsensitiveCommand("admin"))
|
||||
@@ -260,10 +258,12 @@ async def active_lotteries_handler(callback: CallbackQuery):
|
||||
|
||||
|
||||
@router.callback_query(F.data == "back_to_main")
|
||||
async def back_to_main_handler(callback: CallbackQuery):
|
||||
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)
|
||||
await controller.handle_start(callback.message, actor=callback.from_user)
|
||||
# Функции обрабатываются в:
|
||||
# - admin_panel.py: создание розыгрышей, управление пользователями, счетами, чатом, статистика
|
||||
# - registration_handlers.py: регистрация пользователей
|
||||
@@ -276,17 +276,38 @@ async def back_to_main_handler(callback: CallbackQuery):
|
||||
# Обработка неизвестных callback и сообщений происходит в соответствующих роутерах
|
||||
|
||||
|
||||
async def main():
|
||||
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) # Управление сообщениями администратором
|
||||
@@ -305,20 +326,46 @@ async def main():
|
||||
# 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
|
||||
HEARTBEAT.unlink(missing_ok=True)
|
||||
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())
|
||||
logger.info("Бот запущен")
|
||||
await dp.start_polling(bot)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при запуске бота: {e}")
|
||||
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
|
||||
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 __name__ == "__main__":
|
||||
@@ -327,4 +374,5 @@ if __name__ == "__main__":
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Бот остановлен пользователем")
|
||||
except Exception as e:
|
||||
logger.error(f"Критическая ошибка: {e}")
|
||||
logger.exception("Критическая ошибка")
|
||||
raise
|
||||
|
||||
Reference in New Issue
Block a user