Обработка обновлений Telegram внутри HTTP-запроса является хрупкой. Telegram ожидает быструю 200 ОК. Медленная работа с базой данных, внешние API или сложная логика приводят к таймаутам и повторным попыткам — и эти повторные попытки могут запускать одно и то же обновление дважды.
Твердый стек веб-перехватчиков Laravel делает четыре вещи: проверяет секретный токен, быстро подтверждает, дедуплицирует update_idи обрабатывать работу в очереди.
Это руководство описывает этот конвейер: промежуточное программное обеспечение для секрета веб-перехватчика, идемпотентность Redis (или DB) и задание в очереди для реальной работы.
Что мы строим (и что пропускаем)
Мы создаем внутренний путь для получения, проверки, дедупликации и постановки в очередь обновлений Telegram. Мы не создаем полноценную диалоговую структуру, длинный опрос или интеграцию мини-приложений.
Шаг 1: Конфигурация и маршрутизация
Храните учетные данные вне источника. Поместите их в Защита экологии и сопоставить их через конфигурацию Laravel.
TELEGRAM_BOT_TOKEN=123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ
TELEGRAM_WEBHOOK_SECRET=a_secure_random_string_here
Зарегистрируйте их в config/services.php:
return [
// ... other services
'telegram' => [
'token' => env('TELEGRAM_BOT_TOKEN'),
'webhook_secret' => env('TELEGRAM_WEBHOOK_SECRET'),
],
];
Telegram публикует обновления как СТОЙКА. Определите маршрут в routes/api.php (или routes/web.php с исключением CSRF) и прикрепите промежуточное программное обеспечение:
use App\Http\Controllers\TelegramWebhookController;
use App\Http\Middleware\VerifyTelegramSecret;
use Illuminate\Support\Facades\Route;
Route::post('/telegram/webhook', TelegramWebhookController::class)
->middleware(VerifyTelegramSecret::class);
Шаг 2. Промежуточное ПО для секретного токена
Когда вы позвоните setWebhook, пройдите a секретный токен. Затем Telegram отправляет это значение при каждом запросе в X-Telegram-Bot-Api-Secret-Token header. Отклоните все, что не соответствует.
php artisan make:middleware VerifyTelegramSecret
Внедрите его в app/Http/Middleware/VerifyTelegramSecret.php;:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class VerifyTelegramSecret
{
public function handle(Request $request, Closure $next): Response
{
$expectedSecret = config('services.telegram.webhook_secret');
if (empty($expectedSecret)) {
return response()->json(['error' => 'Webhook secret is not configured.'], 500);
}
$providedSecret = $request->header('X-Telegram-Bot-Api-Secret-Token');
if (!$providedSecret || !hash_equals($expectedSecret, $providedSecret)) {
return response()->json(['error' => 'Unauthorized.'], 403);
}
return $next($request);
}
}
Использование hash_equals поэтому сравнение безопасно с точки зрения времени.
Шаг 3: Контроллер и идемпотентность
Каждое обновление имеет уникальную update_id. Если вы отвечаете медленно, Telegram может повторно отправить ту же полезную нагрузку. Дедуплицируйте, прежде чем ставить работу в очередь.
С помощью Redis установите ключ с TTL, используя NX (только если отсутствует). Если ключ уже существует, верните 200 и пропустите задание.
php artisan make:controller TelegramWebhookController --invokable
<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessTelegramUpdate;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
class TelegramWebhookController
{
public function __invoke(Request $request): JsonResponse
{
$payload = $request->all();
if (!isset($payload['update_id'])) {
return response()->json(['error' => 'Invalid payload'], 400);
}
$updateId = (int) $payload['update_id'];
$redisKey = "telegram:update:{$updateId}";
// 24h TTL; set only if the key does not exist
$isUnique = Redis::connection()->client()->set(
$redisKey,
'1',
'EX',
86400,
'NX'
);
if (!$isUnique) {
return response()->json(['status' => 'duplicate_ignored'], 200);
}
ProcessTelegramUpdate::dispatch($payload);
return response()->json(['status' => 'queued'], 200);
}
}
Без Redis, вставьте update_id в таблицу с уникальным ограничением. Поймайте ошибку дубликата ключа и все равно верните 200.
Шаг 4. Задание в очереди
Задание анализирует обновление, запускает бизнес-логику и вызывает API бота. Держите HTTP-отклик светлым; сделайте работу здесь.
php artisan make:job ProcessTelegramUpdate
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Throwable;
class ProcessTelegramUpdate implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $backoff = 5;
public function __construct(protected array $payload)
{
}
public function handle(): void
{
if (isset($this->payload['message']['text'])) {
$chatId = $this->payload['message']['chat']['id'];
$text = $this->payload['message']['text'];
if (str_starts_with($text, '/start')) {
$this->sendTextMessage($chatId, 'Hello! Welcome to our bot.');
}
}
if (isset($this->payload['callback_query'])) {
$callbackQuery = $this->payload['callback_query'];
$callbackQueryId = $callbackQuery['id'];
$data = $callbackQuery['data'] ?? '';
$this->answerCallbackQuery($callbackQueryId, 'Action received: ' . $data);
}
}
protected function sendTextMessage(int $chatId, string $text): void
{
$token = config('services.telegram.token');
$url = "https://api.telegram.org/bot{$token}/sendMessage";
$safeText = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
$response = Http::timeout(10)
->post($url, [
'chat_id' => $chatId,
'text' => $safeText,
'parse_mode' => 'HTML',
]);
if ($response->failed()) {
Log::error('Telegram API Error', [
'status' => $response->status(),
'body' => $response->body(),
]);
throw new \RuntimeException('Failed to send Telegram message');
}
$responseData = $response->json();
if (!($responseData['ok'] ?? false)) {
Log::error('Telegram returned ok=false', ['response' => $responseData]);
throw new \RuntimeException('Telegram API returned success status false');
}
}
protected function answerCallbackQuery(string $callbackQueryId, string $text): void
{
$token = config('services.telegram.token');
$url = "https://api.telegram.org/bot{$token}/answerCallbackQuery";
Http::timeout(5)->post($url, [
'callback_query_id' => $callbackQueryId,
'text' => $text,
'show_alert' => false,
]);
}
public function failed(Throwable $exception): void
{
Log::error('Telegram update processing failed permanently', [
'update_id' => $this->payload['update_id'] ?? null,
'exception' => $exception->getMessage(),
]);
}
}
Производственные заметки
Ограничение данных обратного вызова
callback_data имеет колпачок при 64 Байта. Не набивайте большие JSON на кнопки. Сохраните состояние в Redis или базе данных и введите короткий идентификатор в кнопку.
Ограничения скорости и повторные попытки
Telegram ограничивает исходящий трафик (примерно 30 сообщений в секунду по всему миру, около 1 в секунду на чат). По HTTP 429чтение, @ item: intext Access permission, concatenated Повторное соединение после: (или parameters.retry_after) и отпустите задание с этой задержкой.
Состояние записи перед подтверждением
Для платежей или лидов зафиксируйте запись в базу данных перед отправкой подтверждения. Пример:
$leadId = bin2hex(random_bytes(7));
DB::table('leads')->insert([
'lead_id' => $leadId,
'chat_id' => $chatId,
'created_at' => now(),
]);
$this->sendTextMessage($chatId, "Your lead ID is: <b>{$leadId}</b>");
Если вставка не срабатывает, задание не срабатывает, и пользователь никогда не получает поддельного подтверждения.
Нужна студия, которая поставляет этот вид стека веб-перехватчиков для ботов и мини-приложений? Начало в botservice.biz.