Інтерактивні Inline-меню в Telegram на PHP: стиснення payload, editMessageText та обробка callback_query

При створенні інтерактивних ботів у Telegram ключовим інструментом взаємодії з користувачем виступають інлайн-кнопки (InlineKeyboardMarkup). На відміну від звичайних клавіатур (ReplyKeyboardMarkup), натискання на інлайн-кнопку генерує подію callback_query, яка надсилається на ваш Webhook без засмічення чату текстовими повідомленнями.

Однак при розробці логіки зміни екранів, фільтрів або вибору товарів розробники регулярно стикаються з жорсткими технічними обмеженнями Telegram API: суворо лімітованим розміром поля callback_data, підтримкою ідемпотентності запитів і необхідністю негайного підтвердження натискання кнопки.

Ліміт 64 байти в callback_data: чому не можна передавати JSON

Головне архітектурне обмеження параметра callback_data в об'єкті InlineKeyboardButton — його розмір не повинен перевищувати 64 байти. Важливо враховувати, що йдеться саме про байти, а не про символи. У кодуванні UTF-8 кириличні символи займають по 2 байти, а спеціальні символи та emoji — до 4 байт.

Спроба передати структурований JSON на зразок {\"action\":\"show_category\",\"id\":1042,\"page\":3} призведе до того, що Telegram API поверне помилку 400 Bad Request: BUTTON_DATA_INVALID. Нижче наведено приклад коректної функції для надсилання cURL-запитів до API з перевіркою помилок.

<?php

function sendTelegramApiRequest(string $method, array $params = []): array
{
$token = getenv('TELEGRAM_BOT_TOKEN');
if (!$token) {
throw new RuntimeException('TELEGRAM_BOT_TOKEN environment variable is not set.');
}

$url = "https://api.telegram.org/bot{$token}/{$method}";
$ch = curl_init($url);

curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($params),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
]);

$response = curl_exec($ch);
$curlError = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($response === false) {
throw new RuntimeException("cURL error during Telegram API call: {$curlError}");
}

$decoded = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException('Failed to parse JSON response from Telegram API.');
}

if ($httpCode !== 200 || !isset($decoded['ok']) || $decoded['ok'] !== true) {
$description = $decoded['description'] ?? 'Unknown error';
throw new RuntimeException("Telegram API error [HTTP {$httpCode}]: {$description}");
}

return $decoded['result'];
}

Архітектура коротких payload: префікси та генератор хешів

Для вкладання в ліміт 64 байти застосовують дві стратегії:

  • Префіксна схема з двокрапкою або слешем: підходить для простих дій. Наприклад: act:item:1042:3 (де act — дія, item — сутність, 1042 — ID, 3 — сторінка). Це займає близько 16 байт.
  • Схема з тимчасовим сховищем у БД (Payload Registry): якщо контекст кнопки занадто великий (складні фільтри, довгі UUID, попередній склад замовлення), у callback_data передають короткий унікальний ідентифікатор, а дані зберігають у PostgreSQL/MySQL або Redis.

Нижче наведено приклад створення короткого ключа запису для форми заявки з використанням криптографічно стійкого ідентифікатора lead_id.

<?php

function createLeadContext(PDO $pdo, int $userId, array $leadData): string
{
// Генерируем короткий уникальный ID длиною 14 символов (7 байт hex)
$leadId = bin2hex(random_bytes(7));

$stmt = $pdo->prepare('
INSERT INTO lead_contexts (lead_id, user_id, payload_data, created_at)
VALUES (:lead_id, :user_id, :payload_data, NOW())
');

$stmt->execute([
':lead_id' => $leadId,
':user_id' => $userId,
':payload_data' => json_encode($leadData, JSON_UNESCAPED_UNICODE),
]);

// Формируем callback_data, который занимает всего 18 байт: "confirm_lead:a1b2c3d4e5f6g7"
return "cnf_ld:{$leadId}";
}

Валідація Webhook, Secret Token та ідемпотентність

При обробці вхідних оновлень від Telegram необхідно суворо перевіряти заголовок X-Telegram-Bot-Api-Secret-Token, заданий при виклику setWebhook. Це захищає вашу кінцеву точку від підроблених HTTP-запитів.

Крім того, Telegram гарантує доставку оновлень за принципом «at least once» (принаймні один раз). Через мережеві збої один і той самий update_id може прийти повторно. Щоб не виконувати списання коштів або дублювання заявок двічі, оброблені update_id слід зберігати в базі даних і перевіряти перед запуском бізнес-логіки.

Обов'язковий виклик answerCallbackQuery та безшовний UX з editMessageText

При натисканні на інлайн-кнопку на клієнті користувача з'являється індикатор завантаження (годинничок, що крутиться на кнопці). Якщо сервер не відповість викликом методу answerCallbackQuery протягом 10 секунд, інтерфейс зависне, а Telegram на клієнті покаже помилку тайм-аута.

Правильний життєвий цикл обробки події callback_query виглядає наступним чином:

  1. Валідувати секретный токен і спарсити вхідний JSON.
  2. Перевірити update_id на предмет повторної обробки (ідемпотентність).
  3. Викликати answerCallbackQuery для зняття індикатора завантаження на кнопці (можна передати text та show_alert => true для спливаючого повідомлення).
  4. Оновити поточне повідомлення через editMessageText або editMessageReplyMarkup для відмальовування нового стану інтерфейсу.
<?php

// Входная точка обработки Webhook
$secretHeader = $_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] ?? '';
$expectedSecret = getenv('TELEGRAM_WEBHOOK_SECRET');

