XEP-0297 forwards

This commit is contained in:
Bohdan Horbeshko 2026-09-24 11:03:25 -04:00
parent b99a2bcc5f
commit d8b7c92439
8 changed files with 468 additions and 28 deletions

View file

@ -813,18 +813,12 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool,
return "Cannot parse target chat ID", true, false
}
messages, err := c.client.ForwardMessages(&client.ForwardMessagesRequest{
ChatId: targetChatId,
FromChatId: chatID,
MessageIds: []int64{messageId},
})
messages, err := c.ForwardMessage(chatID, targetChatId, []int64{messageId})
if err != nil {
return err.Error(), true, false
}
if messages != nil && messages.Messages != nil {
for _, message := range messages.Messages {
c.ProcessIncomingMessage(targetChatId, message)
}
for _, message := range messages {
c.ProcessIncomingMessage(targetChatId, message)
}
// print vCard
case "vcard":

View file

@ -411,7 +411,7 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
isCarbon = c.isCarbonsEnabled() && message.IsOutgoing && !isMUC
// reply correction support in clients is suboptimal yet, so cut them out for now
prefix, _ = c.messageToPrefix(message, "", "", true)
prefix, _ = c.messageToPrefix(message, "", "", true, false)
} else {
log.Errorf("No message %v/%v found, cannot reliably determine if it is a carbon and if it is edited: %v", update.ChatId, update.MessageId, messageErr.Error())
}

View file

