Обновления

getUpdates и update_id

Практическое руководство по getUpdates: как получать обновления, работать с update_id и offset, защищаться от дублей и парсить входящие сообщения на PHP.

Telegram отдаёт события боту объектами Update. В режиме polling вы сами запрашиваете их методом getUpdates. Это первая страница, где мы реально читаем входящие данные — до этого мы только делали getMe.

Как вызвать getUpdates

Используем функцию telegramApi(), которую мы сделали в главе Первый запрос: getMe. Там мы написали обёртку над cURL с проверкой HTTP-кода, json_last_error и поля ok — здесь просто вызываем её.

<?php
// get_updates.php
require_once __DIR__ . '/src/TelegramClient.php'; // там функция telegramApi()

$token = getenv('TELEGRAM_BOT_TOKEN');
if (!$token) {
die('TELEGRAM_BOT_TOKEN не задан в окружении\n');
}

$offset = 0; // первый запуск — берём всё
$updates = telegramApi($token, 'getUpdates', [
'offset' => $offset,
'limit' => 100, // макс. 100 за раз
'timeout' => 0, // 0 = короткий запрос (short polling)
]);

// Смотрим, что пришло — в терминале удобнее json_encode с флагами
// print_r($updates); // тоже работает, но JSON читаемее для вложенных структур
echo json_encode($updates, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

Пример ответа API

Успешный ответ всегда имеет ok: true и массив в result:

{
"ok": true,
"result": [
{
"update_id": 847392101,
"message": {
"message_id": 12,
"from": {
"id": 123456789,
"is_bot": false,
"first_name": "Ivan",
"username": "ivan_php",
"language_code": "ru"
},
"chat": {
"id": 123456789,
"first_name": "Ivan",
"username": "ivan_php",
"type": "private"
},
"date": 1724501234,
"text": "/start"
}
},
{
"update_id": 847392102,
"message": {
"message_id": 13,
"from": { ... },
"chat": { ... },
"date": 1724501235,
"text": "Привет, бוט!"
}
}
]
}

Что смотреть:

  • ok — true значит запрос прошёл, false — в description будет ошибка (например, "Bad Request: offset must be greater than...").
  • result — массив Update. Пустой массив [] — новых событий нет.
  • update_id — монотонно растущий идентификатор. Каждый следующий Update имеет больший update_id.
  • message — входящее сообщение. Поля: message_id, from (User), chat (Chat), date (Unix timestamp), text и др.

Параметр offset — курсор обработки

offset говорит серверу: «всё с update_id меньше этого значения уже обработано, не присылай повторно». Значение — последний обработанный update_id + 1.

<?php
// После обработки пачки обновляем offset
$nextOffset = $offset;
foreach ($updates as $update) {
// ... ваша логика: processUpdate($update);
$nextOffset = max($nextOffset, $update['update_id'] + 1);
}
// Сохраняем $nextOffset в файл/БД/Redis для следующего запуска
file_put_contents(__DIR__ . '/.offset', (string)$nextOffset);

Важно: если скрипт упал после обработки сообщения, но до сохранения offset — при следующем запуске этот Update придёт снова. Это нормально, нужно писать идемпотентную логику (см. ниже).

Идемпотентность: защита от дублей

Самый надёжный паттерн — таблица с UNIQUE на update_id. Вставляем перед бизнес-логикой; если дубликат — пропускаем.

<?php
// Пример с PDO (таблицу создайте заранее: CREATE TABLE telegram_updates (update_id BIGINT PRIMARY KEY, received_at INT NOT NULL);)
$pdo = new PDO('sqlite:' . __DIR__ . '/bot.db');
$pdo->exec("CREATE TABLE IF NOT EXISTS telegram_updates (
update_id BIGINT PRIMARY KEY,
received_at INT NOT NULL
);");

foreach ($updates as $update) {
$id = $update['update_id'];
try {
$pdo->prepare('INSERT INTO telegram_updates (update_id, received_at) VALUES (?, ?)')
->execute([$id, time()]);
} catch (PDOException $e) {
if ($e->getCode() === '23000') { // UNIQUE constraint violation
continue; // дубль — уже обрабатывали
}
throw $e;
}
// Здесь ваша бизнес-логика
processUpdate($update);
}

Что внутри Update — типы событий

У каждого Update ровно одно из полей-событий. Частые:

  • message — новое сообщение (текст, фото, стикер, документ…)
  • edited_message — отредактированное сообщение
  • callback_query — нажатие inline-кнопки (см. callback_query)
  • inline_query — inline-режим
  • my_chat_member — бот добавлен/удалён из чата
  • chat_member — изменился статус участника
  • pre_checkout_query, shipping_query — платежи

Обрабатывайте только нужные типы, остальные игнорируйте явно:

<?php
foreach ($updates as $update) {
if (isset($update['message'])) {
handleMessage($update['message']);
} elseif (isset($update['callback_query'])) {
handleCallback($update['callback_query']);
} elseif (isset($update['my_chat_member'])) {
handleChatMember($update['my_chat_member']);
} else {
// Можно логировать неизвестные типы для отладки
error_log('Unknown update type: ' . json_encode(array_keys($update)));
}
}

Параметр allowed_updates — экономия трафика

По умолчанию Telegram присылает все типы. Если бот работает только с сообщениями и кнопками — ограничьте список:

<?php
$updates = telegramApi($token, 'getUpdates', [
'offset' => $nextOffset,
'limit' => 100,
'timeout' => 0,
'allowed_updates' => ['message', 'callback_query', 'my_chat_member'],
]);

Это уменьшает размер ответа и парсинг на вашей стороне.

Как проверить в терминале

  1. Напишите боту что-нибудь в Telegram.
  2. Запустите скрипт: php get_updates.php.
  3. Вы увидите массив с update_id, message, from, chat, text.
  4. Запустите повторно — вернётся [] (пустой массив), потому что offset не сохранялся.
  5. Добавьте сохранение offset в файл — повторный запуск будет чистым.

Для продакшена short polling (timeout=0) не подходит — слишком много пустых запросов. В следующей главе переходим к Long polling: держим соединение открытым до 30 секунд и забираем пачки сразу по появлению.

Дальше: Long polling — держать соединение открытым и получать обновления без задержек