Some checks reported errors
continuous-integration/drone/push Build encountered an error
Реализована полнофункциональная система чата с двумя режимами работы: ## Режимы работы: - 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 строк)
1046 lines
45 KiB
Python
1046 lines
45 KiB
Python
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("Завершение работы") |