if (!hash_equals($expectedSecret, $secretHeader)) {
http_response_code(403);
echo json_encode(['error' => 'Invalid secret token']);
exit;
}

$rawInput = file_get_contents('php://input');
$update = json_decode($rawInput, true);

if (!$update || !isset($update['update_id'])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON update']);
exit;
}

$pdo = new PDO('mysql:host=127.0.0.1;dbname=bot_db;charset=utf8mb4', 'db_user', 'db_pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

$updateId = (int)$update['update_id'];

// Проверка идемпотентности
$stmt = $pdo->prepare('INSERT IGNORE INTO processed_updates (update_id) VALUES (:update_id)');
$stmt->execute([':update_id' => $updateId]);

if ($stmt->rowCount() === 0) {
// Данный update_id уже был успешно обраборан ранее
http_response_code(200);
echo json_encode(['status' => 'already_processed']);
exit;
}

if (isset($update['callback_query'])) {
$callback = $update['callback_query'];
$callbackId = $callback['id'];
$callbackData = $callback['data'] ?? '';
$message = $callback['message'];
$chatId = $message['chat']['id'];
$messageId = $message['message_id'];

// 1. Снимаем лоадер с кнопки
sendTelegramApiRequest('answerCallbackQuery', [
'callback_query_id' => $callbackId,
'text' => 'Загрузка...',
'show_alert' => false,
]);

// 2. Разбираем сжатый payload
$parts = explode(':', $callbackData);
$action = $parts[0] ?? '';

if ($action === 'cnf_ld') {
$leadId = $parts[1] ?? '';

// Получаем контекст лида из БД
$stmtContext = $pdo->prepare('SELECT payload_data FROM lead_contexts WHERE lead_id = :lead_id');
$stmtContext->execute([':lead_id' => $leadId]);
$contextRow = $stmtContext->fetch();

if ($contextRow) {
$leadData = json_decode($contextRow['payload_data'], true);

// 3. Обновляем текст сообщения и менять клавиатуру
sendTelegramApiRequest('editMessageText', [
'chat_id' => $chatId,
'message_id' => $messageId,
'text' => "Заявка #{$leadId} успешно подтверждена!
Услуга: " . htmlspecialchars($leadData['service'] ?? 'Неуказана'),
'parse_mode' => 'HTML',
'reply_markup' => [
'inline_keyboard' => [
[
['text' => '« Назад в меню', 'callback_data' => 'main_menu']
]
]
]
]);
}
}
}

http_response_code(200);
echo json_encode(['status' => 'ok']);

Типові помилки при роботі з Inline-клавіатурами

  • Ігнорування помилок editMessageText: якщо спробувати відредагувати повідомлення, передавши точно такий самий текст і reply_markup, Telegram API поверне помилку 400 Bad Request: message is not modified. Логуйте відгуки API та перехоплюйте винятки.
  • Передача чутливих даних у callback_data: пам'ятайте, що callback_data не шифрується всередині інфраструктури Telegram і може залишитися в логах клієнта або сервера. Не передавайте там токени авторизації або персональні дані.
  • Відсутність обробки застарілих кнопок: користувач може натиснути на інлайн-кнопку в повідомленні тижневої давнини. Система повинна коректно обробляти ситуації, коли тимчасовий контекст у БД вже очищено за TTL.

Якщо вам потрібна професійна інтеграція складних Telegram-бот ів та Mini Apps з надійною архітектурою, замовте розробку у фахівців на BotCreator.

"

Нові статті — у Telegram

Розбираємо, що автоматизувати в бізнесі та як це працює на практиці. Без спаму.