Обработка Telegram InlineKeyboardMarkup на PHP: callback_query, answerCallbackQuery и ограничение полезной нагрузки 64 байта

Обработка Telegram InlineKeyboardMarkup на PHP: callback_query, answerCallbackQuery и ограничение полезной нагрузки 64 байта

В этом уроке мы построим небольшой автономный обработчик PHP, который покажет, как использовать InlineKeyboardMarkup от Telegram Bot API правильно. Мы будем:

- собрать клавиатуру кнопок, на которой callback_data остается ниже 64-байтового лимита, установленного API бота, - получить Обновить методом callback_query-Звонить answerCallbackQuery чтобы пользователь перестал видеть на кнопке загрузчик, - отредактируйте исходное сообщение с помощью editMessageText, - храните все вызовы внутри крошечной оболочки cURL, которая проверяет HTTP-статус и ok = false; конверт, который Telegram возвращает при ошибках.

Мы будем not постарайтесь быть полноценным бот-фреймворком. Здесь нет DI-контейнера, нет очереди, нет секретного токена webhook — это отдельные проблемы. Цель - это наименьший рабочий путь, который можно вставить в промежуточную конечную точку, а затем расширить.

1. Клиент Bot API

Две вещи имеют значение, когда вы вызываете API Telegram Bot из PHP: код состояния HTTP, возвращаемый транспортом, и OK флага внутри тела JSON. Telegram с радостью вернет http 200 с {"ok":false, "error_code":400,"description":"Неверный запрос: ..."}и скрипт, который проверяет только curl_getinfo($ch, CURLINFO_HTTP_CODE) будет скучать по нему.

<?php
// telegram.php

function tgApi(string $method, array $params, ?string $token = null): array
{
$token = $token ?? getenv('TELEGRAM_BOT_TOKEN');
if (!$token) {
throw new RuntimeException('TELEGRAM_BOT_TOKEN is not set');
}

$url = 'https://api.telegram.org/bot' . $token . '/' . $method;
$body = http_build_query($params, '', '&');

$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
]);

$raw = curl_exec($ch);
if ($raw === false) {
$err = curl_error($ch);
curl_close($ch);
throw new RuntimeException('cURL error: ' . $err);
}

$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status !== 200) {
throw new RuntimeException('HTTP ' . $status . ' from ' . $method);
}

$decoded = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException('JSON decode failed: ' . json_last_error_msg());
}

if (!is_array($decoded) || !($decoded['ok'] ?? false)) {
$desc = $decoded['description'] ?? 'unknown error';
$code = $decoded['error_code'] ?? 0;
throw new RuntimeException('Telegram ok:false (' . $code . '): ' . $desc);
}

return $decoded['result'];
}

Токен загружается из среды с getenv. Никогда не кодируйте его жестко, никогда не фиксируйте его и никогда не повторяйте его в ответ на ошибку. Если вы предпочитаете конфигурационный файл, прочитайте его один раз при загрузке и вставьте в эту функцию; контракт остается прежним.

2. Создание InlineKeyboardMarkup

InlineKeyboardMarkup это просто массив строк JSON, каждая строка является массивом InlineKeyboardButton объектов. Кнопка имеет тексте и одно из нескольких полей действий: callback_data, URL, веб-приложениеld a, (hl) switch_inline_query_* поле. Для этого урока мы используем только callback_data, что запускает callback_query обновление, которое бот получает на своем веб-перехватчике.

Жесткое ограничение: callback_data представляет собой строку UTF-8 не более 64 байт. Считайте байты, а не символы. Распространенная ошибка заключается в том, чтобы поместить числовой идентификатор и метку в структуру, закодированную в JSON, и отправить ее как callback_data — только JSON составляет более 64 байт практически для любого нетривиального объекта, и ваша клавиатура будет тихо отбрасываться с Неверный ЗАПРОС: BUTTON_DATA_Invalid ошибка.

Чистый подход заключается в отправке короткого непрозрачного токена, а затем разрешить его на стороне сервера. Ниже мы отображаем нравится:42 (7 байт) в сохраненную строку на PHP и сохраните отдельную удобочитаемую метку для текста кнопки.

<?php
// keyboards.php

require_once __DIR__ . '/telegram.php';

