Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 12 из 25 ~18 мин +20 XP

Медиа: фото, документы и voice

Отправка фото в telegram-боте на Go — это NewPhoto и bot.Send, две строки кода, но настоящая сила начинается с file_id: файл, который пользователь уже прислал, не нужно скачивать и перезаливать, он живёт на серверах Telegram и выдаётся по короткой строке. Разберём отправку фото и документов, приём файлов от пользователя и лимиты Bot API на tgbotapi. Отдельно разберём загадочный LOC_ERR, который иногда показывает клиент Telegram рядом с медиа, — и почему он не имеет отношения к коду вашего бота.

Содержание урока · 9 разделов

Бот, который умеет только текст, быстро упирается в потолок: пользователи шлют фото чеков вместо описания, просят отчёт файлом, а не простынёй текста, и ждут, что бот ответит голосом. Telegram передаёт пять основных видов контента — фото, видео, документы, аудио и голосовые, — и у каждого в библиотеке свой конструктор с одинаковой логикой. Общая идея у всех одна: файл либо загружается ботом впервые, либо уже лежит на серверах Telegram, и тогда вместо данных передаётся его идентификатор. Второй случай важнее и экономнее, поэтому урок построен вокруг него: сначала отправим фото двумя способами, потом научимся принимать фото от пользователя и забирать у него file_id, а в конце — отдавать документы и честно поговорим про лимиты, которые у Bot API на удивление скромные. Весь код урока работает в личке и в группе одинаково, отличия только в том, чей ID использовать для ответа — эта развилка разобрана ещё в шестом уроке, здесь она просто соблюдается. К концу урока бот примет и вернёт фото, отдаст PDF и узнает про медиа-группы.

Отправка фото: file_id против загрузки#

NewPhoto принимает два аргумента — chat_id и файл, а файл может быть трёх видов. InputFileString(fileID) — строка-идентификатор: Telegram уже хранит этот файл, и новая загрузка не нужна. FileBytes — структура с полями Name и Bytes: бот читает файл с диска и загружает его, отправляя байты на сервер. Третий путь — URL: NewPhoto умеет передавать ссылку, и Telegram скачает картинку сам, без участия вашего кода. К каждому фото прикрепляется подпись: поле Caption у PhotoConfig, до 1024 символов по документации Bot API. Правило выбора простое: если файл уже приходил боту или вы отправляете его повторно — только file_id, загрузка байтов это трата трафика и секунд ожидания. Загрузка нужна один раз — при первом знакомстве бота с файлом, после чего его file_id стоит сохранить и пользоваться им всегда.

Два способа отправить одно фото

GO
data, err := os.ReadFile("cat.png")
if err != nil {
        log.Println(err)
        return
}

first := tgbotapi.NewPhoto(chatID,
        tgbotapi.FileBytes{Name: "cat.png", Bytes: data})
first.Caption = "Кот из архива, первая отправка"
bot.Send(first)

second := tgbotapi.NewPhoto(chatID, tgbotapi.InputFileString(fileID))
second.Caption = "Тот же кот, без повторной загрузки"
bot.Send(second)

Первая отправка несёт байты файла: FileBytes с именем и содержимым, прочитанным os.ReadFile. Telegram сохраняет файл и в ответе присылает его fileID, который стоит сохранить. Вторая отправка идёт по InputFileString — тот же NewPhoto, но файл не передаётся повторно, сработает и на медленном канале, и при большом размере.

Как бот принимает фото от пользователя#

Пользователь отправил фото — апдейт приходит с Message, у которого поле Photo заполнено. Нюанс, о котором спотыкаются все: Photo это срез PhotoSize, а не один размер. Telegram хранит несколько версий картинки — превью, средний размер, оригинал, — и каждая версия имеет свой file_id, ширину, высоту и вес. Крупнейший вариант всегда последний элемент среза, и его file_id и нужен боту: photo[len(photo)-1].FileID. У пустого сообщения Photo равен nil, и проверка на nil перед обращением к срезу обязательна — иначе паника на первом же стикере вместо фото. Обратная дорога короткая: полученный fileID передаётся в NewPhoto через InputFileString, и бот вернёт пользователю его же картинку. Для ответов в группах работает то же правило, что и раньше: отправлять надо в Chat.ID, а личность автора смотреть в From. Отдельный случай — фото прислали без сжатия, как документ: тогда оно лежит в Message.Document, а Photo равен nil, и боту, принимающему любые изображения, надо проверять оба поля.

Эхо фото: возвращаем присланное

GO
if update.Message == nil {
        return
}

photo := update.Message.Photo
if photo == nil {
        return
}

largest := photo[len(photo)-1]
cfg := tgbotapi.NewPhoto(update.Message.Chat.ID,
        tgbotapi.InputFileString(largest.FileID))
