При интеграции Telegram-ботов в приложения Laravel разработчики часто обращаются к тяжелым, самоуверенным SDK. Хотя такие фреймворки, как Nutgram или SDK, такие как irazasyed/telegram-bot-sdk, отлично подходят для сложных диалоговых ботов, они могут привносить ненужную абстракцию, накладные расходы на обслуживание и трение при обновлении во время основных выпусков Laravel.
Для многих приложений тонкая оболочка HTTP-клиента в сочетании с собственными утилитами маршрутизации, очередей и тестирования Laravel является более чистой, быстрой и простой в обслуживании.
В этом уроке демонстрируется, как с нуля построить готовую к производству интеграцию веб-перехватчиков Telegram в Laravel. Мы создадим легкий HTTP-клиент, защитим конечную точку веб-перехватчика, используя заголовок секретного токена Telegram, разгрузим обработку на задание в очереди, чтобы поддерживать время отклика менее 100 мс, реализуем проверку идемпотентности и напишем комплексный интеграционный тест с использованием встроенных инструментов тестирования Laravel. Мы не претендуем на создание полноценного разговорного конечного автомата; вместо этого мы фокусируемся на создании надежного, безопасного и проверяемого архитектурного фундамента.
1. Конфигурация и тонкий HTTP-клиент
Мы начинаем с настройки переменных среды и создания облегченного класса обслуживания для взаимодействия с API Telegram Bot. Этот сервис использует родной Laravel Подсветка\Опора\Фасады\Http клиент, который предоставляет чистый API для отправки запросов, обработки тайм-аутов и насмешек над ответами во время тестирования.
Сначала добавьте свои учетные данные в Telegram config/services.php Файл:
// config/services.php
return [
// ... other services
'telegram' => [
'bot_token' => env('TELEGRAM_BOT_TOKEN'),
'secret_token' => env('TELEGRAM_SECRET_TOKEN'),
],
];
Затем создайте класс обслуживания. Этот класс обрабатывает исходящие запросы к API Telegram Bot, проверяет код состояния HTTP, проверяет OK в ответе JSON и регистрирует сбои.
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use RuntimeException;
class TelegramClient
{
protected string $baseUrl;
public function __construct()
{
$token = config('services.telegram.bot_token');
if (!$token) {
throw new RuntimeException('Telegram Bot Token is not configured.');
}
$this->baseUrl = "https://api.telegram.org/bot{$token}&quo t;;
}
/**
* Send a message to a specific chat.
*/
public function sendMessage(array $payload): array
{
return $this->call('sendMessage', $payload);
}
/**
* Acknowledge a callback query from an inline keyboard.
*/
public function answerCallbackQuery(array $payload): array
{
return $this->call('answerCallbackQuery', $payload);
}
/**
* Execute the HTTP request to the Telegram Bot API.
*/
protected function call(string $method, array $payload): array
{
$response = Http::timeout(10)
->connectTimeout(5)
->post("{$this->baseUrl}/{$method}", $payload);
if (!$response->successful()) {
Log::error("Telegram API HTTP Error on {$method}", [
'status' => $response->status(),
'body' => $response->body(),
]);
throw new RuntimeException("Telegram API returned status code {$response->status()}");
}
$data = $response->json();
if (json_last_error() !== JSON_ERROR_NONE) {
Log::error("Telegram API returned invalid JSON on {$method}", [
'body' => $response->body(),
]);
throw new RuntimeException('Telegram API returned invalid JSON.');
}
if (!($data['ok'] ?? false)) {
Log::error("Telegram API returned ok:false on {$method}", [
'response' => $data,
]);
throw new RuntimeException("Telegram API error: " . ($data['description'] ?? 'Unknown error'));
}
return $data;
}
}
2. Защита и маршрутизация веб-перехватчика
Telegram позволяет указать X-Telegram-Bot-Api-Secret-Token заголовка при настройке веб-перехватчика. Telegram будет передавать этот токен при каждом входящем запросе. Ваше приложение должно подтвердить этот токен, чтобы убедиться, что запрос исходит от Telegram, а не от несанкционированной третьей стороны.
Сначала определите маршрут в routes/api.php:
use App\Http\Controllers\TelegramWebhookController;
use Illuminate\Support\Facades\Route;
Route::post('/telegram/webhook', TelegramWebhookController::class)
->name('telegram.webhook');
Затем создайте контроллер. Исключительной ответственностью контроллера является проверка входящего запроса как можно быстрее, отправка задания в очереди для обработки тяжелого подъема и возврат HTTP 200 ОК ответ. Telegram повторяет попытку доставки, если ваш сервер не отвечает быстро, поэтому вы не должны выполнять запросы к базе данных, вызовы API или сложную бизнес-логику непосредственно внутри контроллера.
namespace App\Http\Controllers;
use App\Jobs\ProcessTelegramUpdate;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
class TelegramWebhookController
{
public function __invoke(Request $request): Response
{
$configuredSecret = config('services.telegram.secret_token');
$incomingSecret = $request->header('X-Telegram-Bot-Api-Secret-Token');
if (!$configuredSecret || !hash_equals($configuredSecret, (string) $incomingSecret)) {
Log::warning('Unauthorized Telegram webhook attempt detected.', [
'ip' => $request->ip(),
]);
return response('Unauthorized', 403);
}
$payload = $request->all();
if (!isset($payload['update_id'])) {
return response('Invalid payload', 400);
}
// Dispatch the job to the queue
ProcessTelegramUpdate::dispatch($payload);
return response('OK', 200);
}
}
3. Обработка обновлений с поставленными в очередь заданиями и идемпотентностью
Поскольку проблемы с сетью могут привести к тому, что Telegram повторит попытку отправки обновления, которое ваше приложение уже обработало, вы должны реализовать идемпотентность. Мы можем достичь этого, сохраняя обработанные update_id значения в Redis или кэше базы данных.
Создайте задание в очереди с помощью Artisan:
php artisan make:job ProcessTelegramUpdate
Откройте созданное задание и реализуйте логику обработки. В этом примере мы проверяем наличие повторяющихся обновлений, избегаем динамического ввода данных пользователем, используя htmlspecialchars для предотвращения ошибок синтаксического анализа HTML и обработки как стандартных текстовых сообщений, так и запросов обратного вызова.
namespace App\Jobs;
use App\Services\TelegramClient;
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\Cache;
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(TelegramClient $telegram): void
{
$updateId = $this->payload['update_id'];
$cacheKey = "telegram_update:{$updateId}";
// Prevent processing the same update multiple times (1-hour lock)
if (!Cache::add($cacheKey, true, 3600)) {
Log::info("Duplicate Telegram update ignored: {$updateId}");
return;
}
try {
if (isset($this->payload['message'])) {
$this->handleMessage($this->payload['message'], $telegram);
} elseif (isset($this->payload['callback_query'])) {
$this->handleCallbackQuery($this->payload['callback_query'], $telegram);
}
} catch (Throwable $e) {
// Release the lock on failure so the retried job can run
Cache::forget($cacheKey);
throw $e;
}
}
protected function handleMessage(array $message, TelegramClient $telegram): void
{
$chatId = $message['chat']['id'] ?? null;
$text = $message['text'] ?? '';
if (!$chatId) {
return;
}
// Example: Handle deep-linking payload from t.me/Bot?start=payload
if (str_starts_with($text, '/start')) {
$parts = explode(' ', $text, 2);
$startPayload = $parts[1] ?? null;
if ($startPayload) {
// Map start payload to application state here
Log::info("User started bot with payload: {$startPayload}");
}
}
$safeName = htmlspecialchars($message['from']['first_name'] ?? 'User', ENT_QUOTES, 'UTF-8');
$telegram->sendMessage([
'chat_id' => $chatId,
'text' => "Hello, <b>{$safeName}</b>! Welcome to our service.",
'parse_mode' => 'HTML',
]);
}
protected function handleCallbackQuery(array $callbackQuery, TelegramClient $telegram): void
{
$callbackQueryId = $callbackQuery['id'];
$data = $callbackQuery['data'] ?? '';
$chatId = $callbackQuery['message']['chat']['id'] ?? null;
// Always answer callback queries to remove the loading state on the user's screen
$telegram->answerCallbackQuery([
'callback_query_id' => $callbackQueryId,
]);
if ($chatId && $data === 'ping') {
$telegram->sendMessage([
'chat_id' => $chatId,
'text' => 'Pong!',
]);
}
}
}
4. Написание интеграционных тестов с поддельными обновлениями
Чтобы убедиться, что ваша интеграция остается функциональной во время обновления приложений, напишите тест интеграции. Мы будем имитировать очередь и HTTP-клиент, чтобы убедиться, что контроллер проверяет секретный токен, отклоняет несанкционированные запросы и отправляет задание обработки с правильной полезной нагрузкой.
Создайте тест функций:
php artisan make:test TelegramWebhookTest
Внедрите тестовые примеры:
namespace Tests\Feature;
use App\Jobs\ProcessTelegramUpdate;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class TelegramWebhookTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config([
'services.telegram.secret_token' => 'super-secret-token',
'services.telegram.bot_token' => '123456:ABC-DEF',
]);
}
public function test_it_rejects_requests_with_missing_or_invalid_secret_token(): void
{
Queue::fake();
$payload = [
'update_id' => 100001,
'message' => [
'chat' => ['id' => 12345],
'text' => 'Hello',
],
];
// Missing token
$response = $this->postJson(route('telegram.webhook'), $payload);
$response->assertStatus(403);
// Invalid token
$response = $this->postJson(route('telegram.webhook'), $payload, [
'X-Telegram-Bot-Api-Secret-Token' => 'wrong-token',
]);
$response->assertStatus(403);
Queue::assertNothingDispatched();
}
public function test_it_accepts_valid_requests_and_dispatches_queued_job(): void
{
Queue::fake();
$payload = [
'update_id' => 100002,
'message' => [
'chat' => ['id' => 12345],
'text' => 'Hello',
],
];
$response = $this->postJson(route('telegram.webhook'), $payload, [
'X-Telegram-Bot-Api-Secret-Token' => 'super-secret-token',
]);
$response->assertStatus(200);
$response->assertSeeText('OK');
Queue::assertDispatched(ProcessTelegramUpdate::class, function ($job) use ($payload) {
return $job->payload['update_id'] === $payload['update_id'];
});
}
}
5. Соображения, связанные с производством
При развертывании этой настройки в производственной среде учитывайте следующие архитектурные ограничения:
* Ограничения скорости (HTTP 429): Telegram ограничивает исходящие сообщения до 30 сообщений в секунду во всех чатах и 1 сообщение в секунду для определенного чата. Если вы достигнете этих лимитов, Telegram вернет HTTP Слишком много запросов код состояния с Повторное соединение после: поле. Убедитесь, что рабочая конфигурация очереди корректно обрабатывает повторные попытки, или внедрите драйвер очереди с ограничением скорости (например, ограничение скорости Redis) для ограничения исходящих запросов. * Ограничения данных обратного вызова: - callback_data поле на встроенных клавиатурах имеет строгий предел 64 байта. Если вам нужно передать сложное состояние, сгенерируйте уникальный короткий ключ (например, используя bin2hex(random_bytes(7))), сохраните состояние в базе данных или кэше и передайте только ключ в полезной нагрузке обратного вызова. * Регистрация вебхука Вы должны зарегистрировать URL-адрес веб-перехватчика в Telegram один раз. Вы можете сделать это, отправив запрос на ПУБЛИКАЦИЮ на адрес https://api.telegram.org/bot<YOUR_TOKEN>/setWebhoo k потребностям URL и секретный токен параметры. Храните этот скрипт в шаге развертывания или в команде Artisan.
Для получения более подробной информации об основных конечных точках API и структурах полезной нагрузки см. официальную документацию по адресу https://botservice.biz/telegram-bot-ap i.
Если вам нужна помощь в создании, масштабировании или защите ваших интеграций в Telegram, посетите BotCreator — студию, которая поставляет ботов Telegram/мини-приложения.