/**
* Build an InlineKeyboardMarkup for a "like / dislike" pair on a post.
* $postId is the integer id of a post in your application.
* callback_data is kept strictly under 64 bytes.
*/
function likeDislikeKeyboard(int $postId, ?int $currentVote = null): array
{
// "l:1234567890" -> 1 byte + ':' + up to 10 digits = 12 bytes max.
// Plenty of headroom under 64. If your ids are larger, switch to hex.
$likePayload = 'l:' . $postId;
$dislikePayload = 'd:' . $postId;

$likeText = $currentVote === 1 ? '👍 Liked' : '👍 Like';
$dislikeText = $currentVote === -1 ? '👎 Disliked' : '👎 Dislike';

return [
'inline_keyboard' => [
[
['text' => $likeText, 'callback_data' => $likePayload],
['text' => $dislikeText, 'callback_data' => $dislikePayload],
],
],
];
}

/** Send a post message with the keyboard attached. */
function sendPostWithKeyboard(int $chatId, string $text, int $postId): array
{
return tgApi('sendMessage', [
'chat_id' => $chatId,
'text' => $text,
'reply_markup' => json_encode(likeDislikeKeyboard($postId)),
]);
}

Вариант производства использует bin2hex(random_bytes(7)) (14 шестнадцатеричных символов) для кратковременных операций, таких как подсказки «подтвердить удаление», где вы генерируете идентификатор, ВСТАВЛЯЕТЕ строку, которая сопоставляет его с вашей реальной сущностью, а затем отправляете клавиатуру. Никогда не поворачивать вспять: отправьте клавиатуру до того, как появится строка, или два одновременных щелчка могут ускориться.

3. Вебхук: получение callback_query

Telegram поставляет обновления в виде JSON по HTTPS. Для callback_query, форма:

{
"update_id": 123456789,
"callback_query": {
"id": "...",
"from": {"id": 111, "is_bot": false, "first_name": "..."},
"chat_instance": "...",
"message": {
"message_id": 17,
"chat": {"id": 111, "type": "private"},
"date": 1700000000,
"text": "..."
},
"data": "l:42"
}
}

Вы должны сделать две вещи после его получения: позвонить answerCallbackQuery с идентификатором запроса и либо отредактируйте сообщение, либо отправьте новое. answerCallbackQuery звонок - это то, что закрывает индикатор «загрузка…» на кнопке; пользователь видит крошечное уведомление (текст передается как тексте параметр), и спиннер уходит. Пропускать его в продакшене необязательно: пользователь увидит, что его кнопка застряла до тех пор, пока не истечет время ожидания клиента Telegram.

<?php
// webhook.php (front controller)

declare(strict_types=1);

require_once __DIR__ . '/telegram.php';
require_once __DIR__ . '/keyboards.php';

header('Content-Type: application/json');

$raw = file_get_contents('php://input');
if ($raw === false || $raw === '') {
http_response_code(400);
echo json_encode(['error' => 'empty body']);
return;
}

$update = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($update)) {
http_response_code(400);
echo json_encode(['error' => 'bad json']);
return;
}

try {
if (isset($update['callback_query'])) {
handleCallback($update['callback_query']);
} elseif (isset($update['message']['text']) && $update['message']['text'] === '/start') {
// Demo: send a post with like/dislike.
sendPostWithKeyboard(
(int) $update['message']['chat']['id'],
'Demo post — try the buttons.',
42
);
}
} catch (Throwable $e) {
// Log and still return 200 so Telegram does not retry forever.
error_log('[telegram] ' . $e->getMessage());
}

http_response_code(200);
echo '{"ok":true}';

function handleCallback(array $cq): void
{
$queryId = (string) $cq['id'];
$data = (string) ($cq['data'] ?? '');
$message = $cq['message'] ?? null;
$chatId = (int) ($message['chat']['id'] ?? 0);
$messageId = (int) ($message['message_id'] ?? 0);

// Parse "l:42" / "d:42". Anything malformed -> answer with an alert.
if (!preg_match('/^([ld]):(\d{1,10})$/', $data, $m)) {
tgApi('answerCallbackQuery', [
'callback_query_id' => $queryId,
'text' => 'Unknown action.',
'show_alert' => true,
]);
return;
}

$vote = $m[1] === 'l' ? 1 : -1;
$postId = (int) $m[2];

// Persist the vote. Implementation depends on your storage; the point
// here is that we resolve the opaque payload into a domain object.
$current = recordVote($chatId, $postId, $vote);

// 1) Always answer the callback so the client stops spinning.
tgApi('answerCallbackQuery', [
'callback_query_id' => $queryId,
'text' => 'Recorded.',
]);

// 2) Optionally edit the original message so the button reflects state.
tgApi('editMessageReplyMarkup', [
'chat_id' => $chatId,
'message_id' => $messageId,
'reply_markup' => json_encode(likeDislikeKeyboard($postId, $current)),
]);
}

