max-telegram-bridge-bot/format.go
Andrey Lugovskoy f57105d2b6
Some checks failed
Build / build (push) Has been cancelled
crosspost: fix long album caption + strip custom emoji and whitespace padding
- MAX→TG album: caption over Telegram's 1024 media-group limit is no longer lost —
  the album is sent without an inline caption and the full text follows as a
  separate message (sendMaxAlbumToTg, covers both direct and queued delivery).
- Forwarded text: strip Telegram custom_emoji (MAX can't render them; the fallback
  glyph only littered the copy) in tgEntitiesToHTML, and collapse whitespace padding
  (runs of spaces/nbsp, trailing spaces, 3+ blank lines) in crosspost captions —
  cuts bloated length (MAX ~4000 char limit) and fixes ugly gaps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ajcn7YhgeexPwbfrUNMj8u
2026-07-03 13:00:46 +04:00

209 lines
6.3 KiB
Go
Raw 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 (
"html"
"regexp"
"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
}
// formatAttributionHTML — имя жирным в HTML: "<b>Имя</b>: текст". Имя экранируется;
// text уже должен быть HTML (прошёл через tgEntitiesToHTML).
func formatAttributionHTML(name, text string, newline bool) string {
bold := "<b>" + html.EscapeString(name) + "</b>"
if newline {
return bold + ":\n" + text
}
return bold + ": " + text
}
// tgForwardLine — метка «↪️ Переслано из X\n» (HTML, italic) для зеркалируемых форвардов.
// "" если сообщение не форвард. Подмешивается в начало тела (после атрибуции автора).
func tgForwardLine(msg *TGMessage) string {
if msg.ForwardFrom == "" {
return ""
}
return "↪️ <i>Переслано из " + html.EscapeString(msg.ForwardFrom) + "</i>\n"
}
// tgContactText — текстовое представление контакта (sharing телефона), чтобы зеркало
// не было пустым. "" если контакта нет.
func tgContactText(msg *TGMessage) string {
if msg.Contact == nil {
return ""
}
name := strings.TrimSpace(msg.Contact.FirstName + " " + msg.Contact.LastName)
if name == "" {
name = "Контакт"
}
return "📇 " + name + "\n📞 " + msg.Contact.PhoneNumber
}
// formatTgCaption — для пересылки (текст или caption)
func formatTgCaption(msg *TGMessage, prefix, newline bool) string {
name := tgName(msg)
text := msg.Text
if text == "" {
text = msg.Caption
}
if text == "" {
text = tgContactText(msg)
}
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 == "" {
text = tgContactText(msg)
}
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 в HTML (format="html" в MAX), чтобы сохранить ссылки,
// форматирование, КОД и ЦИТАТЫ (MAX-markdown их не рендерит). Replacements
// применяются поверх HTML.
func formatTgCrosspostCaption(msg *TGMessage) string {
text := msg.Text
entities := msg.Entities
if text == "" {
text = msg.Caption
entities = msg.CaptionEntities
}
return collapseWhitespace(tgEntitiesToHTML(text, entities))
}
// formatMaxCrosspostCaption — для кросспостинга каналов (без attribution и префиксов)
func formatMaxCrosspostCaption(upd *maxschemes.MessageCreatedUpdate) string {
return collapseWhitespace(upd.Message.Body.Text)
}
// collapseWhitespace убирает «паддинг» из пересылаемого текста: длинные пробельные
// прогоны и лишние пустые строки (часто в постах-простынях), которые раздувают длину
// (лимит MAX ~4000) и уродуют вид. Схлопывает 2+ пробелов/табов/nbsp → 1, обрезает
// хвостовые пробелы строк, 3+ переносов → максимум одна пустая строка.
var (
reTrailSpace = regexp.MustCompile(`[ \t\x{00A0}]+\n`)
reManyBlank = regexp.MustCompile(`\n{3,}`)
reManySpace = regexp.MustCompile(`[ \t\x{00A0}]{2,}`)
)
func collapseWhitespace(s string) string {
if s == "" {
return s
}
s = strings.ReplaceAll(s, "\r\n", "\n")
s = reTrailSpace.ReplaceAllString(s, "\n")
s = reManyBlank.ReplaceAllString(s, "\n\n")
s = reManySpace.ReplaceAllString(s, " ")
return strings.TrimSpace(s)
}
// 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"
}