Інтеграція веб-перекидників Telegram у Laravel: тестування тонких клієнтів, очірок та підділених оновлень

При інтеграції Telegram-бот ов у приложення Laravel розробники часто звертаються до важких, самовпевнених SDK. Хоча такі фреймворки, як Nutgram або SDK, такі як irazasyed/telegram-bot-sdk, чудово підходять для складних діалогових ботів, вони можуть приносити непотрібну абстракцію, накладні витрати на обслуговування та тертя при оновленні під час основних релізів Laravel.

\n

Для багатьох прикладів тонкий HTTP-клієнт, поєднуваний зі своїми утилітами маршрутизації, очередей та тестування Laravel, є більш чистим, швидким і простішим у обслуговування.

\n

У цьому урокі демонструється, як з нуля побудувати готовий до виробництва інтеграцію веб-перекидників Telegram у Laravel. Ми створимо легкий HTTP-клієнт, захистимо кінцевий ендпоінт веб-перекидника, використовуючи заголовок секретного токена Telegram, відкладимо обробку на очередь, щоб підтримувати час відповіді менше 100 мс, реалізуємо перевірку ідемпотентності та написамо комплексний інтеграційний тест з використанням вбудованих інструментів тестування Laravel. Ми не претендуємо на створення повноцінного діалогового конечного автомата; замість цього ми фокусуємося на створенні надійного, безпечного і перевіреного архітектурного фундаменту.

\n

1. Конфігурація та тонкий HTTP-клієнт

\n

Ми починаємо з налаштування перемень середовища та створення легкого класу обслуговування для взаємодії з API Telegram Bot. Цей сервіс використовує оригінальний Laravel Подсвітка\\Опора\\Фасади\\HTTP клієнта, який забезпечує чистий API для надсилки замовлень, обробки таймаутів та іронії над відповідями під час тестування.

\n

Перш ніж це, додайте свої дані доступу в Telegram config/services.php Файл:

\n
// config/services.php
return [
// ... other services
'telegram' => [
'bot_token' => env('TELEGRAM_BOT_TOKEN'),
'secret_token' => env('TELEGRAM_SECRET_TOKEN'),
],
];
\n

Потім створийте клас обслуговування. Цей клас обробляє вихідні запити до API Telegram Bot, перевіряє код стану HTTP, перевіряє OK у відповіді JSON та реєструє помилки.

\n
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;
}
}
\n

2. Захист і маршрутизація веб-перехватника

\n

Telegram дозволяє вказати X-Telegram-Bot-Api-Secret-Token заголовок при налаштуванні веб-перекидника. Telegram передасть цей токен при кожному входячому заявленні. Ваш приклад має підтвердити цей токен, щоб переконатися, що заявлення йде від Telegram, а не від несанкционуваної третьєї сторони.

\n

Потім визначте маршрут у routes/api.php:

\n
use App\Http\Controllers\TelegramWebhookController;
use Illuminate\Support\Facades\Route;

Route::post('/telegram/webhook', TelegramWebhookController::class)
->name('telegram.webhook');
\n

Потім створийте контролер. Існульною відповідальністю контролера є перевірка входячого заявлення так швидко, як можливо, надсилка задания в очереди для обробки важкого підйому та повернення HTTP 200 OK відповіді. Telegram повторить спробу доставки, якщо ваш сервер не відповідає швидко, тому ви не повинні виконувати запити до базі даних, виклики API або складну бізнес-логіку безпосередньо всередині контролера.

\n
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);
}
}
\n

3. Обробка оновлень із поставленими у очереді завданнями та ідемпотентністю

\n

Оскільки проблеми з мережею можуть призвести до того, що Telegram повторить спробу надсилки оновлення, яке ваш приклад вже обробила, ви повинні реалізувати ідемпотентність. Ми можемо це досягти, зберігаючи оброблені update_id значення в Redis або кеші бази даних.

\n

Створіть завдання в очереді за допомогою Artisan:

\n
php artisan make:job ProcessTelegramUpdate
\n

Відкрийте створене завдання і реалізуйте логіку обробки. У цьому прикладі ми перевіряємо наявність повторюваних оновлень, уникаємо динамічного введення даних користувачем, використовуючи htmlspecialchars для запобігання помилкам синтаксичного аналізу HTML та обробки як стандартних текстових повідомлень, так і запитів-відповідей.

\n
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!',
]);
}
}
}
\n

4. Навішування інтеграційних тестів з підробленими оновленнями

\n

Щоб переконатися, що ваша інтеграція залишається функціональною під час оновлення прикладів, напишіть тест інтеграції. Ми будемо імітувати очередь та HTTP-клієнт, щоб переконатися, що контролер перевіряє секретный токен, відхиляє несанкционові заявлення та відправляє завдання обробки з правильним корисним навантаженням.

\n

Створіть тест функцій:

\n
php artisan make:test TelegramWebhookTest
\n

Інтегруйте тестові приклади:

\n
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'];
});
}
}
\n

5. Розуміння, пов'язанне з виробництвом

\n

При розгортанні цієї конфігурації в виробничому середовищі враховайте наступні архітектурні обмеження:

\n

* Ограничення швидкості (HTTP 429): Telegram обмежує вихідні повідомлення до 30 повідомлень на секунду у всіх чатах та 1 повідомлення на секунду для певного чата. Якщо ви досягли цих лімітів, Telegram поверне код стану HTTP Слишком many запитів з полем Повторне зв'язання після:. Переконайтеся, що робочий конфігураційний очередь правильно обробляє повторні спроби, або впроваджте драйвер очереді з обмеженням швидкості (наприклад, обмеження швидкості Redis) для обмеження вихідних заявлень. * Ограничення даних обратного виклику: - callback_data поле на вбудованих клавіях має сувору межу 64 байти. Якщо вам потрібно передати складне стан, генеруйте унікальний короткий ключ (наприклад, використовуючи bin2hex(random_bytes(7))), збережіть стан у базі даних або кеші та передайте лише ключ у корисному навантаженні обратного виклику. * Регистрация вебхука Ви повинні зареєструвати URL-адрес веб-перехватника в Telegram один раз. Ви можете це зробити, відправляючи запит на ПУБЛИКАЦІЮ на адрес https://api.telegram.org/bot<YOUR_TOKEN>/setWebhoo k параметри URL та секретний токен. Зберігайте цю скрипт у крокі розгортання або в команду Artisan.

\n

Для отримання більш детальної інформації про головні API-ендпоинти та структури корисних навантажень дивіться офіційну документацію за адресою https://botservice.biz/telegram-bot-api.

\n

Якщо вам потрібна допомога у створенні, масштабуванні або захисті ваших інтеграцій у Telegram, відвідайте BotCreator — студію, яка постачає ботів Telegram/міні-прикладів.

"}

Нові статті — у Telegram

Розбираємо, що автоматизувати в бізнесі та як це працює на практиці. Без спаму.