function recordVote(int $chatId, int $postId, int $vote): int
{
// Placeholder: real implementation would UPSERT into your DB and
// return the resulting vote state for this (chatId, postId) pair.
// We just echo the new state so the keyboard updates visibly.
static $store = [];
$store[$chatId][$postId] = ($store[$chatId][$postId] ?? 0) === $vote ? 0 : $vote;
return $store[$chatId][$postId];
}

Три вещи, на которые стоит обратить внимание.

Первый вопрос: answerCallbackQuery называется безоговорочно, даже если последующее editMessageReplyMarkup; бросает. Заверните редактирование в свою попытку/уловку, если вы хотите сохранить ответ независимо. Документы Telegram явные: ответ на запрос обратного вызова должен быть дан в течение ~30 секунд или клиент перестанет ждать.

Во-вторых, editMessageText и editMessageReplyMarkup; поднимет и то, и другое Неверный запрос: сообщение не изменено если вы отправляете идентичный контент. То есть not ошибка, которую стоит повторить; обработайте ее как no-op. Вы можете обнаружить ее, проверив сообщение о выброшенном исключении перед входом в систему.

В-третьих, когда вы создаете новый текст сообщения с помощью parse_mode=HTML, экранируйте пользовательский ввод с помощью htmlspecialchars($s, ENT_QUOTES | ENT_REPLUTE, 'UTF-8') . вы вставляете его в HTML. API бота не анализирует Markdown каким-либо безопасным способом, а HTML-инъекция из чата - это реальный риск, если вы вставляете имена в ответ.

4. Производственные заметки (помеченные как таковые)

Это элементы, которые превращают фрагмент выше во что-то, что вы можете отправить. Ни один из них не требуется для того, чтобы учебное пособие работало в постановке.

- Секретный код webhook-а. В качестве секретный токен где setWebhook. Telegram отправит его в X-Telegram-Bot-Api-Secret-Token header. Отклоните любой запрос, если заголовок отсутствует или не совпадает. Это мешает случайным актерам публиковать поддельные обновления на вашей конечной точке. - Идемпотентность Telegram может повторно доставить то же самое update_id. Храните самые высокие update_id вы обработали (DB или Redis) и удалили дубликаты. Не полагайтесь на один курсор в памяти; он теряется при перезапуске и ломается в тот момент, когда вы запускаете более одного работника. - Ограничения ставки. API бота возвращает 429 с parameters.retry_afterОплетание. tgApi поэтому он спит и повторяет ограниченное количество раз; не повторяйте вечно. Для ботов с большим объемом вызовов отправьте звонки в исходящую очередь с идентификатором чата, поскольку Telegram также ограничивает флуды для каждого чата. - callback_data design. Оставайтесь под 64 байтами; бюджет префикса тоже. Если вам нужно закодировать более одного поля, отдайте предпочтение короткому маркеру перед подробной строкой и разрешите его на стороне сервера. Шестнадцатеричный код из bin2hex(random_bytes(7)) дает 14 символов энтропии, что достаточно для быстрого подтверждения и тривиально подходит. - Глубокие ссылки t.me/YourBot?start=payload является только Увольнение ПУСК, а не на встроенных клавиатурах. Если вашей клавиатуре необходимо открыть приватный чат с полезной нагрузкой, сгенерируйте https://t.me/YourBot?start=... Кнопка URL вместо кнопки обратного вызова.

5. Быстрый контрольный список здравомыслия

- callback_data ≤ 64 байт, считается как UTF-8 байт, а не символы. - answerCallbackQuery вызывается в течение ~30 секунд, даже при ошибках. - HTTP-статус - 200 и Тело JSON имеет {"ok": true} прежде чем считать звонок успешным. - HTML-эскейп все, что пользователь контролирует до parse_mode=HTML. - Секрет веб-перехватчика проверен, дубликаты обновлений отброшены на update_id.

Вот и весь цикл: клавиатура при отправке, обратный вызов при получении, ответ + редактирование, короткая полезная нагрузка. Как только эти части щелкнут, остальная поверхность встроенной клавиатуры (switch_inline_query, веб-приложение, разбитые на страницы списки) - это просто вариации одной и той же сантехники.

Если вы в конечном итоге отправляете нетривиального бота вокруг этих примитивов — пользовательских клавиатур, мини-приложений, потоков платежей — BotCreator - это студия, которая отправляет ботов Telegram и мини-приложения из конца в конец. Для быстрого ознакомления с API вместе с этим руководством, страница Telegram Bot API docs является разумной закладкой.

Новые статьи — в Telegram

Разбираем, что автоматизировать в бизнесе и как это работает на практике. Без спама.