max-telegram-bridge-bot/format.go
Andrey Lugovskoy 63b95e4c8c Preserve links in TG→MAX crossposts; deliver text when video fails
Two issues reported by a user via the bridge:

1. Links vanish on TG channel → MAX crosspost. formatTgCrosspostCaption
   returned raw text and forwardTgToMax dropped markdown format for the
   crosspost branch, so text_link entities became plain text. Now
   formatTgCrosspostCaption runs tgEntitiesToMarkdown, and
   forwardTgToMax always uses markdown. Media group flusher also
   skips the second conversion for crosspost items (caption is already
   markdown) and keeps markdown format.

2. When a channel video is too big for Bot API getFile (e.g. HD > 2 GB
   on local server), the whole post was dropped — even the caption
   text was lost. Video upload failure no longer returns from
   forwardTgToMax; the fallback branch appends a "[Видео]" marker so
   subscribers in MAX at least get the text and know a video was
   attached but not relayed. The fallback condition now checks
   presence of any media instead of only msg.Text == "" (it missed
   video-with-caption posts) and also covers Animation and Photo.

Tradeoff: crosspost replacements now operate on markdown text. URL-
and phrase-level replacements keep working; regex rules touching
markdown characters (_, *, [, ]) may need adjustment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:26:24 +04:00

147 lines
3.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"strings"
maxschemes "github.com/max-messenger/max-bot-api-client-go/schemes"
)
func tgName(msg *TGMessage) string {
if msg.From == nil {
if msg.SenderChat != nil {
return msg.SenderChat.Title
}
return "Unknown"
}
name := msg.From.FirstName
if msg.From.LastName != "" {
name += " " + msg.From.LastName
}
return name
}
// formatAttribution собирает строку "Имя: текст" или "Имя:\nтекст" в зависимости от настройки.
func formatAttribution(name, text string, newline bool) string {
if newline {
return name + ":\n" + text
}
return name + ": " + text
}
// formatAttributionMD собирает строку с жирным именем в markdown: "**Имя**: текст".
func formatAttributionMD(name, text string, newline bool) string {
bold := "**" + name + "**"
if newline {
return bold + ":\n" + text
}
return bold + ": " + text
}
// formatTgCaption — для пересылки (текст или caption)
func formatTgCaption(msg *TGMessage, prefix, newline bool) string {
name := tgName(msg)
text := msg.Text
if text == "" {
text = msg.Caption
}
if prefix {
return formatAttribution("[TG] "+name, text, newline)
}
return formatAttribution(name, text, newline)
}
// formatTgMessage — для edit (полный формат)
func formatTgMessage(msg *TGMessage, prefix, newline bool) string {
name := tgName(msg)
text := msg.Text
if text == "" {
text = msg.Caption
}
if text == "" {
return ""
}
if prefix {
return formatAttribution("[TG] "+name, text, newline)
}
return formatAttribution(name, text, newline)
}
func maxName(upd *maxschemes.MessageCreatedUpdate) string {
name := upd.Message.Sender.Name
if name == "" {
name = upd.Message.Sender.Username
}
return name
}
// formatMaxCaption — для пересылки
func formatMaxCaption(upd *maxschemes.MessageCreatedUpdate, prefix, newline bool) string {
name := maxName(upd)
text := upd.Message.Body.Text
if prefix {
return formatAttribution("[MAX] "+name, text, newline)
}
return formatAttribution(name, text, newline)
}
// formatTgCrosspostCaption — для кросспостинга каналов (без attribution и префиксов).
// Конвертирует entities в markdown, чтобы ссылки и форматирование сохранились
// при пересылке в MAX. Replacements, если настроены, применяются поверх markdown.
func formatTgCrosspostCaption(msg *TGMessage) string {
text := msg.Text
entities := msg.Entities
if text == "" {
text = msg.Caption
entities = msg.CaptionEntities
}
return tgEntitiesToMarkdown(text, entities)
}
// formatMaxCrosspostCaption — для кросспостинга каналов (без attribution и префиксов)
func formatMaxCrosspostCaption(upd *maxschemes.MessageCreatedUpdate) string {
return upd.Message.Body.Text
}
// mimeToFilename генерирует имя файла из MIME-типа, если оригинальное имя отсутствует.
func mimeToFilename(base, mime string) string {
ext := ""
// sub = часть после "/" в mime type
if i := strings.Index(mime, "/"); i >= 0 {
sub := mime[i+1:]
switch sub {
case "mp4":
ext = ".mp4"
case "webm":
ext = ".webm"
case "x-matroska":
ext = ".mkv"
case "quicktime":
ext = ".mov"
case "mpeg":
ext = ".mpeg"
case "ogg":
ext = ".ogg"
case "pdf":
ext = ".pdf"
case "gif":
ext = ".gif"
default:
ext = "." + sub
}
}
return base + ext
}
// fileNameFromURL извлекает имя файла из URL, fallback "file".
func fileNameFromURL(rawURL string) string {
if idx := strings.LastIndex(rawURL, "/"); idx >= 0 {
name := rawURL[idx+1:]
if q := strings.Index(name, "?"); q >= 0 {
name = name[:q]
}
if name != "" {
return name
}
}
return "file"
}