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 вы получите ошибку «Невозможно изменить сообщение».

В данном контексте возможны следующие варианты решения:

  1. Менять описание, клавиатуру или медиа, когда вы не вызывали изначально никакой клавиатуры или вызывали с медиа инлайн-клавиатуру (тогда проблем не будет).

  2. Сохранять объект отправленного сообщения с медиа (достаем 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 и посмотреть, какие действия имеются.

1bba2e7536827f2a65f8807a9a795578.jpg

Далее, если бот может выполнить действие очень быстро (например, отправка сообщения с видео через 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, базы данных (это я вмещу в одну статью), ну и, может, еще парочку тем, но все остальное уже есть. Просто повторяйте за мной, и у вас все получится.

На этом у меня все. Надеюсь на ваш позитивный отклик.

© Habrahabr.ru