Telegram Боты на Aiogram 3.x: Отправка и обработка медиа сообщений
Приветствую вас! В очередной раз хочу поблагодарить за вашу поддержку и интерес к моему творчеству. Приятно осознавать, что информация, которой я делюсь, полезна многим из вас, а в некоторых случаях вызывает даже ажиотаж.
Сегодня мы вновь погрузимся в мир Telegram ботов на Aiogram 3.x и рассмотрим отправку и обработку медиа сообщений.
Если вы ещё не ознакомились с моей предыдущей статьей, где я подробно раскрывал основы работы с Message хендлером, настоятельно рекомендую это сделать. В той статье я рассмотрел такие важные аспекты, как:
Копирование сообщений (метод copy)
Замена текста в сообщении (метод edit_text)
Замена клавиатуры в сообщениях (метод edit_markup)
Отправка сообщений методами forward, answer, reply и send_message
Эти знания крайне важны для понимания работы с медиа сообщениями, так как для них используются схожие методы с небольшими отличиями, зависящими от типа сообщения. Например, для работы с фото используются методы answer_photo, reply_photo, send_photo, а для документов — answer_document, reply_document, send_document и так далее.
Также важно помнить, что правила, применяемые к текстовым сообщениям, распространяются и на медиа сообщения, за исключением того, что в медиа отсутствует объект текста (message.text). Там, где это возможно (например, при добавлении подписи к фото или видео), используется элемент caption
Методы для редактирования текста в медиа заменяются на методы для редактирования подписей (edit_caption), а для замены медиа контента используется метод edit_media. Замена клавиатур для всех поддерживающих их медиа сообщений ничем не отличается от замены клавиатур в текстовых сообщениях.
Сегодня мы детально рассмотрим различные типы медиа контента и их особенности:
Фото
Видео
Видео сообщения
Аудио
Аудио сообщения
Отправка медиагруппы
Отправка анимаций, стикеров и прочего
Разберем имитацию действий
Покажу несколько трюков, которые позволят вам обойти некоторые ограничения Telegram, да и в целом сохранят вам много времени, если надумаете ботами серьезно заниматься
Чтобы максимально понять работу с объектом message и особенностями медиа сообщений, настоятельно рекомендую изучить мою прошлую статью, прежде чем углубляться в сегодняшний материал.
Приступим к изучению медиа в Telegram ботах на Aiogram 3.x!
Отправка файлов
Независимо от типа медиафайлов отправка может быть осуществлена следующим способом:
Физическая отправка файлов (байтов) через FSInputFile
Через ID файла (неважно это фото, видео, аудио и так далее)
Через URL ссылку (формат поддерживается для большинства типов файлов, важно чтоб ссылка вела на меда файл, а не просто на страницу где этот файл лежит).
Давайте приступим к разбору. Сейчас мы будем рассматривать тему отправки файлов и я, чтобы было проще демонстрировать вам этот процесс, подготовил папку с именем all_media
в корне проекта бота. В него я поместил следующие типы файлов:
Несколько обычных видео
Квадратное видео небольших размеров (для отправки «кругляша»)
Пару небольших аудио файлов
Несколько фотографий разного размера
Также я подготовил несколько ссылок на медиаконтент (на фото). Настоятельно рекомендую и вам сейчас сделать паузу и подготовить медиа для тестирования отправок. Как минимум это будет интереснее.
В корневом файле create_bot.py импортирую os
и прописываю путь к файлу таким образом:
all_media_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'all_media')
Далее просто буду импортировать этот путь (переменную) в хендлерах бота.
Я настоятельно рекомендую вам также указывать путь к папкам и файлам таким образом. Данная структура позволит вам не беспокоиться о доступах к файлам (путях) когда вы будете перевозить своего бота с Windows на Ubuntu, например (пути к файлам по-разному записываются).
Далее, для указания конкретного файла можно будет использовать такую запись:
photo_file = os.path.join(all_media_dir, 'photo.jpg')
Согласитесь, это удобно.
1. Физическая отправка файлов (байтов) через FSInputFile
Этот метод позволяет загружать файлы с вашего локального устройства (сервер, локальный компьютер). Для использования этого метода сначала нужно импортировать:
from aiogram.types import Message, FSInputFile
Давайте теперь отправим аудио сообщение и просто аудио. Для этого я предлагаю завязывать будущую отправку файлов на специальные команды: /send_audio, /send_voice и так далее.
Пишем хендлер для отправки аудио сообщения:
@start_router.message(Command('send_audio'))
async def cmd_start(message: Message, state: FSMContext):
audio_file = FSInputFile(path=os.path.join(all_media_dir, 'new_message_tone.mp3'))
await message.answer_audio(audio=audio_file)
Если вы не хотите чтоб бот отправлял настоящее имя файла, то передавайте аргумент filename в FSInputFile с указанием имени файла.
Из того, на что стоит обратить внимание — это то, как мы передали путь к файлу. По этому же принципу можно передавать путь к любому медиа файлу (документ, фото, видео, анимация и прочее).
Далее, как я и говорил выше, метод для ответа на команду не особо отличается от аналогичного у текста. Единственное, что обязательно нужно передать путь к аудио файлу, ссылку на него (именно прямая ссылка на загрузку) или ID файла (об этом чуть дальше).
Вместе с аудио сообщением можно передать клавиатуру (инлайн или текстовую) и можно добавить описание (обычное или отформатированное) через caption. Почему с текстовыми клавиатурами нужно быть осторожным вы узнаете далее, а пока пример:
@start_router.message(Command('send_audio'))
async def cmd_start(message: Message, state: FSMContext):
audio_file = FSInputFile(path=os.path.join(all_media_dir, 'new_message_tone.mp3'))
msg_id = await message.answer_audio(audio=audio_file, reply_markup=main_kb(message.from_user.id),
caption='Моя отформатированная подпись к файлу')
print(msg_id.message_id)
На примере я вывел print(msg_id.message_id)
, но это не единственное, что нас может заинтересовать в этом объекте. После отправки файла мы можем перехватить его file_id. Это очень полезно и важно.
Я вывел в консоль msg_id.audio.file_id
и вот что получил:
CQACAgIAAxkDAAIBu2ZsgniQlznR1VxJqbHB2pwjKuq2AALmSAACGKhoS96YeMoflmQgNQQ
Надеюсь, что вы поняли принцип. Если боту будут отправляться медиа сообщения (фото, видео, аудио и так далее) или он их отправит сам — у вас будет возможность перехватить file_id этих медиа файлов.
По сути, ID файла это некая ссылка на серверах Telegram, благодаря которой у бота, который является владельцем этого файла, появляется возможность повторно отправлять файлы, но уже не физически, а при помощи отправки ссылки на этот файл.
Давайте попробуем выполнить отправку этого аудио через идентификатор.
@start_router.message(Command('send_audio'))
async def cmd_start(message: Message, state: FSMContext):
# audio_file = FSInputFile(path=os.path.join(all_media_dir, 'new_message_tone.mp3'))
audio_id = 'CQACAgIAAxkDAAIBu2ZsgniQlznR1VxJqbHB2pwjKuq2AALmSAACGKhoS96YeMoflmQgNQQ'
msg_id = await message.answer_audio(audio=audio_id, reply_markup=main_kb(message.from_user.id),
caption='Моя отформатированная подпись к файлу')
Смотрим и видим, что бот смог отправить аудио.
Кроме очевидного преимущества в экономии дискового пространства, такой подход позволяет отправлять файлы, которые весят сотни мегабайт буквально за миллисекунды. Например, вам прислали в Telegram большой файл, пусть это будет целый фильм. Он вам понравился и вы решаете переслать это видео другому человеку.
На пересылку вы потратите секунды, при этом у вас не будет необходимости скачивать файл. По этому же принципу работает отправка файлов через их идентификаторы (ID).
Вот как забирать идентификаторы для разных типов контента:
Аудио —
message.audio.file_id
Документ —
message.document.file_id
Видео —
message.video.file_id
И так далее. Единственное отличие будет у фотографий, и сейчас мы на этом заострим внимание.
Отправка и обработка фото
Наверняка вы знаете, что фотографии в Telegram можно отправлять с сжатием и без сжатия. Кроме того, фотографии имеют свои превью (маленького размера). За счет всех этих особенностей у каждого файла фотографии всегда несколько идентификаторов, и чтобы забрать фото наилучшего качества, нам необходимо сделать следующее:
msg_id.photo[-1].file_id
То есть отправляя одно фото, вы как бы отправляете целый список фотографий, а фото самого лучшего качества в этом списке будет стоять последним (индекс -1). Давайте посмотрим на примере:
@start_router.message(Command('send_photo'))
async def cmd_start(message: Message, state: FSMContext):
photo_file = FSInputFile(path=os.path.join(all_media_dir, 'photo_2024-06-14_20-13-40.jpg'))
msg_id = await message.answer_photo(photo=photo_file, reply_markup=main_kb(message.from_user.id),
caption='Моя отформатированная подпись к фото')
print(msg_id.photo[-1].file_id)
Мы видим, что фото отправилось, а в консоли я получил идентификатор фото:
AgACAgIAAxkDAAIBwGZshp7dSSQi0VKxt6RKJgseyMHxAALM4DEbGKhoS4tvyaZWY29DAQADAgADeAADNQQ
Копируем и пробуем отправить уже через идентификатор.
@start_router.message(Command('send_photo'))
async def cmd_start(message: Message, state: FSMContext):
# photo_file = FSInputFile(path=os.path.join(all_media_dir, 'photo_2024-06-14_20-13-40.jpg'))
photo_id = 'AgACAgIAAxkDAAIBwGZshp7dSSQi0VKxt6RKJgseyMHxAALM4DEbGKhoS4tvyaZWY29DAQADAgADeAADNQQ'
msg_id = await message.answer_photo(photo=photo_id, reply_markup=main_kb(message.from_user.id),
caption='Моя отформатированная подпись к фото')
print(msg_id.photo[-1].file_id)
Все получилось. Отлично.
Теперь давайте отправим фотографию по ссылке (также по ссылке можно отправлять и другой тип контента, но важно, чтобы ссылка вела именно на файл, а не просто на страницу, где хранится медиа-контент).
@start_router.message(Command('send_photo'))
async def cmd_start(message: Message, state: FSMContext):
# photo_file = FSInputFile(path=os.path.join(all_media_dir, 'photo_2024-06-14_20-13-40.jpg'))
# photo_id = 'AgACAgIAAxkDAAIBwGZshp7dSSQi0VKxt6RKJgseyMHxAALM4DEbGKhoS4tvyaZWY29DAQADAgADeAADNQQ'
photo_url = 'https://indirimlerce.com/wp-content/uploads/2023/02/phyton-ile-neler-yapilabilir.jpg'
msg_id = await message.answer_photo(photo=photo_url, reply_markup=main_kb(message.from_user.id),
caption='Моя отформатированная подпись к фото')
print(msg_id.photo[-1].file_id)
Все получилось. Выбирайте формат отправки файла, который вам удобен. Продолжим.
Теперь давайте рассмотрим методы edit_caption
(перезапись описания к медиа) и edit_media
, но прежде чем мы продолжим, хочу обратить внимание на очень важный момент. Редактирование медиа и описания (edit_caption
) будет невозможным, если вы привязали к своему медиа-сообщению текстовую клавиатуру.
То есть, допустим, вы отправляете видео с подписью и текстовой клавиатурой. Проблем никаких нет, все отправляется, но при попытке вызвать метод edit_caption
или edit_media
вы получите ошибку «Невозможно изменить сообщение».
В данном контексте возможны следующие варианты решения:
Менять описание, клавиатуру или медиа, когда вы не вызывали изначально никакой клавиатуры или вызывали с медиа инлайн-клавиатуру (тогда проблем не будет).
Сохранять объект отправленного сообщения с медиа (достаем ID медиа и описание, копируем, удаляем медиа и делаем повторную отправку).
Попробуем:
@start_router.message(Command('send_video'))
async def cmd_start(message: Message, state: FSMContext):
video_file = FSInputFile(path=os.path.join(all_media_dir, 'IMG_3998.MP4'))
msg = await message.answer_video(video=video_file, reply_markup=main_kb(message.from_user.id),
caption='Моя отформатированная подпись к файлу')
await asyncio.sleep(2)
await msg.edit_caption(caption='Новое описание к моему видео.')
Получаем ошибку:
aiogram.exceptions.TelegramBadRequest: Telegram server says - Bad Request: message can't be edited
Просто уберем клавиатуру и повторим попытку:
@start_router.message(Command('send_video'))
async def cmd_start(message: Message, state: FSMContext):
video_file = FSInputFile(path=os.path.join(all_media_dir, 'IMG_3998.MP4'))
msg = await message.answer_video(video=video_file, caption='Моя отформатированная подпись к файлу')
await asyncio.sleep(2)
await msg.edit_caption(caption='Новое описание к моему видео.')
И все прекрасно отрабатывает (с инлайн клавиатурой так же все будет корректно работать).
Теперь покажу, как обойти проблему с невозможностью изменить описание при наличии текстовой клавиатуры:
@start_router.message(Command('send_video'))
async def cmd_start(message: Message, state: FSMContext):
video_file = FSInputFile(path=os.path.join(all_media_dir, 'IMG_3998.MP4'))
msg = await message.answer_video(video=video_file, reply_markup=main_kb(message.from_user.id),
caption='Моя отформатированная подпись к файлу')
await asyncio.sleep(2)
await message.answer_video(video=msg.video.file_id, caption='Новое описание к тому же видосу',
reply_markup=main_kb(message.from_user.id))
await msg.delete()
Все получилось.
Обратите внимание: метод edit_caption
также перезаписывает клавиатуру. То есть, если у вас была инлайн-клавиатура с медиа-сообщением, то в случае, если вы не передадите в edit_caption
reply_markup
, клавиатура удалится.
Метод edit_media
Этот метод принимает один обязательный аргумент: media
. Там должен быть один из классов: InputMediaAnimation
, InputMediaDocument
, InputMediaAudio
, InputMediaPhoto
или InputMediaVideo
. Импортируются все они из aiogram.types
. Тут есть интересный момент: вы можете без проблем заменять один тип файла на другой. К примеру, было фото с описанием, а вместо него будет видео с описанием. Все зависит от вашей фантазии.
Записывается по такой конструкции на примере видео:
new_video_file = FSInputFile(path=os.path.join(all_media_dir, 'IMG_4044.MP4'))
media = InputMediaVideo(media=new_video_file, caption='Новое видео и у него новое описание.')
Обратите внимание, что внутри можно передать описание, но тут нужно быть внимательным. Описание передается как аргумент к InputMediaVideo
.
Давайте посмотрим на конкретном примере, чтобы было понятно:
@start_router.message(Command('send_video'))
async def cmd_start(message: Message, state: FSMContext):
video_file = FSInputFile(path=os.path.join(all_media_dir, 'IMG_3998.MP4'))
msg_1 = await message.answer_video(video=video_file,
caption='Моя отформатированная подпись к файлу')
await asyncio.sleep(2)
await msg_1.edit_caption(caption='Новое описание к видео 1')
await asyncio.sleep(2)
new_video_file = FSInputFile(path=os.path.join(all_media_dir, 'IMG_4044.MP4'))
await msg_1.edit_media(media=InputMediaVideo(media=new_video_file, caption='Новое видео и у него новое описание.'),
reply_markup=inline_kb())
Тут мы скомбинировали answer_video
, edit_caption
и edit_media
. Советую вам сохранить этот код где-то, например, добавив статью в закладки. Такую информацию в контексте aiogram 3 вы вряд ли где-то найдете.
Надеюсь, что с этим понятно. В случае вопросов — пишите в комментариях. Подскажу.
Пример отправки голосового и видео сообщения:
@start_router.message(Command('send_voice'))
async def cmd_start(message: Message, state: FSMContext):
await message.answer_voice(voice=FSInputFile(
path=os.path.join(all_media_dir, 'krasivyie-snyi-nevinnost-zvezdnyiy-fon-zvukovyie-effektyi-43378.mp3')))
@start_router.message(Command('send_video_note'))
async def cmd_start(message: Message, state: FSMContext):
await message.answer_video_note(video_note=FSInputFile(path=os.path.join(all_media_dir, 'IMG_4044.MP4')))
Для того чтобы видео сообщение отправилось кругляшом — оно должно быть изначально квадратным, но, из личного опыта скажу, если хотите имитацию делать видео-сообщений — лучше их записать отдельно и через админку сохранить.
Вот небольшой пример. Без админки, правда, но вы поймете в чем смысл.
@start_router.message(F.video_note)
async def cmd_start(message: Message, state: FSMContext):
print(message.video_note.file_id)
Понятное дело, что тут должен быть FSM и подключенная база данных, но мы и до этого дойдем. Сейчас просто «поймаем» через магический фильтр F.video_note
видео-сообщение и выведем его идентификатор в консоль.
Получился такой ID:
DQACAgIAAxkBAAICKGZspGExG2ZPTe6cxgrHFgl9V8caAALvSgACGKhoS8XEd0xdU4AKNQQ
Отправляем:
@start_router.message(Command('send_video_note'))
async def cmd_start(message: Message, state: FSMContext):
await message.answer_video_note(video_note="DQACAgIAAxkBAAICKGZspGExG2ZPTe6cxgrHFgl9V8caAALvSgACGKhoS8XEd0xdU4AKNQQ")
Видим, что видео-сообщение успешно отправлено.
Отправка медиа группы
Медиа группа в Telegram API — это самый странный и, как по мне, самый недоработанный элемент. Несмотря на то, что медиагруппа это вроде как отдельный объект, воспринимается она как совокупность отдельных объектов.
Никакого хендлера и фильтра нет для того, чтобы вычислить конкретно медиагруппу. Получается, что когда появляется необходимость обработать файлы из медиагруппы, приходится отдельно обрабатывать фото и видео (да, медиагруппа может состоять только из фото, видео, фото + видео). Это полбеды, но aiogram 3 асинхронный. Вот и получается дурдом при обработке.
Недавно я писал проект. Задача была переносить посты из Telegram канала в Одноклассники, а там каждый пост — это медиагруппа и всегда видео + фото + текст. Будет интересно, как-то расскажу, как задачу закрыл.
Теперь, когда я поделился своей болью, покажу, как отправить медиагруппу. Тут все четко будет. Для отправки медиагруппы нам нужно будет импортировать из aiogram.types
InputMediaVideo
(с ним вы уже умеете работать) и InputMediaPhoto
.
Далее нам необходимо отдельно сформировать список из медиаобъектов. В список могут попасть до 10 медиа. Сейчас я соберу свой список, продемонстрирую код и результат, а после обсудим.
@start_router.message(Command('send_media_group'))
async def cmd_start(message: Message, state: FSMContext):
photo_1 = InputMediaPhoto(type='photo',
media=FSInputFile(path=os.path.join(all_media_dir, 'photo_2024-06-05_09-32-15.jpg')),
caption='Описание ко ВСЕЙ медиагруппе')
photo_2 = InputMediaPhoto(type='photo',
media=FSInputFile(path=os.path.join(all_media_dir, 'photo_2024-06-14_20-13-40.jpg')))
photo_3 = InputMediaPhoto(type='photo',
media=FSInputFile(path=os.path.join(all_media_dir, 'photo_2024-06-05_09-32-15.jpg')))
video_1 = InputMediaVideo(type='video',
media=FSInputFile(path=os.path.join(all_media_dir, 'IMG_4045.MP4')))
photo_4 = InputMediaPhoto(type='photo',
media=FSInputFile(path=os.path.join(all_media_dir, 'photo_2024-06-14_20-16-27.jpg')))
video_2 = InputMediaVideo(type='video',
media=FSInputFile(path=os.path.join(all_media_dir, 'IMG_3978.MP4')))
media = [photo_1, photo_2, photo_3, video_1, photo_4, video_2]
await message.answer_media_group(media=media)
Особенности, на которые нужно обратить внимание:
К медиагруппе нельзя привязать клавиатуру никакую (разве что вы привяжете текстовую клавиатуру к какому-то сообщению до медиагруппы).
Если оставить комментарий к первому объекту медиагруппы, то он будет общим. Но если вы будете привязывать описание к нескольким элементам медиагруппы, то для его прочтения нужно будет открывать каждый элемент и читать.
Медиагруппа может состоять только из фото, видео, фото + видео.
Имитация действий бота
Мы уже разбирали имитацию набора текста, но ещё есть:
Имитация записи голосового сообщения
Имитация записи видео сообщения
Имитация отправки видео
Для имитации действий ботом нам необходимо импортировать:
from aiogram.utils.chat_action import ChatActionSender
Общий принцип действия следующий. Работая с ChatActionSender
, мы используем асинхронный менеджер with
. В ChatActionSender
мы передаем объект бота, чат, в котором он должен начать имитацию действий, и тип имитации, который тот должен сделать.
Другой вариант — это использование специальных методов из ChatActionSender
. В таком случае не будет необходимости передавать параметр action
.
typing
— набор текстаupload_video
— загрузка видеоrecord_video_note
— запись видео-сообщенияrecord_voice
— запись голосового сообщения
Если вы пользуетесь PyCharm, то можете указать точку после ChatActionSender
и посмотреть, какие действия имеются.
Далее, если бот может выполнить действие очень быстро (например, отправка сообщения с видео через file_id или отправка чистого текста), мы можем поставить асинхронную паузу в 2–3 секунды, чтобы пользователь успел увидеть имитацию.
Но бывают случаи, когда необходимо делать отправку крупного медиафайла (видео, например) напрямую с локальной машины. В этом случае данная имитация становится незаменимой. Пользователь не думает, что бот завис, а видит, что сейчас происходит загрузка видео, запись голосового сообщения, запись видео-сообщения и прочее.
Выглядит, на самом деле, интересно.
Давайте добавим имитацию в хендлеры по отправке аудио и видео сообщений.
@start_router.message(Command('send_voice'))
async def cmd_start(message: Message, state: FSMContext):
async with ChatActionSender.record_voice(bot=bot, chat_id=message.from_user.id):
await asyncio.sleep(3)
await message.answer_voice(voice=FSInputFile(
path=os.path.join(all_media_dir, 'krasivyie-snyi-nevinnost-zvezdnyiy-fon-zvukovyie-effektyi-43378.mp3')))
@start_router.message(Command('send_video_note'))
async def cmd_start(message: Message, state: FSMContext):
async with ChatActionSender.record_video_note(bot=bot, chat_id=message.from_user.id):
await asyncio.sleep(3)
await message.answer_video_note(
video_note="DQACAgIAAxkBAAICKGZspGExG2ZPTe6cxgrHFgl9V8caAALvSgACGKhoS8XEd0xdU4AKNQQ")
Имитация записи голосового сообщения
Имитация записи видео-сообщения
Заключение
Друзья, я понимаю, что информации в этой и прошлой статье достаточно много. Вам не обязательно все это в голове держать. Самое главное — общее понимание принципов, а все частности придут с опытом.
Кроме того, со своего опыта скажу, самое важное — это не просто поглощение контента, а практика. На данный момент, если вы меня читаете, то я раскрыл все базовые темы взаимодействия с ботом, за исключением FSM, базы данных (это я вмещу в одну статью), ну и, может, еще парочку тем, но все остальное уже есть. Просто повторяйте за мной, и у вас все получится.
На этом у меня все. Надеюсь на ваш позитивный отклик.