cfg.Caption = fmt.Sprintf("Принято, %dx%d",
        largest.Width, largest.Height)
bot.Send(cfg)

Три проверки перед работой: сообщение существует, это именно фото, срез не пуст. Крупнейший вариант — последний элемент среза, его FileID уходит в InputFileString. Подпись собирает размеры из Width и Height. На присланное фото бот отвечает тем же фото без единого байта загрузки — вся дорога через идентификатор.

Документы и voice#

Документ — тот же механизм с другим конструктором: NewDocument(chatID, file) отправляет файл как есть, без сжатия и превью, и это правильный путь для PDF, таблиц и архивов. Отличие от фото принципиально: фото Telegram предлагает сжать и показать превью, документ сохраняет байты нетронутыми. Голосовые — NewVoice: файл в формате OGG с кодеком Opus, Telegram покажет его как аудиосообщение с волновой дорожкой; обычный MP3 голосовым не станет — уедет как аудиофайл через SendAudio. По лимитам Bot API честная картина такая: отправить бот может файл до 50 МБ, а вот скачать присланное пользователем через GetFile — до 20 МБ, и это ограничение регулярно удивляет. Ссылка на скачивание из GetFile живёт около часа, поэтому качать надо сразу, а не класть URL в базу на потом. Отправка всех трёх типов собирается одинаково: конструктор, поле Caption при желании, bot.Send — различия живут внутри библиотеки.

Отдаём отчёт документом

GO
data, err := os.ReadFile("report-march.pdf")
if err != nil {
        log.Println("read:", err)
        return
}

doc := tgbotapi.NewDocument(chatID,
        tgbotapi.FileBytes{Name: "report-march.pdf", Bytes: data})
doc.Caption = "Отчёт за март. Вопросы - в /help"
bot.Send(doc)

Документ собирается так же, как фото: байты и имя, под которым пользователь увидит файл. Caption с подписью не обязателен, но с ним отчёт находится в истории чата поиском. Отправка через bot.Send единая для всех типов — конструктор выбирает метод API за вас.

Кейс: бот-архив чеков#

Реальный бот-помощник бухгалтерии принимает фото чеков и возвращает их по запросу «покажи март». Решение на Material из прошлого урока: при получении фото бот берёт file_id крупнейшего размера и сохраняет его вместе с датой в хранилище — ключ bot:receipt:100, значение JSON со списком идентификаторов. По запросу бот собирает ответы NewPhoto по InputFileString за доли секунды, потому что ни одного файла не передаёт. Telegram здесь работает как бесплатный CDN: хранение, раздача и резервные копии уже оплачены самим фактом существования Bot API. Обратная сторона — file_id уникален для бота, и при смене токена архив придётся перезагружать заново байтами. Для учебного проекта это приемлемая цена, для боевого — повод держать копии файлов у себя, хотя бы в момент первого приёма. Подпись у каждого возвращённого чека возвращает месяц и сумму — ровно те метаданные, что сохранялись рядом с file_id.

Что чем отправлять#

КонтентКонструкторНюанс
ФотоNewPhotoсжатие и превью; FileID — без загрузки
ДокументNewDocumentбез сжатия, подходит для PDF и таблиц
VoiceNewVoiceOGG с кодеком Opus, аудиосообщение
Повторная отправкаInputFileString(fileID)файл уже на серверах, байты не нужны
Первичная загрузкаFileBytes{Name, Bytes}файл читается с диска бота

Экономика файлов: байты против file_id#

Бот-каталог отправлял одну и ту же картинку товара при каждом запросе, и код честно читал os.ReadFile и грузил байты заново. Работало — пока пользователь с мобильным интернетом не начал жаловаться на секундные паузы: каждая отправка несла полный файл по сети туда и обратно. Лечение в одну строку: при первой отправке bot.Send возвращает Message, в котором лежит Photo с file_id отправленного файла, — сохраните его в хранилище из прошлого урока, и все дальнейшие отправки пойдут через InputFileString. Разница в скорости — на порядок, потому что Telegram отдаёт файл со своих серверов без вашего участия. Вторая половина ошибки — память: если картинок много, а байты лежат в переменных, бот незаметно съедает гигабайты оперативки; file_id весит десятки байт, и это правильная единица хранения.

Моё рабочее правило для ботов без особых требований: файлы живут в Telegram, у бота только file_id и метаданные. Против него обычно выступает страх «а вдруг Telegram удалит» — но за годы практики я не встретил случая, чтобы файл, присланный боту, исчез из истории чата. Границы, где мнение сдаётся, конкретные: файл больше 20 МБ, который надо качать и обрабатывать; приватные данные с требованием локального хранения; и смена токена бота, после которой идентификаторы умирают. Во всех трёх случаях нужны собственные копии. В остальных — повторная загрузка байтов это время, трафик и место на диске, потраченные без причины.