@ -1403,6 +1403,28 @@ func (c *Client) formatOrigin(origin client.MessageOrigin) string {
return "Unknown origin type"
}
// formatOriginJid is formatOrigin's counterpart for XEP-0297: a bare
// id@component JID standing in for the original forwarded message's
// sender, for the <forwarded><message from=.../> attribute. Hidden-user
// origins carry no id at all, so they resolve to "".
func (c *Client) formatOriginJid(origin client.MessageOrigin) string {
if origin == nil {
return ""
}
switch origin.MessageOriginType() {
case client.TypeMessageOriginUser:
originUser := origin.(*client.MessageOriginUser)
return fmt.Sprintf("%v@%s", originUser.SenderUserId, gateway.Jid.Bare())
case client.TypeMessageOriginChat:
originChat := origin.(*client.MessageOriginChat)
return fmt.Sprintf("%v@%s", originChat.SenderChatId, gateway.Jid.Bare())
case client.TypeMessageOriginChannel:
channel := origin.(*client.MessageOriginChannel)
return fmt.Sprintf("%v@%s", channel.ChatId, gateway.Jid.Bare())
}
return ""
}
func (c *Client) formatFile(file *client.File, compact bool) (string, string) {
if file == nil {
return "", ""
@ -1940,7 +1962,7 @@ func (c *Client) isCarbonsEnabled() bool {
return gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons
}
func (c *Client) messageToPrefix(message *client.Message, previewString string, fileString string, suppressReply bool) (string, *gateway.Reply) {
func (c *Client) messageToPrefix(message *client.Message, previewString string, fileString string, suppressReply bool, suppressForward bool) (string, *gateway.Reply) {
isPM, chat, err := c.IsPM(message.ChatId, true)
if err != nil {
log.Errorf("Could not determine chat type: %v", err)
@ -2010,8 +2032,12 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string,
}
}
if message.ForwardInfo != nil {
prefix = append(prefix, "fwd: "+c.formatOrigin(message.ForwardInfo.Origin))
if message.ForwardInfo != nil && !suppressForward {
forwardWord := "fwd: "
if !c.Session.AsciiArrows {
forwardWord = "↪ " + forwardWord
}
prefix = append(prefix, forwardWord+c.formatOrigin(message.ForwardInfo.Origin))
}
// preview
if previewString != "" {
@ -2257,6 +2283,7 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
var fileMetaDate string
var reply *gateway.Reply
var replyObtained bool
var forwarded *gateway.Forwarded
content := message.Content
if content != nil && content.MessageContentType() == client.TypeMessageChatChangePhoto {
@ -2337,7 +2364,7 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
if oobSwap {
prefixFileName = ""
}
prefix, prefixReply := c.messageToPrefix(message, previewName, prefixFileName, false)
prefix, prefixReply := c.messageToPrefix(message, previewName, prefixFileName, false, false)
reply = prefixReply
replyObtained = true
newText.WriteString(prefix)
@ -2374,6 +2401,27 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
reply, _ = c.getMessageReply(message, false, true)
}
// XEP-0297: embed the original message's own prefix (id/sender/reply
// preview - everything messageToPrefix would normally show for it)
// alongside its content, but without a self-referential "fwd:" line -
// the wrapping <forwarded/> itself already says this is a forward.
if message.ForwardInfo != nil {
innerPrefix, _ := c.messageToPrefix(message, "", "", false, true)
var innerText strings.Builder
innerText.WriteString(innerPrefix)
if rawCaption != "" {
if innerPrefix != "" {
innerText.WriteString(c.getPrefixSeparator(chatId))
}
innerText.WriteString(rawCaption)
}
forwarded = &gateway.Forwarded{
Author: c.formatOriginJid(message.ForwardInfo.Origin),
Timestamp: int64(message.ForwardInfo.Date),
Text: innerText.String(),
}
}
// mark message as read
if !c.Session.Receipts {
c.MarkAsRead(chatId, message.Id)
@ -2532,6 +2580,11 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
HashValue: fileMetaHash,
}))
}
if forwarded != nil && textEnvelope == nil {
// forwarded.Text is plaintext - only rides along on the copy
// of this message that's itself going out unencrypted.
textArgs = append(textArgs, gateway.SMForwarded(forwarded))
}
gateway.SendMessage(jid, from, c.xmpp, textArgs...)
if auxText != "" {
auxArgs := append(commonArgs,
@ -2540,6 +2593,9 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
if oob != "" {
auxArgs = append(auxArgs, gateway.SMAttachToId(sId))
}
if forwarded != nil && auxEnvelope == nil {
auxArgs = append(auxArgs, gateway.SMForwarded(forwarded))
}
gateway.SendMessage(jid, from, c.xmpp, auxArgs...)
}
}
@ -2970,6 +3026,26 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str
return tgMessage, false
}
// ForwardMessage forwards one or more Telegram messages (all from the
// same source chat) natively via TDLib in a single call, so an album
// forwarded together stays grouped instead of arriving as separate
// standalone messages. Shared by the /forward command and incoming
// XEP-0297 forwards.
func (c *Client) ForwardMessage(fromChatId, toChatId int64, messageIds []int64) ([]*client.Message, error) {
result, err := c.client.ForwardMessages(&client.ForwardMessagesRequest{
ChatId: toChatId,
FromChatId: fromChatId,
MessageIds: messageIds,
})
if err != nil {
return nil, err
}
if result == nil {
return nil, nil
}
return result.Messages, nil
}
func (c *Client) returnMessage(returnJid string, chatID int64, text string, code int, isGroupchat bool) {
if isGroupchat {
if code != 0 {
@ -4517,6 +4593,108 @@ func (c *Client) GetPreviousMessage(chatID, messageId int64) (*client.Message, e
return nil, err
}
// GetAlbumMessages returns every message TDLib currently associates with
// the same MediaAlbumId as messageId (messageId's own message included),
// in chat order. TDLib has no direct "messages by album id" query, and
// chasing single next/previous messages via GetNextMessage/
// GetPreviousMessage isn't reliable here - GetChatHistory's negative
// offset (the "newer" direction) doesn't exclude the reference message
// the way a zero/positive offset does for the "older" direction, so a
// limit of 1 can just re-return messageId itself instead of its
// successor. A wide single-window fetch around messageId, filtered by
// MediaAlbumId, sidesteps that boundary quirk entirely. Returns just
// messageId's own message if it isn't part of an album.
func (c *Client) GetAlbumMessages(chatId, messageId int64) ([]*client.Message, error) {
message, err := c.client.GetMessage(&client.GetMessageRequest{
ChatId: chatId,
MessageId: messageId,
})
if err != nil {
return nil, err
}
if message == nil {
return nil, nil
}
if message.MediaAlbumId == 0 {
return []*client.Message{message}, nil
}
// Telegram caps albums at 10 items; a window of 10 either side is
// generously more than enough regardless of where messageId falls
// within the album.
history, err := c.client.GetChatHistory(&client.GetChatHistoryRequest{
ChatId: chatId,
FromMessageId: messageId,
Offset: -10,
Limit: 20,
})
if err != nil {
return []*client.Message{message}, nil
}
seen := map[int64]bool{message.Id: true}
messages := []*client.Message{message}
for _, m := range history.Messages {
if m == nil || m.MediaAlbumId != message.MediaAlbumId || seen[m.Id] {
continue
}
seen[m.Id] = true
messages = append(messages, m)
}
sort.Slice(messages, func(i, j int) bool { return messages[i].Id < messages[j].Id })
ids := make([]int64, len(messages))
for i, m := range messages {
ids[i] = m.Id
}
log.Infof("GetAlbumMessages %v/%v: MediaAlbumId=%v found %v", chatId, messageId, message.MediaAlbumId, ids)
return messages, nil
}
// ResolveAlbumFileNames expands the album messageId (in chatId) belongs
// to and maps each of names - a XEP-0447 <file><name> value, itself
// PermastoreFile's own deterministic "<remote unique id><ext>" naming -
// back to the specific Telegram message it names, so a forwarded
// reference bundling several files under one id (see SendAlbumToGateway's
// anchor) can be split back into its real per-file messages. Names with
// no match are omitted; result order follows names, not chat order.
func (c *Client) ResolveAlbumFileNames(chatId, messageId int64, names []string) []int64 {
messages, err := c.GetAlbumMessages(chatId, messageId)
if err != nil || len(messages) == 0 {
log.Infof("ResolveAlbumFileNames %v/%v: GetAlbumMessages failed: %v", chatId, messageId, err)
return nil
}
var result []int64
for _, name := range names {
matched := false
for _, message := range messages {
if message == nil || message.Content == nil {
continue
}
file, _ := c.contentToFile(message.Content)
var uniqueId string
if file != nil && file.Remote != nil {
uniqueId = file.Remote.UniqueId
}
log.Infof("ResolveAlbumFileNames %v/%v: candidate %v uniqueId=%q vs name=%q", chatId, messageId, message.Id, uniqueId, name)
if uniqueId == "" {
continue
}
if strings.HasPrefix(name, uniqueId) {
result = append(result, message.Id)
matched = true
break
}
}
if !matched {
log.Infof("ResolveAlbumFileNames %v/%v: no candidate matched name=%q", chatId, messageId, name)
}
}
return result
}
// GetErrorCode obtains an error code from a Telegram response error
func GetErrorCode(err error) (int32, bool) {
responseError, ok := err.(client.ResponseError)

View file

@ -452,8 +452,8 @@ func TestMessageToPrefix1(t *testing.T) {
},
},
}
prefix, gatewayReply := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "", "", false)
if prefix != "➡ 42 | fwd: ziz" {
prefix, gatewayReply := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "", "", false, false)
if prefix != "➡ 42 | ↪ fwd: ziz" {
t.Errorf("Wrong prefix: %v", prefix)
}
if gatewayReply != nil {
@ -470,8 +470,8 @@ func TestMessageToPrefix2(t *testing.T) {
},
},
}
prefix, gatewayReply := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "y.jpg", "", false)
if prefix != "⬅ 56 | fwd: (zaz) | preview: y.jpg" {
prefix, gatewayReply := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "y.jpg", "", false, false)
if prefix != "⬅ 56 | ↪ fwd: (zaz) | preview: y.jpg" {
t.Errorf("Wrong prefix: %v", prefix)
}
if gatewayReply != nil {
@ -488,7 +488,7 @@ func TestMessageToPrefix3(t *testing.T) {
},
},
}
prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "a.jpg", false)
prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "a.jpg", false, false)
if prefix != "< 56 | fwd: (zuz) | file: a.jpg" {
t.Errorf("Wrong prefix: %v", prefix)
}
@ -502,7 +502,7 @@ func TestMessageToPrefix4(t *testing.T) {
Id: 23,
IsOutgoing: true,
}
prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", false)
prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", false, false)
if prefix != "> 23" {
t.Errorf("Wrong prefix: %v", prefix)
}
@ -520,7 +520,7 @@ func TestMessageToPrefix5(t *testing.T) {
},
},
}
prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "h.jpg", "a.jpg", false)
prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "h.jpg", "a.jpg", false, false)
if prefix != "< 560 | fwd: (zyz) | preview: h.jpg | file: a.jpg" {
t.Errorf("Wrong prefix: %v", prefix)
}
@ -546,7 +546,7 @@ func TestMessageToPrefix6(t *testing.T) {
},
},
}
prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", false)
prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", false, false)
if prefix != "> 23 | reply: ziz @ unknown contact: TDlib instance is offline | tist uz iz" {
t.Errorf("Wrong prefix: %v", prefix)
}
@ -572,7 +572,7 @@ func TestMessageToPrefix7(t *testing.T) {
},
},
}
prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", false)
prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", false, false)
if prefix != "> 23 | reply: (zaz) @ unknown contact: TDlib instance is offline | tist" {
t.Errorf("Wrong prefix: %v", prefix)
}
@ -598,7 +598,7 @@ func TestMessageToPrefix8(t *testing.T) {
},
},
}
prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", true)
prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", true, false)
if prefix != "> 23" {
t.Errorf("Wrong prefix: %v", prefix)
}

