Передача контекста при переходе пользователя в 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 разработает отказоустойчивое решение под ключ.