Команда консолі обслуговування бота Yii2: setWebhook, getMe та обертання журналу

Керування Webhooks вручну через браузер або сторонні HTTP-клієнти є частим джерелом проблем у виробництві. Завдяки ручному налаштуванню легко забути про перенесення secret_token, пропустіть зміну URL-адреси, коли змінюється середовище, або втратьте контроль над накопиченими оновленнями. Єдино правильним підходом для комерційної розробки є передача сервісних завдань бота командам CLI фреймворку.

У цій статті ми обговоримо створення консольного контролера в Yii2, який відповідає за перевірку доступності API (health-check), безпечну реєстрацію та видалення Webhook, а також автоматичне очищення таблиць ідемпотентності та журналів оновлень.

Навіщо переносити обслуговування API Telegram на консоль Yii2

Використовуючи консольні команди Yii2 (класи, успадковані від yii\console\controller) дає відразу кілька інженерних переваг:

  • Інтеграція конвеєра CI/CD: Під час кожного розгортання конвеєр може автоматично викликати перевірку здоров 'я та оновлювати URL-адресу Webhook з поточною secret_token.
  • Одинарна конфігурація: Токени та параметри беруться з навколишнього середовища. (getenv()АБО Yii::$app->params, що виключає витік токенів в історію команд cURL.
  • Безпечно видаляйте накопичені повідомлення: Прапор drop_pending_updates при скиданні вебхука, дозволяє очистити чергу накопичених повідомлень після збою сервера.
  • Автоматичне запобігання БД: Столи, в яких зберігаються update_id щоб забезпечити ідемпотентність, вони з часом зростають до мільйонів рядків, що вимагає регулярного планового прибирання через Cron.

Реалізація контролера CLI: getMe, setWebhook та deleteWebhook

Створити консольный контроллер commands/TelegramController.php. Для взаємодії з Telegram Bot API використовуйте чистий cURL з обов 'язковою валідацією http коду відповіді, верифікацією json_last_error та поля Гаразд в тілі реагування.

<?php

namespace app\commands;

use Yii;
use yii\console\Controller;
use yii\console\ExitCode;

class TelegramController extends Controller
{
/**
* Отправка POST-запроса к Telegram Bot API через cURL
*/
private function sendApiRequest(string $method, array $params = []): array
{
$token = getenv('TELEGRAM_BOT_TOKEN') ?: (Yii::$app->params['telegramBotToken'] ?? null);
if (!$token) {
$this->stderr("Ошибка: Не задан TELEGRAM_BOT_TOKEN в getenv() или params.php\n");
return ['ok' => false, 'error' => 'Missing bot token'];
}

$url = "https://api.telegram.org/bot{$token}/{$method}";
$ch = curl_init($url);

curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($params),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);

if ($response === false) {
$this->stderr("cURL Error: {$curlError}\n");
return ['ok' => false, 'error' => $curlError];
}

$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$this->stderr("JSON Error: " . json_last_error_msg() . "\n");
return ['ok' => false, 'error' => 'Invalid JSON response'];
}

if ($httpCode !== 200 || empty($data['ok'])) {
$description = $data['description'] ?? 'Unknown API error';
$this->stderr("Telegram API Error [HTTP {$httpCode}]: {$description}\n");
}

return $data;
}

/**
* Health-check доступности бота (getMe)
*/
public function actionHealth(): int
{
$res = $this->sendApiRequest('getMe');
if (!empty($res['ok'])) {
$botUsername = $res['result']['username'] ?? 'unknown_bot';
$botName = $res['result']['first_name'] ?? 'Bot';
$this->stdout("Health-check пройден успешно. Имя: {$botName} (@{$botUsername})\n");
return ExitCode::OK;
}

$this->stderr("Health-check провален! Проверьте валидность токена.\n");
return ExitCode::UNSPECIFIED_ERROR;
}

/**
* Установка Webhook с привязкой Secret Token
*/
public function actionSetWebhook(string $url): int
{
$secretToken = getenv('TELEGRAM_SECRET_TOKEN') ?: (Yii::$app->params['telegramSecretToken'] ?? '');

$params = [
'url' => $url,
'allowed_updates' => ['message', 'callback_query', 'my_chat_member'],
];

if (!empty($secretToken)) {
$params['secret_token'] = $secretToken;
}

$res = $this->sendApiRequest('setWebhook', $params);
if (!empty($res['ok'])) {
$this->stdout("Webhook успешно зарегистрирован на URL: {$url}\n");
return ExitCode::OK;
}

return ExitCode::UNSPECIFIED_ERROR;
}

