Обработка обновлених Telegram-повідомлень всередині HTTP-запросу є хрупкою. Telegram очікує швидкого 200 OK. Впроваджене повільне робовання з базою даних, зовнішні API або складна логіка призводять до таймаутів та повторних спроб — а ці повторні спроби можуть запустити одне й те саме оновлення двічі.
Стабільний стек веб-перекидчиків Laravel виконує чотирнадцять речей: перевіряє секретный токен, швидко підтверджує, дедуплікує update_id і обробляє роботу в очереді.
Це посібник описує цей конвейер: проміжне програмне забезпечення для секретного токена веб-перекидчика, ідемпотентність Redis (або DB) та розміщення у очерті для реальної роботи.
\n\nЩо ми будуємо (і що пропускаємо)
\nМи створюємо внутрішній шлях для отримання, перевірки, дедуплікації та розміщення в очерті оновлень Telegram. Ми не створюємо повну діалогову структуру, довгий опитування чи інтеграцію міні-прикладів.
\n\nШаг 1: Конфігурація та маршрутизація
\nХраніть учетні дані поза джерелом. Поместіть їх у Захист екології і відповідайте на них через конфігурацію Laravel.
TELEGRAM_BOT_TOKEN=123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ
TELEGRAM_WEBHOOK_SECRET=a_secure_random_string_here\nРеєструйте їх у config/services.php:
return [
// ... other services
'telegram' => [
'token' => env('TELEGRAM_BOT_TOKEN'),
'webhook_secret' => env('TELEGRAM_WEBHOOK_SECRET'),
],
];\nTelegram публікує оновлення як СТОЙКА. Визначте маршрут у 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);\n\nШаг 2. Проміжне ПО для секретного токена
\nКоли ви викликаєте setWebhook, ви проходите через секретний токен. Потім Telegram надсилає цю значення на кожен запит у X-Telegram-Bot-Api-Secret-Token header. Відкиньте все, що не відповідає.
php artisan make:middleware VerifyTelegramSecret\nІнтегруйте його у 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);
}
}\nВикористання hash_equals робить порівняння безпечним з точки зору часу.
Шаг 3: Контролер та ідемпотентність
\nКожне оновлення має унікальний update_id. Якщо ви відповідаєте повільно, Telegram може повторити ту саму полезну нагадування. Дедуплікуйте, перш ніж ставити роботу в очерті.
З використанням Redis встановіть ключ з TTL, використовуючи NX (тільки якщо його немає). Якщо ключ вже існує, поверніть 200 і пропустіть завдання.
php artisan make:controller TelegramWebhookController --invokable\n<?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);
}
}\nБез Redis вставте update_id у таблицю з унікальним обмеженням. Вловить помилку дублювання ключа і все ж поверніть 200.
Шаг 4. Задання в очерті
\nЗавдання аналізує оновлення, запускає бізнес-логіку та викликає API бота. Держте HTTP-відповідь яскраво; зробіть роботу тут.
\nphp artisan make:job ProcessTelegramUpdate\n<?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(),
]);
}
}\n\nПроизводційні замітки
\nОбмеження даних обратного виклику
\ncallback_data має колапс при 64 Байт. Не наповнюйте великі JSON у кнопки. Зберігайте стан у Redis або базі даних і введайте короткий ідентифікатор у кнопку.
Обмеження швидкості та повторні спроби
\nTelegram обмежує вихідний трафік (приблизно 30 повідомлень на секунду по всьому світу, близько 1 на хвилину на чат). За HTTP 429 читання, @ item: intext Access permission, concatenated Повторне зв'язання після: (або parameters.retry_after) та розпустіть завдання з цим затриманням.
Стан запису перед підтвердженням
\nДля платежів або lead'ів зафіксуйте запис у базі даних перед наданням підтвердження. Приклад:
\n$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>");\nЯкщо вставка не працює, завдання не працює, і користувач ніколи не отримує фальшивого підтвердження.
\n\nПотрібна студія, яка постачає цей тип стеку веб-перекидчиків для ботів та міні-прикладів? Початок у botservice.biz.
"}