Files
avandClaude Opus 4.8 f75d6b1f91 tgbot: выбор кандидата метабазы inline-кнопками в карточке ревью
Когда для распознавания сохранены кандидаты метабазы, карточка подтверждения
бота показывает кнопку «🗂 База (N)». По ней двухшагово (как delete/dismiss)
разворачивается список кандидатов inline-кнопками; выбор пиннит источник через
worker.ChooseCandidate (ручной матч, без авто-раскладки) и обновляет карточку.
Веб остаётся точкой точных правок (ручной ввод id/URL, «без базы»).

Безопасность границы: id кандидата из callback_data валидируется как ULID
(ident.Parse) до доменного вызова, как в вебе. Текст inline-кнопок Telegram не
парсится как HTML — название кандидата в подписи не экранируется.

SDD: change telegram-vybor-nahodok — дельта notifications (ADDED «Выбор
кандидата метабазы из карточки подтверждения бота») + review (MODIFIED
«Разделение труда транспортов»: быстрый выбор кандидата — Telegram-действие).
Влито в specs, change заархивирован. Миграций БД нет (кандидаты уже в БД).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 16:02:47 +03:00

5.1 KiB
Raw Permalink Blame History

Контекст

Ядро выбора источника уже есть: worker.ChooseCandidate(ctx, id, candidateID) пиннит кандидата (override provider/id/title/year/director), помечает chosen, обновляет display_name — но не запускает раскладку (человек подтверждает «Применить»). Веб-транспорт зовёт её из handleChooseCandidate, валидируя candidate_id через ident.Parse. В боте механики выбора нет.

Карточка ревью в боте (reviewCard/reviewKeyboard) уже двухшаговая для необратимых действий (delete/dismiss): первая кнопка меняет только клавиатуру (editMarkup), подтверждение зовёт домен и обновляет карточку (refreshCard). Тот же паттерн переиспользуем для выбора кандидата.

Решение

Поверхность. В reviewKeyboard, когда len(rd.Candidates) > 0, добавляем кнопку «🗂 База (N)» с callback sources:<id>. Она не трогает домен — через editMarkup разворачивает клавиатуру списка кандидатов:

  • по одной кнопке на кандидата (full-width row), текст provider · Title (Year), у выбранного (Chosen) — префикс «✓ »; дедуп по provider:provider_id, потолок кнопок (кандидатов и так ≤ maxCandidates=8);
  • callback кандидата — pick:<downloadID>:<candidateID>;
  • нижняя кнопка «← Назад» с callback srcback:<id> — возвращает карточку (refreshCard), домен не трогает.

Обработка callback (handleCallback).

  • sourcesanswer("") + editMarkup(candidatesKeyboard(rd)) (перечитываем ReviewData ради свежего списка), домен не трогаем;
  • srcbackanswer("Отменено") + refreshCard (как delete_cancel);
  • pickcandidateID из val (третий сегмент, parseCallback уже отдал его как value) валидируем ident.Parse (невалидный → answer("Кнопка устарела…"), состояние не меняем), затем reviewer.ChooseCandidate(ctx, id, candidateID); при успехе answer("Источник выбран") + refreshCard, при ошибке — общая ветка ошибки.

Интерфейс. В Reviewer (bot.go) добавляем ChooseCandidate(ctx, id, candidateID string) error; фейк в тестах дополняем.

Границы и безопасность

  • Недоверенный вход: candidate_id из callback_data валидируется как ULID на границе транспорта (как в вебе). Принадлежность кандидата текущему распознаванию доменно проверяет сам ChooseCandidate (не наш кандидат → ErrInvalidInput).
  • Авто-раскладка не двигается: ChooseCandidate только пиннит источник; раскладка — отдельным «Применить». Инвариант «авто только по подтверждённому матчу» не затрагивается.
  • Экранирование: текст inline-кнопок Telegram НЕ парсится как HTML — инъекция разметки из названия кандидата в кнопке невозможна; усечение длинных названий — косметика. sources/srcback меняют только клавиатуру; pick перерисовывает и текст карточки (refreshCardbaseLine с новым provider/id), но новых неэкранированных фрагментов не появляется — baseLine уже экранирует provider/id/URL (действующее требование «Показ записи матча…»).

Бюджет callback_data

Лимит Telegram — 64 байта. Худший: pick:<26>:<26> = 4+1+26+1+26 = 58 ≤ 64. sources:<26> = 34, srcback:<26> = 34. Запас есть.

Отвергнутые альтернативы

  • Показывать «без базы» (нейтраль) и ручной ввод id в боте — это точные правки, зона веба (ClearProvider/SetProviderID); в боте раздули бы интерфейс и клавиатуру. Бот — быстрый выбор из готового списка кандидатов.
  • Отдельное сообщение со списком вместо editMarkup — расходится с уже принятым двухшаговым паттерном (delete/dismiss) и плодит сообщения.