/**
* Удаление Webhook с опциональным сбросом очереди накопившихся апдейтов
*/
public function actionDeleteWebhook(bool $dropPending = false): int
{
$params = ['drop_pending_updates' => $dropPending];
$res = $this->sendApiRequest('deleteWebhook', $params);

if (!empty($res['ok'])) {
$status = $dropPending ? "с очисткой входящей очереди" : "без очистки очереди";
$this->stdout("Webhook успешно удален ({$status}).\n");
return ExitCode::OK;
}

return ExitCode::UNSPECIFIED_ERROR;
}
}

Обертання журналів оновлень та очищення таблиць ідемпотентності

При отриманні оновлення від Telegram до контролера Webhook, його update_id зберігається в базі даних (наприклад, в таблиці telegram_update_log), щоб уникнути дублювання обробки. З часом ця таблиця розростається до сотень тисяч записів, займаючи дисковий простір і знижуючи швидкість пошуку індексу.

Додамо метод до нашого консольного контролера для фонового очищення застарілих записів, зберігаючи лише дані за останні N днів.

    /**
* Очистка старых логов входящих апдейтов (для поддержания идемпотентности)
* Пример вызова: php yii telegram/clean-logs 14
*/
public function actionCleanLogs(int $days = 14): int
{
if ($days < 1) {
$this->stderr("Параметр days должен быть больше или равен 1.\n");
return ExitCode::DATAERR;
}

$thresholdDate = date('Y-m-d H:i:s', strtotime("-{$days} days"));

try {
$deletedCount = Yii::$app->db->createCommand()
->delete('{{%telegram_update_log}}', ['<', 'created_at', $thresholdDate])
->execute();

$this->stdout("Успешно удалено {$deletedCount} записей update_id старше {$days} дней ({$thresholdDate}).\n");
return ExitCode::OK;
} catch (\Exception $e) {
$this->stderr("Ошибка при выполнении очистки БД: " . $e->getMessage() . "\n");
return ExitCode::UNSPECIFIED_ERROR;
}
}

Автоматизація: використання в CI/CD та Crontab

Розроблена консольна команда може бути легко реалізована в скрипті розгортання (наприклад, в скрипті bash при розгортанні нової версії програми) та в системному планувальнику завдань Linux (crontab).

# 1. Скрипт деплоя приложения (deploy.sh)
#!/usr/bin/env bash
set -e

APP_ENV="prod"
WEBHOOK_URL="https://bot.example.com/telegram/webhook"

echo "Проверка подключения к Telegram API..."
php yii telegram/health

echo "Регистрация целевого Webhook..."
php yii telegram/set-webhook "$WEBHOOK_URL"

# 2. Задача в /etc/cron.d/telegram-bot
# Запуск очистки устаревших логов update_id каждое воскресенье в 03:00 ночи (хранить 14 дней)
0 3 * * 0 www-data /usr/bin/php /var/www/app/yii telegram/clean-logs 14 >> /var/www/app/runtime/logs/cron-telegram.log 2>&1

Обробка помилок і типові підводні камені

Використовуючи команди CLI в середовищі продукту, враховуйте наступні нюанси:

  • Коди повернення (ExitCode): Команди повинні повернутися ExitCode::OK (0) щодо кодів успіху та ненульових кодів (наприклад, ExitCode::unspecified_ERROR від кулі 1) при збоях. Це вимагає, щоб сценарії CI/CD переривали збирання в той час, коли виникають помилки API.
  • Секретний токен: Переданий параметр secret_token при виклику setWebhook має містити від 1 до 256 символів (дозволені символи A-Z, a-z, 0-9, _ і -). Підтвердьте назву X-Telegram-Bot-Api-Secret-Token при отриманні запитів на стороні веб-контролера.
  • Відкинути оновлення, що очікують на розгляд: Будьте обережні з прапором dropPending = true при видаленні або перевстановленні веб-гачка. Використовуйте його лише тоді, коли сталася помилка, а накопичена черга застарілих команд може поставити вашу бізнес-логіку або призвести до дублювання дій.

Якщо вам потрібна кваліфікована допомога у розробці архітектури бота з високим навантаженням або інтеграції веб-перехоплювачів із CRM-системами, зверніться до BotCreator.

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

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