mirror of
https://github.com/BEARlogin/max-telegram-bridge-bot.git
synced 2026-04-28 03:39:46 +00:00
Fix formatting in both TG→MAX and MAX→TG directions
Some checks are pending
Build / build (push) Waiting to run
Some checks are pending
Build / build (push) Waiting to run
TG→MAX: tgEntitiesToMarkdown was called on caption with "Name: " prefix, but entity offsets are relative to raw text — formatting markers landed in wrong positions. Now entities are converted on raw text first, then attribution is applied. MAX→TG: condition `caption == text` was always false for bridge chats (caption has "Name: " prefix), so markups were never converted to HTML. Now markups are always applied, with proper HTML-escaped attribution. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ec166754c3
commit
3bd42a5a5e
2 changed files with 113 additions and 48 deletions
66
max.go
66
max.go
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
|
@ -93,11 +94,28 @@ func (b *Bridge) listenMax(ctx context.Context) {
|
|||
if strings.HasPrefix(text, "[TG]") || strings.HasPrefix(text, "[MAX]") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Конвертируем markups в HTML если есть
|
||||
var fwd string
|
||||
if prefix {
|
||||
fwd = formatAttribution("[MAX] "+name, text, b.cfg.MessageNewline)
|
||||
var editParseMode string
|
||||
if len(editUpd.Message.Body.Markups) > 0 {
|
||||
htmlText := maxMarkupsToHTML(text, editUpd.Message.Body.Markups)
|
||||
escapedName := html.EscapeString(name)
|
||||
if prefix {
|
||||
escapedName = "[MAX] " + escapedName
|
||||
}
|
||||
if b.cfg.MessageNewline {
|
||||
fwd = escapedName + ":\n" + htmlText
|
||||
} else {
|
||||
fwd = escapedName + ": " + htmlText
|
||||
}
|
||||
editParseMode = "HTML"
|
||||
} else {
|
||||
fwd = formatAttribution(name, text, b.cfg.MessageNewline)
|
||||
if prefix {
|
||||
fwd = formatAttribution("[MAX] "+name, text, b.cfg.MessageNewline)
|
||||
} else {
|
||||
fwd = formatAttribution(name, text, b.cfg.MessageNewline)
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем вложения в edit — если есть медиа, используем editMessageMedia
|
||||
|
|
@ -131,16 +149,16 @@ func (b *Bridge) listenMax(ctx context.Context) {
|
|||
var mediaIM TGInputMedia
|
||||
switch mediaType {
|
||||
case "photo":
|
||||
mediaIM = TGInputMedia{Type: "photo", File: FileArg{Name: name, Bytes: data}, Caption: fwd}
|
||||
mediaIM = TGInputMedia{Type: "photo", File: FileArg{Name: name, Bytes: data}, Caption: fwd, ParseMode: editParseMode}
|
||||
case "video":
|
||||
mediaIM = TGInputMedia{Type: "video", File: FileArg{Name: name, Bytes: data}, Caption: fwd}
|
||||
mediaIM = TGInputMedia{Type: "video", File: FileArg{Name: name, Bytes: data}, Caption: fwd, ParseMode: editParseMode}
|
||||
case "document":
|
||||
mediaIM = TGInputMedia{Type: "document", File: FileArg{Name: name, Bytes: data}, Caption: fwd}
|
||||
mediaIM = TGInputMedia{Type: "document", File: FileArg{Name: name, Bytes: data}, Caption: fwd, ParseMode: editParseMode}
|
||||
}
|
||||
if err := b.tg.EditMessageMedia(ctx, tgChatID, tgMsgID, mediaIM); err != nil {
|
||||
slog.Error("MAX→TG edit media failed", "err", err, "uid", editUpd.Message.Sender.UserId)
|
||||
// Fallback — отправляем как новое сообщение
|
||||
go b.sendTgMediaFromURL(ctx, tgChatID, mediaURL, mediaType, fwd, "", 0, 0, b.cfg.maxMaxFileBytes())
|
||||
go b.sendTgMediaFromURL(ctx, tgChatID, mediaURL, mediaType, fwd, editParseMode, 0, 0, b.cfg.maxMaxFileBytes())
|
||||
} else {
|
||||
slog.Info("MAX→TG edited media", "tgMsg", tgMsgID, "type", mediaType, "uid", editUpd.Message.Sender.UserId)
|
||||
}
|
||||
|
|
@ -151,7 +169,11 @@ func (b *Bridge) listenMax(ctx context.Context) {
|
|||
if text == "" {
|
||||
continue
|
||||
}
|
||||
if err := b.tg.EditMessageText(ctx, tgChatID, tgMsgID, fwd, nil); err != nil {
|
||||
var editOpts *SendOpts
|
||||
if editParseMode != "" {
|
||||
editOpts = &SendOpts{ParseMode: editParseMode}
|
||||
}
|
||||
if err := b.tg.EditMessageText(ctx, tgChatID, tgMsgID, fwd, editOpts); err != nil {
|
||||
slog.Error("MAX→TG edit failed", "err", err, "uid", editUpd.Message.Sender.UserId, "maxChat", editUpd.Message.Recipient.ChatId)
|
||||
} else {
|
||||
slog.Info("MAX→TG edited", "tgMsg", tgMsgID, "uid", editUpd.Message.Sender.UserId, "maxChat", editUpd.Message.Recipient.ChatId)
|
||||
|
|
@ -910,11 +932,27 @@ func (b *Bridge) forwardMaxToTg(ctx context.Context, msgUpd *maxschemes.MessageC
|
|||
mediaSent := false
|
||||
var qAttType, qAttURL string // для очереди при ошибке
|
||||
|
||||
// Определяем HTML caption если есть markups (для кросспостинга)
|
||||
// Определяем HTML caption если есть markups
|
||||
htmlCaption := caption
|
||||
useHTML := len(body.Markups) > 0 && caption == text
|
||||
useHTML := len(body.Markups) > 0
|
||||
if useHTML {
|
||||
htmlCaption = maxMarkupsToHTML(text, body.Markups)
|
||||
htmlText := maxMarkupsToHTML(text, body.Markups)
|
||||
if caption == text {
|
||||
// Кросспостинг: caption = сырой текст, без атрибуции
|
||||
htmlCaption = htmlText
|
||||
} else {
|
||||
// Bridge: caption с атрибуцией — конвертируем текст отдельно, потом строим атрибуцию
|
||||
name := maxName(msgUpd)
|
||||
if b.repo.HasPrefix("max", msgUpd.Message.Recipient.ChatId) {
|
||||
name = "[MAX] " + name
|
||||
}
|
||||
escapedName := html.EscapeString(name)
|
||||
if b.cfg.MessageNewline {
|
||||
htmlCaption = escapedName + ":\n" + htmlText
|
||||
} else {
|
||||
htmlCaption = escapedName + ": " + htmlText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Собираем вложения: фото/видео → albumMedia (отправляем вместе), остальные → soloMedia
|
||||
|
|
@ -1059,10 +1097,8 @@ func (b *Bridge) forwardMaxToTg(ctx context.Context, msgUpd *maxschemes.MessageC
|
|||
if text == "" {
|
||||
return
|
||||
}
|
||||
// Если есть markups и caption = оригинальный текст (кросспостинг), конвертируем в HTML
|
||||
if len(body.Markups) > 0 && caption == text {
|
||||
htmlText := maxMarkupsToHTML(text, body.Markups)
|
||||
sentMsgID, sendErr = b.tg.SendMessage(ctx, tgChatID, htmlText, &SendOpts{ParseMode: "HTML", ReplyToID: replyToID, ThreadID: threadID})
|
||||
if useHTML {
|
||||
sentMsgID, sendErr = b.tg.SendMessage(ctx, tgChatID, htmlCaption, &SendOpts{ParseMode: "HTML", ReplyToID: replyToID, ThreadID: threadID})
|
||||
} else {
|
||||
sentMsgID, sendErr = b.tg.SendMessage(ctx, tgChatID, caption, &SendOpts{ReplyToID: replyToID, ThreadID: threadID})
|
||||
}
|
||||
|
|
|
|||
95
telegram.go
95
telegram.go
|
|
@ -89,12 +89,26 @@ func (b *Bridge) listenTelegram(ctx context.Context) {
|
|||
continue
|
||||
}
|
||||
|
||||
// Текстовый edit
|
||||
fwd := formatTgMessage(edited, prefix, b.cfg.MessageNewline)
|
||||
if fwd == "" {
|
||||
// Текстовый edit — конвертируем entities в markdown
|
||||
rawText := edited.Text
|
||||
editEntities := edited.Entities
|
||||
if rawText == "" {
|
||||
rawText = edited.Caption
|
||||
editEntities = edited.CaptionEntities
|
||||
}
|
||||
if rawText == "" {
|
||||
continue
|
||||
}
|
||||
mdText := tgEntitiesToMarkdown(rawText, editEntities)
|
||||
name := tgName(edited)
|
||||
if prefix {
|
||||
name = "[TG] " + name
|
||||
}
|
||||
fwd := formatAttribution(name, mdText, b.cfg.MessageNewline)
|
||||
m := maxbot.NewMessage().SetChat(maxChatID).SetText(fwd)
|
||||
if mdText != rawText {
|
||||
m.SetFormat("markdown")
|
||||
}
|
||||
if err := b.maxApi.Messages.EditMessage(ctx, maxMsgID, m); err != nil {
|
||||
slog.Error("TG→MAX edit failed", "err", err, "uid", tgUserID(edited), "tgChat", edited.Chat.ID)
|
||||
} else {
|
||||
|
|
@ -463,11 +477,19 @@ func (b *Bridge) forwardTgToMax(ctx context.Context, msg *TGMessage, maxChatID i
|
|||
if checkSize(photo.FileSize, "") {
|
||||
return
|
||||
}
|
||||
// Конвертируем entities в markdown для caption фото
|
||||
photoEntities := msg.CaptionEntities
|
||||
mdCaption := tgEntitiesToMarkdown(caption, photoEntities)
|
||||
// Конвертируем entities в markdown на сыром тексте (до атрибуции, иначе офсеты съезжают)
|
||||
rawText := msg.Caption
|
||||
if rawText == "" {
|
||||
rawText = msg.Text
|
||||
}
|
||||
mdText := tgEntitiesToMarkdown(rawText, msg.CaptionEntities)
|
||||
name := tgName(msg)
|
||||
if b.repo.HasPrefix("tg", msg.Chat.ID) {
|
||||
name = "[TG] " + name
|
||||
}
|
||||
mdCaption := formatAttribution(name, mdText, b.cfg.MessageNewline)
|
||||
m := maxbot.NewMessage().SetChat(maxChatID).SetText(mdCaption)
|
||||
if mdCaption != caption {
|
||||
if mdText != rawText {
|
||||
m.SetFormat("markdown")
|
||||
}
|
||||
if b.cfg.TgAPIURL != "" {
|
||||
|
|
@ -687,6 +709,16 @@ func (b *Bridge) forwardTgToMax(ctx context.Context, msg *TGMessage, maxChatID i
|
|||
}
|
||||
}
|
||||
|
||||
// Конвертируем TG entities в markdown на сыром тексте (до атрибуции, иначе офсеты съезжают)
|
||||
rawText := msg.Text
|
||||
entities := msg.Entities
|
||||
if rawText == "" {
|
||||
rawText = msg.Caption
|
||||
entities = msg.CaptionEntities
|
||||
}
|
||||
mdText := tgEntitiesToMarkdown(rawText, entities)
|
||||
hasFormatting := mdText != rawText
|
||||
|
||||
// Fallback для неудавшейся загрузки медиа
|
||||
if mediaAttType == "" && msg.Text == "" {
|
||||
mediaType := ""
|
||||
|
|
@ -706,7 +738,7 @@ func (b *Bridge) forwardTgToMax(ctx context.Context, msg *TGMessage, maxChatID i
|
|||
default:
|
||||
return
|
||||
}
|
||||
caption = caption + mediaType
|
||||
mdText = mdText + mediaType
|
||||
}
|
||||
|
||||
// Reply ID
|
||||
|
|
@ -717,13 +749,11 @@ func (b *Bridge) forwardTgToMax(ctx context.Context, msg *TGMessage, maxChatID i
|
|||
}
|
||||
}
|
||||
|
||||
// Конвертируем TG entities в markdown для MAX
|
||||
entities := msg.Entities
|
||||
if entities == nil {
|
||||
entities = msg.CaptionEntities
|
||||
name := tgName(msg)
|
||||
if b.repo.HasPrefix("tg", msg.Chat.ID) {
|
||||
name = "[TG] " + name
|
||||
}
|
||||
mdCaption := tgEntitiesToMarkdown(caption, entities)
|
||||
hasFormatting := mdCaption != caption
|
||||
mdCaption := formatAttribution(name, mdText, b.cfg.MessageNewline)
|
||||
|
||||
var mid string
|
||||
var sendErr error
|
||||
|
|
@ -771,14 +801,26 @@ func (b *Bridge) editTgMediaInMax(ctx context.Context, msg *TGMessage, maxChatID
|
|||
uid := tgUserID(msg)
|
||||
m := maxbot.NewMessage().SetChat(maxChatID)
|
||||
|
||||
// Конвертируем entities в markdown на сыром тексте (до атрибуции)
|
||||
rawText := msg.Caption
|
||||
editEntities := msg.CaptionEntities
|
||||
if rawText == "" {
|
||||
rawText = msg.Text
|
||||
editEntities = msg.Entities
|
||||
}
|
||||
mdText := tgEntitiesToMarkdown(rawText, editEntities)
|
||||
name := tgName(msg)
|
||||
if b.repo.HasPrefix("tg", msg.Chat.ID) {
|
||||
name = "[TG] " + name
|
||||
}
|
||||
mdCaption := formatAttribution(name, mdText, b.cfg.MessageNewline)
|
||||
m.SetText(mdCaption)
|
||||
if mdText != rawText {
|
||||
m.SetFormat("markdown")
|
||||
}
|
||||
|
||||
if msg.Photo != nil {
|
||||
photo := msg.Photo[len(msg.Photo)-1]
|
||||
photoEntities := msg.CaptionEntities
|
||||
mdCaption := tgEntitiesToMarkdown(caption, photoEntities)
|
||||
m.SetText(mdCaption)
|
||||
if mdCaption != caption {
|
||||
m.SetFormat("markdown")
|
||||
}
|
||||
if b.cfg.TgAPIURL != "" {
|
||||
if uploaded, err := b.uploadTgPhotoToMax(ctx, photo.FileID); err == nil {
|
||||
m.AddPhoto(uploaded)
|
||||
|
|
@ -794,19 +836,6 @@ func (b *Bridge) editTgMediaInMax(ctx context.Context, msg *TGMessage, maxChatID
|
|||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Видео, документ, анимация, голос, аудио — для edit обновляем только текст,
|
||||
// т.к. замена нефотовложений через edit не поддерживается MAX API (только photo payload).
|
||||
// Медиа остаётся прежним, обновляется caption.
|
||||
entities := msg.CaptionEntities
|
||||
if entities == nil {
|
||||
entities = msg.Entities
|
||||
}
|
||||
mdCaption := tgEntitiesToMarkdown(caption, entities)
|
||||
m.SetText(mdCaption)
|
||||
if mdCaption != caption {
|
||||
m.SetFormat("markdown")
|
||||
}
|
||||
}
|
||||
|
||||
if err := b.maxApi.Messages.EditMessage(ctx, maxMsgID, m); err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue