Some checks failed
continuous-integration/drone/pr Build is failing
- Create emoji_mappings table to store emoji->emoji_id mappings - Add EmojiMappingService for managing emoji registration and replacement - Add admin emoji handlers (/add_emoji, /my_emojis, /delete_emoji, /all_emojis) - Create emoji message helper for automatic emoji processing - Add Alembic migration for emoji_mappings table - Integrate emoji router into main dispatcher - Add comprehensive documentation (EMOJI_SYSTEM.md) - Fix migration chain issue with merge_migration Features: - Admins can register premium emojis via /add_emoji command - Automatic emoji->emoji_id replacement before sending messages - Per-admin unique constraint on emoji registration - Track last used timestamp for analytics - Bulk operations support
330 lines
14 KiB
Python
330 lines
14 KiB
Python
"""
|
||
Новая модульная версия 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
|
||
from aiogram.fsm.context import FSMContext
|
||
|
||
from src.filters.case_insensitive import CaseInsensitiveCommand
|
||
|
||
from src.core.config import BOT_TOKEN
|
||
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=logging.INFO,
|
||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||
)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Создание бота и диспетчера
|
||
bot = Bot(token=BOT_TOKEN)
|
||
storage = MemoryStorage()
|
||
dp = Dispatcher(storage=storage)
|
||
router = Router()
|
||
|
||
|
||
# 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):
|
||
"""Обработчик команды /start (регистронезависимо)"""
|
||
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.display.message_formatter 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)
|
||
text += lottery_info + "\n" + "="*30 + "\n\n"
|
||
|
||
# Получаем информацию о регистрации пользователя
|
||
user_service = UserService(session)
|
||
user = await user_service.get_or_create_user(
|
||
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=message.from_user.id in ADMIN_IDS,
|
||
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 RegistrationStates
|
||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||
|
||
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"))
|
||
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 == "<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):
|
||
"""Обработчик кнопки 'Главная'"""
|
||
await cmd_start(message)
|
||
|
||
|
||
@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):
|
||
"""Обработчик возврата в главное меню"""
|
||
async with get_controller() as controller:
|
||
await controller.handle_start(callback.message)
|
||
# Функции обрабатываются в:
|
||
# - admin_panel.py: создание розыгрышей, управление пользователями, счетами, чатом, статистика
|
||
# - registration_handlers.py: регистрация пользователей
|
||
# - admin_account_handlers.py: управление счетами
|
||
# - admin_chat_handlers.py: управление чатом
|
||
# - chat_handlers.py: пользовательский чат
|
||
|
||
|
||
# === FALLBACK HANDLERS ===
|
||
# Обработка неизвестных callback и сообщений происходит в соответствующих роутерах
|
||
|
||
|
||
async def main():
|
||
"""Главная функция запуска бота"""
|
||
logger.info("Запуск бота...")
|
||
|
||
# Подключаем middleware для отслеживания активности
|
||
dp.message.middleware(ActivityMiddleware())
|
||
dp.callback_query.middleware(ActivityMiddleware())
|
||
|
||
# Подключаем роутеры в правильном порядке
|
||
# 1. Основной роутер main.py с базовыми командами (/start, /help, /admin)
|
||
dp.include_router(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
|
||
|
||
# Запускаем планировщик задач
|
||
bot_scheduler.start()
|
||
logger.info("Планировщик задач запущен")
|
||
|
||
# Запускаем polling
|
||
try:
|
||
logger.info("Бот запущен")
|
||
await dp.start_polling(bot)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при запуске бота: {e}")
|
||
finally:
|
||
# Останавливаем планировщик
|
||
bot_scheduler.shutdown()
|
||
await bot.session.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
asyncio.run(main())
|
||
except KeyboardInterrupt:
|
||
logger.info("Бот остановлен пользователем")
|
||
except Exception as e:
|
||
logger.error(f"Критическая ошибка: {e}") |