tgbot: download id моноширинным (code) для tap-to-copy
Включён HTML parse mode у всех исходящих сообщений бота (send + edit-путь
refreshCard), download id выводится моноширинным <code> — в клиентах Telegram
по нему работает tap-to-copy (скопировать id для /download/{id} или диагностики).
Префикс # / download_id= остаётся вне <code>, чтобы копировался чистый id.
Parse mode делает разметку значимой для всех текстов, поэтому добавлен
escape-хелпер и экранированы все внешние/недоверенные фрагменты: display name,
распознанное название, источник/контекст, целевой путь, причины, provider,
error_code/error_msg (инвариант «выход LLM недоверенный»). esc применяется
последним шагом, после усечения, чтобы обрез не разрубил HTML-сущность.
Capability notifications: два ADDED-требования (формат id + экранирование).
Беклог: задача закрыта, зонтичный telegram-revyu-uvedomleniy обновлён.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+16
-7
@@ -161,7 +161,7 @@ func (b *Bot) handleMessage(ctx context.Context, m *tgbotapi.Message) {
|
||||
b.send(m.Chat.ID, opErr("Не удалось обработать подсказку", id), nil)
|
||||
return
|
||||
}
|
||||
b.send(m.Chat.ID, "Подсказка принята, перераспознаю #"+id+"…", nil)
|
||||
b.send(m.Chat.ID, "Подсказка принята, перераспознаю #"+idCode(id)+"…", nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -275,17 +275,17 @@ func (b *Bot) ingestAndReply(ctx context.Context, chatID int64, req ingest.Reque
|
||||
switch res.State {
|
||||
case store.StateTargetMissing:
|
||||
// Источник жив, цель удалена — из target_missing доступна перепривязка.
|
||||
b.send(chatID, fmt.Sprintf("♻️ Этот торрент уже есть как запись #%s без цели — привяжите заново или закройте её.", res.DownloadID), nil)
|
||||
b.send(chatID, fmt.Sprintf("♻️ Этот торрент уже есть как запись #%s без цели — привяжите заново или закройте её.", idCode(res.DownloadID)), nil)
|
||||
case store.StateOrphaned:
|
||||
// Источник пропал: relink из orphaned нет, рабочий путь — закрыть и
|
||||
// добавить заново (тогда приём заведёт свежую загрузку).
|
||||
b.send(chatID, fmt.Sprintf("♻️ Этот торрент уже есть как осиротевшая запись #%s — закройте её, затем добавьте заново.", res.DownloadID), nil)
|
||||
b.send(chatID, fmt.Sprintf("♻️ Этот торрент уже есть как осиротевшая запись #%s — закройте её, затем добавьте заново.", idCode(res.DownloadID)), nil)
|
||||
default:
|
||||
b.send(chatID, fmt.Sprintf("♻️ Дубль уже активной загрузки #%s — добавление отменено.", res.DownloadID), nil)
|
||||
b.send(chatID, fmt.Sprintf("♻️ Дубль уже активной загрузки #%s — добавление отменено.", idCode(res.DownloadID)), nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
b.send(chatID, fmt.Sprintf("Принято #%s — добавляю в qBittorrent.\nПозову, когда нужно подтверждение.", res.DownloadID), nil)
|
||||
b.send(chatID, fmt.Sprintf("Принято #%s — добавляю в qBittorrent.\nПозову, когда нужно подтверждение.", idCode(res.DownloadID)), nil)
|
||||
}
|
||||
|
||||
const helpText = `jellybit-бот: пришлите magnet-ссылку, .torrent-файл или перешлите сообщение торрент-бота — поставлю на закачку.
|
||||
@@ -360,7 +360,7 @@ func (b *Bot) handleCallback(ctx context.Context, cq *tgbotapi.CallbackQuery) {
|
||||
case "refine":
|
||||
b.setPending(chatID, id)
|
||||
b.answer(cq.ID, "Жду подсказку")
|
||||
b.send(chatID, "Ответьте сообщением с подсказкой для #"+id+".", nil)
|
||||
b.send(chatID, "Ответьте сообщением с подсказкой для #"+idCode(id)+".", nil)
|
||||
return
|
||||
default:
|
||||
b.answer(cq.ID, "")
|
||||
@@ -406,6 +406,9 @@ func (b *Bot) refreshCard(ctx context.Context, chatID int64, msgID int, id strin
|
||||
} else {
|
||||
edit = tgbotapi.NewEditMessageText(chatID, msgID, text)
|
||||
}
|
||||
// Тот же HTML parse mode, что и в send(): текст карточки содержит <code>-id и
|
||||
// экранированные внешние фрагменты — правка на месте должна их так же трактовать.
|
||||
edit.ParseMode = tgbotapi.ModeHTML
|
||||
if _, err := b.api.Send(edit); err != nil {
|
||||
b.log.Warn("telegram edit card failed", "download_id", id, "error", logging.SanitizeErr(err))
|
||||
}
|
||||
@@ -441,6 +444,10 @@ func (b *Bot) Notify(ctx context.Context, downloadID string, event worker.Notify
|
||||
|
||||
func (b *Bot) send(chatID int64, text string, kb *tgbotapi.InlineKeyboardMarkup) {
|
||||
msg := tgbotapi.NewMessage(chatID, text)
|
||||
// HTML parse mode — ради моноширинного <code> у download id (tap-to-copy).
|
||||
// Разметка становится значимой для ВСЕХ сообщений: внешний текст (имена,
|
||||
// пути, причины, ошибки) обязан быть экранирован через esc() при рендере.
|
||||
msg.ParseMode = tgbotapi.ModeHTML
|
||||
msg.DisableWebPagePreview = true
|
||||
if kb != nil {
|
||||
msg.ReplyMarkup = *kb
|
||||
@@ -489,7 +496,9 @@ func (b *Bot) takePending(chatID int64) (string, bool) {
|
||||
// операции ещё нет (downloadID == "") — дружелюбный текст без ключа.
|
||||
func opErr(msg string, downloadID string) string {
|
||||
if downloadID != "" {
|
||||
return fmt.Sprintf("%s (download_id=%s).", msg, downloadID)
|
||||
// download_id моноширинным для tap-to-copy; msg — контролируемый литерал
|
||||
// вызывающего (без спецсимволов разметки), потому не экранируется.
|
||||
return fmt.Sprintf("%s (download_id=%s).", msg, idCode(downloadID))
|
||||
}
|
||||
return msg + "."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user