Перейти к содержанию

Медиафайлы

Используемая версия aiogram: 3.30.0

В этой главе мы разберёмся, как работать с медиафайлами: отправлять их разными способами, скачивать, собирать в альбомы, а заодно рассмотрим служебные сообщения.

Отправка файлов

Помимо обычных текстовых сообщений Telegram позволяет обмениваться медиафайлами различных типов: фото, видео, гифки, геолокации, стикеры и т.д. У большинства медиафайлов есть свойства file_id и file_unique_id. Первый можно использовать для повторной отправки одного и того же файла много раз, причём отправка будет мгновенной, т.к. сам файл уже лежит на серверах Telegram. Это самый предпочтительный способ.
К примеру, следующий код заставит бота моментально ответить пользователю той же гифкой, что была прислана:

@dp.message(F.animation)
async def echo_gif(message: Message):
    await message.reply_animation(message.animation.file_id)

Всегда используйте правильные file_id!

Бот должен использовать для отправки только те file_id, которые получил напрямую сам, например, в личке от пользователя или «увидев» медиафайл в группе/канале. При этом, если попытаться использовать file_id от другого бота, то это может сработать, но через какое-то время вы получите ошибку wrong url/file_id specified. Поэтому — только свои file_id!

В отличие от file_id, идентификатор file_unique_id нельзя использовать для повторной отправки или скачивания медиафайла, но зато он одинаковый у всех ботов для конкретного медиа. Нужен file_unique_id обычно тогда, когда нескольким ботам требуется знать, что их собственные file_id относятся к одному и тому же файлу.

Если файл ещё не существует на сервере Telegram, бот может загрузить его тремя различными способами: как файл в файловой системе, по ссылке и напрямую набор байтов. Для ускорения отправки и в целом для более бережного отношения к серверам мессенджера, загрузку (upload) файлов Telegram правильнее производить один раз, а в дальнейшем использовать file_id, который будет доступен после первой загрузки медиа.

В aiogram 3.x присутствуют 3 класса для отправки файлов и медиа - FSInputFile, BufferedInputFile, URLInputFile, с ними можно ознакомиться в документации.

Рассмотрим простой пример отправки изображений всеми различными способами:

from aiogram.types import FSInputFile, URLInputFile, BufferedInputFile

@dp.message(Command('images'))
async def upload_photo(message: Message):
    # Сюда будем помещать file_id отправленных файлов, чтобы потом ими воспользоваться
    file_ids = []

    # Чтобы продемонстрировать BufferedInputFile, воспользуемся "классическим"
    # открытием файла через `open()`. Но, вообще говоря, этот способ
    # лучше всего подходит для отправки байтов из оперативной памяти
    # после проведения каких-либо манипуляций, например, редактированием через Pillow
    with open("buffer_emulation.jpg", "rb") as image_from_buffer:
        result = await message.answer_photo(
            BufferedInputFile(
                image_from_buffer.read(),
                filename="image from buffer.jpg"
            ),
            caption="Изображение из буфера"
        )
        file_ids.append(result.photo[-1].file_id)

    # Отправка файла из файловой системы
    image_from_pc = FSInputFile("image_from_pc.jpg")
    result = await message.answer_photo(
        image_from_pc,
        caption="Изображение из файла на компьютере"
    )
    file_ids.append(result.photo[-1].file_id)

    # Отправка файла по ссылке
    image_from_url = URLInputFile("https://picsum.photos/seed/groosha/400/300")
    result = await message.answer_photo(
        image_from_url,
        caption="Изображение по ссылке"
    )
    file_ids.append(result.photo[-1].file_id)
    await message.answer("Отправленные файлы:\n"+"\n".join(file_ids))

Подпись у фото, видео и GIF можно перенести наверх:

@dp.message(Command("gif"))
async def send_gif(message: Message):
    await message.answer_animation(
        animation="<file_id гифки>",
        caption="Я сегодня:",
        show_caption_above_media=True
    )

подпись над анимацией

Скачивание файлов

Помимо переиспользования для отправки, бот может скачать медиа к себе на компьютер/сервер. Для этого у объекта типа Bot есть метод download(). В примерах ниже файлы скачиваются сразу в файловую систему, но никто не мешает вместо этого сохранить в объект BytesIO в памяти, чтобы передать в какое-то приложение дальше (например, pillow).

@dp.message(F.photo)
async def download_photo(message: Message, bot: Bot):
    await bot.download(
        message.photo[-1],
        destination=f"/tmp/{message.photo[-1].file_id}.jpg"
    )


@dp.message(F.sticker)
async def download_sticker(message: Message, bot: Bot):
    await bot.download(
        message.sticker,
        # для Windows пути надо подправить
        destination=f"/tmp/{message.sticker.file_id}.webp"
    )

В случае с изображениями мы использовали не message.photo, а message.photo[-1], почему? Фотографии в Telegram в сообщении приходят сразу в нескольких экземплярах; это одно и то же изображение с разным размером. Соответственно, если мы берём последний элемент (индекс -1), то работаем с максимально доступным размером фото.

Скачивание больших файлов

