Files
new_lottery_bot/main.py
Andrew K. Choi b6c27b7b70
Some checks reported errors
continuous-integration/drone/push Build encountered an error
feat: добавлена система чата с модерацией
Реализована полнофункциональная система чата с двумя режимами работы:

## Режимы работы:
- Broadcast: рассылка сообщений всем пользователям
- Forward: пересылка сообщений в указанную группу/канал

## Функционал:
- Поддержка всех типов сообщений: text, photo, video, document, animation, sticker, voice
- Система банов: личные баны пользователей и глобальный бан чата
- Модерация: удаление сообщений с отслеживанием в БД
- История сообщений с сохранением ID пересланных сообщений

## Структура БД (миграция 005):
- chat_settings: настройки чата (режим, ID канала, глобальный бан)
- banned_users: история банов с причинами и информацией о модераторе
- chat_messages: история сообщений с типами, файлами и картой доставки (JSONB)

## Сервисы:
- ChatSettingsService: управление настройками чата
- BanService: управление банами пользователей
- ChatMessageService: работа с историей сообщений
- ChatPermissionService: проверка прав на отправку сообщений

## Обработчики:
- chat_handlers.py: обработка сообщений пользователей (7 типов контента)
- admin_chat_handlers.py: админские команды управления чатом

## Админские команды:
- /chat_mode - переключение режима (broadcast/forward)
- /set_forward <chat_id> - установка ID канала для пересылки
- /ban <user_id> [причина] - бан пользователя
- /unban <user_id> - разбан пользователя
- /banlist - список забаненных
- /global_ban - включение/выключение глобального бана
- /delete_msg - удаление сообщения (ответ на сообщение)
- /chat_stats - статистика чата

## Документация:
- docs/CHAT_SYSTEM.md: полное описание системы с примерами использования

Изменено файлов: 7 (2 modified, 5 new)
- main.py: подключены chat_router и admin_chat_router
- src/core/models.py: добавлены модели ChatSettings, BannedUser, ChatMessage
- migrations/versions/005_add_chat_system.py: миграция создания таблиц
- src/core/chat_services.py: сервисный слой для чата (267 строк)
- src/handlers/chat_handlers.py: обработчики сообщений (447 строк)
- src/handlers/admin_chat_handlers.py: админские команды (369 строк)
- docs/CHAT_SYSTEM.md: документация (390 строк)
2025-11-16 14:25:09 +09:00

