Messages

Photos, documents, media groups

Текстовые sendMessage — это треть всех ботов. Остальные две трети — картинки, PDF, голосовые и альбомы. Для них Bot API отдаёт семейство методов sendPhoto, sendDocument, sendVideo, sendAudio, sendVoice, sendAnimation, sendVideoNote. Сигнатура у них одинаковая: chat_id, что-то медиа-специфичное (photo, document, video и т.д.), опциональные caption и parse_mode.

Способы передачи файла

Поле медиа принимает три формы:

  • file_id строкой — файл уже загружен в Telegram раньше (любым сообщением, в т.ч. от пользователя). Бесплатно и моментально.
  • HTTP URL — Telegram сам скачает и зальёт к себе. Удобно для генерации картинок на лету (QR, графики), но не больше 5 МБ для фото и 20 МБ для остального.
  • multipart/form-data — загрузка через бота. До 10 МБ на фото, 50 МБ на документ (через локальный Bot API — до 2 ГБ).

Правило: если планируете пересылать один и тот же файл много раз — сначала отправьте его один раз, заберите file_id из ответа и дальше используйте строку.

Отправка фото по URL

$api = 'https://api.telegram.org/bot' . BOT_TOKEN;

function sendPhotoByUrl(string $api, int $chatId, string $url, string $caption = ''): array {
    $payload = [
        'chat_id'    => $chatId,
        'photo'      => $url,
        'caption'    => $caption,
        'parse_mode' => 'HTML',
    ];

    $ch = curl_init($api . '/sendPhoto');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $payload,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
    ]);
    $body = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($body === false || $code !== 200) {
        throw new RuntimeException("sendPhoto failed: HTTP $code");
    }

    $resp = json_decode($body, true);
    if (!($resp['ok'] ?? false)) {
        throw new RuntimeException('Telegram error: ' . ($resp['description'] ?? 'unknown'));
    }
    return $resp['result'];
}

Возвращается объект Message, в нём message.photo — массив объектов PhotoSize разных размеров. Самый большой — последний, его file_id берите для повторной отправки.

Загрузка файла с диска

Когда нужна multipart-загрузка (документы, большие файлы, локальные картинки):

function sendDocument(string $api, int $chatId, string $path, string $filename = ''): array {
    if (!is_file($path)) {
        throw new InvalidArgumentException("File not found: $path");
    }
    if ($filename === '') {
        $filename = basename($path);
    }

    $payload = [
        'chat_id'  => $chatId,
        'document' => new CURLFile($path, mime_content_type($path) ?: 'application/octet-stream', $filename),
    ];

    $ch = curl_init($api . '/sendDocument');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $payload,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 60,
    ]);
    $body = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $resp = json_decode($body, true);
    if ($code !== 200 || !($resp['ok'] ?? false)) {
        throw new RuntimeException('sendDocument error: ' . ($resp['description'] ?? "HTTP $code"));
    }
    return $resp['result'];
}

Ключевое: CURLFile для PHP 5.5+. В caption можно дать подпись до 1024 символов с тем же parse_mode HTML/Markdown, что и в HTML и Markdown.

Media group: альбомы

Альбом из 2–10 объектов одного типа (фото или видео) отправляется через sendMediaGroup. Особенности:

  • Все элементы — в массиве media, каждый как JSON-строка с полями type, media, опционально caption, parse_mode.
  • Подпись (caption) можно задать только первому элементу, остальные её игнорируют.
  • В update приходит одно сообщение с полем media_group_id; у каждого Message в массиве — свой file_id.
function sendAlbum(string $api, int $chatId, array $urls): array {
    if (count($urls) < 2 || count($urls) > 10) {
        throw new InvalidArgumentException('Album must contain 2..10 items');
    }

    $media = array_map(fn(string $u) => json_encode([
        'type'   => 'photo',
        'media'  => $u,
    ]), $urls);

    $payload = [
        'chat_id' => $chatId,
        'media'   => $media, // array of JSON strings
    ];

    $ch = curl_init($api . '/sendMediaGroup');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $payload,
        CURLOPT_RETURNTRANSFER => true,
    ]);
    $body = curl_exec($ch);
    $resp = json_decode($body, true);
    curl_close($ch);

    if (!($resp['ok'] ?? false)) {
        throw new RuntimeException('sendMediaGroup: ' . ($resp['description'] ?? 'fail'));
    }
    return $resp['result']; // array of Message
}

Смешивать фото и видео в одном альбоме нельзя — только один тип. Если нужен микс — отправляйте двумя группами подряд.

Получение файла от пользователя

Когда приходит update с message.photo или message.document:

  1. Берёте file_id нужного размера (для фото — обычно последний, самый большой).
  2. Вызываете getFile с этим file_id, получаете file_path — относительный путь на серверах Telegram.
  3. Скачиваете по https://api.telegram.org/file/bot<TOKEN>/<file_path>. Ссылка живёт минимум час.

Подводные камни

  • URL-картинка должна отдавать корректный Content-Type и быть доступна боту без редиректов на логин.
  • Один и тот же URL Telegram перезальёт к себе и сгенерирует новый file_id — не путайте с переиспользованием.
  • caption поддерживает parse_mode, но длинные подписи (>1024) обрезаются.
  • Видео в альбоме требует одинаковых пропорций — иначе Telegram склеит криво.

Медиа-сообщения нельзя отредактировать как текст — только подпись. Для замены картинки придётся удалить сообщение и отправить новое. Об этом — на следующей странице.

Дальше: Редактирование и удаление.