Yii2-team for servicing the Telegram bot: setWebhook, delete, getMe and rotate update logs

The webhook works, the bot answers, leads arrive. Once a week you still need to change the URL, ping getMe, or shrink an update log that ate the disk. Hitting api.telegram.org by hand is error-prone. One Yii2 console controller covers setWebhook, deleteWebhook, health checks, and log rotation.

What the command should do

  • actionSetWebhook — register the webhook with secret_token.
  • actionDeleteWebhook — remove the webhook before deploy or migration.
  • actionMe — liveness via getMe.
  • actionRotateLogs — rotate the raw updates file.

Full 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;
}
}

Writing the raw log from the webhook

// inside actionWebhook after json_decode:
$file = Yii::getAlias('@runtime/tg-updates.jsonl');
file_put_contents(
$file,
json_encode($update, JSON_UNESCAPED_UNICODE) . "\n",
FILE_APPEND | LOCK_EX
);

Run examples

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

# set webhook
php yii tg/set-webhook
php yii tg/set-webhook https://new.example.com/telegram/webhook

# remove before migration (1 = drop pending)
php yii tg/delete-webhook 1

# rotate logs hourly
php yii tg/rotate-logs @runtime/tg-updates.jsonl 20 7

Practical notes

  • Token and secret belong in ENV / params-local.php only.
  • After rotating secret_token, update the webhook controller check too.
  • Before moving hosts: deleteWebhook on the old server, setWebhook on the new one.
  • A non-zero exit from tg/me is easy to wire into monitoring.

Related: Yii2 webhook controller and local long polling.

Common maintenance flows are already packaged at botservice.biz.

New articles on Telegram

We explain what to automate in your business and how it works in practice. No spam.