fix: tolerate malformed telegram callbacks, constrain channel id

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCrwHHnGCB5XH968Nxokqw
This commit is contained in:
nikita.hohlov
2026-09-04 07:59:45 -03:00
parent 90d17ec378
commit 44f50e1d66
4 changed files with 24 additions and 4 deletions

View File

@@ -67,21 +67,31 @@ class TelegramWebhookController extends Controller
$this->telegram->touch($chatId); $this->telegram->touch($chatId);
} }
$callbackId = (string) ($callback['id'] ?? '');
[$action, $token] = array_pad(explode(':', (string) ($callback['data'] ?? ''), 2), 2, ''); [$action, $token] = array_pad(explode(':', (string) ($callback['data'] ?? ''), 2), 2, '');
$delivery = Delivery::where('action_token', $token)->with('event.user')->first(); $delivery = Delivery::where('action_token', $token)->with('event.user')->first();
if ($delivery === null || $delivery->event->isTerminal() || ! in_array($action, ['ack', 'done'], true)) { if ($delivery === null || $delivery->event->isTerminal() || ! in_array($action, ['ack', 'done'], true)) {
$this->telegram->answerCallback((string) $callback['id'], 'Уже неактуально'); $this->answerCallback($callbackId, 'Уже неактуально');
return; return;
} }
if ($action === 'done') { if ($action === 'done') {
$this->actions->done($delivery->event); $this->actions->done($delivery->event);
$this->telegram->answerCallback((string) $callback['id'], 'Закрыто'); $this->answerCallback($callbackId, 'Закрыто');
} else { } else {
$this->actions->ack($delivery->event); $this->actions->ack($delivery->event);
$this->telegram->answerCallback((string) $callback['id'], 'Ок, напомню позже'); $this->answerCallback($callbackId, 'Ок, напомню позже');
} }
} }
private function answerCallback(string $callbackId, string $text): void
{
if ($callbackId === '') {
// Нет id — отвечать нечему (malformed payload), но не считаем это ошибкой.
return;
}
$this->telegram->answerCallback($callbackId, $text);
}
} }

View File

@@ -22,6 +22,6 @@ Route::middleware('remote.user')->group(function () {
Route::post('/me/events/{event}/done', [MeEvents::class, 'done']); Route::post('/me/events/{event}/done', [MeEvents::class, 'done']);
Route::get('/me/channels', [ChannelsController::class, 'index']); Route::get('/me/channels', [ChannelsController::class, 'index']);
Route::post('/me/channels', [ChannelsController::class, 'store']); Route::post('/me/channels', [ChannelsController::class, 'store']);
Route::delete('/me/channels/{channel}', [ChannelsController::class, 'destroy']); Route::delete('/me/channels/{channel}', [ChannelsController::class, 'destroy'])->whereNumber('channel');
Route::post('/me/channels/telegram/link', [ChannelsController::class, 'telegramLink']); Route::post('/me/channels/telegram/link', [ChannelsController::class, 'telegramLink']);
}); });

View File

@@ -49,6 +49,8 @@ class ChannelsApiTest extends TestCase
$this->getJson('/me', ['X-Remote-User' => 'other']); $this->getJson('/me', ['X-Remote-User' => 'other']);
$foreign = Channel::create(['user_id' => User::where('login', 'other')->sole()->id, 'type' => ChannelType::Webhook, 'config' => ['deliver_url' => 'u', 'presence_url' => 'p'], 'enabled' => true]); $foreign = Channel::create(['user_id' => User::where('login', 'other')->sole()->id, 'type' => ChannelType::Webhook, 'config' => ['deliver_url' => 'u', 'presence_url' => 'p'], 'enabled' => true]);
$this->deleteJson('/me/channels/'.$foreign->id, [], $this->as)->assertNotFound(); $this->deleteJson('/me/channels/'.$foreign->id, [], $this->as)->assertNotFound();
$this->deleteJson('/me/channels/abc', [], $this->as)->assertNotFound();
} }
public function test_telegram_link_code(): void public function test_telegram_link_code(): void

View File

@@ -96,4 +96,12 @@ class TelegramWebhookTest extends TestCase
{ {
$this->postJson('/hooks/telegram', ['edited_message' => ['chat' => ['id' => 1]]], $this->secret())->assertNoContent(); $this->postJson('/hooks/telegram', ['edited_message' => ['chat' => ['id' => 1]]], $this->secret())->assertNoContent();
} }
public function test_malformed_callback_is_ignored(): void
{
$this->postJson('/hooks/telegram', ['callback_query' => ['data' => 'ack:nope']], $this->secret())->assertNoContent();
$this->postJson('/hooks/telegram', ['callback_query' => ['id' => 'x', 'data' => 'garbage']], $this->secret())->assertNoContent();
Http::assertSent(fn (Request $r) => str_ends_with($r->url(), '/answerCallbackQuery') && $r['text'] === 'Уже неактуально');
}
} }