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,18 +813,12 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool,
|
||||||
return "Cannot parse target chat ID", true, false
|
return "Cannot parse target chat ID", true, false
|
||||||
}
|
}
|
||||||
|
|
||||||
messages, err := c.client.ForwardMessages(&client.ForwardMessagesRequest{
|
messages, err := c.ForwardMessage(chatID, targetChatId, []int64{messageId})
|
||||||
ChatId: targetChatId,
|
|
||||||
FromChatId: chatID,
|
|
||||||
MessageIds: []int64{messageId},
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err.Error(), true, false
|
return err.Error(), true, false
|
||||||
}
|
}
|
||||||
if messages != nil && messages.Messages != nil {
|
for _, message := range messages {
|
||||||
for _, message := range messages.Messages {
|
c.ProcessIncomingMessage(targetChatId, message)
|
||||||
c.ProcessIncomingMessage(targetChatId, message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// print vCard
|
// print vCard
|
||||||
case "vcard":
|
case "vcard":
|
||||||
|
|
|
||||||
|
|
@ -411,7 +411,7 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
|
||||||
|
|
||||||
isCarbon = c.isCarbonsEnabled() && message.IsOutgoing && !isMUC
|
isCarbon = c.isCarbonsEnabled() && message.IsOutgoing && !isMUC
|
||||||
// reply correction support in clients is suboptimal yet, so cut them out for now
|
// 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 {
|
} 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())
|
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"
|
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) {
|
func (c *Client) formatFile(file *client.File, compact bool) (string, string) {
|
||||||
if file == nil {
|
if file == nil {
|
||||||
return "", ""
|
return "", ""
|
||||||
|
|
@ -1940,7 +1962,7 @@ func (c *Client) isCarbonsEnabled() bool {
|
||||||
return gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons
|
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)
|
isPM, chat, err := c.IsPM(message.ChatId, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Could not determine chat type: %v", err)
|
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 {
|
if message.ForwardInfo != nil && !suppressForward {
|
||||||
prefix = append(prefix, "fwd: "+c.formatOrigin(message.ForwardInfo.Origin))
|
forwardWord := "fwd: "
|
||||||
|
if !c.Session.AsciiArrows {
|
||||||
|
forwardWord = "↪ " + forwardWord
|
||||||
|
}
|
||||||
|
prefix = append(prefix, forwardWord+c.formatOrigin(message.ForwardInfo.Origin))
|
||||||
}
|
}
|
||||||
// preview
|
// preview
|
||||||
if previewString != "" {
|
if previewString != "" {
|
||||||
|
|
@ -2257,6 +2283,7 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
|
||||||
var fileMetaDate string
|
var fileMetaDate string
|
||||||
var reply *gateway.Reply
|
var reply *gateway.Reply
|
||||||
var replyObtained bool
|
var replyObtained bool
|
||||||
|
var forwarded *gateway.Forwarded
|
||||||
|
|
||||||
content := message.Content
|
content := message.Content
|
||||||
if content != nil && content.MessageContentType() == client.TypeMessageChatChangePhoto {
|
if content != nil && content.MessageContentType() == client.TypeMessageChatChangePhoto {
|
||||||
|
|
@ -2337,7 +2364,7 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
|
||||||
if oobSwap {
|
if oobSwap {
|
||||||
prefixFileName = ""
|
prefixFileName = ""
|
||||||
}
|
}
|
||||||
prefix, prefixReply := c.messageToPrefix(message, previewName, prefixFileName, false)
|
prefix, prefixReply := c.messageToPrefix(message, previewName, prefixFileName, false, false)
|
||||||
reply = prefixReply
|
reply = prefixReply
|
||||||
replyObtained = true
|
replyObtained = true
|
||||||
newText.WriteString(prefix)
|
newText.WriteString(prefix)
|
||||||
|
|
@ -2374,6 +2401,27 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
|
||||||
reply, _ = c.getMessageReply(message, false, true)
|
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
|
// mark message as read
|
||||||
if !c.Session.Receipts {
|
if !c.Session.Receipts {
|
||||||
c.MarkAsRead(chatId, message.Id)
|
c.MarkAsRead(chatId, message.Id)
|
||||||
|
|
@ -2532,6 +2580,11 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
|
||||||
HashValue: fileMetaHash,
|
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...)
|
gateway.SendMessage(jid, from, c.xmpp, textArgs...)
|
||||||
if auxText != "" {
|
if auxText != "" {
|
||||||
auxArgs := append(commonArgs,
|
auxArgs := append(commonArgs,
|
||||||
|
|
@ -2540,6 +2593,9 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
|
||||||
if oob != "" {
|
if oob != "" {
|
||||||
auxArgs = append(auxArgs, gateway.SMAttachToId(sId))
|
auxArgs = append(auxArgs, gateway.SMAttachToId(sId))
|
||||||
}
|
}
|
||||||
|
if forwarded != nil && auxEnvelope == nil {
|
||||||
|
auxArgs = append(auxArgs, gateway.SMForwarded(forwarded))
|
||||||
|
}
|
||||||
gateway.SendMessage(jid, from, c.xmpp, auxArgs...)
|
gateway.SendMessage(jid, from, c.xmpp, auxArgs...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2970,6 +3026,26 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str
|
||||||
return tgMessage, false
|
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) {
|
func (c *Client) returnMessage(returnJid string, chatID int64, text string, code int, isGroupchat bool) {
|
||||||
if isGroupchat {
|
if isGroupchat {
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
|
|
@ -4517,6 +4593,108 @@ func (c *Client) GetPreviousMessage(chatID, messageId int64) (*client.Message, e
|
||||||
return nil, err
|
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
|
// GetErrorCode obtains an error code from a Telegram response error
|
||||||
func GetErrorCode(err error) (int32, bool) {
|
func GetErrorCode(err error) (int32, bool) {
|
||||||
responseError, ok := err.(client.ResponseError)
|
responseError, ok := err.(client.ResponseError)
|
||||||
|
|
|
||||||
|
|
@ -452,8 +452,8 @@ func TestMessageToPrefix1(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
prefix, gatewayReply := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "", "", false)
|
prefix, gatewayReply := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "", "", false, false)
|
||||||
if prefix != "➡ 42 | fwd: ziz" {
|
if prefix != "➡ 42 | ↪ fwd: ziz" {
|
||||||
t.Errorf("Wrong prefix: %v", prefix)
|
t.Errorf("Wrong prefix: %v", prefix)
|
||||||
}
|
}
|
||||||
if gatewayReply != nil {
|
if gatewayReply != nil {
|
||||||
|
|
@ -470,8 +470,8 @@ func TestMessageToPrefix2(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
prefix, gatewayReply := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "y.jpg", "", false)
|
prefix, gatewayReply := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "y.jpg", "", false, false)
|
||||||
if prefix != "⬅ 56 | fwd: (zaz) | preview: y.jpg" {
|
if prefix != "⬅ 56 | ↪ fwd: (zaz) | preview: y.jpg" {
|
||||||
t.Errorf("Wrong prefix: %v", prefix)
|
t.Errorf("Wrong prefix: %v", prefix)
|
||||||
}
|
}
|
||||||
if gatewayReply != nil {
|
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" {
|
if prefix != "< 56 | fwd: (zuz) | file: a.jpg" {
|
||||||
t.Errorf("Wrong prefix: %v", prefix)
|
t.Errorf("Wrong prefix: %v", prefix)
|
||||||
}
|
}
|
||||||
|
|
@ -502,7 +502,7 @@ func TestMessageToPrefix4(t *testing.T) {
|
||||||
Id: 23,
|
Id: 23,
|
||||||
IsOutgoing: true,
|
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" {
|
if prefix != "> 23" {
|
||||||
t.Errorf("Wrong prefix: %v", prefix)
|
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" {
|
if prefix != "< 560 | fwd: (zyz) | preview: h.jpg | file: a.jpg" {
|
||||||
t.Errorf("Wrong prefix: %v", prefix)
|
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" {
|
if prefix != "> 23 | reply: ziz @ unknown contact: TDlib instance is offline | tist uz iz" {
|
||||||
t.Errorf("Wrong prefix: %v", prefix)
|
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" {
|
if prefix != "> 23 | reply: (zaz) @ unknown contact: TDlib instance is offline | tist" {
|
||||||
t.Errorf("Wrong prefix: %v", prefix)
|
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" {
|
if prefix != "> 23" {
|
||||||
t.Errorf("Wrong prefix: %v", prefix)
|
t.Errorf("Wrong prefix: %v", prefix)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,65 @@ type ClientMessage struct {
|
||||||
Extensions []stanza.MsgExtension `xml:",omitempty"`
|
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
|
// Replace is from XEP-0308
|
||||||
type Replace struct {
|
type Replace struct {
|
||||||
XMLName xml.Name `xml:"urn:xmpp:message-correct:0 replace"`
|
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"`
|
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
|
// MAM2MessageResult is from XEP-0313
|
||||||
type MAM2MessageResult struct {
|
type MAM2MessageResult struct {
|
||||||
XMLName xml.Name `xml:"urn:xmpp:mam:2 result"`
|
XMLName xml.Name `xml:"urn:xmpp:mam:2 result"`
|
||||||
|
|
@ -1054,4 +1118,16 @@ func init() {
|
||||||
"urn:xmpp:reactions:0",
|
"urn:xmpp:reactions:0",
|
||||||
"reactions",
|
"reactions",
|
||||||
}, 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
|
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
|
// Reactions is a XEP-0444 reaction set: the sender's complete current
|
||||||
// list of emoji on the message identified by Id (always a full
|
// list of emoji on the message identified by Id (always a full
|
||||||
// replacement, never a diff).
|
// replacement, never a diff).
|
||||||
|
|
@ -410,6 +417,9 @@ var SMOMEMOEnvelope = args.New()
|
||||||
// SMReactions is a XEP-0444 reaction set (*Reactions)
|
// SMReactions is a XEP-0444 reaction set (*Reactions)
|
||||||
var SMReactions = args.New()
|
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) {
|
func sendMessageWrapper(to, from string, component *xmpp.Component, args ...args.V) {
|
||||||
body := SMBody.Get(args)
|
body := SMBody.Get(args)
|
||||||
subject := SMSubject.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)
|
mucUserItem, _ := SMMucUserItem.Get(args).(*MUCUserItem)
|
||||||
envelope, _ := SMOMEMOEnvelope.Get(args).(*e2ee.Envelope)
|
envelope, _ := SMOMEMOEnvelope.Get(args).(*e2ee.Envelope)
|
||||||
reactions, _ := SMReactions.Get(args).(*Reactions)
|
reactions, _ := SMReactions.Get(args).(*Reactions)
|
||||||
|
forwarded, _ := SMForwarded.Get(args).(*Forwarded)
|
||||||
|
|
||||||
toJid, err := stanza.NewJid(to)
|
toJid, err := stanza.NewJid(to)
|
||||||
if err != nil {
|
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"))
|
log.Error(errors.Wrap(err, "Failed to encode OMEMO envelope"))
|
||||||
} else {
|
} else {
|
||||||
message.Extensions = append(message.Extensions, ext, eme)
|
message.Extensions = append(message.Extensions, ext, eme)
|
||||||
if message.Body == "" {
|
// body is whatever plaintext SMBody was called with - once it's
|
||||||
message.Body = omemoFallbackBody
|
// 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 != "" {
|
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))
|
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 == "" {
|
if !isGroupchat && !isCarbon && toJid.Resource != "" && inviteFrom == "" {
|
||||||
message.Extensions = append(message.Extensions, stanza.HintNoCopy{})
|
message.Extensions = append(message.Extensions, stanza.HintNoCopy{})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||||
return
|
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{
|
log.WithFields(log.Fields{
|
||||||
"from": msg.From,
|
"from": msg.From,
|
||||||
"to": msg.To,
|
"to": msg.To,
|
||||||
|
|
@ -284,17 +284,24 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||||
msg.Get(&replace)
|
msg.Get(&replace)
|
||||||
msg.Get(&attachTo)
|
msg.Get(&attachTo)
|
||||||
msg.Get(&reactions)
|
msg.Get(&reactions)
|
||||||
|
forwardedList := collectForwardedMessages(msg)
|
||||||
log.Debugf("reply: %#v", reply)
|
log.Debugf("reply: %#v", reply)
|
||||||
log.Debugf("fallback: %#v", fallback)
|
log.Debugf("fallback: %#v", fallback)
|
||||||
log.Debugf("replace: %#v", replace)
|
log.Debugf("replace: %#v", replace)
|
||||||
log.Debugf("attachTo: %#v", attachTo)
|
log.Debugf("attachTo: %#v", attachTo)
|
||||||
log.Debugf("reactions: %#v", reactions)
|
log.Debugf("reactions: %#v", reactions)
|
||||||
|
log.Debugf("forwarded: %#v", forwardedList)
|
||||||
|
|
||||||
if reactions.Id != "" {
|
if reactions.Id != "" {
|
||||||
handleIncomingReaction(component, msg, session, bare, toID, isGroupchat, reactions)
|
handleIncomingReaction(component, msg, session, bare, toID, isGroupchat, reactions)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(forwardedList) > 0 {
|
||||||
|
handleIncomingForward(component, msg, session, bare, toID, isGroupchat, forwardedList)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var replyId int64
|
var replyId int64
|
||||||
text := msg.Body
|
text := msg.Body
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue