Шаблоны отправки сообщений
This commit is contained in:
818
bot/templates/message_callback.py
Normal file
818
bot/templates/message_callback.py
Normal file
@@ -0,0 +1,818 @@
|
|||||||
|
"""
|
||||||
|
Универсальные шаблоны для отправки сообщений
|
||||||
|
"""
|
||||||
|
from typing import Union, Optional, List, Dict, Callable
|
||||||
|
from pathlib import Path
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
from aiogram import Bot
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import (
|
||||||
|
Message,
|
||||||
|
CallbackQuery,
|
||||||
|
InlineKeyboardMarkup,
|
||||||
|
ReplyKeyboardMarkup,
|
||||||
|
ReplyKeyboardRemove,
|
||||||
|
FSInputFile,
|
||||||
|
InputMediaPhoto,
|
||||||
|
InputMediaVideo,
|
||||||
|
InputMediaAudio,
|
||||||
|
InputMediaDocument,
|
||||||
|
BufferedInputFile
|
||||||
|
)
|
||||||
|
from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder
|
||||||
|
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
||||||
|
from aiogram.enums import ParseMode, ChatAction
|
||||||
|
|
||||||
|
from middleware.loggers import logger
|
||||||
|
from ..utils.state_utils import safe_answer_callback
|
||||||
|
from ..utils.auto_delete import auto_delete_manager
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'msg',
|
||||||
|
'msg_photo',
|
||||||
|
'msg_video',
|
||||||
|
'msg_document',
|
||||||
|
'msg_audio',
|
||||||
|
'msg_voice',
|
||||||
|
'msg_media_group',
|
||||||
|
'edit_msg',
|
||||||
|
'delete_msg',
|
||||||
|
'forward_msg',
|
||||||
|
'send_action',
|
||||||
|
'markups',
|
||||||
|
'MessageTemplate',
|
||||||
|
'batch_send'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MessageTemplate:
|
||||||
|
"""
|
||||||
|
Класс для хранения шаблонов сообщений.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```python
|
||||||
|
# Создание шаблона
|
||||||
|
welcome = MessageTemplate(
|
||||||
|
text="👋 Привет, {name}! Добро пожаловать в {chat}",
|
||||||
|
parse_mode=ParseMode.HTML
|
||||||
|
)
|
||||||
|
|
||||||
|
# Использование
|
||||||
|
await welcome.send(
|
||||||
|
message,
|
||||||
|
name=user.first_name,
|
||||||
|
chat=chat.title
|
||||||
|
)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
parse_mode: Optional[str] = ParseMode.HTML,
|
||||||
|
disable_web_page_preview: bool = False,
|
||||||
|
markup: Optional[Union[InlineKeyboardBuilder, InlineKeyboardMarkup]] = None
|
||||||
|
):
|
||||||
|
self.text = text
|
||||||
|
self.parse_mode = parse_mode
|
||||||
|
self.disable_web_page_preview = disable_web_page_preview
|
||||||
|
self.markup = markup
|
||||||
|
|
||||||
|
def format(self, **kwargs) -> str:
|
||||||
|
"""Форматирует текст с подстановкой переменных"""
|
||||||
|
return self.text.format(**kwargs)
|
||||||
|
|
||||||
|
async def send(
|
||||||
|
self,
|
||||||
|
target: Union[Message, CallbackQuery, int],
|
||||||
|
bot: Optional[Bot] = None,
|
||||||
|
**format_kwargs
|
||||||
|
) -> Optional[Message]:
|
||||||
|
"""
|
||||||
|
Отправляет сообщение по шаблону.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target: Куда отправить (Message, CallbackQuery или chat_id)
|
||||||
|
bot: Экземпляр бота (если target это chat_id)
|
||||||
|
**format_kwargs: Переменные для форматирования
|
||||||
|
"""
|
||||||
|
text = self.format(**format_kwargs)
|
||||||
|
|
||||||
|
if isinstance(target, int):
|
||||||
|
# Отправка по chat_id
|
||||||
|
if not bot:
|
||||||
|
raise ValueError("Bot instance required for chat_id")
|
||||||
|
|
||||||
|
return await bot.send_message(
|
||||||
|
chat_id=target,
|
||||||
|
text=text,
|
||||||
|
parse_mode=self.parse_mode,
|
||||||
|
disable_web_page_preview=self.disable_web_page_preview,
|
||||||
|
reply_markup=markups(self.markup)
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Отправка через Message/CallbackQuery
|
||||||
|
return await msg(
|
||||||
|
target,
|
||||||
|
text=text,
|
||||||
|
parse_mode=self.parse_mode,
|
||||||
|
disable_web_page_preview=self.disable_web_page_preview,
|
||||||
|
markup=self.markup
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ================= MARKUP UTILS =================
|
||||||
|
|
||||||
|
def markups(
|
||||||
|
markup: Union[
|
||||||
|
InlineKeyboardBuilder,
|
||||||
|
ReplyKeyboardBuilder,
|
||||||
|
InlineKeyboardMarkup,
|
||||||
|
ReplyKeyboardMarkup,
|
||||||
|
ReplyKeyboardRemove,
|
||||||
|
None
|
||||||
|
] = None,
|
||||||
|
resize_keyboard: bool = True,
|
||||||
|
one_time_keyboard: bool = False
|
||||||
|
) -> Optional[Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove]]:
|
||||||
|
"""
|
||||||
|
Конвертирует builder в готовый markup.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
markup: Builder или готовая клавиатура
|
||||||
|
resize_keyboard: Автоматический размер (для ReplyKeyboard)
|
||||||
|
one_time_keyboard: Скрыть после нажатия (для ReplyKeyboard)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Готовый markup или None
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>> builder = InlineKeyboardBuilder()
|
||||||
|
>> builder.button(text="Test", callback_data="test")
|
||||||
|
>> keyboard = markups(builder)
|
||||||
|
"""
|
||||||
|
if markup is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(markup, InlineKeyboardBuilder):
|
||||||
|
return markup.as_markup()
|
||||||
|
|
||||||
|
if isinstance(markup, ReplyKeyboardBuilder):
|
||||||
|
return markup.as_markup(
|
||||||
|
resize_keyboard=resize_keyboard,
|
||||||
|
one_time_keyboard=one_time_keyboard
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(markup, (InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove)):
|
||||||
|
return markup
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ================= TEXT MESSAGES =================
|
||||||
|
|
||||||
|
async def msg(
|
||||||
|
update: Union[Message, CallbackQuery],
|
||||||
|
text: str,
|
||||||
|
state: Optional[FSMContext] = None,
|
||||||
|
markup: Union[InlineKeyboardBuilder, InlineKeyboardMarkup, None] = None,
|
||||||
|
parse_mode: Optional[str] = ParseMode.HTML,
|
||||||
|
disable_web_page_preview: bool = False,
|
||||||
|
answer_callback: bool = True,
|
||||||
|
state_clear: bool = False,
|
||||||
|
edit_if_possible: bool = True,
|
||||||
|
delete_previous: bool = False,
|
||||||
|
auto_delete: Optional[int] = None,
|
||||||
|
disable_notification: bool = False,
|
||||||
|
protect_content: bool = False,
|
||||||
|
show_typing: bool = False,
|
||||||
|
log: bool = False
|
||||||
|
) -> Optional[Message]:
|
||||||
|
"""
|
||||||
|
Универсальная отправка/редактирование текстового сообщения.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
update: Message или CallbackQuery
|
||||||
|
text: Текст сообщения
|
||||||
|
state: FSM контекст
|
||||||
|
markup: Клавиатура
|
||||||
|
parse_mode: Режим парсинга (HTML, Markdown, None)
|
||||||
|
disable_web_page_preview: Отключить предпросмотр ссылок
|
||||||
|
answer_callback: Ответить на callback
|
||||||
|
state_clear: Очистить состояние
|
||||||
|
edit_if_possible: Попытаться отредактировать (для callback)
|
||||||
|
delete_previous: Удалить предыдущее сообщение перед отправкой
|
||||||
|
auto_delete: Автоудаление через N секунд
|
||||||
|
disable_notification: Без звука
|
||||||
|
protect_content: Защита от пересылки
|
||||||
|
show_typing: Показать "печатает"
|
||||||
|
log: Логировать отправку
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Отправленное сообщение
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>> # Простая отправка
|
||||||
|
>> await msg(message, "Привет!")
|
||||||
|
|
||||||
|
>> # С клавиатурой и автоудалением
|
||||||
|
>> builder = InlineKeyboardBuilder()
|
||||||
|
>> builder.button(text="OK", callback_data="ok")
|
||||||
|
>> await msg(
|
||||||
|
... callback,
|
||||||
|
... "Сообщение удалится через 10 секунд",
|
||||||
|
... markup=builder,
|
||||||
|
... auto_delete=10
|
||||||
|
... )
|
||||||
|
"""
|
||||||
|
# Получаем message объект
|
||||||
|
message = update.message if isinstance(update, CallbackQuery) else update
|
||||||
|
|
||||||
|
if not message:
|
||||||
|
logger.warning("Невозможно получить message объект", log_type='MESSAGE')
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Показываем typing если нужно
|
||||||
|
if show_typing:
|
||||||
|
await send_action(message, ChatAction.TYPING)
|
||||||
|
|
||||||
|
# Удаляем предыдущее сообщение если нужно
|
||||||
|
if delete_previous:
|
||||||
|
with suppress(TelegramBadRequest, TelegramForbiddenError):
|
||||||
|
await message.delete()
|
||||||
|
|
||||||
|
keyboard = markups(markup)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Попытка редактирования (для callback)
|
||||||
|
if edit_if_possible and isinstance(update, CallbackQuery):
|
||||||
|
sent_message = await message.edit_text(
|
||||||
|
text=text,
|
||||||
|
reply_markup=keyboard,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
disable_web_page_preview=disable_web_page_preview
|
||||||
|
)
|
||||||
|
|
||||||
|
if log:
|
||||||
|
logger.debug(
|
||||||
|
f"Сообщение отредактировано: {message.message_id}",
|
||||||
|
log_type='MESSAGE'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise TelegramBadRequest
|
||||||
|
|
||||||
|
except (TelegramBadRequest, TelegramForbiddenError):
|
||||||
|
# Отправка нового сообщения
|
||||||
|
try:
|
||||||
|
sent_message = await message.answer(
|
||||||
|
text=text,
|
||||||
|
reply_markup=keyboard,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
disable_web_page_preview=disable_web_page_preview,
|
||||||
|
disable_notification=disable_notification,
|
||||||
|
protect_content=protect_content
|
||||||
|
)
|
||||||
|
|
||||||
|
if log:
|
||||||
|
logger.debug(
|
||||||
|
f"Сообщение отправлено: {sent_message.message_id}",
|
||||||
|
log_type='MESSAGE'
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка отправки сообщения: {e}", log_type='MESSAGE')
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Отвечаем на callback
|
||||||
|
if answer_callback and isinstance(update, CallbackQuery):
|
||||||
|
await safe_answer_callback(update)
|
||||||
|
|
||||||
|
# Очищаем состояние
|
||||||
|
if state_clear and state:
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
# Планируем автоудаление
|
||||||
|
if auto_delete and sent_message:
|
||||||
|
await auto_delete_manager.schedule(
|
||||||
|
bot=message.bot,
|
||||||
|
chat_id=sent_message.chat.id,
|
||||||
|
message_id=sent_message.message_id,
|
||||||
|
delay=auto_delete,
|
||||||
|
reason="template_auto_delete"
|
||||||
|
)
|
||||||
|
|
||||||
|
return sent_message
|
||||||
|
|
||||||
|
|
||||||
|
# ================= MEDIA MESSAGES =================
|
||||||
|
|
||||||
|
async def msg_photo(
|
||||||
|
update: Union[Message, CallbackQuery],
|
||||||
|
photo: Union[str, Path, FSInputFile, BufferedInputFile],
|
||||||
|
caption: Optional[str] = None,
|
||||||
|
state: Optional[FSMContext] = None,
|
||||||
|
markup: Union[InlineKeyboardBuilder, InlineKeyboardMarkup, None] = None,
|
||||||
|
parse_mode: Optional[str] = ParseMode.HTML,
|
||||||
|
answer_callback: bool = True,
|
||||||
|
state_clear: bool = False,
|
||||||
|
edit_if_possible: bool = True,
|
||||||
|
auto_delete: Optional[int] = None,
|
||||||
|
has_spoiler: bool = False,
|
||||||
|
log: bool = False
|
||||||
|
) -> Optional[Message]:
|
||||||
|
"""
|
||||||
|
Универсальная отправка/редактирование фото.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
update: Message или CallbackQuery
|
||||||
|
photo: Путь к файлу, FSInputFile или BufferedInputFile
|
||||||
|
caption: Подпись к фото
|
||||||
|
state: FSM контекст
|
||||||
|
markup: Клавиатура
|
||||||
|
parse_mode: Режим парсинга
|
||||||
|
answer_callback: Ответить на callback
|
||||||
|
state_clear: Очистить состояние
|
||||||
|
edit_if_possible: Попытаться отредактировать
|
||||||
|
auto_delete: Автоудаление через N секунд
|
||||||
|
has_spoiler: Спойлер
|
||||||
|
log: Логировать
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Отправленное сообщение
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>> await msg_photo(
|
||||||
|
... message,
|
||||||
|
... photo="assets/welcome.jpg",
|
||||||
|
... caption="Добро пожаловать!",
|
||||||
|
... auto_delete=30
|
||||||
|
... )
|
||||||
|
"""
|
||||||
|
message = update.message if isinstance(update, CallbackQuery) else update
|
||||||
|
|
||||||
|
if not message:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Конвертируем путь в FSInputFile
|
||||||
|
if isinstance(photo, (str, Path)):
|
||||||
|
photo = FSInputFile(photo)
|
||||||
|
|
||||||
|
keyboard = markups(markup)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Попытка редактирования медиа
|
||||||
|
if edit_if_possible and isinstance(update, CallbackQuery):
|
||||||
|
media = InputMediaPhoto(
|
||||||
|
media=photo,
|
||||||
|
caption=caption,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
has_spoiler=has_spoiler
|
||||||
|
)
|
||||||
|
|
||||||
|
await message.edit_media(
|
||||||
|
media=media,
|
||||||
|
reply_markup=keyboard
|
||||||
|
)
|
||||||
|
|
||||||
|
sent_message = message
|
||||||
|
|
||||||
|
if log:
|
||||||
|
logger.debug("Фото отредактировано", log_type='MESSAGE')
|
||||||
|
else:
|
||||||
|
raise TelegramBadRequest
|
||||||
|
|
||||||
|
except (TelegramBadRequest, TelegramForbiddenError):
|
||||||
|
# Отправка нового фото
|
||||||
|
try:
|
||||||
|
sent_message = await message.answer_photo(
|
||||||
|
photo=photo,
|
||||||
|
caption=caption,
|
||||||
|
reply_markup=keyboard,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
has_spoiler=has_spoiler
|
||||||
|
)
|
||||||
|
|
||||||
|
if log:
|
||||||
|
logger.debug("Фото отправлено", log_type='MESSAGE')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка отправки фото: {e}", log_type='MESSAGE')
|
||||||
|
return None
|
||||||
|
|
||||||
|
if answer_callback and isinstance(update, CallbackQuery):
|
||||||
|
await safe_answer_callback(update)
|
||||||
|
|
||||||
|
if state_clear and state:
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
if auto_delete and sent_message:
|
||||||
|
await auto_delete_manager.schedule(
|
||||||
|
bot=message.bot,
|
||||||
|
chat_id=sent_message.chat.id,
|
||||||
|
message_id=sent_message.message_id,
|
||||||
|
delay=auto_delete
|
||||||
|
)
|
||||||
|
|
||||||
|
return sent_message
|
||||||
|
|
||||||
|
|
||||||
|
async def msg_video(
|
||||||
|
update: Union[Message, CallbackQuery],
|
||||||
|
video: Union[str, Path, FSInputFile, BufferedInputFile],
|
||||||
|
caption: Optional[str] = None,
|
||||||
|
**kwargs
|
||||||
|
) -> Optional[Message]:
|
||||||
|
"""
|
||||||
|
Отправка видео.
|
||||||
|
|
||||||
|
Поддерживает те же параметры что и msg_photo.
|
||||||
|
"""
|
||||||
|
message = update.message if isinstance(update, CallbackQuery) else update
|
||||||
|
|
||||||
|
if not message:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(video, (str, Path)):
|
||||||
|
video = FSInputFile(video)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sent = await message.answer_video(
|
||||||
|
video=video,
|
||||||
|
caption=caption,
|
||||||
|
parse_mode=kwargs.get('parse_mode', ParseMode.HTML),
|
||||||
|
reply_markup=markups(kwargs.get('markup'))
|
||||||
|
)
|
||||||
|
|
||||||
|
if kwargs.get('answer_callback') and isinstance(update, CallbackQuery):
|
||||||
|
await safe_answer_callback(update)
|
||||||
|
|
||||||
|
if kwargs.get('state_clear') and kwargs.get('state'):
|
||||||
|
await kwargs['state'].clear()
|
||||||
|
|
||||||
|
if kwargs.get('auto_delete'):
|
||||||
|
await auto_delete_manager.schedule(
|
||||||
|
bot=message.bot,
|
||||||
|
chat_id=sent.chat.id,
|
||||||
|
message_id=sent.message_id,
|
||||||
|
delay=kwargs['auto_delete']
|
||||||
|
)
|
||||||
|
|
||||||
|
return sent
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка отправки видео: {e}", log_type='MESSAGE')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def msg_document(
|
||||||
|
update: Union[Message, CallbackQuery],
|
||||||
|
document: Union[str, Path, FSInputFile, BufferedInputFile],
|
||||||
|
caption: Optional[str] = None,
|
||||||
|
filename: Optional[str] = None,
|
||||||
|
**kwargs
|
||||||
|
) -> Optional[Message]:
|
||||||
|
"""
|
||||||
|
Отправка документа.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: Имя файла для отображения
|
||||||
|
:param filename:
|
||||||
|
:param caption:
|
||||||
|
:param document:
|
||||||
|
:param update:
|
||||||
|
"""
|
||||||
|
message = update.message if isinstance(update, CallbackQuery) else update
|
||||||
|
|
||||||
|
if not message:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(document, (str, Path)):
|
||||||
|
document = FSInputFile(document, filename=filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sent = await message.answer_document(
|
||||||
|
document=document,
|
||||||
|
caption=caption,
|
||||||
|
parse_mode=kwargs.get('parse_mode', ParseMode.HTML),
|
||||||
|
reply_markup=markups(kwargs.get('markup'))
|
||||||
|
)
|
||||||
|
|
||||||
|
if kwargs.get('answer_callback') and isinstance(update, CallbackQuery):
|
||||||
|
await safe_answer_callback(update)
|
||||||
|
|
||||||
|
if kwargs.get('state_clear') and kwargs.get('state'):
|
||||||
|
await kwargs['state'].clear()
|
||||||
|
|
||||||
|
if kwargs.get('auto_delete'):
|
||||||
|
await auto_delete_manager.schedule(
|
||||||
|
bot=message.bot,
|
||||||
|
chat_id=sent.chat.id,
|
||||||
|
message_id=sent.message_id,
|
||||||
|
delay=kwargs['auto_delete']
|
||||||
|
)
|
||||||
|
|
||||||
|
return sent
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка отправки документа: {e}", log_type='MESSAGE')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def msg_audio(
|
||||||
|
update: Union[Message, CallbackQuery],
|
||||||
|
audio: Union[str, Path, FSInputFile, BufferedInputFile],
|
||||||
|
caption: Optional[str] = None,
|
||||||
|
**kwargs
|
||||||
|
) -> Optional[Message]:
|
||||||
|
"""Отправка аудио"""
|
||||||
|
message = update.message if isinstance(update, CallbackQuery) else update
|
||||||
|
|
||||||
|
if not message:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(audio, (str, Path)):
|
||||||
|
audio = FSInputFile(audio)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await message.answer_audio(
|
||||||
|
audio=audio,
|
||||||
|
caption=caption,
|
||||||
|
parse_mode=kwargs.get('parse_mode', ParseMode.HTML),
|
||||||
|
reply_markup=markups(kwargs.get('markup'))
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка отправки аудио: {e}", log_type='MESSAGE')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def msg_voice(
|
||||||
|
update: Union[Message, CallbackQuery],
|
||||||
|
voice: Union[str, Path, FSInputFile, BufferedInputFile],
|
||||||
|
caption: Optional[str] = None,
|
||||||
|
**kwargs
|
||||||
|
) -> Optional[Message]:
|
||||||
|
"""Отправка голосового сообщения"""
|
||||||
|
message = update.message if isinstance(update, CallbackQuery) else update
|
||||||
|
|
||||||
|
if not message:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(voice, (str, Path)):
|
||||||
|
voice = FSInputFile(voice)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await message.answer_voice(
|
||||||
|
voice=voice,
|
||||||
|
caption=caption,
|
||||||
|
parse_mode=kwargs.get('parse_mode', ParseMode.HTML),
|
||||||
|
reply_markup=markups(kwargs.get('markup'))
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка отправки голосового: {e}", log_type='MESSAGE')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def msg_media_group(
|
||||||
|
message: Message,
|
||||||
|
media: List[Union[InputMediaPhoto, InputMediaVideo, InputMediaAudio, InputMediaDocument]],
|
||||||
|
caption: Optional[str] = None
|
||||||
|
) -> Optional[List[Message]]:
|
||||||
|
"""
|
||||||
|
Отправка media group (альбом).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Объект сообщения
|
||||||
|
media: Список медиа
|
||||||
|
caption: Подпись (будет добавлена к первому элементу)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Список отправленных сообщений
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>> media = [
|
||||||
|
... InputMediaPhoto(media=FSInputFile("photo1.jpg")),
|
||||||
|
... InputMediaPhoto(media=FSInputFile("photo2.jpg")),
|
||||||
|
... InputMediaVideo(media=FSInputFile("video.mp4"))
|
||||||
|
... ]
|
||||||
|
>> await msg_media_group(message, media, caption="Альбом")
|
||||||
|
"""
|
||||||
|
if not media:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Добавляем подпись к первому элементу
|
||||||
|
if caption and media:
|
||||||
|
media[0].caption = caption
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await message.answer_media_group(media=media)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка отправки media group: {e}", log_type='MESSAGE')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ================= MESSAGE ACTIONS =================
|
||||||
|
|
||||||
|
async def edit_msg(
|
||||||
|
message: Message,
|
||||||
|
text: Optional[str] = None,
|
||||||
|
caption: Optional[str] = None,
|
||||||
|
markup: Optional[InlineKeyboardMarkup] = None,
|
||||||
|
parse_mode: Optional[str] = ParseMode.HTML
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Безопасное редактирование сообщения.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если успешно отредактировано
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if text:
|
||||||
|
await message.edit_text(
|
||||||
|
text=text,
|
||||||
|
reply_markup=markup,
|
||||||
|
parse_mode=parse_mode
|
||||||
|
)
|
||||||
|
elif caption:
|
||||||
|
await message.edit_caption(
|
||||||
|
caption=caption,
|
||||||
|
reply_markup=markup,
|
||||||
|
parse_mode=parse_mode
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await message.edit_reply_markup(reply_markup=markup)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except (TelegramBadRequest, TelegramForbiddenError) as e:
|
||||||
|
logger.debug(f"Не удалось отредактировать сообщение: {e}", log_type='MESSAGE')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_msg(
|
||||||
|
message: Message,
|
||||||
|
delay: Optional[int] = None
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Безопасное удаление сообщения.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Сообщение для удаления
|
||||||
|
delay: Задержка перед удалением (секунды)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если успешно удалено
|
||||||
|
"""
|
||||||
|
if delay:
|
||||||
|
await auto_delete_manager.schedule(
|
||||||
|
bot=message.bot,
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
message_id=message.message_id,
|
||||||
|
delay=delay
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
await message.delete()
|
||||||
|
return True
|
||||||
|
except (TelegramBadRequest, TelegramForbiddenError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def forward_msg(
|
||||||
|
message: Message,
|
||||||
|
to_chat_id: int,
|
||||||
|
disable_notification: bool = False,
|
||||||
|
protect_content: bool = False
|
||||||
|
) -> Optional[Message]:
|
||||||
|
"""
|
||||||
|
Пересылка сообщения.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Исходное сообщение
|
||||||
|
to_chat_id: ID чата куда переслать
|
||||||
|
disable_notification: Без звука
|
||||||
|
protect_content: Защита от пересылки
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Пересланное сообщение
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return await message.forward(
|
||||||
|
chat_id=to_chat_id,
|
||||||
|
disable_notification=disable_notification,
|
||||||
|
protect_content=protect_content
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка пересылки сообщения: {e}", log_type='MESSAGE')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def send_action(
|
||||||
|
message: Message,
|
||||||
|
action: ChatAction = ChatAction.TYPING
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Отправка chat action (печатает, загружает фото и т.д.).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Объект сообщения
|
||||||
|
action: Тип действия
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если успешно
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>> await send_action(message, ChatAction.TYPING)
|
||||||
|
>> await send_action(message, ChatAction.UPLOAD_PHOTO)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await message.bot.send_chat_action(
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
action=action
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ================= BATCH SENDING =================
|
||||||
|
|
||||||
|
async def batch_send(
|
||||||
|
bot: Bot,
|
||||||
|
chat_ids: List[int],
|
||||||
|
text: str,
|
||||||
|
markup: Optional[InlineKeyboardMarkup] = None,
|
||||||
|
parse_mode: Optional[str] = ParseMode.HTML,
|
||||||
|
disable_notification: bool = False,
|
||||||
|
on_success: Optional[Callable] = None,
|
||||||
|
on_error: Optional[Callable] = None
|
||||||
|
) -> Dict[str, int]:
|
||||||
|
"""
|
||||||
|
Массовая рассылка сообщений.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot: Экземпляр бота
|
||||||
|
chat_ids: Список ID чатов
|
||||||
|
text: Текст сообщения
|
||||||
|
markup: Клавиатура
|
||||||
|
parse_mode: Режим парсинга
|
||||||
|
disable_notification: Без звука
|
||||||
|
on_success: Callback при успехе (chat_id)
|
||||||
|
on_error: Callback при ошибке (chat_id, error)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict со статистикой: {'success': N, 'failed': N}
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>> stats = await batch_send(
|
||||||
|
... bot,
|
||||||
|
... [123, 456, 789],
|
||||||
|
... "Важное объявление!"
|
||||||
|
... )
|
||||||
|
>> print(f"Отправлено: {stats['success']}")
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
success_count = 0
|
||||||
|
failed_count = 0
|
||||||
|
|
||||||
|
for chat_id in chat_ids:
|
||||||
|
try:
|
||||||
|
await bot.send_message(
|
||||||
|
chat_id=chat_id,
|
||||||
|
text=text,
|
||||||
|
reply_markup=markup,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
disable_notification=disable_notification
|
||||||
|
)
|
||||||
|
|
||||||
|
success_count += 1
|
||||||
|
|
||||||
|
if on_success:
|
||||||
|
await on_success(chat_id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
failed_count += 1
|
||||||
|
|
||||||
|
if on_error:
|
||||||
|
await on_error(chat_id, e)
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
f"Не удалось отправить сообщение в чат {chat_id}: {e}",
|
||||||
|
log_type='BATCH'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Небольшая задержка чтобы избежать rate limit
|
||||||
|
await asyncio.sleep(0.05)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Рассылка завершена: успешно={success_count}, ошибок={failed_count}",
|
||||||
|
log_type='BATCH'
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': success_count,
|
||||||
|
'failed': failed_count
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user