Yii2-команда для обслуживания Telegram-бота: setWebhook, удаление, getMe и ротация логов апдейтов

Вебхук работает, бот отвечает, заявки идут. Раз в неделю нужно сменить URL, проверить getMe или подчистить лог апдейтов. Ходить руками на api.telegram.org — источник ошибок. Одна консольная команда Yii2 закрывает setWebhook, deleteWebhook, health-check и ротацию логов.

Что должна уметь команда

  • actionSetWebhook — установить webhook с secret_token.
  • actionDeleteWebhook — снять webhook перед деплоем или переносом.
  • actionMe — liveness через getMe.
  • actionRotateLogs — ротация файла сырых апдейтов.

Полный Controller

<?php
namespace app\commands;

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

class TgController extends Controller
{
private function token(): string
{
$token = (string) (Yii::$app->params['telegram.botToken']
?? getenv('TG_BOT_TOKEN')
?: '');
if ($token === '') {
$this->stderr("TG_BOT_TOKEN is not set\n", Console::FG_RED);
exit(ExitCode::CONFIG);
}
return $token;
}

private function bot(string $method, array $params = []): array
{
$ch = curl_init(
'https://api.telegram.org/bot' . $this->token() . '/' . $method
);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 20,
CURLOPT_POSTFIELDS => $params,
]);
$body = curl_exec($ch);
$http = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($body === false) {
$err = curl_error($ch);
curl_close($ch);
$this->stderr("cURL error: {$err}\n", Console::FG_RED);
exit(ExitCode::UNSPECIFIED_ERROR);
}
curl_close($ch);

$data = json_decode($body, true);
if (!is_array($data) || empty($data['ok'])) {
$desc = is_array($data)
? ($data['description'] ?? 'unknown')
: 'bad JSON';
$this->stderr("TG {$http}: {$desc}\n", Console::FG_RED);
exit(ExitCode::UNSPECIFIED_ERROR);
}
return $data['result'];
}

/**
* php yii tg/set-webhook
*/
public function actionSetWebhook(?string $url = null): int
{
$url = $url
?: (string) (Yii::$app->params['telegram.webhookUrl'] ?? '');
$secret = (string) (Yii::$app->params['telegram.webhookSecret'] ?? '');
if ($url === '' || $secret === '') {
$this->stderr("webhookUrl / webhookSecret missing\n");
return ExitCode::CONFIG;
}

$result = $this->bot('setWebhook', [
'url' => $url,
'secret_token' => $secret,
'allowed_updates' => json_encode([
'message',
'callback_query',
]),
'drop_pending_updates' => 'false',
]);

$this->stdout("setWebhook ok: " . json_encode($result) . "\n");
return ExitCode::OK;
}

/**
* php yii tg/delete-webhook
*/
public function actionDeleteWebhook(int $dropPending = 0): int
{
$result = $this->bot('deleteWebhook', [
'drop_pending_updates' => $dropPending ? 'true' : 'false',
]);
$this->stdout("deleteWebhook ok: " . json_encode($result) . "\n");
return ExitCode::OK;
}

/**
* php yii tg/me
*/
public function actionMe(): int
{
$me = $this->bot('getMe');
$this->stdout(sprintf(
"id=%s username=@%s is_bot=%s can_join_groups=%s\n",
$me['id'] ?? '?',
$me['username'] ?? '?',
!empty($me['is_bot']) ? 'yes' : 'no',
!empty($me['can_join_groups']) ? 'yes' : 'no'
));
return ExitCode::OK;
}

/**
* php yii tg/rotate-logs
*/
public function actionRotateLogs(
string $file = '@runtime/tg-updates.jsonl',
int $maxMb = 20,
int $keep = 7
): int {
$path = Yii::getAlias($file);
if (!is_file($path)) {
$this->stdout("no log file yet: {$path}\n");
return ExitCode::OK;
}

$sizeMb = filesize($path) / (1024 * 1024);
if ($sizeMb < $maxMb) {
$this->stdout(sprintf(
"skip rotate: %.2f MB < %d MB\n",
$sizeMb,
$maxMb
));
return ExitCode::OK;
}

$rotated = $path . '-' . date('Y-m-d-His');
if (!rename($path, $rotated)) {
$this->stderr("rename failed\n");
return ExitCode::UNSPECIFIED_ERROR;
}
touch($path);
$this->stdout("rotated to {$rotated}\n");

$pattern = $path . '-*';
$files = glob($pattern) ?: [];
rsort($files);
foreach (array_slice($files, $keep) as $old) {
@unlink($old);
$this->stdout("removed {$old}\n");
}
return ExitCode::OK;
}
}

Запись сырого лога в webhook

// в actionWebhook после json_decode:
$file = Yii::getAlias('@runtime/tg-updates.jsonl');
file_put_contents(
$file,
json_encode($update, JSON_UNESCAPED_UNICODE) . "\n",
FILE_APPEND | LOCK_EX
);

Примеры запуска

# health-check (cron */5)
php yii tg/me

# установить webhook
php yii tg/set-webhook
php yii tg/set-webhook https://new.example.com/telegram/webhook

# снять перед миграцией
php yii tg/delete-webhook 1

# ротация логов каждый час
php yii tg/rotate-logs @runtime/tg-updates.jsonl 20 7

Практические замечания

  • Токен и секрет — только в ENV / params-local.php.
  • После смены secret_token обновите и проверку в webhook-контроллере.
  • Перед деплоем на новый хост: deleteWebhook на старом, setWebhook на новом.
  • Ненулевой exit code у tg/me удобно вешать на мониторинг.

Связанные материалы: webhook-контроллер Yii2 и локальный long polling.

Типовые операции обслуживания уже собраны в модулях на botservice.biz.

Новые статьи — в Telegram

Разбираем, что автоматизировать в бизнесе и как это работает на практике. Без спама.