View file

@ -192,6 +192,65 @@ type ClientMessage struct {
Extensions []stanza.MsgExtension `xml:",omitempty"`
}
// UnmarshalXML mirrors stanza.Message's own TypeRegistry-driven dispatch,
// needed because the default reflection-based decoder can't populate an
// interface-typed field like Extensions on its own.
func (msg *ClientMessage) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
msg.XMLName = start.Name
for _, attr := range start.Attr {
switch attr.Name.Local {
case "id":
msg.Id = attr.Value
case "type":
msg.Type = stanza.StanzaType(attr.Value)
case "to":
msg.To = attr.Value
case "from":
msg.From = attr.Value
case "lang":
msg.Lang = attr.Value
}
}
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
if msgExt := stanza.TypeRegistry.GetMsgExtension(tt.Name); msgExt != nil {
if err := d.DecodeElement(msgExt, &tt); err != nil {
return err
}
msg.Extensions = append(msg.Extensions, msgExt)
} else {
var err error
switch tt.Name.Local {
case "body":
err = d.DecodeElement(&msg.Body, &tt)
case "thread":
err = d.DecodeElement(&msg.Thread, &tt)
case "subject":
err = d.DecodeElement(&msg.Subject, &tt)
case "error":
err = d.DecodeElement(&msg.Error, &tt)
}
if err != nil {
return err
}
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
// Replace is from XEP-0308
type Replace struct {
XMLName xml.Name `xml:"urn:xmpp:message-correct:0 replace"`
@ -422,6 +481,11 @@ type ForwardedMessage struct {
Delay *MessageDelay `xml:"urn:xmpp:delay delay,omitempty"`
}
// Namespace is a namespace!
func (c ForwardedMessage) Namespace() string {
return c.XMLName.Space
}
// MAM2MessageResult is from XEP-0313
type MAM2MessageResult struct {
XMLName xml.Name `xml:"urn:xmpp:mam:2 result"`
@ -1054,4 +1118,16 @@ func init() {
"urn:xmpp:reactions:0",
"reactions",
}, Reactions{})
// forwarded message
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{
"urn:xmpp:forward:0",
"forwarded",
}, ForwardedMessage{})
// file sharing
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{
"urn:xmpp:sfs:0",
"file-sharing",
}, FileSharing{})
}

