diff --git a/bot/handlers/admin/broadcast.py b/bot/handlers/admin/broadcast.py index 916ef3a..c0e66b3 100644 --- a/bot/handlers/admin/broadcast.py +++ b/bot/handlers/admin/broadcast.py @@ -19,6 +19,7 @@ from bot.keyboards.inline.admin_keyboards import ( ) from bot.middlewares.i18n import JsonI18n from bot.utils.message_queue import get_queue_manager +from bot.utils import get_message_content, send_message_by_type, send_message_via_queue, MessageContent router = Router(name="admin_broadcast_router") @@ -77,122 +78,33 @@ async def process_broadcast_message_handler( _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) # Определяем тип содержимого и сохраняем данные в state - text = (message.text or message.caption or "").strip() entities = message.entities or message.caption_entities or [] - - content_type = "text" - file_id = None - - if message.photo: - content_type = "photo" - # Берем самое большое фото - file_id = message.photo[-1].file_id - elif message.video: - content_type = "video" - file_id = message.video.file_id - elif message.animation: - content_type = "animation" - file_id = message.animation.file_id - elif message.document: - content_type = "document" - file_id = message.document.file_id - elif message.audio: - content_type = "audio" - file_id = message.audio.file_id - elif message.voice: - content_type = "voice" - file_id = message.voice.file_id - elif message.sticker: - content_type = "sticker" - file_id = message.sticker.file_id - elif message.video_note: - content_type = "video_note" - file_id = message.video_note.file_id + content = get_message_content(message) # Если нет ни текста, ни медиа — ошибка - if not text and not file_id: + if not content.text and not content.file_id: await message.answer(_("admin_broadcast_error_no_message")) return # Сохраняем данные для рассылки await state.update_data( - broadcast_text=text, + broadcast_text=content.text, broadcast_entities=entities, - broadcast_content_type=content_type, - broadcast_file_id=file_id, + broadcast_content_type=content.content_type, + broadcast_file_id=content.file_id, broadcast_target="all", ) # Отправляем превью-копию того, что будет разослано try: - if content_type == "text": - await bot.send_message( - chat_id=message.chat.id, - text=text, - parse_mode="HTML", - disable_web_page_preview=True, - disable_notification=True, - ) - elif content_type == "photo": - await bot.send_photo( - chat_id=message.chat.id, - photo=file_id, - caption=text or None, - parse_mode="HTML", - disable_notification=True, - ) - elif content_type == "video": - await bot.send_video( - chat_id=message.chat.id, - video=file_id, - caption=text or None, - parse_mode="HTML", - disable_notification=True, - ) - elif content_type == "animation": - await bot.send_animation( - chat_id=message.chat.id, - animation=file_id, - caption=text or None, - parse_mode="HTML", - disable_notification=True, - ) - elif content_type == "document": - await bot.send_document( - chat_id=message.chat.id, - document=file_id, - caption=text or None, - parse_mode="HTML", - disable_notification=True, - ) - elif content_type == "audio": - await bot.send_audio( - chat_id=message.chat.id, - audio=file_id, - caption=text or None, - parse_mode="HTML", - disable_notification=True, - ) - elif content_type == "voice": - await bot.send_voice( - chat_id=message.chat.id, - voice=file_id, - caption=text or None, - parse_mode="HTML", - disable_notification=True, - ) - elif content_type == "sticker": - await bot.send_sticker( - chat_id=message.chat.id, - sticker=file_id, - disable_notification=True, - ) - elif content_type == "video_note": - await bot.send_video_note( - chat_id=message.chat.id, - video_note=file_id, - disable_notification=True, - ) + await send_message_by_type( + bot, + chat_id=message.chat.id, + content=content, + parse_mode="HTML", + disable_web_page_preview=True, + disable_notification=True, + ) except TelegramBadRequest as e: await message.answer( _( @@ -308,11 +220,15 @@ async def confirm_broadcast_callback_handler( user_fsm_data = await state.get_data() if action == "send": - text = user_fsm_data.get("broadcast_text") + # Создаем объект контента из сохраненных данных + content = MessageContent( + content_type=user_fsm_data.get("broadcast_content_type", "text"), + file_id=user_fsm_data.get("broadcast_file_id"), + text=user_fsm_data.get("broadcast_text") + ) entities = user_fsm_data.get("broadcast_entities", []) - content_type = user_fsm_data.get("broadcast_content_type", "text") - file_id = user_fsm_data.get("broadcast_file_id") - if not text and content_type == "text": + + if not content.text and content.content_type == "text": await callback.message.edit_text(_("admin_broadcast_error_no_message")) await state.clear() await callback.answer( @@ -335,7 +251,7 @@ async def confirm_broadcast_callback_handler( failed_count = 0 admin_user = callback.from_user logging.info( - f"Admin {admin_user.id} broadcasting '{text[:50]}...' to {len(user_ids)} users." + f"Admin {admin_user.id} broadcasting '{(content.text or '')[:50]}...' to {len(user_ids)} users." ) # Get message queue manager @@ -347,45 +263,13 @@ async def confirm_broadcast_callback_handler( # Queue all messages for sending for uid in user_ids: try: - if content_type == "text": - await queue_manager.send_message( - chat_id=uid, - text=text, - parse_mode="HTML", - disable_web_page_preview=True, - ) - elif content_type == "photo": - await queue_manager.send_photo( - chat_id=uid, photo=file_id, caption=text or None, parse_mode="HTML" - ) - elif content_type == "video": - await queue_manager.send_video( - chat_id=uid, video=file_id, caption=text or None, parse_mode="HTML" - ) - elif content_type == "animation": - await queue_manager.send_animation( - chat_id=uid, animation=file_id, caption=text or None, parse_mode="HTML" - ) - elif content_type == "document": - await queue_manager.send_document( - chat_id=uid, document=file_id, caption=text or None, parse_mode="HTML" - ) - elif content_type == "audio": - await queue_manager.send_audio( - chat_id=uid, audio=file_id, caption=text or None, parse_mode="HTML" - ) - elif content_type == "voice": - await queue_manager.send_voice( - chat_id=uid, voice=file_id, caption=text or None, parse_mode="HTML" - ) - elif content_type == "sticker": - await queue_manager.send_sticker( - chat_id=uid, sticker=file_id - ) - elif content_type == "video_note": - await queue_manager.send_video_note( - chat_id=uid, video_note=file_id - ) + await send_message_via_queue( + queue_manager, + uid, + content, + parse_mode="HTML", + disable_web_page_preview=True, + ) sent_count += 1 # Log successful queuing @@ -396,7 +280,7 @@ async def confirm_broadcast_callback_handler( "telegram_username": admin_user.username, "telegram_first_name": admin_user.first_name, "event_type": "admin_broadcast_queued", - "content": f"To user {uid}: [{content_type}] {(text or '')[:70]}...", + "content": f"To user {uid}: [{content.content_type}] {(content.text or '')[:70]}...", "is_admin_event": True, "target_user_id": uid, }, diff --git a/bot/handlers/admin/user_management.py b/bot/handlers/admin/user_management.py index 50ba1e7..24de068 100644 --- a/bot/handlers/admin/user_management.py +++ b/bot/handlers/admin/user_management.py @@ -16,6 +16,7 @@ from bot.keyboards.inline.admin_keyboards import get_back_to_admin_panel_keyboar from bot.services.subscription_service import SubscriptionService from bot.services.panel_api_service import PanelApiService from bot.middlewares.i18n import JsonI18n +from bot.utils import get_message_content, send_direct_message from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton router = Router(name="admin_user_management_router") @@ -615,120 +616,33 @@ async def process_direct_message_handler(message: types.Message, state: FSMConte await state.clear() return - # Prepare admin signature and content type + # Prepare admin signature and get content admin_signature = _( "admin_direct_message_signature", default="\n\n---\n💬 Сообщение от администратора" ) + + content = get_message_content(message) - content_type = "text" - file_id = None - - if message.photo: - content_type = "photo" - file_id = message.photo[-1].file_id - elif message.video: - content_type = "video" - file_id = message.video.file_id - elif message.animation: - content_type = "animation" - file_id = message.animation.file_id - elif message.document: - content_type = "document" - file_id = message.document.file_id - elif message.audio: - content_type = "audio" - file_id = message.audio.file_id - elif message.voice: - content_type = "voice" - file_id = message.voice.file_id - elif message.sticker: - content_type = "sticker" - file_id = message.sticker.file_id - elif message.video_note: - content_type = "video_note" - file_id = message.video_note.file_id - - if not text and not file_id: + if not content.text and not content.file_id: await message.answer(_( "admin_direct_empty_message", default="❌ Пустое сообщение. Отправьте текст или медиа." )) return - caption_with_signature = (text + admin_signature) if text else None + caption_with_signature = (content.text + admin_signature) if content.text else None - # Send to target user similar to broadcast + # Send to target user using our fancy match/case function try: - if content_type == "text": - await bot.send_message( - target_user_id, - (caption_with_signature or admin_signature), - parse_mode="HTML", - disable_web_page_preview=True, - ) - elif content_type == "photo": - await bot.send_photo( - chat_id=target_user_id, - photo=file_id, - caption=caption_with_signature, - parse_mode="HTML", - ) - elif content_type == "video": - await bot.send_video( - chat_id=target_user_id, - video=file_id, - caption=caption_with_signature, - parse_mode="HTML", - ) - elif content_type == "animation": - await bot.send_animation( - chat_id=target_user_id, - animation=file_id, - caption=caption_with_signature, - parse_mode="HTML", - ) - elif content_type == "document": - await bot.send_document( - chat_id=target_user_id, - document=file_id, - caption=caption_with_signature, - parse_mode="HTML", - ) - elif content_type == "audio": - await bot.send_audio( - chat_id=target_user_id, - audio=file_id, - caption=caption_with_signature, - parse_mode="HTML", - ) - elif content_type == "voice": - await bot.send_voice( - chat_id=target_user_id, - voice=file_id, - caption=caption_with_signature, - parse_mode="HTML", - ) - elif content_type == "sticker": - # Stickers do not support captions; send sticker and then optional signature/text - await bot.send_sticker(chat_id=target_user_id, sticker=file_id) - if caption_with_signature: - await bot.send_message( - target_user_id, - caption_with_signature, - parse_mode="HTML", - disable_web_page_preview=True, - ) - elif content_type == "video_note": - # Video notes do not support captions; send media then optional signature/text - await bot.send_video_note(chat_id=target_user_id, video_note=file_id) - if caption_with_signature: - await bot.send_message( - target_user_id, - caption_with_signature, - parse_mode="HTML", - disable_web_page_preview=True, - ) + await send_direct_message( + bot, + target_user_id, + content, + extra_text=admin_signature, + parse_mode="HTML", + disable_web_page_preview=True, + ) except TelegramBadRequest as e: await message.answer(_( "admin_broadcast_invalid_html", diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py index 512ba75..bab4294 100644 --- a/bot/utils/__init__.py +++ b/bot/utils/__init__.py @@ -1 +1,229 @@ -# Bot utilities package \ No newline at end of file +# Bot utilities package + +from dataclasses import dataclass +from typing import Optional +from aiogram import types + + +@dataclass +class MessageContent: + """Класс для хранения информации о контенте сообщения""" + content_type: str + file_id: Optional[str] = None + text: Optional[str] = None + + +def get_message_content(message: types.Message) -> MessageContent: + """ + Определяет тип контента сообщения и возвращает его данные. + Использует match/case вместо длинных if-elif цепочек. + """ + text = (message.text or message.caption or "").strip() + + # Проверяем наличие медиа-контента + media_content = None + if message.photo: + media_content = ("photo", message.photo[-1].file_id) + elif message.video: + media_content = ("video", message.video.file_id) + elif message.animation: + media_content = ("animation", message.animation.file_id) + elif message.document: + media_content = ("document", message.document.file_id) + elif message.audio: + media_content = ("audio", message.audio.file_id) + elif message.voice: + media_content = ("voice", message.voice.file_id) + elif message.sticker: + media_content = ("sticker", message.sticker.file_id) + elif message.video_note: + media_content = ("video_note", message.video_note.file_id) + + # Используем match/case для определения типа контента + match media_content: + case (content_type, file_id): + return MessageContent(content_type=content_type, file_id=file_id, text=text) + case None: + return MessageContent(content_type="text", text=text) + case _: + return MessageContent(content_type="text", text=text) + + +async def send_message_by_type(bot, chat_id: int, content: MessageContent, **kwargs) -> None: + """ + Отправляет сообщение указанного типа. + Использует match/case вместо длинных if-elif цепочек. + """ + match content.content_type: + case "text": + await bot.send_message( + chat_id=chat_id, + text=content.text, + **kwargs + ) + case "photo": + await bot.send_photo( + chat_id=chat_id, + photo=content.file_id, + caption=content.text or None, + **kwargs + ) + case "video": + await bot.send_video( + chat_id=chat_id, + video=content.file_id, + caption=content.text or None, + **kwargs + ) + case "animation": + await bot.send_animation( + chat_id=chat_id, + animation=content.file_id, + caption=content.text or None, + **kwargs + ) + case "document": + await bot.send_document( + chat_id=chat_id, + document=content.file_id, + caption=content.text or None, + **kwargs + ) + case "audio": + await bot.send_audio( + chat_id=chat_id, + audio=content.file_id, + caption=content.text or None, + **kwargs + ) + case "voice": + await bot.send_voice( + chat_id=chat_id, + voice=content.file_id, + caption=content.text or None, + **kwargs + ) + case "sticker": + await bot.send_sticker( + chat_id=chat_id, + sticker=content.file_id, + # stickers не поддерживают caption - удаляем его из kwargs + **{k: v for k, v in kwargs.items() if k != 'caption'} + ) + case "video_note": + await bot.send_video_note( + chat_id=chat_id, + video_note=content.file_id, + # video_note не поддерживает caption - удаляем его из kwargs + **{k: v for k, v in kwargs.items() if k != 'caption'} + ) + case _: + # Fallback для неизвестных типов + await bot.send_message( + chat_id=chat_id, + text=content.text or "Unknown content type", + **{k: v for k, v in kwargs.items() if k != 'caption'} + ) + + +async def send_message_via_queue(queue_manager, uid: int, content: MessageContent, **kwargs) -> None: + """ + Отправляет сообщение через очередь в зависимости от типа контента. + Использует match/case вместо длинных if-elif цепочек. + """ + match content.content_type: + case "text": + await queue_manager.send_message( + chat_id=uid, text=content.text, **kwargs + ) + case "photo": + await queue_manager.send_photo( + chat_id=uid, photo=content.file_id, caption=content.text or None, **kwargs + ) + case "video": + await queue_manager.send_video( + chat_id=uid, video=content.file_id, caption=content.text or None, **kwargs + ) + case "animation": + await queue_manager.send_animation( + chat_id=uid, animation=content.file_id, caption=content.text or None, **kwargs + ) + case "document": + await queue_manager.send_document( + chat_id=uid, document=content.file_id, caption=content.text or None, **kwargs + ) + case "audio": + await queue_manager.send_audio( + chat_id=uid, audio=content.file_id, caption=content.text or None, **kwargs + ) + case "voice": + await queue_manager.send_voice( + chat_id=uid, voice=content.file_id, caption=content.text or None, **kwargs + ) + case "sticker": + await queue_manager.send_sticker( + chat_id=uid, sticker=content.file_id + ) + case "video_note": + await queue_manager.send_video_note( + chat_id=uid, video_note=content.file_id + ) + case _: + # Fallback для неизвестных типов + await queue_manager.send_message( + chat_id=uid, text=content.text or "Unknown content type", **kwargs + ) + + +async def send_direct_message(bot, chat_id: int, content: MessageContent, extra_text: str = "", **kwargs) -> None: + """ + Отправляет прямое сообщение с дополнительной обработкой для sticker и video_note. + Для этих типов медиа отправляется отдельное текстовое сообщение, т.к. они не поддерживают caption. + """ + match content.content_type: + case "sticker": + # Отправляем стикер + await bot.send_sticker( + chat_id=chat_id, + sticker=content.file_id, + **{k: v for k, v in kwargs.items() if k != 'caption'} + ) + # Если есть текст с подписью, отправляем отдельно + if content.text or extra_text: + text_to_send = (content.text + extra_text) if content.text else extra_text + await bot.send_message( + chat_id, + text_to_send, + **{k: v for k, v in kwargs.items() if k not in ['caption']} + ) + case "video_note": + # Отправляем видео-заметку + await bot.send_video_note( + chat_id=chat_id, + video_note=content.file_id, + **{k: v for k, v in kwargs.items() if k != 'caption'} + ) + # Если есть текст с подписью, отправляем отдельно + if content.text or extra_text: + text_to_send = (content.text + extra_text) if content.text else extra_text + await bot.send_message( + chat_id, + text_to_send, + **{k: v for k, v in kwargs.items() if k not in ['caption']} + ) + case "text": + # Для текста объединяем с extra_text + final_text = (content.text + extra_text) if content.text else extra_text + await bot.send_message( + chat_id=chat_id, + text=final_text, + **kwargs + ) + case _: + # Для остальных типов медиа используем caption + final_caption = (content.text + extra_text) if content.text else None + await send_message_by_type( + bot, chat_id, + MessageContent(content.content_type, content.file_id, final_caption), + **kwargs + ) \ No newline at end of file