1046 lines
45 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

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.core.models import User
from src.handlers.admin_panel import admin_router
from src.handlers.account_handlers import account_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.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")]
]
if not is_admin_user:
buttons.extend([
[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="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_registered = user.is_registered
is_admin_user = is_admin(message.from_user.id)
welcome_text = f"Добро пожаловать, {message.from_user.first_name}! 🎉\n\n"
welcome_text += "Это бот для проведения розыгрышей.\n\n"
# Для обычных пользователей - проверяем регистрацию
if not is_admin_user and not is_registered:
welcome_text += "⚠️ Для участия в розыгрышах необходимо пройти регистрацию.\n\n"
buttons = [
[InlineKeyboardButton(text="📝 Зарегистрироваться", callback_data="start_registration")],
[InlineKeyboardButton(text="🎲 Активные розыгрыши", callback_data="list_lotteries")]
]
await message.answer(
welcome_text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)
)
return
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:
# Безопасное отображение победителя
if winner.user:
if winner.user.username:
winner_display = f"@{winner.user.username}"
else:
winner_display = f"{winner.user.first_name}"
elif winner.account_number:
winner_display = f"Счет: {winner.account_number}"
else:
winner_display = "Участник"
text += f"{winner.place}. {winner_display} - {winner.prize}\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
# Используем правильный метод ParticipationService
success = await ParticipationService.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)
async def notify_winners_async(bot: Bot, lottery_id: int, results: dict):
"""
Асинхронно отправить уведомления победителям с кнопкой подтверждения
Вызывается после проведения розыгрыша
"""
async with async_session_maker() as session:
from src.core.registration_services import AccountService, WinnerNotificationService
from src.core.models import Winner
from sqlalchemy import select
# Получаем информацию о розыгрыше
lottery = await LotteryService.get_lottery(session, lottery_id)
if not lottery:
return
# Получаем всех победителей из БД
winners_result = await session.execute(
select(Winner).where(Winner.lottery_id == lottery_id)
)
winners = winners_result.scalars().all()
for winner in winners:
try:
# Если у победителя есть account_number, ищем владельца
if winner.account_number:
owner = await AccountService.get_account_owner(session, winner.account_number)
if owner and owner.telegram_id:
# Создаем токен верификации
verification = await WinnerNotificationService.create_verification_token(
session,
winner.id
)
# Формируем сообщение с кнопкой подтверждения
message = (
f"🎉 **Поздравляем! Ваш счет выиграл!**\n\n"
f"🎯 Розыгрыш: {lottery.title}\n"
f"🏆 Место: {winner.place}\n"
f"🎁 Приз: {winner.prize}\n"
f"💳 **Выигрышный счет: {winner.account_number}**\n\n"
f"⏰ **У вас есть 24 часа для подтверждения!**\n\n"
f"Нажмите кнопку ниже, чтобы подтвердить получение приза по этому счету.\n"
f"Если вы не подтвердите в течение 24 часов, "
f"приз будет разыгран заново.\n\n"
f" Если у вас несколько выигрышных счетов, "
f"подтвердите каждый из них отдельно."
)
# Создаем кнопку подтверждения с указанием счета
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=f"✅ Подтвердить счет {winner.account_number}",
callback_data=f"confirm_win_{winner.id}"
)],
[InlineKeyboardButton(
text="📞 Связаться с администратором",
url=f"tg://user?id={ADMIN_IDS[0]}"
)]
])
# Отправляем уведомление с кнопкой
await bot.send_message(
owner.telegram_id,
message,
reply_markup=keyboard,
parse_mode="Markdown"
)
# Отмечаем, что уведомление отправлено
winner.is_notified = True
await session.commit()
logger.info(f"Отправлено уведомление победителю {owner.telegram_id} за счет {winner.account_number}")
# Если победитель - обычный пользователь (старая система)
elif winner.user_id:
user_result = await session.execute(
select(User).where(User.id == winner.user_id)
)
user = user_result.scalar_one_or_none()
if user and user.telegram_id:
message = (
f"🎉 Поздравляем! Вы выиграли!\n\n"
f"🎯 Розыгрыш: {lottery.title}\n"
f"🏆 Место: {winner.place}\n"
f"🎁 Приз: {winner.prize}\n\n"
f"⏰ **У вас есть 24 часа для подтверждения!**\n\n"
f"Нажмите кнопку ниже, чтобы подтвердить получение приза."
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text="✅ Подтвердить получение приза",
callback_data=f"confirm_win_{winner.id}"
)]
])
await bot.send_message(
user.telegram_id,
message,
reply_markup=keyboard,
parse_mode="Markdown"
)
winner.is_notified = True
await session.commit()
logger.info(f"Отправлено уведомление победителю {user.telegram_id}")
except Exception as e:
logger.error(f"Ошибка при отправке уведомления победителю: {e}")
@router.callback_query(F.data.startswith("confirm_win_"))
async def confirm_winner_response(callback: CallbackQuery):
"""Обработка подтверждения выигрыша победителем"""
winner_id = int(callback.data.split("_")[2])
async with async_session_maker() as session:
from src.core.models import Winner
from sqlalchemy import select
from sqlalchemy.orm import joinedload
# Получаем выигрыш с загрузкой связанного розыгрыша
winner_result = await session.execute(
select(Winner)
.options(joinedload(Winner.lottery))
.where(Winner.id == winner_id)
)
winner = winner_result.scalar_one_or_none()
if not winner:
await callback.answer("❌ Выигрыш не найден", show_alert=True)
return
# Проверяем, не подтвержден ли уже этот конкретный счет
if winner.is_claimed:
await callback.message.edit_text(
"✅ **Выигрыш этого счета уже подтвержден!**\n\n"
f"🎯 Розыгрыш: {winner.lottery.title}\n"
f"🏆 Место: {winner.place}\n"
f"🎁 Приз: {winner.prize}\n"
f"💳 Счет: {winner.account_number}\n\n"
"Администратор свяжется с вами для передачи приза.",
parse_mode="Markdown"
)
return
# Проверяем, что подтверждает владелец именно ЭТОГО счета
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if winner.account_number:
# Проверяем что счет принадлежит текущему пользователю
from src.core.registration_services import AccountService
owner = await AccountService.get_account_owner(session, winner.account_number)
if not owner or owner.telegram_id != callback.from_user.id:
await callback.answer(
f"❌ Счет {winner.account_number} вам не принадлежит",
show_alert=True
)
return
elif winner.user_id:
# Старая логика для выигрышей без счета
if not user or user.id != winner.user_id:
await callback.answer("❌ Это не ваш выигрыш", show_alert=True)
return
# Подтверждаем выигрыш ЭТОГО конкретного счета
from datetime import datetime, timezone
winner.is_claimed = True
winner.claimed_at = datetime.now(timezone.utc)
await session.commit()
# Обновляем сообщение с указанием счета
confirmation_text = (
"✅ **Выигрыш успешно подтвержден!**\n\n"
f"🎯 Розыгрыш: {winner.lottery.title}\n"
f"🏆 Место: {winner.place}\n"
f"🎁 Приз: {winner.prize}\n"
)
if winner.account_number:
confirmation_text += f"💳 Счет: {winner.account_number}\n"
confirmation_text += (
"\n🎊 Поздравляем! Администратор свяжется с вами "
"для передачи приза в ближайшее время.\n\n"
"Спасибо за участие!"
)
await callback.message.edit_text(
confirmation_text,
parse_mode="Markdown"
)
# Уведомляем администраторов о подтверждении конкретного счета
for admin_id in ADMIN_IDS:
try:
admin_msg = (
f"✅ **Победитель подтвердил получение приза!**\n\n"
f"🎯 Розыгрыш: {winner.lottery.title}\n"
f"🏆 Место: {winner.place}\n"
f"🎁 Приз: {winner.prize}\n"
)
# Обязательно показываем счет
if winner.account_number:
admin_msg += f"<EFBFBD> **Подтвержденный счет: {winner.account_number}**\n\n"
if user:
admin_msg += f"👤 Владелец: {user.first_name}"
if user.username:
admin_msg += f" (@{user.username})"
admin_msg += f"\n🎫 Клубная карта: {user.club_card_number}\n"
if user.phone:
admin_msg += f"📱 Телефон: {user.phone}\n"
await callback.bot.send_message(admin_id, admin_msg, parse_mode="Markdown")
except:
pass
logger.info(
f"Победитель {callback.from_user.id} подтвердил выигрыш {winner_id} "
f"(счет: {winner.account_number})"
)
await callback.answer("✅ Выигрыш подтвержден!", show_alert=True)
@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:
lottery = await LotteryService.get_lottery(session, lottery_id)
if not lottery:
await callback.answer("❌ Розыгрыш не найден", show_alert=True)
return
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_obj = winner_info['user']
prize = winner_info['prize']
# Безопасное отображение победителя
if hasattr(user_obj, 'username') and user_obj.username:
winner_display = f"@{user_obj.username}"
elif hasattr(user_obj, 'first_name'):
winner_display = f"{user_obj.first_name}"
elif hasattr(user_obj, 'account_number'):
winner_display = f"Счет: {user_obj.account_number}"
else:
winner_display = "Участник"
text += f"{place}. {winner_display}\n"
text += f" 🎁 {prize}\n\n"
# Отправляем уведомления победителям асинхронно
asyncio.create_task(notify_winners_async(callback.bot, lottery_id, results))
text += "📨 Уведомления отправляются победителям...\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)
if not user:
await message.answer("❌ Ошибка получения данных пользователя")
await state.clear()
return
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
# Проверяем регистрацию
if not user.is_registered:
text = "❌ **Вы не зарегистрированы**\n\n"
text += "Пройдите регистрацию для доступа к счетам"
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📝 Зарегистрироваться", callback_data="start_registration")],
[InlineKeyboardButton(text="🔙 Главное меню", callback_data="back_to_main")]
]),
parse_mode="Markdown"
)
return
# Получаем счета пользователя
from src.core.registration_services import AccountService
accounts = await AccountService.get_user_accounts(session, user.id)
text = "💳 **Ваши счета**\n\n"
if accounts:
text += f"🎫 Клубная карта: `{user.club_card_number}`\n"
text += f"<EFBFBD> Код верификации: `{user.verification_code}`\n\n"
text += f"**Счета ({len(accounts)}):**\n\n"
for i, acc in enumerate(accounts, 1):
status = "✅ Активен" if acc.is_active else "❌ Неактивен"
text += f"{i}. `{acc.account_number}`\n"
text += f" {status}\n\n"
text += " Счета используются для участия в розыгрышах"
else:
text += f"🎫 Клубная карта: `{user.club_card_number}`\n\n"
text += "У вас нет счетов\n\n"
text += "Обратитесь к администратору для добавления счетов"
buttons = [[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(registration_router) # Роутер регистрации (первый)
dp.include_router(admin_account_router) # Роутер админских команд для счетов
dp.include_router(admin_chat_router) # Роутер админских команд чата (до обычных обработчиков)
dp.include_router(redraw_router) # Роутер повторного розыгрыша
dp.include_router(account_router) # Роутер для работы со счетами
dp.include_router(chat_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("Завершение работы")