tgbot: показывать запись матча метабазы (провайдер+id+ссылка)
В карточке подтверждения и уведомлении о готовности бот теперь показывает
запись матча метабазы — провайдер, id и кликабельную ссылку на страницу
записи, — как веб-страница /download/{id} и экран ревью. Так ошибочную
привязку видно и из Telegram.
Билдер URL записи (providerURL/matchURL) вынесен из internal/httpapi в ядро
internal/worker (worker.ProviderURL + метод (*ReviewData).MatchURL()), чтобы
оба транспорта строили ссылку одинаково; httpapi делегирует туда. baseLine
переведён на эффективные provider/id (с учётом ручных правок), URL в href
экранируется escHref (сверх esc закрывает кавычку — иначе изготовленный id
разорвал бы атрибут и Telegram отклонил бы сообщение). При отсутствии матча
карточка ревью показывает «нет матча», уведомление о готовности строку
опускает.
Capability notifications: ADDED «Показ записи матча метабазы» + MODIFIED
требования об экранировании (id матча и URL, контекст href).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+42
-12
@@ -20,6 +20,15 @@ import (
|
||||
// `<` на битую разметку.
|
||||
func esc(s string) string { return tgbotapi.EscapeText(tgbotapi.ModeHTML, s) }
|
||||
|
||||
// escHref экранирует URL для вставки в значение атрибута `href`. Контекст
|
||||
// атрибута строже текстового: esc (EscapeText) закрывает `<`/`>`/`&`, но НЕ
|
||||
// трогает `"`, а недоверенный id уходит в URL сырым (см. worker.ProviderURL) —
|
||||
// кавычка в id разорвала бы атрибут и Telegram отклонил бы сообщение (parse
|
||||
// error → уведомление не доставится). Поэтому поверх esc заменяем `"` на
|
||||
// `"`; порядок безопасен: esc уже перевёл `&` в `&`, повторно `&` в
|
||||
// `"` не удвоится.
|
||||
func escHref(url string) string { return strings.ReplaceAll(esc(url), `"`, """) }
|
||||
|
||||
// idCode оборачивает download id в моноширинный <code> — в клиентах Telegram по
|
||||
// нему работает tap-to-copy (скопировать id для /download/{id} или диагностики).
|
||||
// Визуальный префикс (`#` / `download_id=`) держим ВНЕ code, чтобы копировался
|
||||
@@ -69,8 +78,11 @@ func (b *Bot) reviewCard(rd *worker.ReviewData) (string, *tgbotapi.InlineKeyboar
|
||||
// guessLine/baseLine возвращают уже экранированный текст (внешние title/provider
|
||||
// внутри) — повторно не экранируем.
|
||||
fmt.Fprintf(&sb, "Похоже на: %s\n", guessLine(rd))
|
||||
if base := baseLine(rd.Recognition); base != "" {
|
||||
// В ревью показываем «нет матча» явно — полезно видеть, что база не выбрана.
|
||||
if base := baseLine(rd); base != "" {
|
||||
fmt.Fprintf(&sb, "База: %s\n", base)
|
||||
} else {
|
||||
sb.WriteString("База: нет матча\n")
|
||||
}
|
||||
if reasons := rd.Recognition.ReasonList(); len(reasons) > 0 {
|
||||
fmt.Fprintf(&sb, "Причины: %s\n", esc(strings.Join(reasons, " · ")))
|
||||
@@ -127,14 +139,22 @@ func titleLabel(rd *worker.ReviewData) string {
|
||||
return "#" + idCode(rd.Download.ID)
|
||||
}
|
||||
|
||||
// renderDone — короткое сообщение о готовности.
|
||||
// renderDone — короткое сообщение о готовности. Дополняем строкой матча (база +
|
||||
// ссылка), чтобы ошибочную привязку было видно и в финальном пинге; без матча
|
||||
// строку опускаем — в готовности «нет матча» лишний шум.
|
||||
func (b *Bot) renderDone(rd *worker.ReviewData) string {
|
||||
label := titleLabel(rd)
|
||||
var sb strings.Builder
|
||||
n := len(rd.Preview)
|
||||
if n == 0 {
|
||||
return fmt.Sprintf("✅ Готово: %s разложен.", label)
|
||||
fmt.Fprintf(&sb, "✅ Готово: %s разложен.", label)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, "✅ Готово: %s — разложено файлов: %d.", label, n)
|
||||
}
|
||||
return fmt.Sprintf("✅ Готово: %s — разложено файлов: %d.", label, n)
|
||||
if base := baseLine(rd); base != "" {
|
||||
fmt.Fprintf(&sb, "\nБаза: %s", base)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// renderDesync — уведомление о рассинхроне (источник/цель удалены вручную).
|
||||
@@ -267,16 +287,26 @@ func guessLine(rd *worker.ReviewData) string {
|
||||
return s
|
||||
}
|
||||
|
||||
// baseLine возвращает уже экранированный текст (provider/provider_id — внешние,
|
||||
// из метабазы/LLM): вызывающий вставляет как есть, без повторного esc.
|
||||
func baseLine(rec *store.Recognition) string {
|
||||
if rec == nil || !rec.Provider.Valid || rec.Provider.String == "" || rec.Provider.String == "none" {
|
||||
return "нет матча"
|
||||
// baseLine — запись матча метабазы для уведомлений: provider и id (эффективные,
|
||||
// с учётом ручных правок — как на веб-странице загрузки), при возможности
|
||||
// построить URL — ссылкой на страницу записи (тот же билдер, что и веб:
|
||||
// worker.ReviewData.MatchURL). Возвращает уже экранированный HTML (provider/id —
|
||||
// внешние, URL — в контексте href): вызывающий вставляет как есть. Пусто, если
|
||||
// матча нет (провайдер пуст/none) — поверхность сама решает, показывать ли
|
||||
// индикатор «нет матча».
|
||||
func baseLine(rd *worker.ReviewData) string {
|
||||
prov := rd.Provider
|
||||
if prov == "" || prov == "none" {
|
||||
return ""
|
||||
}
|
||||
if rec.ProviderID.Valid && rec.ProviderID.String != "" {
|
||||
return esc(rec.Provider.String) + " " + esc(rec.ProviderID.String)
|
||||
label := esc(prov)
|
||||
if rd.ProviderID != "" {
|
||||
label += " " + esc(rd.ProviderID)
|
||||
}
|
||||
return esc(rec.Provider.String)
|
||||
if url := rd.MatchURL(); url != "" {
|
||||
return fmt.Sprintf(`<a href="%s">%s ↗</a>`, escHref(url), label)
|
||||
}
|
||||
return label
|
||||
}
|
||||
|
||||
func contextOrSource(rd *worker.ReviewData) string {
|
||||
|
||||
Reference in New Issue
Block a user