mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-10-07 16:51:47 +00:00
XEP-0297 forwards
This commit is contained in:
parent
b99a2bcc5f
commit
d8b7c92439
8 changed files with 468 additions and 28 deletions
|
|
@ -813,19 +813,13 @@ 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 {
|
||||
for _, message := range messages {
|
||||
c.ProcessIncomingMessage(targetChatId, message)
|
||||
}
|
||||
}
|
||||
// print vCard
|
||||
case "vcard":
|
||||
info, err := c.GetVcardInfo(chatID)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
157
xmpp/forwards.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,11 +556,12 @@ 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 == "" {
|
||||
// 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 != "" {
|
||||
message.Extensions = append(message.Extensions, stanza.OOB{
|
||||
URL: 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{})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue