Пять независимых bugfix'ов из ревью приёма (docs/backlog/review-f7-f10-ingest-ui-fixes.md): - F7: oversized .torrent через веб отдавал 500. Введён sentinel ingest.ErrTorrentTooLarge, classifyErr транслирует его в 400. - F8: гонка fast-path attach с cancel. Пред-рид FindReingestBlockingByInfohash больше не короткозамыкает активную запись — авторитетное дедуп-решение принимает CreateDownloadIfNoActive под BEGIN IMMEDIATE; короткозамыкание оставлено только для терминальных desync-записей (target_missing/orphaned). F6-апгрейд сохранён. - F9: magnet — регистронезависимый URN-префикс xt (RFC 2141); tgbot.ParseMessage срезает хвостовую пунктуацию, приклеенную жадным matchем. - F10: cap контекста до 16 KiB в ingest.Ingest (единственное место слияния — покрывает все транспорты), рунобезопасная обрезка + маркер. - N2: httpapi.shorten режет по рунам, не байтам — кириллица не рвётся в U+FFFD. Добавлены юнит-тесты на каждое исправленное поведение. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
83 lines
3.5 KiB
Go
83 lines
3.5 KiB
Go
// Package tgbot — Telegram-адаптер: приём magnet/пересланных сообщений
|
||
// торрент-бота, подтверждение раскладки кнопками и исходящие пинги.
|
||
package tgbot
|
||
|
||
import (
|
||
"regexp"
|
||
"strings"
|
||
)
|
||
|
||
var (
|
||
magnetRe = regexp.MustCompile(`magnet:\?[^\s]+`)
|
||
// parenURL — markdown-хвост " (https://…)" в строках сообщения бота.
|
||
parenURL = regexp.MustCompile(`\s*\(https?://[^)]+\)`)
|
||
)
|
||
|
||
// magnetTrailingPunct — хвостовая пунктуация, которую жадный magnetRe (`[^\s]+`)
|
||
// приклеивает к ссылке, когда magnet стоит в конце предложения или в скобках
|
||
// («…magnet:?xt=…).»). Такой хвост сдвигает последний xt и ломает разбор
|
||
// (длина btih становится 41 → отказ). TrimRight трактует cutset как множество
|
||
// рун, поэтому многобайтовая «»» отсекается корректно.
|
||
const magnetTrailingPunct = ".,;:)]}>»"
|
||
|
||
// noisePrefixes — начала строк UI торрент-бота, которые в контекст не несём.
|
||
var noisePrefixes = []string{
|
||
"Открыть magnet", "или получить .torrent", "Оценить", "Следить",
|
||
"В закладки", "Добавить в закладки", "cправка", "справка",
|
||
"[список файлов]", "[список файлов]", "⚡", "сохранённая копия",
|
||
}
|
||
|
||
// ParseMessage извлекает из текста magnet-ссылку и человекочитаемый контекст
|
||
// (заголовок релиза без ссылок, команд и UI-мусора бота). ok=false, если
|
||
// magnet не найден. Текст может быть как «сырым» magnet, так и пересланным
|
||
// сообщением торрент-бота (формат — см. tmp/examples.md).
|
||
func ParseMessage(text string) (source, context string, ok bool) {
|
||
m := magnetRe.FindString(text)
|
||
if m == "" {
|
||
return "", "", false
|
||
}
|
||
// Срезаем хвостовую пунктуацию, приклеенную жадным матчем (см. константу).
|
||
m = strings.TrimRight(m, magnetTrailingPunct)
|
||
return m, cleanContext(text, m), true
|
||
}
|
||
|
||
// cleanContext оставляет содержательные строки (заголовок, метаданные),
|
||
// выкидывая ссылки, команды (/...) и UI-строки бота.
|
||
func cleanContext(text, magnet string) string {
|
||
var keep []string
|
||
for _, line := range strings.Split(text, "\n") {
|
||
line = strings.TrimSpace(line)
|
||
if line == "" || strings.Contains(line, magnet) {
|
||
continue
|
||
}
|
||
if isNoise(line) {
|
||
continue
|
||
}
|
||
line = strings.TrimSpace(parenURL.ReplaceAllString(line, ""))
|
||
if line != "" {
|
||
keep = append(keep, line)
|
||
}
|
||
}
|
||
return strings.Join(keep, "\n")
|
||
}
|
||
|
||
func isNoise(line string) bool {
|
||
if strings.HasPrefix(line, "/") {
|
||
return true
|
||
}
|
||
// Строки-ссылки и строки рейтинга/команд бота (содержат /g_ /r_ /us_ …).
|
||
if strings.Contains(line, "hashurl.ru") || strings.Contains(line, "exfreedomist.com") {
|
||
return true
|
||
}
|
||
for _, p := range noisePrefixes {
|
||
if strings.HasPrefix(line, p) {
|
||
return true
|
||
}
|
||
}
|
||
// Строки вида "👍: /g_ad035f или 👎🏿: /r_ad035f".
|
||
if strings.Contains(line, ": /") {
|
||
return true
|
||
}
|
||
return false
|
||
}
|