157
xmpp/forwards.go Normal file
View file

@ -0,0 +1,157 @@
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)
}
}
}

View file

@ -33,6 +33,13 @@ type Reply struct {
End uint64
}
// Forwarded is a XEP-0297 native forward reference.
type Forwarded struct {
Author string
Timestamp int64
Text string
}
// Reactions is a XEP-0444 reaction set: the sender's complete current
// list of emoji on the message identified by Id (always a full
// replacement, never a diff).
@ -410,6 +417,9 @@ var SMOMEMOEnvelope = args.New()
// SMReactions is a XEP-0444 reaction set (*Reactions)
var SMReactions = args.New()
// SMForwarded is a XEP-0297 native forward reference (*Forwarded)
var SMForwarded = args.New()
func sendMessageWrapper(to, from string, component *xmpp.Component, args ...args.V) {
body := SMBody.Get(args)
subject := SMSubject.Get(args)
@ -440,6 +450,7 @@ func sendMessageWrapper(to, from string, component *xmpp.Component, args ...args
mucUserItem, _ := SMMucUserItem.Get(args).(*MUCUserItem)
envelope, _ := SMOMEMOEnvelope.Get(args).(*e2ee.Envelope)
reactions, _ := SMReactions.Get(args).(*Reactions)
forwarded, _ := SMForwarded.Get(args).(*Forwarded)
toJid, err := stanza.NewJid(to)
if err != nil {
@ -545,9 +556,10 @@ func sendMessageWrapper(to, from string, component *xmpp.Component, args ...args
log.Error(errors.Wrap(err, "Failed to encode OMEMO envelope"))
} else {
message.Extensions = append(message.Extensions, ext, eme)
if message.Body == "" {
message.Body = omemoFallbackBody
}
// body is whatever plaintext SMBody was called with - once it's
// been encrypted into ext above, it must never also go out in
// the clear, regardless of whether it started out empty.
message.Body = omemoFallbackBody
}
}
if oob != "" {
@ -564,6 +576,22 @@ func sendMessageWrapper(to, from string, component *xmpp.Component, args ...args
message.Extensions = append(message.Extensions, extensions.NewReplyFallback(reply.Start, reply.End))
}
}
if forwarded != nil && envelope == nil {
// forwarded.Text is plaintext - never attach it alongside an
// OMEMO envelope, since XEP-0384 only encrypts <body>/<encrypted>,
// not arbitrary extensions like this one.
innerMessage := &extensions.ClientMessage{
Body: forwarded.Text,
}
if forwarded.Author != "" {
innerMessage.From = forwarded.Author
}
delay := extensions.NewMessageDelay(forwarded.Timestamp, "")
message.Extensions = append(message.Extensions, extensions.ForwardedMessage{
Message: innerMessage,
Delay: &delay,
})
}
if !isGroupchat && !isCarbon && toJid.Resource != "" && inviteFrom == "" {
message.Extensions = append(message.Extensions, stanza.HintNoCopy{})
}

View file

@ -221,7 +221,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
return
}
if msg.Type != "error" && (msg.Body != "" || hasOMEMOPayload(msg) || hasReactionsPayload(msg)) {
if msg.Type != "error" && (msg.Body != "" || hasOMEMOPayload(msg) || hasReactionsPayload(msg) || hasForwardedPayload(msg)) {
log.WithFields(log.Fields{
"from": msg.From,
"to": msg.To,
@ -284,17 +284,24 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
msg.Get(&replace)
msg.Get(&attachTo)
msg.Get(&reactions)
forwardedList := collectForwardedMessages(msg)
log.Debugf("reply: %#v", reply)
log.Debugf("fallback: %#v", fallback)
log.Debugf("replace: %#v", replace)
log.Debugf("attachTo: %#v", attachTo)
log.Debugf("reactions: %#v", reactions)
log.Debugf("forwarded: %#v", forwardedList)
if reactions.Id != "" {
handleIncomingReaction(component, msg, session, bare, toID, isGroupchat, reactions)
return
}
if len(forwardedList) > 0 {
handleIncomingForward(component, msg, session, bare, toID, isGroupchat, forwardedList)
return
}
var replyId int64
text := msg.Body