Боты, использующие Telegram Bot API, могут скачивать файлы размером не более 20 мегабайт. Если вы планируете скачивать/заливать большие файлы, лучше рассмотрите библиотеки, взаимодействующие с Telegram Client API, а не с Telegram Bot API, например, Telethon или Pyrogram.
Немногие знают, но Client API могут использовать не только обычные аккаунты, но ещё и боты.

А начиная с Bot API версии 5.0, можно использовать собственный сервер Bot API для работы с большими файлами.

Альбомы

То, что мы называем «альбомами» (медиагруппами) в Telegram, на самом деле отдельные сообщения с медиа, у которых есть общий media_group_id и которые визуально «склеиваются» на клиентах. Начиная с версии 3.1, в aiogram есть «сборщик» альбомов, работу с которым мы сейчас рассмотрим. Но прежде стоит упомянуть несколько особенностей медиагрупп:

  • К ним нельзя прицепить инлайн-клавиатуру или отправить реплай-клавиатуру вместе с ними. Никак. Вообще никак.
  • У каждого медиафайла в альбоме может быть своя подпись (caption). Если подпись есть только у одного медиа, то она будет выводиться как общая подпись ко всему альбому.
  • Фотографии можно отправлять вперемешку с видео в одном альбоме, файлы (Document) и музыку (Audio) нельзя ни с чем смешивать, только с медиа того же типа.
  • В альбоме может быть не больше 10 (десяти) медиафайлов.

Теперь посмотрим, как это сделать в aiogram:

from aiogram.filters import Command
from aiogram.types import FSInputFile, Message
from aiogram.utils.media_group import MediaGroupBuilder

@dp.message(Command("album"))
async def cmd_album(message: Message):
    album_builder = MediaGroupBuilder(
        caption="Общая подпись для будущего альбома"
    )
    album_builder.add(
        type="photo",
        media=FSInputFile("image_from_pc.jpg")
        # caption="Подпись к конкретному медиа"

    )
    # Если мы сразу знаем тип, то вместо общего add
    # можно сразу вызывать add_<тип>
    album_builder.add_photo(
        # Для ссылок или file_id достаточно сразу указать значение
        media="https://picsum.photos/seed/groosha/400/300"
    )
    album_builder.add_photo(
        media="<ваш file_id>"
    )
    await message.answer_media_group(
        # Не забудьте вызвать build()
        media=album_builder.build()
    )

Результат:

Результат работы билдера

А вот со скачиванием альбомов всё сильно хуже... Как уже было сказано выше, альбомы — это просто сгруппированные отдельные сообщения, а это значит, что боту они прилетают тоже в разных апдейтах. Вряд ли существует 100% надёжный способ принять весь альбом одним куском, но можно попытаться сделать это с минимальными потерями. Обычно это делается через мидлвари, мою собственную реализацию приёма медиагрупп можно найти по этой ссылке (остальной код в том репозитории лучше не смотреть, он безнадёжно устарел).

Сервисные (служебные) сообщения

Сообщения в Telegram делятся на текстовые, медиафайлы и служебные (они же — сервисные). Настало время поговорить о последних.

Сервисные сообщения

Несмотря на то, что они выглядят необычно и взаимодействие с ними ограничено, это всё ещё сообщения, у которых есть свои айдишники и даже владелец. Стоит отметить, что спектр применения сервисных сообщений с годами менялся и сейчас, скорее всего, ваш бот с ними работать не будет, либо только удалять.

Не будем сильно углубляться в детали и рассмотрим один конкретный пример: отправка приветственного сообщения вошедшему участнику. У такого служебного сообщения будет content_type равный "new_chat_members", но вообще это объект Message, у которого заполнено одноимённое поле.

@dp.message(F.new_chat_members)
async def somebody_added(message: Message):
    for user in message.new_chat_members:
        # проперти full_name берёт сразу имя И фамилию 
        # (на скриншоте выше у юзеров нет фамилии)
        await message.reply(f"Привет, {user.full_name}")

Добавлены несколько юзеров

Важно помнить, что message.new_chat_members является списком, потому что один пользователь может добавить сразу нескольких участников. Также не надо путать поля message.from_user и message.new_chat_members. Первое — это субъект, т.е. тот, кто совершил действие. Второе — это объекты действия. Т.е. если вы видите сообщение вида «Анна добавила Бориса и Виктора», то message.from_user — это информация об Анне, а список message.new_chat_members содержит информацию о Борисе с Виктором.

Не стоит целиком полагаться на сервисные сообщения!

У служебных сообщений о добавлении (new_chat_members) и выходе (left_chat_member) есть одна неприятная особенность: они ненадёжны, т.е. они могут не создаваться вообще.
К примеру, сообщение о new_chat_members перестаёт создаваться при ~10k участников в группе, а left_chat_member уже при 50 (но при написании этой главы я столкнулся с тем, что в одной из групп left_chat_member не появился и при 9 участниках. А через полчаса там же появился при выходе другого человека).

С выходом Bot API 5.0 у разработчиков появился гораздо более надёжный способ видеть входы/выходы участников в группах любого размера, а также в каналах. Но об этом поговорим в другой раз.

На этом всё. До следующих глав!
Ставьте лайки, подписывайтесь, прожимайте колокольчик