From 9b8ddb39dab4a7d0fb731405d1e76f1e265aa253 Mon Sep 17 00:00:00 2001 From: machka-pasla Date: Wed, 20 Aug 2025 15:30:06 +0300 Subject: [PATCH] Enhance broadcast message handling to support multiple content types - Updated the process_broadcast_message_handler to determine the content type of incoming messages (text, photo, video, etc.) and store relevant data in the state. - Implemented new methods in MessageQueueManager for queuing various media types, improving the flexibility of the broadcast system. - Adjusted confirmation prompts to provide a concise message preview, enhancing user experience. - Added localization for the new confirmation prompt in both English and Russian. --- bot/handlers/admin/broadcast.py | 121 +++++++++++++++++++++---------- bot/services/referral_service.py | 4 +- bot/utils/message_queue.py | 70 ++++++++++++++++++ locales/en.json | 1 + locales/ru.json | 1 + 5 files changed, 156 insertions(+), 41 deletions(-) diff --git a/bot/handlers/admin/broadcast.py b/bot/handlers/admin/broadcast.py index 48ec0f0..64456dc 100644 --- a/bot/handlers/admin/broadcast.py +++ b/bot/handlers/admin/broadcast.py @@ -76,47 +76,55 @@ async def process_broadcast_message_handler( _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) - # Сохраняем в state исходный текст и entities + # Определяем тип содержимого и сохраняем данные в state text = (message.text or message.caption or "").strip() entities = message.entities or message.caption_entities or [] - # Если текст пустой (например, прислали стикер/фото без подписи) — просим ввести текст - if not text: + 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: await message.answer(_("admin_broadcast_error_no_message")) return - # Предварительная проверка HTML: попробуем отправить и сразу удалить - # Если HTML некорректный, Telegram вернёт ошибку парсинга - try: - test_msg = await bot.send_message( - chat_id=message.chat.id, - text=text, - parse_mode="HTML", - disable_web_page_preview=True, - disable_notification=True, - ) - # Удалим тестовое сообщение - try: - await bot.delete_message(chat_id=message.chat.id, message_id=test_msg.message_id) - except Exception: - pass - except TelegramBadRequest as e: - await message.answer( - _( - "admin_broadcast_invalid_html", - default="❌ Некорректный HTML в сообщении. Пожалуйста, отправьте корректный HTML (поддерживаются теги Telegram) или уберите теги.\nОшибка: {error}", - error=str(e), - ) - ) - return - + # Сохраняем данные для рассылки await state.update_data( broadcast_text=text, broadcast_entities=entities, + broadcast_content_type=content_type, + broadcast_file_id=file_id, broadcast_target="all", ) - confirmation_prompt = _("admin_broadcast_confirm_prompt", message_preview=text) + # Показываем короткое подтверждение без дублирования текста — сообщение выше служит превью + confirmation_prompt = _("admin_broadcast_confirm_prompt_short") await message.answer( confirmation_prompt, @@ -148,10 +156,9 @@ async def change_broadcast_target_handler( await state.update_data(broadcast_target=new_target) user_fsm_data = await state.get_data() - text = user_fsm_data.get("broadcast_text", "") _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) confirmation_prompt = _( - "admin_broadcast_confirm_prompt", message_preview=text + "admin_broadcast_confirm_prompt_short" ) try: await callback.message.edit_text( @@ -223,8 +230,9 @@ async def confirm_broadcast_callback_handler( if action == "send": text = user_fsm_data.get("broadcast_text") entities = user_fsm_data.get("broadcast_entities", []) - - if not text: + 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": await callback.message.edit_text(_("admin_broadcast_error_no_message")) await state.clear() await callback.answer( @@ -259,12 +267,45 @@ async def confirm_broadcast_callback_handler( # Queue all messages for sending for uid in user_ids: try: - await queue_manager.send_message( - chat_id=uid, - text=text, - parse_mode="HTML", - disable_web_page_preview=True, - ) + 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 + ) sent_count += 1 # Log successful queuing @@ -275,7 +316,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}: {text[:70]}...", + "content": f"To user {uid}: [{content_type}] {(text or '')[:70]}...", "is_admin_event": True, "target_user_id": uid, }, diff --git a/bot/services/referral_service.py b/bot/services/referral_service.py index 82cb9a0..9e518c8 100644 --- a/bot/services/referral_service.py +++ b/bot/services/referral_service.py @@ -50,7 +50,9 @@ class ReferralService: # If configured to apply referral bonuses only once per invited user, # check if the referee already has succeeded payments. - if self.settings.REFERRAL_ONE_BONUS_PER_REFEREE: + # Use getattr with a safe default (True) to avoid AttributeError if + # running with an older settings schema. + if getattr(self.settings, "REFERRAL_ONE_BONUS_PER_REFEREE", True): try: succeeded_count = await payment_dal.count_user_succeeded_payments( session, referee_user_id, exclude_payment_id=current_payment_db_id diff --git a/bot/utils/message_queue.py b/bot/utils/message_queue.py index a0704b8..40e3c0b 100644 --- a/bot/utils/message_queue.py +++ b/bot/utils/message_queue.py @@ -151,6 +151,76 @@ class MessageQueueManager: ) await queue.add_message(message) + async def send_photo(self, chat_id: int, **kwargs) -> None: + """Queue a send_photo call""" + queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue + message = QueuedMessage( + chat_id=chat_id, + method_name='send_photo', + kwargs=kwargs + ) + await queue.add_message(message) + + async def send_video(self, chat_id: int, **kwargs) -> None: + """Queue a send_video call""" + queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue + message = QueuedMessage( + chat_id=chat_id, + method_name='send_video', + kwargs=kwargs + ) + await queue.add_message(message) + + async def send_animation(self, chat_id: int, **kwargs) -> None: + """Queue a send_animation (GIF) call""" + queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue + message = QueuedMessage( + chat_id=chat_id, + method_name='send_animation', + kwargs=kwargs + ) + await queue.add_message(message) + + async def send_audio(self, chat_id: int, **kwargs) -> None: + """Queue a send_audio call""" + queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue + message = QueuedMessage( + chat_id=chat_id, + method_name='send_audio', + kwargs=kwargs + ) + await queue.add_message(message) + + async def send_voice(self, chat_id: int, **kwargs) -> None: + """Queue a send_voice call""" + queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue + message = QueuedMessage( + chat_id=chat_id, + method_name='send_voice', + kwargs=kwargs + ) + await queue.add_message(message) + + async def send_sticker(self, chat_id: int, **kwargs) -> None: + """Queue a send_sticker call""" + queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue + message = QueuedMessage( + chat_id=chat_id, + method_name='send_sticker', + kwargs=kwargs + ) + await queue.add_message(message) + + async def send_video_note(self, chat_id: int, **kwargs) -> None: + """Queue a send_video_note call""" + queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue + message = QueuedMessage( + chat_id=chat_id, + method_name='send_video_note', + kwargs=kwargs + ) + await queue.add_message(message) + async def answer_callback_query(self, callback_query_id: str, **kwargs) -> None: """Send callback query answer immediately (not rate limited)""" await self.bot.answer_callback_query(callback_query_id, **kwargs) diff --git a/locales/en.json b/locales/en.json index 0a04e97..1644f35 100644 --- a/locales/en.json +++ b/locales/en.json @@ -155,6 +155,7 @@ "admin_broadcast_enter_message": "Enter the broadcast message (HTML supported):", "admin_broadcast_confirm_prompt": "You are about to send the following message:\n\n{message_preview}\n\nConfirm sending?", + "admin_broadcast_confirm_prompt_short": "The message above will be sent. Confirm?", "broadcast_target_all_button": "👥 All", "broadcast_target_active_button": "✅ Active", "broadcast_target_inactive_button": "⌛ Inactive", diff --git a/locales/ru.json b/locales/ru.json index 64f9e70..b723756 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -155,6 +155,7 @@ "admin_broadcast_enter_message": "Введите сообщение для рассылки (HTML поддерживается):", "admin_broadcast_confirm_prompt": "Вы собираетесь отправить следующее сообщение:\n\n{message_preview}\n\nПодтверждаете отправку?", + "admin_broadcast_confirm_prompt_short": "Сообщение выше будет отправлено. Подтвердить отправку?", "broadcast_target_all_button": "👥 Все", "broadcast_target_active_button": "✅ Активные", "broadcast_target_inactive_button": "⌛ Неактивные",