Проверка: повторная отправка

Пользователь уже присылал боту фото, fileID сохранён. Как отправить это фото без повторной загрузки?

Проверка: самый большой размер

Как взять file_id фото в максимальном качестве?

Что напечатает разбор среза

Прочитайте код и выберите, что он выведет:

photo := update.Message.Photo

var widths []int
for _, p := range photo {
        widths = append(widths, p.Width)
}

fmt.Println(len(photo), widths[0] < widths[len(widths)-1])

Соберите отправку фото с подписью

Впишите структуру загрузки, поле подписи и конструктор повторной отправки.

cfg := tgbotapi.NewPhoto(chatID, tgbotapi.{Name: "cat.png", Bytes: data}) cfg. = "Кот из архива" again := tgbotapi.NewPhoto(chatID, tgbotapi.(fileID))

Шесть строк отправки фото с диска

Расставьте отправку файла с диска: от чтения до обработки ошибки.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: медиа и файлы

Термин

file_id

нажмите, чтобы перевернуть
Определение

Идентификатор файла на серверах Telegram, привязан к боту, получившему файл. Повторная отправка — без байтов, только строка.

1 / 6

Медиа добавили боту выразительности, следующий шаг — добавить её тексту: урок про форматирование научит подписи с жирным, курсивом и ссылками через ParseMode. Хранилище file_id из кейса этого урока строится на карте с мьютексом и Redis из одиннадцатой части, стикеры и реакции — продолжение темы медиа в двадцать втором уроке. Базовая отправка сообщений осталась в шестом уроке, полный курс — на его странице.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • NewPhoto принимает три вида файлов: InputFileString для уже загруженного, FileBytes для загрузки с диска, URL для ссылки.
  • file_id экономит трафик и время: файл, который уже приходил боту, не нужно перезаливать.
  • Photo в сообщении — срез PhotoSize от меньшего к большему; оригинал — последний элемент, у каждого размера свой file_id.
  • file_id привязан к токену бота: между ботами не переносится, file_unique_id универсален, но только для сравнения.
  • Документы — NewDocument без сжатия, voice — NewVoice с OGG/Opus.
  • Лимиты Bot API: отправка до 50 МБ, скачивание через GetFile до 20 МБ, ссылка живёт около часа.
  • Telegram работает как бесплатный CDN бота, пока не потребовались свои копии — границу задают приватность и смена токена.

Домашнее задание

Научите локального бота принимать фото и возвращать его же с подписью с размерами в пикселях. Затем добавьте команду /report, которая отправляет любой файл с диска документом, и сохраните file_id первого присланного фото в хранилище из одиннадцатого урока: команда /again должна вернуть это фото без повторной загрузки.

Личные заметкиПусто
Хранятся только в вашем браузере (Telegram · Медиа: фото, документы и voice)

Вопросы и ответы по уроку

Почему Photo иногда nil, хотя пользователь прислал картинку?

Картинка могла прийти сжатием группы или как документ — тогда заполнено Message.Document вместо Photo. Проверяйте оба поля, если бот должен принимать любые изображения.

Можно ли отправить фото по ссылке из интернета?

Да: NewPhoto принимает URL-строку, Telegram скачает и разошлёт картинку сам. Ограничение — 5 МБ для фото по URL и требование, чтобы ссылка отдавала контент напрямую, а не HTML-страницу.

Как скачать фото, которое прислал пользователь?

Через GetFile по file_id: библиотека вернёт объект File, у него есть метод Link — прямая ссылка для http.Get. Лимит 20 МБ на файл и час жизни ссылки, качать надо сразу.

Что значит LOC_ERR в Telegram — это ошибка моего бота?

Нет, бот ни при чём: LOC_ERR (например, «LOC_ERR: Members_many») — это сбой локализации клиента Telegram, который показывает служебный ключ вместо переведённой строки. Встречается в сторонних клиентах и старых версиях официального приложения, чаще у медиа-сообщений и счётчиков участников. Лечится обновлением официального Telegram — код бота менять не нужно.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Prometheus25 уроков~12 чпрактика25 уроков уже доступны

Мониторинг Go с Prometheus: метрики, Grafana, алерты

Курс по метрикам в Go: Counter, Gauge, Histogram, лейблы, PromQL, Grafana-дашборды, алерты и бизнес-метрики. 25 уроков с примерами client_golang.

  • Понимать четыре типа метрик и выбирать правильный
  • Экспортировать метрики из Go через promhttp
  • Инструментировать HTTP-запросы и бизнес-события
  • Писать PromQL-запросы: rate, sum, p99