Обробка Telegram InlineKeyboardMarkup на PHP: callback_query, answerCallbackQuery та обмеження корисного навантаження 64 байт…

InlineKeyboardMarkup на PHP

У цьому уроці ми зібрали невеликий обробник PHP для вбудованих клавіатур Telegram. Ви:

  • зібрати кнопки, які callback_data знаходиться в межах ліміту API бота в 64 байти;
  • прийняти оновлення за допомогою callback_query;
  • зателефонувати answerCallbackQueryзняти спінер на кнопці;
  • оновити повідомлення в editMessageReplyMarkup;
  • використовувати тонку обгортку cURL зі статусом http та перевіркою прапорця Гаразд.

Це не повноцінний фреймворк бота: без DI, черги та секретного токена вебхука. Мета — це робочий шлях для постановки, який легко розширити.

1. Клієнт API бота

Перевірте HTTP-код і поле Гаразд в JSON. Telegram часто відповідає HTTP 200 з тілом {"ok":false,...}.

<?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'];
}

Візьміть маркер з навколишнього середовища. Не кодуйте його жорстко і не повертайте його в текстах помилок.

2. Збірка InlineKeyboardMarkup

InlineKeyboardMarkup — JSON масив рядків кнопок. Кнопка має text та одне поле діяльності: callback_data, url, web_app від кулі switch_inline_query_*. Тут ми використовуємо callback_data — генерує callback_query на вебхук.

Жорсткий ліміт: callback_data не больше. 64 байта UTF-8 (байти, а не символи) Об 'єкти JSON майже завжди перевищують ліміт — отримати BUTTON_DATA_INVALID.

Надішліть короткий непрозорий токен і вирішіть його на сервері. Нижче l:42 / d:42 вписуються в бюджет з маржею.

<?php
// keyboards.php

require_once __DIR__ . '/telegram.php';

function likeDislikeKeyboard(int $postId, ?int $currentVote = null): array
{
// "l:1234567890" -> максимум ~12 байт
$likePayload = 'l:' . $postId;
$dislikePayload = 'd:' . $postId;

$likeText = $currentVote === 1 ? 'Нравится' : 'Лайк';
$dislikeText = $currentVote === -1 ? 'Не нравится' : 'Дизлайк';

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

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. Webhook: прийом 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 із ідентифікатором запиту, а потім відредагуйте або надішліть нове повідомлення. Без відповіді кнопка обертає обертач до тайм-ауту клієнта.

<?php
// webhook.php

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') {
sendPostWithKeyboard(
(int) $update['message']['chat']['id'],
'Демо-пост — нажмите кнопки.',
42
);
}
} catch (Throwable $e) {
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);

if (!preg_match('/^([ld]):(\d{1,10})$/', $data, $m)) {
tgApi('answerCallbackQuery', [
'callback_query_id' => $queryId,
'text' => 'Неизвестное действие.',
'show_alert' => true,
]);
return;
}

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

tgApi('answerCallbackQuery', [
'callback_query_id' => $queryId,
'text' => 'Сохранено.',
]);

tgApi('editMessageReplyMarkup', [
'chat_id' => $chatId,
'message_id' => $messageId,
'reply_markup' => json_encode(likeDislikeKeyboard($postId, $current)),
]);
}

function recordVote(int $chatId, int $postId, int $vote): int
{
static $store = [];
$store[$chatId][$postId] = ($store[$chatId][$postId] ?? 0) === $vote ? 0 : $vote;
return $store[$chatId][$postId];
}

Три важливі моменти:

  • Відповідайте на зворотний дзвінок, навіть якщо наступне редагування не вдалося. Telegram чекає на відповідь близько 30 секунд.
  • editMessageText / editMessageReplyMarkup може повернути "повідомлення не змінено" з тим самим вмістом — це неможливо, а не причина для відступу.
  • При parse_mode=HTML захистити користувацький ввід через htmlspecialchars перед вставкою в HTML.

4. Виробничі примітки

  • Секрет вебхука. Встановити secret_token в setWebhook і перевірте назву X-Telegram-Bot-Api-Secret-Token.
  • Ідемпотентність. Магазин оброблено update_id і відкиньте дублікати.
  • Ліміти О 429 спостерігати parameters.retry_after. Коли навантаження високе, поставте вихідні дзвінки в чергу з ідентифікатором чату.
  • Callback_data design. Зберігати менше 64 байт. Короткий жетон краще, ніж довгий рядок.
  • Deeplinks. t.me/Bot?start=payload спрацьовує лише на Start. Щоб відкрити приватний чат з корисним навантаженням з клавіатури, скористайтеся кнопкою URL-адреси.

5. Контрольний список

  • callback_data ≤ 64 байт UTF-8
  • answerCallbackQuery протягом ~30 секунд, навіть з помилками
  • HTTP 200 і "ok":true перш ніж виклик вважатиметься успішним
  • Екранування HTML користувацького тексту, коли parse_mode=HTML
  • Секрет веб-перехоплювача перевірено; дублікати відфільтровано за update_id

Клавіатура при відправці, зворотний дзвінок при отриманні, відповідь плюс редагування, коротке корисне навантаження — весь цикл.

Потрібно більше, ніж фрагмент: клавіатури, міні-додатки, платежі? Почніть з botservice.biz.

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

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