Фото, файлы, стикеры, голосовые
До сих пор твой Цыплёнок умел читать и писать только текст — сегодня он научится видеть фото, ловить присланные файлы, узнавать стикеры и слушать голосовые.
file_id — это короткий идентификатор, который Telegram выдаёт на каждый загруженный файл (фото, документ, стикер, голосовое). По нему бот может переслать тот же файл сколько угодно раз, ничего не скачивая и не загружая заново.
Зачем нам этот урок
Представь: ты сделал бота, который проводит опрос в чате игрового клана. Друг присылает ему скриншот своего ранга, чтобы похвастаться, а бот в ответ молчит — потому что ты научил его понимать только текст. Или ты хочешь, чтобы бот по команде /meme кидал тебе любимую картинку, а по /cheatsheet — PDF со шпаргалкой по Python перед контрольной. Без работы с медиа ничего этого не выйдет.
Подумай, как ты сам общаешься в мессенджерах. Вряд ли ты переписываешься только текстом: ты кидаешь мемы, отправляешь голосовые, когда лень печатать, скидываешь другу скрин с ответом на задачу. Медиа — это половина живого общения. И если твой бот понимает только буквы, он сразу чувствуется чем-то ненастоящим, неживым. А вот бот, который умеет принять фото и тут же что-то про него сказать или прислать тебе картинку по команде, ощущается как полноценный собеседник.
В прошлом уроке мы разобрали, как хэндлеры ловят текстовые сообщения и команды. Сегодня мы сделаем следующий шаг: научим того же бота Цыплёнка-помощника понимать, что именно ему прислали — фото, документ, стикер или голосовое, — и самому отправлять картинки и файлы. А ещё познакомимся с маленькой магией под названием file_id, благодаря которой бот отправляет одну и ту же картинку мгновенно и бесплатно по трафику. Ничего страшного в этом нет: ты уже умеешь писать хэндлеры, а сегодня мы просто научим их реагировать не на текст, а на вложения.
Вот к чему придём — кусочек кода, который мы будем разбирать по частям:
@dp.message(F.photo)
async def on_photo(message: Message):
file_id = message.photo[-1].file_id
await message.answer(f"Классное фото! Его file_id:\n{file_id}")Результат: в чате бот ответит на любое присланное фото сообщением «Классное фото!» и покажет длинную строку — это и есть file_id той картинки.
Сообщение — это не только текст
Главная мысль урока вот в чём: каждое сообщение в Telegram — это как посылка с разными отделениями. В одном отделении может лежать текст, в другом — фото, в третьем — голосовое. У объекта message, который приходит в хэндлер, есть отдельное поле под каждый тип содержимого:
message.text— обычный текст (его мы уже знаем);message.photo— список размеров присланной фотографии;message.document— присланный файл (PDF, архив, что угодно);message.sticker— стикер;message.voice— голосовое сообщение;message.audio— музыкальный файл,message.video— видео.
Если в сообщении пришло фото — поле message.photo заполнено, а message.text равно None (пусто). Если пришёл текст — наоборот. Бот должен сначала понять, что за «отделение» посылки заполнено, и только потом решать, что делать.
Почему вообще Telegram раскладывает всё по разным полям, а не сваливает в одну кучу? Потому что у каждого типа контента — свои свойства, которые тебе пригодятся. У документа есть имя файла и размер, у голосового — длительность в секундах, у стикера — связанный с ним эмодзи. Если бы всё лежало в одном поле, добраться до этих свойств было бы куда сложнее. Вот что полезного лежит внутри самых частых типов:
| Тип | Поле message | Что полезного внутри |
| Текст | message.text | сама строка с текстом |
| Фото | message.photo | список размеров; у каждого есть file_id |
| Документ | message.document | file_name, file_size, file_id |
| Стикер | message.sticker | emoji, file_id, флаг is_animated |
| Голосовое | message.voice | duration (секунды), file_id |
| Видео | message.video | duration, width, height, file_id |
Заметь общую деталь: у каждого медиа-вложения есть file_id. Это и есть тот универсальный «номерок», к которому мы вернёмся чуть позже. А пока запомни главное: чтобы добраться до свойств вложения, сначала надо понять его тип.
Как понять, что именно прислали
Проще всего разложить это на чистом Python, без всякого бота. Представь, что сообщение — это словарь, где заполнено только одно поле, а остальные пустые (None). Напишем функцию, которая определяет тип:
def content_type(message):
if message.get("text"):
return "текст"
if message.get("photo"):
return "фото"
if message.get("document"):
return "документ"
if message.get("voice"):
return "голосовое"
return "что-то ещё"
print(content_type({"text": "Привет", "photo": None}))
print(content_type({"text": None, "photo": ["small", "big"]}))
print(content_type({"text": None, "voice": "abc123"}))Вывод:
текст фото голосовое
Видишь логику? Проверяем поля по очереди и возвращаем название первого заполненного. Именно так под капотом и работает фильтр, который мы сейчас применим в aiogram, — только там за нас всё делает сама библиотека.
Реагируем на типы вложений с помощью F
В aiogram 3.x есть удобный инструмент — «магический фильтр» F (его импортируют как from aiogram import F). Думай о нём как о вопросе к сообщению: F.photo значит «в этом сообщении есть фото?», F.voice — «есть голосовое?». Если ответ «да» — сработает нужный хэндлер.
import asyncio
import os
from aiogram import Bot, Dispatcher, F
from aiogram.types import Message
bot = Bot(token=os.getenv("BOT_TOKEN"))
dp = Dispatcher()
@dp.message(F.photo)
async def on_photo(message: Message):
await message.answer("Вижу фото! Красивое 🐤")
@dp.message(F.document)
async def on_document(message: Message):
await message.answer(f"Принял файл: {message.document.file_name}")
@dp.message(F.sticker)
async def on_sticker(message: Message):
await message.answer(f"О, стикер! Его эмодзи: {message.sticker.emoji}")
@dp.message(F.voice)
async def on_voice(message: Message):
seconds = message.voice.duration
await message.answer(f"Голосовое на {seconds} сек. Слушаю внимательно!")
async def main():
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())Результат: пришлёшь боту фото — он ответит «Вижу фото! Красивое 🐤»; пришлёшь файл — назовёт его имя; пришлёшь стикер — покажет эмодзи стикера; запишешь голосовое — скажет, сколько оно длится. На каждый тип контента — свой отдельный хэндлер.
Обрати внимание: мы берём токен через os.getenv("BOT_TOKEN") — это переменная окружения, чтобы секретный токен не лежал прямо в коде. Объекты bot и dp — те же самые, что и в прошлых уроках, мы просто дописываем новые хэндлеры в наш общий bot.py.
Почему у фото — список, а у документа нет
Заметил странность? У фото мы писали message.photo[-1] со скобками и индексом, а у документа — просто message.document. Дело в том, что Telegram хранит каждую фотографию в нескольких размерах сразу: крошечную превьюшку, среднюю и оригинал. Поэтому message.photo — это список, отсортированный от самого маленького размера к самому большому. Запись message.photo[-1] берёт последний элемент списка, то есть фото в максимальном качестве. А документ, стикер и голосовое приходят в одном экземпляре — там список не нужен.
Бот сам отправляет фото и файлы
Принимать медиа научились — теперь пусть бот сам что-нибудь пришлёт. Для этого у сообщения есть методы:
message.answer_photo(...)— отправить фотографию;message.answer_document(...)— отправить файл;message.answer_sticker(...)— отправить стикер;message.answer_voice(...)— отправить голосовое.
Откуда бот возьмёт картинку? Есть три способа, и у каждого свой смысл.
Способ 1: отправить по ссылке из интернета
@dp.message(F.text == "/cat")
async def send_cat(message: Message):
url = "https://cataas.com/cat"
await message.answer_photo(photo=url, caption="Держи котика 🐱")Результат: на команду /cat бот пришлёт картинку с котиком, скачав её по ссылке, и подпишет её «Держи котика 🐱». Параметр caption — это подпись под фото.
Способ 2: отправить файл со своего компьютера
Если картинка лежит рядом с твоим bot.py, её нужно «обернуть» в специальный объект FSInputFile (от слов File System Input File — «файл из файловой системы»). Это как сказать боту: «возьми вот этот файл с диска и загрузи его в Telegram».
from aiogram.types import FSInputFile
@dp.message(F.text == "/meme")
async def send_meme(message: Message):
photo = FSInputFile("memes/chick.jpg")
await message.answer_photo(photo=photo, caption="Мем дня 🐤")Результат: на команду /meme бот загрузит картинку chick.jpg из папки memes рядом с ботом и пришлёт её с подписью «Мем дня 🐤».
Способ 3: отправить по file_id (самый быстрый)
А вот теперь — обещанная магия. Когда кто-то впервые отправляет файл боту (или бот загружает файл с диска), Telegram присваивает этому файлу file_id — короткую строку-удостоверение. Если у тебя есть этот file_id, бот может отправить тот же самый файл мгновенно: ничего не качается из интернета и не загружается с диска, Telegram просто берёт уже знакомый ему файл со своих серверов.
SAVED_PHOTO = "AgACAgIAAxkBAAID..." # сюда вставляешь file_id один раз
@dp.message(F.text == "/logo")
async def send_logo(message: Message):
await message.answer_photo(photo=SAVED_PHOTO, caption="Наш логотип 🐤")Результат: на команду /logo бот мгновенно пришлёт ранее сохранённую картинку по её file_id, без задержки на загрузку.
Как добыть этот file_id? Очень просто: вспомни самый первый хэндлер из урока — он печатал file_id любого присланного фото. Отправь боту нужную картинку один раз, скопируй из его ответа длинную строку и вставь её в код. Дальше бот будет переотправлять её хоть тысячу раз без скачивания.
Аналогия, чтобы запомнить
Представь школьную раздевалку. Ты сдаёшь куртку (загружаешь файл) и получаешь номерок (file_id). Пока куртка висит в гардеробе Telegram, тебе достаточно показать номерок — и куртку выдадут мгновенно, не надо тащить её заново из дома. file_id — это и есть такой номерок на файл, лежащий на серверах Telegram.
Какой способ когда выбирать
Три способа — не «лучше» и «хуже», у каждого своя роль. Держи короткую шпаргалку, чтобы не путаться:
- По ссылке — когда картинка живёт где-то в интернете и постоянно меняется (например, свежая погодная карта или случайный котик). Telegram скачает её на лету.
- Через
FSInputFile— когда файл лежит у тебя на диске рядом с ботом: твой логотип, заранее подготовленная картинка, PDF-шпаргалка. - По
file_id— когда одну и ту же картинку бот шлёт часто и она не меняется. Это самый быстрый и экономный способ: ничего не качается и не загружается заново.
На практике профессиональные боты делают так: загружают картинку с диска один раз, забирают из ответа Telegram её file_id и дальше шлют уже по нему. Получается и удобно, и быстро.
А если прислали несколько фото сразу
Бывает, друг отправляет боту не одну картинку, а целый альбом — пять скриншотов разом. С точки зрения Telegram это не одно сообщение, а несколько отдельных, просто связанных общим номером — он лежит в поле message.media_group_id. Поэтому твой хэндлер F.photo сработает столько раз, сколько фото в альбоме, — по одному разу на каждое. Это нормально: на старте достаточно знать, что так бывает, чтобы не удивляться пяти одинаковым ответам бота. Аккуратная «склейка» альбома в одно сообщение — задача чуть более продвинутая, и мы её здесь не трогаем.
Частые ошибки и подводные камни
Вот на чём обычно спотыкаются новички — сверься, чтобы не потерять полдня на отладку.
1. Забыть про список у фото
Самая частая ошибка — написать message.photo.file_id вместо message.photo[-1].file_id. У списка нет поля file_id, поэтому код упадёт с ошибкой AttributeError. Запомни: message.photo — это всегда список размеров, бери из него элемент по индексу.
2. Думать, что file_id вечный и универсальный
Чужой file_id не подойдёт твоему боту: каждый бот видит файлы под своими идентификаторами. Если друг прислал тебе file_id из своего бота — у тебя он не сработает. Бери только те file_id, которые получил твой собственный бот. Обычно они живут долго, но это не «навсегда»: для чего-то важного лучше держать оригинал файла под рукой.
3. Перепутать порядок хэндлеров с общим фильтром
Если выше по коду стоит хэндлер @dp.message() вообще без фильтра, он перехватит все сообщения, и до F.photo очередь уже не дойдёт. aiogram проверяет хэндлеры сверху вниз и останавливается на первом подходящем. Правило: сначала конкретные хэндлеры (F.photo, F.voice), а «ловушку на всё остальное» оставляй в самом низу.
4. Отправлять локальный файл как обычную строку
Если написать answer_photo(photo="memes/chick.jpg"), Telegram решит, что это file_id или ссылка, не найдёт такого — и пришлёт ошибку. Файл с диска нужно обязательно обернуть в FSInputFile("memes/chick.jpg"). Просто строка с путём — не работает. И ещё про пути: FSInputFile("memes/chick.jpg") ищет файл относительно того места, откуда ты запускаешь бота. Если запускаешь из другой папки — бот файл не найдёт. Пока проще всего держать картинки рядом с bot.py и запускать бота из его же папки.
5. Путать caption и обычный текст
Подпись к фото передаётся параметром caption в тот же метод answer_photo, а не отдельным message.answer("текст"). Если послать фото и текст разными вызовами, в чате это будут два разных сообщения, а не одно красивое фото с подписью. И ещё: подпись не может быть длиннее 1024 символов — для длинного текста придётся слать его отдельно.
Мини-практика: бот-сортировщик контента
Теперь твоя очередь. Допиши Цыплёнка так, чтобы он стал вежливым приёмщиком любых вложений:
- На фото — отвечает «Сохранил твоё фото!» и показывает его
file_id(пригодится для способа 3). - На документ — отвечает именем файла и его размером в килобайтах (подсказка:
message.document.file_size— это размер в байтах, раздели на 1024). - На голосовое — отвечает «Голосовые я пока не расшифровываю, но {длительность} секунд послушал!».
- На команду
/pet— присылает фото какого-нибудь животного по ссылке из интернета (например, того же котика). - Бонус: на стикер — отвечает тем же стикером обратно (подсказка: у тебя уже есть его
message.sticker.file_id, а отправить можно методомanswer_sticker).
Собери всё это в новые хэндлеры внутри своего bot.py и проверь в живом чате. Когда бот начнёт уверенно различать, что ему прислали, и отвечать по делу — задание выполнено.
Итоги
Сегодня твой Цыплёнок перестал быть «только текстовым» и научился работать с медиа. Коротко, что теперь у тебя в руках:
- Сообщение — это посылка с разными полями:
message.photo,message.document,message.sticker,message.voiceи другие; заполнено обычно одно из них. - Фильтр
F(напримерF.photo) позволяет повесить отдельный хэндлер на каждый тип вложения. - Бот сам отправляет медиа методами
answer_photo,answer_document,answer_sticker,answer_voice— по ссылке, черезFSInputFileс диска или мгновенно поfile_id. file_id— номерок на файл в «гардеробе» Telegram: получил один раз и переотправляешь сколько угодно без загрузки.
В следующем уроке мы дадим Цыплёнку кнопки: научимся показывать reply-клавиатуру под полем ввода, чтобы пользователю не надо было вручную набирать команды — достаточно нажать кнопку. Бот станет по-настоящему удобным. До встречи! 🐤