telegabber/xmpp/forwards.go
2026-09-24 11:03:25 -04:00

157 lines
5.3 KiB
Go

package xmpp
import (
"strconv"
"strings"
log "github.com/sirupsen/logrus"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
"dev.narayana.im/narayana/telegabber/telegram"
"dev.narayana.im/narayana/telegabber/xmpp/extensions"
"dev.narayana.im/narayana/telegabber/xmpp/gateway"
)
// collectForwardedMessages returns every XEP-0297 <forwarded/> wrapping a
// message on msg, in stanza order. A client forwarding several selected
// messages at once (e.g. an album plus a reply) puts them all in one
// stanza as sibling <forwarded/> elements - stanza.Message.Get only
// returns the first match of a given extension type, so callers that
// need all of them must scan msg.Extensions directly instead.
func collectForwardedMessages(msg stanza.Message) []*extensions.ForwardedMessage {
var result []*extensions.ForwardedMessage
for _, e := range msg.Extensions {
if forwarded, ok := e.(*extensions.ForwardedMessage); ok && forwarded.Message != nil {
result = append(result, forwarded)
}
}
return result
}
// hasForwardedPayload reports whether msg carries at least one XEP-0297
// <forwarded/> wrapping a message - such stanzas may ride without a plain
// body, mirroring hasReactionsPayload/hasOMEMOPayload's role in
// HandleMessage's body-gate.
func hasForwardedPayload(msg stanza.Message) bool {
return len(collectForwardedMessages(msg)) > 0
}
// fileSharingNames extracts each XEP-0447 <file-sharing><file><name>
// value nested in inner - a client bundling several files (e.g. a
// gallery selection) under one forwarded reference lists them as sibling
// file-sharing elements rather than separate <forwarded/> wrappers.
func fileSharingNames(inner *extensions.ClientMessage) []string {
var names []string
for _, e := range inner.Extensions {
if fs, ok := e.(*extensions.FileSharing); ok && fs.File.Name != "" {
names = append(names, fs.File.Name)
}
}
return names
}
// handleIncomingForward relays one or more native XEP-0297 forwards to
// Telegram via TDLib's own ForwardMessages, ignoring whatever plain-text
// body/fallback rides alongside them - only each wrapped message's own
// identity matters. Message ids are grouped by their resolved source
// chat and forwarded together per chat (in original order) so TDLib can
// still recognize and re-form an album, rather than resending each item
// as its own standalone forward. bare and toID are already resolved by
// HandleMessage's caller; toID is the forward's destination chat.
func handleIncomingForward(component *xmpp.Component, msg stanza.Message, session *telegram.Client, bare string, toID int64, isGroupchat bool, forwardedList []*extensions.ForwardedMessage) {
type sourceGroup struct {
chatId int64
msgIds []int64
seen map[int64]bool
}
var groups []*sourceGroup
groupByChatId := make(map[int64]*sourceGroup)
addToGroup := func(chatId int64, msgIds []int64) {
group, ok := groupByChatId[chatId]
if !ok {
group = &sourceGroup{chatId: chatId, seen: make(map[int64]bool)}
groupByChatId[chatId] = group
groups = append(groups, group)
}
for _, msgId := range msgIds {
if group.seen[msgId] {
continue
}
group.seen[msgId] = true
group.msgIds = append(group.msgIds, msgId)
}
}
for _, forwarded := range forwardedList {
inner := forwarded.Message
if inner.Id == "" {
log.Warn("Forwarded message has no id, cannot resolve its source")
continue
}
var sourceChatId, sourceMsgId int64
if chatId, msgId, err := gateway.IdsDB.GetByXmppId(session.Session.Login, bare, inner.Id); err == nil {
sourceChatId, sourceMsgId = chatId, msgId
} else {
msgId, ok := parseMessageId(inner.Id)
if !ok {
continue
}
fromBare, _, ok := gateway.SplitJID(inner.From)
if !ok {
log.Warn("Could not parse forwarded message sender JID")
continue
}
node := strings.TrimPrefix(strings.SplitN(fromBare, "@", 2)[0], "c")
chatId, parseErr := strconv.ParseInt(node, 10, 64)
if parseErr != nil {
log.Warn("Could not parse forwarded source chat id")
continue
}
sourceChatId, sourceMsgId = chatId, msgId
}
// A reference bundling several files (SendAlbumToGateway's own
// anchor id, or any client that groups a gallery selection this
// way) only ever names one representative message on its own -
// match the embedded file-sharing filenames back to the album's
// real per-file messages instead of resending just that one.
if names := fileSharingNames(inner); len(names) > 1 {
matched := session.ResolveAlbumFileNames(sourceChatId, sourceMsgId, names)
log.Infof("Forwarded album %v/%v: names=%v matched=%v", sourceChatId, sourceMsgId, names, matched)
if len(matched) < len(names) {
log.Warnf("Only matched %v/%v forwarded file-sharing names to album messages for %v/%v", len(matched), len(names), sourceChatId, sourceMsgId)
}
if len(matched) > 0 {
addToGroup(sourceChatId, matched)
continue
}
}
addToGroup(sourceChatId, []int64{sourceMsgId})
}
if len(groups) == 0 {
return
}
var from string
if isGroupchat {
from = gateway.MUCJID(toID)
} else {
from = gateway.CHATNODE(toID)
}
for _, group := range groups {
messages, err := session.ForwardMessage(group.chatId, toID, group.msgIds)
if err != nil {
gateway.SendErrorMessage(msg.From, from, err.Error(), 500, isGroupchat, component)
continue
}
for _, message := range messages {
session.ProcessIncomingMessage(toID, message)
}
}
}