Передача контексту під час переходу користувача в Telegram-бота — стандартне завдання для CRM, интернет-магазин ів та маркетингових кампаній. Механізм Deep Linking дозволяє передати в бота параметр через URL формату https://t.me/YourBot?start=payload. Коли користувач натискає кнопку «Запустити», Telegram надсилає вашому серверу вхідне повідомлення з текстом /start payload.
Однак діплінки часто плутають із системою керування станами діалогу (FSM). Параметр start спрацьовує одноразово в момент відправки команди запуску і слугує лише точкою входу, яка передає зовнішній контекст (ID реферала, код акції, ID заявки з CRM). У цій статті ми розберемо обмеження протоколу, способи запобігання спуфінгу параметрів та продакшен-реалізацію обробки діплінків на PHP.
Механіка Deep Linking у Telegram Bot API та жорсткі ліміти
Telegram накладає суворо регламентовані технічні обмеження на строковий параметр start:
- Допустимі символи: латинські літери
A-Z,a-z, цифри0-9, а також символи підкреслення_та дефіса-. Символи на кшталт=,?,&,%або кирилиця заборонені. - Максимальна довжина: рівно 64 байти.
- Специфіка виклику: якщо користувач уже запускав бота раніше, повторний клік по посиланню
t.me/YourBot?start=payloadвідкриє діалог з ботом, але кнопка «Запустити» внизу чату з'явиться знову. Повідомлення/start payloadбуде відправлено на вебхук лише після фізичного кліку по цій кнопці.
Головна помилка недосвідчених розробників — спроба помістити в payload некодовані JSON-об'єкти або зв'язки ключів виду lead_123_user_456_promo_winter. Такий підхід швидко впирається в ліміт 64 байти та провокує вразливості безпеки.
Безпечно передаємо контекст: генерація та мапінг токенів у БД
Передавати відкриті ідентифікатори сутностей із бази даних (наприклад, ?start=order_1540) вкрай небезпечно. Будь-який користувач може вручну замінити число в URL і запустити бізнес-логіку для чужого замовлення. Також не можна покладатися на сесії ($_SESSION), оскільки перехід відбувається із зовнішнього застосунку Telegram, де куки веббраузера недоступні.
Безпечний підхід полягає у використанні одноразових або тимчасових криптографічно стійких токенів. Токен генерується на стороні бекенду, зберігається в БД із прив'язкою до цільової сутності, після чого формується підсумкове посилання.
<?php
declare(strict_types=1);
function createLeadDeepLink(PDO $pdo, string $botUsername, string $entityType, int $entityId): string
{
// Генерируем 7 случайных байт и переводим в hex (ровно 14 символов, [a-f0-9])
$leadToken = bin2hex(random_bytes(7));
$stmt = $pdo->prepare('
INSERT INTO bot_deep_links (token, entity_type, entity_id, created_at, is_used)
VALUES (:token, :type, :id, NOW(), 0)
');
$stmt->execute([
':token' => $leadToken,
':type' => $entityType,
':id' => $entityId,
]);
return "https://t.me/{$botUsername}?start={$leadToken}";
}
Довжина отриманого токена — 14 байтів, що повністю вкладається в ліміт 64 байти. Навіть якщо зловмисник спробує перебрати значення (brute-force), ентропія в 56 біт зробить атаку нереалізовною.
Обробка Webhook: перевірка Secret Token та захист від дублікатів
При отриманні запиту від Telegram необхідно вирішити два завдання мережевої безпеки та надійності:
- Аутентифікація джерела: валідація заголовка
X-Telegram-Bot-Api-Secret-Token, заданого під час викликуsetWebhook. - Ідемпотентність: виключення повторної обробки однієї й тієї ж події при збоях мережі (Telegram повторно відправляє Update, якщо бекенд не відповів HTTP-кодом 200 вчасно). Дедуплікація виконується за
update_id.
<?php
declare(strict_types=1);
$secretToken = getenv('TELEGRAM_WEBHOOK_SECRET') ?: '';
$botToken = getenv('TELEGRAM_BOT_TOKEN') ?: '';
// 1. Валидация Secret Token
$incomingSecret = $_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] ?? '';
if ($secretToken !== '' && !hash_equals($secretToken, $incomingSecret)) {
http_response_code(403);
echo json_encode(['error' => 'Access denied']);
exit;
}
$rawInput = file_get_contents('php://input');
if (!$rawInput) {
http_response_code(400);
exit;
}
$update = json_decode($rawInput, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($update)) {
http_response_code(400);
exit;
}
$pdo = new PDO('mysql:host=localhost;dbname=app_db;charset=utf8mb4', 'db_user', 'db_pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
// 2. Проверка идемпотентности по update_id
$updateId = (int)($update['update_id'] ?? 0);
if ($updateId > 0) {
$stmt = $pdo->prepare('INSERT IGNORE INTO processed_updates (update_id, created_at) VALUES (:id, NOW())');
$stmt->execute([':id' => $updateId]);
if ($stmt->rowCount() === 0) {
// Уведомление уже обрабатывалось ранее
echo json_encode(['ok' => true, 'description' => 'Duplicate update ignored']);
exit;
}
}
Парсинг команди /start та виконання бізнес-логіки
Коли валідацію запиту завершено, витягуємо текст повідомлення. Повідомлення від Telegram з діплінком містить text вигляду /start 4a8b1c9d0e2f3a. Нам потрібно відокремити команду /start від самого payload і зіставити його із записом у базі даних.
<?php
$message = $update['message'] ?? null;
$text = trim($message['text'] ?? '');
$chatId = $message['chat']['id'] ?? null;
if ($chatId && strpos($text, '/start') === 0) {
$parts = explode(' ', $text, 2);
$payload = isset($parts[1]) ? trim($parts[1]) : null;
handleStartCommand($pdo, $botToken, (int)$chatId, $payload);
}
echo json_encode(['ok' => true]);
function handleStartCommand(PDO $pdo, string $botToken, int $chatId, ?string $payload): void
{
if ($payload === null || $payload === '') {
$replyText = "Добро пожаловать! Вы запустили бота напрямую.";
sendTelegramMessage($botToken, $chatId, $replyText);
return;
}
// Проверяем соответствие payload регулярному выражению лимита Telegram
if (!preg_match('/^[a-zA-Z0-9_-]{1,64}$/', $payload)) {
sendTelegramMessage($botToken, $chatId, "Ошибка: недействительный формат ссылки.");
return;
}
// Ищем токен в БД
$stmt = $pdo->prepare('
SELECT id, entity_type, entity_id, is_used
FROM bot_deep_links
WHERE token = :token
LIMIT 1
');
$stmt->execute([':token' => $payload]);
$linkData = $stmt->fetch();
if (!$linkData) {
sendTelegramMessage($botToken, $chatId, "Ссылка не найдена или ее срок действия истек.");
return;
}
// Пример обработки сущности (например, привязка лида к CRM)
$entityType = $linkData['entity_type'];
$entityId = (int)$linkData['entity_id'];
// Делаем токен использованным (если диплинк одноразовый)
$updateStmt = $pdo->prepare('UPDATE bot_deep_links SET is_used = 1 WHERE id = :id');
$updateStmt->execute([':id' => $linkData['id']]);
$replyText = "Контекст успешно принят! Тип: <strong>{$entityType}</strong>, ID: <strong>{$entityId}</strong>.";
sendTelegramMessage($botToken, $chatId, $replyText);
}
Взаємодія з Telegram Bot API через cURL
Для відправки повідомлень у Telegram не слід застосовувати функцію file_get_contents, оскільки вона не забезпечує гнучкого налаштування таймаутів та коректної обробки мережевих помилок. Використовуємо cURL із явною перевіркою HTTP-кодів відповіді та структури JSON-відповіді Telegram API.
<?php
declare(strict_types=1);
function sendTelegramMessage(string $botToken, int $chatId, string $text): array
{
$url = "https://api.telegram.org/bot{$botToken}/sendMessage";
$payload = [
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => 'HTML',
'disable_web_page_preview' => true,
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 10,
]);
$rawResponse = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($rawResponse === false) {
throw new RuntimeException("cURL Error: {$curlError}");
}
if ($httpCode !== 200) {
throw new RuntimeException("Telegram API returned HTTP code {$httpCode}: {$rawResponse}");
}
$response = json_decode((string)$rawResponse, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException("Failed to decode Telegram JSON response");
}
if (empty($response['ok'])) {
$code = $response['error_code'] ?? 'unknown';
$desc = $response['description'] ?? 'no description';
throw new RuntimeException("Telegram API Error [{$code}]: {$desc}");
}
return $response;
}
Порівняння підходів: Token Mapping vs Signed Payload (HMAC)
Існує два основних архітектурних патерни роботи з start=payload:
1. Мапінг через випадковий токен у БД (Stateful)
Сервер генерує токен (наприклад, через bin2hex(random_bytes(7))) і записує зв'язку в таблицю. При обробці вебхука відбувається SQL-запит.
- Плюси: повна безпека (у посиланні немає реальних ID), можливість анулювати посилання в будь-який момент, можна відстежити статус
is_used. - Мінуси: потрібен запит до БД при кожному кліку.
2. HMAC-підпис даних (Stateless)
Якщо запис у БД небажаний, параметри пакуються та підписуються секретним ключем бекенду: payload = base64url(data) + '.' + hmac_sha256(data, secret). Через ліміт у 64 байти підпис доводиться урізати (наприклад, до 8–12 символів).
- Плюси: не потрібна база даних для перевірки автентичності.
- Мінуси: жорсткі рамки 64 байти сильно обмежують обсяг корисного навантаження; не можна заборонити повторне використання посилання без таблиці використаних підписів.
Для більшості комерційних завдань (авторизація, прив'язка замовлення, сквозная аналитика лідів) варіант із збереженням короткого токена в БД виявляється гнучкішим та надійнішим.
Якщо вам потрібна професійна интеграция Telegram з CRM та сервісами компанії, команда BotCreator розробить відмовостійке рішення під ключ.