From ca0c63a89c2117f054d2333a907ed6f315eb8a37 Mon Sep 17 00:00:00 2001 From: "Andrey K. Choi" Date: Wed, 11 Feb 2026 18:40:37 +0900 Subject: [PATCH] chat+lottery refactor --- src/handlers/chat_handlers.py | 202 +++++++++++++++++++++------------- 1 file changed, 126 insertions(+), 76 deletions(-) diff --git a/src/handlers/chat_handlers.py b/src/handlers/chat_handlers.py index 1bd8b7d..989de0c 100644 --- a/src/handlers/chat_handlers.py +++ b/src/handlers/chat_handlers.py @@ -6,7 +6,7 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.filters import StateFilter, Command from sqlalchemy.ext.asyncio import AsyncSession import asyncio -from typing import List, Dict, Optional, Set +from typing import List, Dict, Optional, Set, Any from collections import deque import time @@ -130,18 +130,21 @@ async def get_all_active_users(session: AsyncSession) -> List: async def broadcast_message_with_scheduler( message: Message, + sender_user: Any, # User model object exclude_user_id: Optional[int] = None, - admin_only: bool = False, - sender_info: Optional[str] = None + admin_only: bool = False ) -> tuple[Dict[str, int], int, int]: """ Разослать сообщение всем пользователям с планировщиком (пакетная отправка). + Подписи формируются динамически в зависимости от получателя: + - Админы видят: nickname (карта: XXXX) + - Обычные пользователи видят: nickname (от пользователя) или "Админ" (от админа) Args: message: Сообщение для рассылки + sender_user: Объект User отправителя exclude_user_id: ID пользователя для исключения admin_only: Рассылать только админам - sender_info: Информация об отправителе (для показа админам) Возвращает: (forwarded_ids, success_count, fail_count) """ @@ -165,12 +168,29 @@ async def broadcast_message_with_scheduler( # Отправляем пакет tasks = [] - for user in batch: - # Для админов добавляем информацию об отправителе, если сообщение от обычного пользователя - if sender_info and user.telegram_id in ADMIN_IDS: - tasks.append(_send_message_to_admin_with_sender(message, user.telegram_id, sender_info)) + for recipient_user in batch: + # Формируем подпись в зависимости от получателя + if recipient_user.telegram_id in ADMIN_IDS: + # Админы видят полную информацию: nickname (карта: XXXX) + sender_name = sender_user.nickname if sender_user.nickname else ( + f"@{sender_user.username}" if sender_user.username else sender_user.first_name + ) + if sender_user.club_card_number: + sender_name += f" (карта: {sender_user.club_card_number})" + sender_info = sender_name + tasks.append(_send_message_to_admin_with_sender(message, recipient_user.telegram_id, sender_info)) else: - tasks.append(_send_message_to_user(message, user.telegram_id)) + # Обычные пользователи видят: + # - "Админ" если отправитель - админ + # - nickname если отправитель - обычный пользователь + if sender_user.telegram_id in ADMIN_IDS: + sender_info = "Админ" + tasks.append(_send_message_to_user_with_sender(message, recipient_user.telegram_id, sender_info)) + else: + sender_info = sender_user.nickname if sender_user.nickname else ( + f"@{sender_user.username}" if sender_user.username else sender_user.first_name + ) + tasks.append(_send_message_to_user_with_sender(message, recipient_user.telegram_id, sender_info)) # Ждем завершения пакета results = await asyncio.gather(*tasks, return_exceptions=True) @@ -205,6 +225,85 @@ async def _send_message_to_user(message: Message, user_telegram_id: int) -> Opti return None +async def _send_message_to_user_with_sender(message: Message, user_telegram_id: int, sender_info: str) -> Optional[int]: + """ + Отправить сообщение обычному пользователю с информацией об отправителе. + Возвращает message_id при успехе или None при ошибке. + """ + try: + # Формируем текст с информацией об отправителе + header = f"📨 {sender_info}:\n\n" + + if message.text: + # Текстовое сообщение + sent_msg = await message.bot.send_message( + user_telegram_id, + header + message.text, + parse_mode="HTML" + ) + elif message.photo: + # Фото + caption = header + (message.caption or "") + sent_msg = await message.bot.send_photo( + user_telegram_id, + photo=message.photo[-1].file_id, + caption=caption, + parse_mode="HTML" + ) + elif message.video: + # Видео + caption = header + (message.caption or "") + sent_msg = await message.bot.send_video( + user_telegram_id, + video=message.video.file_id, + caption=caption, + parse_mode="HTML" + ) + elif message.document: + # Документ + caption = header + (message.caption or "") + sent_msg = await message.bot.send_document( + user_telegram_id, + document=message.document.file_id, + caption=caption, + parse_mode="HTML" + ) + elif message.animation: + # GIF + caption = header + (message.caption or "") + sent_msg = await message.bot.send_animation( + user_telegram_id, + animation=message.animation.file_id, + caption=caption, + parse_mode="HTML" + ) + elif message.sticker: + # Стикер - сначала отправляем заголовок, потом стикер + await message.bot.send_message(user_telegram_id, header, parse_mode="HTML") + sent_msg = await message.bot.send_sticker(user_telegram_id, sticker=message.sticker.file_id) + elif message.voice: + # Голосовое сообщение + sent_msg = await message.bot.send_voice( + user_telegram_id, + voice=message.voice.file_id, + caption=header, + parse_mode="HTML" + ) + elif message.video_note: + # Видео-кружок + await message.bot.send_message(user_telegram_id, header, parse_mode="HTML") + sent_msg = await message.bot.send_video_note(user_telegram_id, video_note=message.video_note.file_id) + else: + # Неизвестный тип - просто копируем + await message.bot.send_message(user_telegram_id, header, parse_mode="HTML") + sent_msg = await message.copy_to(user_telegram_id) + + return sent_msg.message_id + except Exception as e: + print(f"Failed to send message to {user_telegram_id}: {e}") + return None + + async def _send_message_to_admin_with_sender(message: Message, admin_telegram_id: int, sender_info: str) -> Optional[int]: """ Отправить сообщение админу с информацией об отправителе. @@ -443,20 +542,12 @@ async def handle_text_message(message: Message, state: FSMContext): # Обрабатываем в зависимости от режима if settings.mode == 'broadcast': # Режим рассылки с планировщиком - # Формируем информацию об отправителе для админов (если это не админ) - sender_info = None - if not is_admin(message.from_user.id): - # Используем nickname, если есть, иначе fallback на username или first_name - sender_name = user.nickname if user.nickname else (f"@{user.username}" if user.username else user.first_name) - if user.club_card_number: - sender_name += f" (карта: {user.club_card_number})" - sender_info = sender_name - + # Передаем объект user для динамического формирования подписей # ВСЕГДА исключаем отправителя - он не должен получать своё же сообщение forwarded_ids, success, fail = await broadcast_message_with_scheduler( - message, - exclude_user_id=message.from_user.id, - sender_info=sender_info + message, + sender_user=user, + exclude_user_id=message.from_user.id ) # Сохраняем сообщение в историю @@ -532,20 +623,11 @@ async def handle_photo_message(message: Message, state: FSMContext): photo = message.photo[-1] if settings.mode == 'broadcast': - # Формируем информацию об отправителе для админов (если это не админ) - sender_info = None - if not is_admin(message.from_user.id): - # Используем nickname, если есть, иначе fallback на username или first_name - sender_name = user.nickname if user.nickname else (f"@{user.username}" if user.username else user.first_name) - if user.club_card_number: - sender_name += f" (карта: {user.club_card_number})" - sender_info = sender_name - # Рассылаем фото - ВСЕГДА исключаем отправителя forwarded_ids, success, fail = await broadcast_message_with_scheduler( message, - exclude_user_id=message.from_user.id, - sender_info=sender_info + sender_user=user, + exclude_user_id=message.from_user.id ) await ChatMessageService.save_message( @@ -607,19 +689,11 @@ async def handle_video_message(message: Message, state: FSMContext): ) if settings.mode == 'broadcast': - # Формируем информацию об отправителе для админов (если это не админ) - sender_info = None - if not is_admin(message.from_user.id): - # Используем nickname, если есть, иначе fallback на username или first_name - sender_name = user.nickname if user.nickname else (f"@{user.username}" if user.username else user.first_name) - if user.club_card_number: - sender_name += f" (карта: {user.club_card_number})" - sender_info = sender_name - + # Рассылаем видео forwarded_ids, success, fail = await broadcast_message_with_scheduler( message, - exclude_user_id=message.from_user.id, - sender_info=sender_info + sender_user=user, + exclude_user_id=message.from_user.id ) await ChatMessageService.save_message( @@ -681,19 +755,11 @@ async def handle_document_message(message: Message, state: FSMContext): ) if settings.mode == 'broadcast': - # Формируем информацию об отправителе для админов (если это не админ) - sender_info = None - if not is_admin(message.from_user.id): - # Используем nickname, если есть, иначе fallback на username или first_name - sender_name = user.nickname if user.nickname else (f"@{user.username}" if user.username else user.first_name) - if user.club_card_number: - sender_name += f" (карта: {user.club_card_number})" - sender_info = sender_name - + # Рассылаем документ forwarded_ids, success, fail = await broadcast_message_with_scheduler( message, - exclude_user_id=message.from_user.id, - sender_info=sender_info + sender_user=user, + exclude_user_id=message.from_user.id ) await ChatMessageService.save_message( @@ -755,19 +821,11 @@ async def handle_animation_message(message: Message, state: FSMContext): ) if settings.mode == 'broadcast': - # Формируем информацию об отправителе для админов (если это не админ) - sender_info = None - if not is_admin(message.from_user.id): - # Используем nickname, если есть, иначе fallback на username или first_name - sender_name = user.nickname if user.nickname else (f"@{user.username}" if user.username else user.first_name) - if user.club_card_number: - sender_name += f" (карта: {user.club_card_number})" - sender_info = sender_name - + # Рассылаем анимацию forwarded_ids, success, fail = await broadcast_message_with_scheduler( message, - exclude_user_id=message.from_user.id, - sender_info=sender_info + sender_user=user, + exclude_user_id=message.from_user.id ) await ChatMessageService.save_message( @@ -829,19 +887,11 @@ async def handle_sticker_message(message: Message, state: FSMContext): ) if settings.mode == 'broadcast': - # Формируем информацию об отправителе для админов (если это не админ) - sender_info = None - if not is_admin(message.from_user.id): - # Используем nickname, если есть, иначе fallback на username или first_name - sender_name = user.nickname if user.nickname else (f"@{user.username}" if user.username else user.first_name) - if user.club_card_number: - sender_name += f" (карта: {user.club_card_number})" - sender_info = sender_name - + # Рассылаем стикер forwarded_ids, success, fail = await broadcast_message_with_scheduler( message, - exclude_user_id=message.from_user.id, - sender_info=sender_info + sender_user=user, + exclude_user_id=message.from_user.id ) await ChatMessageService.save_message(