MAM support in MUCs

This commit is contained in:
Bohdan Horbeshko 2025-08-12 17:29:13 -04:00
parent 47179e9c77
commit 12ac2fb5dd
11 changed files with 973 additions and 56 deletions

View file

@ -7,6 +7,7 @@
:user: 'www-data' # owner of content files :user: 'www-data' # owner of content files
:quota: '256MB' # maximum storage size :quota: '256MB' # maximum storage size
:tdlib_verbosity: 1 :tdlib_verbosity: 1
:mam_threshold: 7 # in days
:tdlib: :tdlib:
:datadir: './sessions/' :datadir: './sessions/'
:client: :client:

View file

@ -27,10 +27,11 @@ type XMPPConfig struct {
// TelegramConfig is for :telegram: subtree // TelegramConfig is for :telegram: subtree
type TelegramConfig struct { type TelegramConfig struct {
Loglevel string `yaml:":loglevel"` Loglevel string `yaml:":loglevel"`
Content TelegramContentConfig `yaml:":content"` Content TelegramContentConfig `yaml:":content"`
Verbosity uint8 `yaml:":tdlib_verbosity"` Verbosity uint8 `yaml:":tdlib_verbosity"`
Tdlib TelegramTdlibConfig `yaml:":tdlib"` MAMThreshold uint32 `yaml:":mam_threshold"`
Tdlib TelegramTdlibConfig `yaml:":tdlib"`
} }
// TelegramContentConfig is for :content: subtree // TelegramContentConfig is for :content: subtree

View file

@ -33,6 +33,9 @@
":tdlib_verbosity": { ":tdlib_verbosity": {
"type": "integer" "type": "integer"
}, },
":mam_threshold": {
"type": "integer"
},
":tdlib": { ":tdlib": {
"required": [":client"], "required": [":client"],
"type": "object", "type": "object",

View file

@ -374,7 +374,7 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
} }
for _, jid := range jids { for _, jid := range jids {
if safeToSend { if safeToSend {
gateway.SendMessage(jid, from, text.String(), id, c.xmpp, nil, 0, replaceId, isCarbon, isMUC, false, originalFrom, "") gateway.SendMessage(jid, from, text.String(), id, c.xmpp, nil, 0, replaceId, isCarbon, isMUC, false, originalFrom, "", "", "", nil)
} else { } else {
gateway.SendMUCAnnouncement(jid, from, text.String(), nickname, id, c.xmpp) gateway.SendMUCAnnouncement(jid, from, text.String(), nickname, id, c.xmpp)
} }

View file

@ -13,6 +13,7 @@ import (
osUser "os/user" osUser "os/user"
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -1844,7 +1845,7 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) {
} }
log.Debugf("groupChatFrom: %v groupChatTos: %#v, safeToSend: %v", groupChatFrom, groupChatTos, safeToSend) log.Debugf("groupChatFrom: %v groupChatTos: %#v, safeToSend: %v", groupChatFrom, groupChatTos, safeToSend)
if safeToSend { if safeToSend {
c.SendMessageToGateway(chatId, message, "", false, groupChatFrom, groupChatTos) c.SendMessageToGateway(chatId, message, "", false, groupChatFrom, groupChatTos, "")
} else { } else {
mucJID := gateway.MUCJID(chatId) mucJID := gateway.MUCJID(chatId)
gateway.SendErrorMessage(c.jid, mucJID, "Cannot show a message", 500, true, c.xmpp) gateway.SendErrorMessage(c.jid, mucJID, "Cannot show a message", 500, true, c.xmpp)
@ -1852,7 +1853,7 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) {
} }
// SendMessageToGateway transfers a message to XMPP side and marks it as read on Telegram side // SendMessageToGateway transfers a message to XMPP side and marks it as read on Telegram side
func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id string, delay bool, groupChatFrom string, groupChatTos []string) { func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id string, delay bool, groupChatFrom string, groupChatTos []string, mamQueryId string) {
var isCarbon bool var isCarbon bool
var jids []string var jids []string
var isGroupchat bool var isGroupchat bool
@ -1998,15 +1999,52 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
timestamp = int64(message.Date) timestamp = int64(message.Date)
} }
var mucUserItem *gateway.MUCUserItem
var mucJID string
if mamQueryId != "" {
chatMember, err := c.client.GetChatMember(&client.GetChatMemberRequest{
ChatId: chatId,
MemberId: message.SenderId,
})
var status client.ChatMemberStatus
if err == nil {
status = chatMember.Status
}
chat, err := c.GetChatByID(chatId, nil)
if err == nil {
affiliation, role := c.memberStatusToAffiliationAndRole(status, chat)
mucUserItem = &gateway.MUCUserItem{
Affiliation: affiliation,
Jid: gateway.CHATJID(chatId, false),
Role: role,
}
}
mucJID = gateway.MUCJID(chatId)
}
for _, jid := range jids { for _, jid := range jids {
gateway.SendMessageWithOOB(jid, from, text, sId, c.xmpp, reply, timestamp, oob, "", isCarbon, isGroupchat, c.Session.Receipts, originalFrom, stanzaId) gateway.SendMessageWithOOB(jid, from, text, sId, c.xmpp, reply, timestamp, oob, "", isCarbon, isGroupchat, c.Session.Receipts, originalFrom, stanzaId, mamQueryId, mucJID, mucUserItem)
if auxText != "" { if auxText != "" {
gateway.SendMessage(jid, from, auxText, sId, c.xmpp, reply, timestamp, "", isCarbon, isGroupchat, c.Session.Receipts, originalFrom, stanzaId) gateway.SendMessage(jid, from, auxText, sId, c.xmpp, reply, timestamp, "", isCarbon, isGroupchat, c.Session.Receipts, originalFrom, stanzaId, mamQueryId, mucJID, mucUserItem)
} }
} }
c.UpdateLastChatMessageId(chatId, sId) c.UpdateLastChatMessageId(chatId, sId)
} }
// SendDelayedMUCMessage is used to send MUC history via the legacy method or MAM
func (c *Client) SendDelayedMUCMessage(chatId int64, message *client.Message, toJid string, mamQueryId string) {
msgId, _ := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, chatId, message.Id)
c.SendMessageToGateway(
chatId,
message,
msgId,
true,
gateway.MUCJID(chatId) + "/" + c.GetMUCNickname(c.getMessageSenderId(message)),
[]string{toJid},
mamQueryId,
)
}
// MarkAsRead marks a message as read // MarkAsRead marks a message as read
func (c *Client) MarkAsRead(chatId, messageId int64) { func (c *Client) MarkAsRead(chatId, messageId int64) {
c.client.ViewMessages(&client.ViewMessagesRequest{ c.client.ViewMessages(&client.ViewMessagesRequest{
@ -2330,6 +2368,73 @@ func (c *Client) getNLastMessages(chatID int64, limit *MessageLimit) ([]*client.
return messages, nil return messages, nil
} }
// GetMessagesBetween lazily fetches message history between given ids (from exclusive, last inclusive), also calculating completeness flag; negative limit means messages from the end
func (c *Client) GetMessagesBetween(chatID, fromMessageId, lastMessageId int64, limit int32) (messages []*client.Message, complete bool, err error) {
log.WithFields(log.Fields{
"chat_id": chatID,
"from": fromMessageId,
"last": lastMessageId,
"limit": limit,
}).Debug("messages between")
var newMessages *client.Messages
if limit == 0 {
return
}
var reqFromMessageId int64
var reqOffset, reqLimit int32
if limit < 0 {
reqFromMessageId = lastMessageId
reqOffset = -1
reqLimit = limit
} else {
reqFromMessageId = fromMessageId
reqOffset = -limit
reqLimit = limit
}
newMessages, err = c.client.GetChatHistory(&client.GetChatHistoryRequest{
ChatId: chatID,
FromMessageId: reqFromMessageId,
Offset: reqOffset,
Limit: reqLimit,
})
if err == nil {
if len(newMessages.Messages) == 0 {
complete = true
}
if limit < 0 {
fromPos := -1
for i, message := range newMessages.Messages {
if message.Id == fromMessageId {
fromPos = i
break
}
}
if fromPos > -1 {
messages = newMessages.Messages[fromPos:]
} else {
messages = newMessages.Messages
}
complete = true
} else {
for _, message := range newMessages.Messages {
if lastMessageId != 0 {
if message.Id > lastMessageId {
break
complete = true
} else if message.Id == lastMessageId {
complete = true
}
}
messages = append(messages, message)
}
}
}
return
}
// GetFile retrieves a file object by id given by TDlib // GetFile retrieves a file object by id given by TDlib
func (c *Client) GetFile(id int32) (*client.File, error) { func (c *Client) GetFile(id int32) (*client.File, error) {
return c.client.GetFile(&client.GetFileRequest{ return c.client.GetFile(&client.GetFileRequest{
@ -2751,18 +2856,13 @@ func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, p
false, false,
originalFrom, originalFrom,
"", "",
"",
"",
nil,
) )
} }
} else { } else {
msgId, _ := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, chatID, message.Id) c.SendDelayedMUCMessage(chatID, message, toJid, "")
c.SendMessageToGateway(
chatID,
message,
msgId,
true,
mucJid + "/" + c.GetMUCNickname(c.getMessageSenderId(message)),
[]string{toJid},
)
} }
} }
} }
@ -3072,6 +3172,22 @@ func (c *Client) DeleteChat(chatID int64) error {
return err return err
} }
// GetMessage is a handy wrapper for the following TDLib method
func (c *Client) GetMessage(chatID, messageId int64) (*client.Message, error) {
return c.client.GetMessage(&client.GetMessageRequest{
ChatId: chatID,
MessageId: messageId,
})
}
// GetChatMessagePosition is a handy wrapper for the following TDLib method
func (c *Client) GetChatMessagePosition(chatID, messageId int64) (*client.Count, error) {
return c.client.GetChatMessagePosition(&client.GetChatMessagePositionRequest{
ChatId: chatID,
MessageId: messageId,
})
}
// CloneChatPermissions makes a copy of ChatPermissions structure // CloneChatPermissions makes a copy of ChatPermissions structure
func CloneChatPermissions(permissions *client.ChatPermissions) *client.ChatPermissions { func CloneChatPermissions(permissions *client.ChatPermissions) *client.ChatPermissions {
return &client.ChatPermissions{ return &client.ChatPermissions{
@ -3219,6 +3335,46 @@ func (c *Client) GetMUCNicknameByUsername(username string) (string, error) {
return c.GetMUCNickname(chat.Id), nil return c.GetMUCNickname(chat.Id), nil
} }
// FindMessageByTime retrieves the closest message before the given timestamp
func (c *Client) FindMessageByTime(chatID int64, ts time.Time) (*client.Message, error) {
if ts.IsZero() {
return nil, nil
}
return c.client.GetChatMessageByDate(&client.GetChatMessageByDateRequest{
ChatId: chatID,
Date: int32(ts.Unix()), // DUROV!!!!!!!!!!!!!!!1111111
})
}
// GetNextMessage attempts to obtain the next message in the history
func (c *Client) GetNextMessage(chatID, messageId int64) (*client.Message, error) {
messages, err := c.client.GetChatHistory(&client.GetChatHistoryRequest{
ChatId: chatID,
FromMessageId: messageId,
Limit: 1,
Offset: -1,
})
if err == nil && len(messages.Messages) == 1 && messages.Messages[0] != nil && messages.Messages[0].Id != messageId {
return messages.Messages[0], nil
}
return nil, err
}
// GetPreviousMessage attempts to obtain the previous message in the history
func (c *Client) GetPreviousMessage(chatID, messageId int64) (*client.Message, error) {
messages, err := c.client.GetChatHistory(&client.GetChatHistoryRequest{
ChatId: chatID,
FromMessageId: messageId,
Limit: 1,
Offset: 0,
})
if err == nil && len(messages.Messages) == 1 && messages.Messages[0] != nil && messages.Messages[0].Id != messageId {
return messages.Messages[0], nil
}
return nil, err
}
// 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)
@ -3227,3 +3383,20 @@ func GetErrorCode(err error) (int32, bool) {
} }
return responseError.Err.Code, true return responseError.Err.Code, true
} }
// ChronologicallySortMessages is… self-explanatory (Achtung: destructive)
func ChronologicallySortMessages(messages []*client.Message) []*client.Message {
sort.Slice(messages, func(i int, j int) bool {
msg1 := messages[i]
msg2 := messages[j]
return msg1.Date < msg2.Date
})
return messages
}
// ReverseMessagesSlice efficiently reverses a messages slice in-place
func ReverseMessagesSlice(s []*client.Message) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}

View file

@ -5,6 +5,7 @@
:link: 'http://tlgrm.localhost/content' # webserver public address :link: 'http://tlgrm.localhost/content' # webserver public address
:upload: 'https:///xmppfiles.localhost' # xmpp http upload address :upload: 'https:///xmppfiles.localhost' # xmpp http upload address
:tdlib_verbosity: 1 :tdlib_verbosity: 1
:mam_threshold: 7 # in days
:tdlib: :tdlib:
:client: :client:
:api_id: '17349' :api_id: '17349'

View file

@ -5,6 +5,7 @@
:link: '' # webserver public address :link: '' # webserver public address
:upload: '' # xmpp http upload address :upload: '' # xmpp http upload address
:tdlib_verbosity: 1 :tdlib_verbosity: 1
:tdlib_verbosity: 7 # in days
:tdlib: :tdlib:
:client: :client:
:api_id: '17349' :api_id: '17349'

View file

@ -65,6 +65,8 @@ func NewComponent(conf config.XMPPConfig, tc config.TelegramConfig, idsPath stri
} }
} }
gateway.MAMThreshold = tc.MAMThreshold
options := xmpp.ComponentOptions{ options := xmpp.ComponentOptions{
TransportConfiguration: xmpp.TransportConfiguration{ TransportConfiguration: xmpp.TransportConfiguration{
Address: conf.Host + ":" + conf.Port, Address: conf.Host + ":" + conf.Port,

View file

@ -3,6 +3,7 @@ package extensions
import ( import (
"encoding/xml" "encoding/xml"
"strconv" "strconv"
"time"
"gosrc.io/xmpp/stanza" "gosrc.io/xmpp/stanza"
) )
@ -224,6 +225,7 @@ type MessageXMucUserExtension struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"` XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"`
Invite *MessageXMucUserInvite `xml:"invite,omitempty"` Invite *MessageXMucUserInvite `xml:"invite,omitempty"`
Status *MessageXMucUserStatus `xml:"status,omitempty"` Status *MessageXMucUserStatus `xml:"status,omitempty"`
Item PresenceXMucUserItem `xml:"item,omitempty"`
Password string `xml:"password,omitempty"` Password string `xml:"password,omitempty"`
} }
@ -273,10 +275,21 @@ type PresenceXMucUserStatus struct {
// MessageDelay is from XEP-0203 // MessageDelay is from XEP-0203
type MessageDelay struct { type MessageDelay struct {
XMLName xml.Name `xml:"urn:xmpp:delay delay"` XMLName xml.Name `xml:"urn:xmpp:delay delay"`
From string `xml:"from,attr"` From string `xml:"from,attr,omitempty"`
Stamp string `xml:"stamp,attr"` Stamp string `xml:"stamp,attr"`
} }
func NewMessageDelay(timestamp int64, from string) MessageDelay {
return MessageDelay{
From: from,
Stamp: TimestampToRFC3339(timestamp),
}
}
func TimestampToRFC3339(timestamp int64) string {
return time.Unix(timestamp, 0).UTC().Format(time.RFC3339)
}
// MessageDelayLegacy is from XEP-0203 // MessageDelayLegacy is from XEP-0203
type MessageDelayLegacy struct { type MessageDelayLegacy struct {
XMLName xml.Name `xml:"jabber:x:delay x"` XMLName xml.Name `xml:"jabber:x:delay x"`
@ -284,6 +297,13 @@ type MessageDelayLegacy struct {
Stamp string `xml:"stamp,attr"` Stamp string `xml:"stamp,attr"`
} }
func NewMessageDelayLegacy(timestamp int64, from string) MessageDelayLegacy {
return MessageDelayLegacy{
From: from,
Stamp: time.Unix(timestamp, 0).UTC().Format("20060102T15:04:05"),
}
}
// MessageAddresses is from XEP-0033 // MessageAddresses is from XEP-0033
type MessageAddresses struct { type MessageAddresses struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/address addresses"` XMLName xml.Name `xml:"http://jabber.org/protocol/address addresses"`
@ -347,6 +367,65 @@ type MucDestroy struct {
Reason string `xml:"reason,omitempty"` Reason string `xml:"reason,omitempty"`
} }
// MAM2Query is from XEP-0313
type MAM2Query struct {
XMLName xml.Name `xml:"urn:xmpp:mam:2 query"`
Form *stanza.Form `xml:"jabber:x:data x"`
QueryId string `xml:"queryid,attr,omitempty"`
ResultSet *stanza.ResultSet `xml:"set,omitempty"`
FlipPage *FlipPage `xml:"flip-page"`
}
// FlipPage is an extended element from XEP-0313
type FlipPage struct {
XMLName xml.Name `xml:"flip-page"`
}
// ForwardedMessage is from XEP-0297 (go-xmpp lacks Delay)
type ForwardedMessage struct {
XMLName xml.Name `xml:"urn:xmpp:forward:0 forwarded"`
Message *stanza.Message `xml:"message"`
Delay *MessageDelay `xml:"urn:xmpp:delay delay,omitempty"`
}
// MAM2MessageResult is from XEP-0313
type MAM2MessageResult struct {
XMLName xml.Name `xml:"urn:xmpp:mam:2 result"`
Forwarded *ForwardedMessage `xml:"urn:xmpp:forward:0 forwarded,omitempty"`
QueryId string `xml:"queryid,attr,omitempty"`
Id string `xml:"id,attr,omitempty"`
}
// MAM2Fin is from XEP-0313
type MAM2Fin struct {
XMLName xml.Name `xml:"urn:xmpp:mam:2 fin"`
ResultSet *stanza.ResultSet `xml:"set,omitempty"`
Complete bool `xml:"complete,attr,omitempty"`
Stable bool `xml:"stable,attr"`
}
// MAM2Metadata is from XEP-0313
type MAM2Metadata struct {
XMLName xml.Name `xml:"urn:xmpp:mam:2 metadata"`
Start *MAM2MetadataStart `xml:"start"`
End *MAM2MetadataEnd `xml:"end"`
ResultSet *stanza.ResultSet `xml:"set,omitempty"`
}
// MAM2MetadataStart is from XEP-0313
type MAM2MetadataStart struct {
XMLName xml.Name `xml:"start"`
Id string `xml:"id,attr,omitempty"`
Timestamp string `xml:"timestamp,attr,omitempty"`
}
// MAM2MetadataEnd is from XEP-0313
type MAM2MetadataEnd struct {
XMLName xml.Name `xml:"end"`
Id string `xml:"id,attr,omitempty"`
Timestamp string `xml:"timestamp,attr,omitempty"`
}
// Namespace is a namespace! // Namespace is a namespace!
func (c PresenceNickExtension) Namespace() string { func (c PresenceNickExtension) Namespace() string {
return c.XMLName.Space return c.XMLName.Space
@ -452,6 +531,36 @@ func (c QueryMucOwner) GetSet() *stanza.ResultSet {
return c.ResultSet return c.ResultSet
} }
// Namespace is a namespace!
func (c MAM2Query) Namespace() string {
return c.XMLName.Space
}
// GetSet getsets!
func (c MAM2Query) GetSet() *stanza.ResultSet {
return c.ResultSet
}
// Namespace is a namespace!
func (c MAM2Fin) Namespace() string {
return c.XMLName.Space
}
// GetSet getsets!
func (c MAM2Fin) GetSet() *stanza.ResultSet {
return c.ResultSet
}
// Namespace is a namespace!
func (c MAM2Metadata) Namespace() string {
return c.XMLName.Space
}
// GetSet getsets!
func (c MAM2Metadata) GetSet() *stanza.ResultSet {
return c.ResultSet
}
// NewReplyFallback initializes a fallback range // NewReplyFallback initializes a fallback range
func NewReplyFallback(start uint64, end uint64) Fallback { func NewReplyFallback(start uint64, end uint64) Fallback {
return Fallback{ return Fallback{
@ -585,4 +694,28 @@ func init() {
"http://jabber.org/protocol/muc#owner", "http://jabber.org/protocol/muc#owner",
"query", "query",
}, QueryMucOwner{}) }, QueryMucOwner{})
// MAM2 query
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{
"urn:xmpp:mam:2",
"query",
}, MAM2Query{})
// MAM2 message result
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{
"urn:xmpp:mam:2",
"result",
}, MAM2MessageResult{})
// MAM2 fin
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{
"urn:xmpp:mam:2",
"fin",
}, MAM2Fin{})
// MAM2 metadata
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{
"urn:xmpp:mam:2",
"metadata",
}, MAM2Metadata{})
} }

View file

@ -6,7 +6,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time"
"dev.narayana.im/narayana/telegabber/badger" "dev.narayana.im/narayana/telegabber/badger"
"dev.narayana.im/narayana/telegabber/xmpp/extensions" "dev.narayana.im/narayana/telegabber/xmpp/extensions"
@ -38,6 +37,12 @@ type marker struct {
Id string Id string
} }
type MUCUserItem struct {
Affiliation string
Jid string
Role string
}
const NSNick string = "http://jabber.org/protocol/nick" const NSNick string = "http://jabber.org/protocol/nick"
const NodeVCard4 string = "urn:xmpp:vcard4" const NodeVCard4 string = "urn:xmpp:vcard4"
const NodeAvatarMetadata string = "urn:xmpp:avatar:metadata" const NodeAvatarMetadata string = "urn:xmpp:avatar:metadata"
@ -62,6 +67,9 @@ var DirtySessions = false
// MessageOutgoingPermissionVersion contains a XEP-0356 version to fake outgoing messages by foreign JIDs // MessageOutgoingPermissionVersion contains a XEP-0356 version to fake outgoing messages by foreign JIDs
var MessageOutgoingPermissionVersion = 0 var MessageOutgoingPermissionVersion = 0
// MAMThreshold specifies a day limit behind which history should not be requested to avoid abuse detection and storage overload
var MAMThreshold uint32
// CHATNODE converts numeric id to node part of 1-1 chat JID // CHATNODE converts numeric id to node part of 1-1 chat JID
func CHATNODE(chatId int64) string { func CHATNODE(chatId int64) string {
return strconv.FormatInt(chatId, 10) return strconv.FormatInt(chatId, 10)
@ -114,8 +122,8 @@ func ResourcePrep(resource string) (string, error) {
} }
// SendMessage creates and sends a message stanza // SendMessage creates and sends a message stanza
func SendMessage(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom, stanzaId string) { func SendMessage(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom, stanzaId, mamQueryId, mucJID string, mucUserItem *MUCUserItem) {
sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, "", replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "", stanzaId, 0) sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, "", replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "", stanzaId, 0, mamQueryId, mucJID, mucUserItem)
} }
// SendServiceMessage creates and sends a simple message stanza from transport // SendServiceMessage creates and sends a simple message stanza from transport
@ -124,7 +132,7 @@ func SendServiceMessage(to, body string, component *xmpp.Component) {
if uuid, err := uuid.NewRandom(); err == nil { if uuid, err := uuid.NewRandom(); err == nil {
id = uuid.String() id = uuid.String()
} }
sendMessageWrapper(to, "", body, "", "", id, component, nil, nil, 0, "", "", false, false, false, false, "", 0, "", "", 0) sendMessageWrapper(to, "", body, "", "", id, component, nil, nil, 0, "", "", false, false, false, false, "", 0, "", "", 0, "", "", nil)
} }
// SendTextMessage creates and sends a simple message stanza // SendTextMessage creates and sends a simple message stanza
@ -133,7 +141,7 @@ func SendTextMessage(to, from, body string, component *xmpp.Component, isGroupch
if uuid, err := uuid.NewRandom(); err == nil { if uuid, err := uuid.NewRandom(); err == nil {
id = uuid.String() id = uuid.String()
} }
sendMessageWrapper(to, from, body, "", "", id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", 0, "", "", 0) sendMessageWrapper(to, from, body, "", "", id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", 0, "", "", 0, "", "", nil)
} }
// SendMUCAnnouncement creates and sends a message by a temporary occupant // SendMUCAnnouncement creates and sends a message by a temporary occupant
@ -158,7 +166,7 @@ func SendMUCAnnouncement(to, from, body, nickname, id string, component *xmpp.Co
id = uuid.String() id = uuid.String()
} }
} }
sendMessageWrapper(to, fullFrom, body, "", "", id, component, nil, nil, 0, "", "", false, true, false, false, "", 0, "", "", 0) sendMessageWrapper(to, fullFrom, body, "", "", id, component, nil, nil, 0, "", "", false, true, false, false, "", 0, "", "", 0, "", "", nil)
SendPresence( SendPresence(
component, component,
@ -173,22 +181,22 @@ func SendMUCAnnouncement(to, from, body, nickname, id string, component *xmpp.Co
// SendErrorMessage creates and sends an error message stanza // SendErrorMessage creates and sends an error message stanza
func SendErrorMessage(to, from, text string, code int, isGroupchat bool, component *xmpp.Component) { func SendErrorMessage(to, from, text string, code int, isGroupchat bool, component *xmpp.Component) {
sendMessageWrapper(to, from, "", "", text, "", component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "", "", 0) sendMessageWrapper(to, from, "", "", text, "", component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "", "", 0, "", "", nil)
} }
// SendErrorMessageWithBody creates and sends an error message stanza with body payload // SendErrorMessageWithBody creates and sends an error message stanza with body payload
func SendErrorMessageWithBody(to, from, body, errorText, id string, code int, isGroupchat bool, component *xmpp.Component) { func SendErrorMessageWithBody(to, from, body, errorText, id string, code int, isGroupchat bool, component *xmpp.Component) {
sendMessageWrapper(to, from, body, "", errorText, id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "", "", 0) sendMessageWrapper(to, from, body, "", errorText, id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "", "", 0, "", "", nil)
} }
// SendMessageWithOOB creates and sends a message stanza with OOB URL // SendMessageWithOOB creates and sends a message stanza with OOB URL
func SendMessageWithOOB(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom, stanzaId string) { func SendMessageWithOOB(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom, stanzaId, mamQueryId, mucJID string, mucUserItem *MUCUserItem) {
sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, oob, replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "", stanzaId, 0) sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, oob, replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "", stanzaId, 0, mamQueryId, mucJID, mucUserItem)
} }
// SendSubjectMessage creates and sends a MUC subject // SendSubjectMessage creates and sends a MUC subject
func SendSubjectMessage(to, from, subject, id string, component *xmpp.Component, timestamp int64) { func SendSubjectMessage(to, from, subject, id string, component *xmpp.Component, timestamp int64) {
sendMessageWrapper(to, from, "", subject, "", id, component, nil, nil, timestamp, "", "", false, true, true, false, "", 0, "", "", 0) sendMessageWrapper(to, from, "", subject, "", id, component, nil, nil, timestamp, "", "", false, true, true, false, "", 0, "", "", 0, "", "", nil)
} }
// SendMessageMarker creates and sends a message stanza with a XEP-0333 marker // SendMessageMarker creates and sends a message stanza with a XEP-0333 marker
@ -196,20 +204,20 @@ func SendMessageMarker(to string, from string, component *xmpp.Component, marker
sendMessageWrapper(to, from, "", "", "", "", component, nil, &marker{ sendMessageWrapper(to, from, "", "", "", "", component, nil, &marker{
Type: markerType, Type: markerType,
Id: markerId, Id: markerId,
}, 0, "", "", false, false, false, false, "", 0, "", "", 0) }, 0, "", "", false, false, false, false, "", 0, "", "", 0, "", "", nil)
} }
// SendMUCInvite creates and send a MUC invitation message // SendMUCInvite creates and send a MUC invitation message
func SendMUCInvite(to string, from string, component *xmpp.Component, inviteFrom string) { func SendMUCInvite(to string, from string, component *xmpp.Component, inviteFrom string) {
sendMessageWrapper(to, from, "", "", "", "", component, nil, nil, 0, "", "", false, false, false, false, "", 0, inviteFrom, "", 0) sendMessageWrapper(to, from, "", "", "", "", component, nil, nil, 0, "", "", false, false, false, false, "", 0, inviteFrom, "", 0, "", "", nil)
} }
// SendMUCStatusCode creates a groupchat message with a muc#user status code // SendMUCStatusCode creates a groupchat message with a muc#user status code
func SendMUCStatusCode(to string, from string, component *xmpp.Component, statusCode int64) { func SendMUCStatusCode(to string, from string, component *xmpp.Component, statusCode int64) {
sendMessageWrapper(to, from, "", "", "", "", component, nil, nil, 0, "", "", false, true, false, false, "", 0, "", "", statusCode) sendMessageWrapper(to, from, "", "", "", "", component, nil, nil, 0, "", "", false, true, false, false, "", 0, "", "", statusCode, "", "", nil)
} }
func sendMessageWrapper(to, from, body, subject, errorText, id string, component *xmpp.Component, reply *Reply, marker *marker, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, forceSubject, requestReceipt bool, originalFrom string, errorCode int, inviteFrom, stanzaId string, statusCode int64) { func sendMessageWrapper(to, from, body, subject, errorText, id string, component *xmpp.Component, reply *Reply, marker *marker, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, forceSubject, requestReceipt bool, originalFrom string, errorCode int, inviteFrom, stanzaId string, statusCode int64, mamQueryId, mucJID string, mucUserItem *MUCUserItem) {
toJid, err := stanza.NewJid(to) toJid, err := stanza.NewJid(to)
if err != nil { if err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
@ -247,6 +255,8 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component
if isCarbon { if isCarbon {
messageTo = messageFrom messageTo = messageFrom
messageFrom = bareTo + "/" + Jid.Resource messageFrom = bareTo + "/" + Jid.Resource
} else if mucJID != "" {
messageTo = mucJID
} else { } else {
messageTo = to messageTo = to
} }
@ -322,19 +332,13 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component
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{})
} }
if timestamp != 0 { if timestamp != 0 && mamQueryId == "" {
var delayFrom string var delayFrom string
if isGroupchat { if isGroupchat {
delayFrom = bareFrom delayFrom = bareFrom
} }
message.Extensions = append(message.Extensions, extensions.MessageDelay{ message.Extensions = append(message.Extensions, extensions.NewMessageDelay(timestamp, delayFrom))
From: delayFrom, message.Extensions = append(message.Extensions, extensions.NewMessageDelayLegacy(timestamp, delayFrom))
Stamp: time.Unix(timestamp, 0).UTC().Format(time.RFC3339),
})
message.Extensions = append(message.Extensions, extensions.MessageDelayLegacy{
From: delayFrom,
Stamp: time.Unix(timestamp, 0).UTC().Format("20060102T15:04:05"),
})
} }
if originalFrom != "" { if originalFrom != "" {
message.Extensions = append(message.Extensions, extensions.MessageAddresses{ message.Extensions = append(message.Extensions, extensions.MessageAddresses{
@ -377,7 +381,14 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component
Code: strconv.FormatInt(statusCode, 10), Code: strconv.FormatInt(statusCode, 10),
} }
} }
if inviteFrom != "" || statusCode != 0 { if mucUserItem != nil {
userExt.Item = extensions.PresenceXMucUserItem{
Affiliation: mucUserItem.Affiliation,
Jid: &mucUserItem.Jid,
Role: mucUserItem.Role,
}
}
if inviteFrom != "" || statusCode != 0 || mucUserItem != nil {
message.Extensions = append(message.Extensions, userExt) message.Extensions = append(message.Extensions, userExt)
} }
if stanzaId != "" { if stanzaId != "" {
@ -425,6 +436,26 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component
}) })
} }
sendMessage(&privilegeMessage, component) sendMessage(&privilegeMessage, component)
} else if mamQueryId != "" {
delay := extensions.NewMessageDelay(timestamp, "")
mamMessage := stanza.Message{
Attrs: stanza.Attrs{
From: mucJID,
To: to,
Type: messageType,
},
Extensions: []stanza.MsgExtension{
extensions.MAM2MessageResult{
Id: stanzaId,
QueryId: mamQueryId,
Forwarded: &extensions.ForwardedMessage{
Delay: &delay,
Message: &message,
},
},
},
}
sendMessage(&mamMessage, component)
} else { } else {
sendMessage(&message, component) sendMessage(&message, component)
} }

View file

@ -83,6 +83,16 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) {
go handleGetQueryMucOwner(s, iq) go handleGetQueryMucOwner(s, iq)
return return
} }
queryMAM2, ok := iq.Payload.(*extensions.MAM2Query)
if ok {
go handleGetQueryMAM2(s, iq, queryMAM2)
return
}
_, ok = iq.Payload.(*extensions.MAM2Metadata)
if ok {
go handleGetMetadataMAM2(s, iq)
return
}
} else if iq.Type == stanza.IQTypeSet { } else if iq.Type == stanza.IQTypeSet {
queryRegister, ok := iq.Payload.(*extensions.QueryRegister) queryRegister, ok := iq.Payload.(*extensions.QueryRegister)
if ok { if ok {
@ -104,6 +114,11 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) {
go handleSetQueryMucOwner(s, iq, queryMucOwner) go handleSetQueryMucOwner(s, iq, queryMucOwner)
return return
} }
queryMAM2, ok := iq.Payload.(*extensions.MAM2Query)
if ok {
go handleSetQueryMAM2(s, iq, queryMAM2)
return
}
} else if iq.Type == stanza.IQTypeResult { } else if iq.Type == stanza.IQTypeResult {
discoInfo, ok := iq.Payload.(*stanza.DiscoInfo) discoInfo, ok := iq.Payload.(*stanza.DiscoInfo)
if ok { if ok {
@ -202,17 +217,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
log.Debugf("replace tg: %#v %#v", chatId, msgId) log.Debugf("replace tg: %#v %#v", chatId, msgId)
} }
} else { } else {
id := reply.Id replyId, _ = parseMessageId(reply.Id)
if id[0] == 'e' {
idParts := strings.Split(id[1:], ":")
if len(idParts) >= 1 {
id = idParts[0]
}
}
replyId, err = strconv.ParseInt(id, 10, 64)
if err != nil {
log.Warn(errors.Wrap(err, "Failed to parse message ID!"))
}
} }
if replyId != 0 && fallback.For == "urn:xmpp:reply:0" && len(fallback.Body) > 0 { if replyId != 0 && fallback.For == "urn:xmpp:reply:0" && len(fallback.Body) > 0 {
@ -292,11 +297,12 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
false, false,
msg.To + "/" + session.GetMUCNickname(session.GetSenderId(tgMessage.SenderId)), msg.To + "/" + session.GetMUCNickname(session.GetSenderId(tgMessage.SenderId)),
[]string{msg.From}, []string{msg.From},
"",
) )
} }
} else if isCommand && isGroupchat && session.Session.MUC { } else if isCommand && isGroupchat && session.Session.MUC {
// pong outgoing commands back to groupchats // pong outgoing commands back to groupchats
gateway.SendMessage(msg.From, msg.To + "/" + session.GetMUCNickname(0), text, "", component, nil, 0, "", false, isGroupchat, false, "", "") gateway.SendMessage(msg.From, msg.To + "/" + session.GetMUCNickname(0), text, "", component, nil, 0, "", false, isGroupchat, false, "", "", "", "", nil)
} else { } else {
/* /*
// if a message failed to edit on Telegram side, match new XMPP ID with old Telegram ID anyway // if a message failed to edit on Telegram side, match new XMPP ID with old Telegram ID anyway
@ -970,6 +976,8 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) {
"muc_unsecured", "muc_unsecured",
"http://jabber.org/protocol/muc#stable_id", "http://jabber.org/protocol/muc#stable_id",
"jabber:iq:register", "jabber:iq:register",
"urn:xmpp:mam:2",
"urn:xmpp:mam:2#extended",
"urn:xmpp:sid:0", "urn:xmpp:sid:0",
"vcard-temp", "vcard-temp",
) )
@ -1374,6 +1382,113 @@ func handleGetQueryMucOwner(s xmpp.Sender, iq *stanza.IQ) {
} }
} }
func handleGetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Query) {
component, answer, ok := iqResultStub(s, iq)
if !ok {
return
}
defer gateway.ResumableSend(component, answer)
payload := &extensions.MAM2Query{}
answer.Payload = payload
payload.Form = &stanza.Form{
Type: stanza.FormTypeForm,
Fields: []*stanza.Field{
&stanza.Field{
Var: "FORM_TYPE",
Type: stanza.FieldTypeHidden,
ValuesList: []string{"urn:xmpp:mam:2"},
},
&stanza.Field{
Var: "with",
Type: stanza.FieldTypeJidSingle,
},
&stanza.Field{
Var: "start",
Type: stanza.FieldTypeTextSingle,
},
&stanza.Field{
Var: "end",
Type: stanza.FieldTypeTextSingle,
},
&stanza.Field{
Var: "before-id",
Type: stanza.FieldTypeTextSingle,
},
&stanza.Field{
Var: "after-id",
Type: stanza.FieldTypeTextSingle,
},
&stanza.Field{
Var: "ids",
Type: stanza.FieldTypeListMulti,
},
},
}
log.Debugf("MAM info request: %#v", query)
}
func handleGetMetadataMAM2(s xmpp.Sender, iq *stanza.IQ) {
component, answer, ok := iqResultStub(s, iq)
if !ok {
return
}
defer gateway.ResumableSend(component, answer)
bare, _, fromOk := gateway.SplitJID(iq.From)
if !fromOk {
iqAnswerSetError(answer, 400)
return
}
session, sessionOk := sessions[bare]
if !sessionOk || !session.Session.MUC {
iqAnswerSetError(answer, 403)
return
}
toID, toOk, toIsGroup := toToID(iq.To)
if !toOk || !toIsGroup {
iqAnswerSetError(answer, 405)
return
}
chat, _, err := session.GetContactByID(toID, nil)
if err != nil || chat == nil || !session.IsGroup(chat) {
iqAnswerSetError(answer, 405)
return
}
payload := &extensions.MAM2Metadata{}
answer.Payload = payload
quotaTs := time.Now().AddDate(0, 0, -int(gateway.MAMThreshold))
preFirstMessage, preFirstMessageErr := session.FindMessageByTime(toID, quotaTs)
var preFirstMessageId int64
if preFirstMessageErr == nil && preFirstMessage != nil {
preFirstMessageId = preFirstMessage.Id
} else {
preFirstMessageId = 1
}
startMessage, startMessageErr := session.GetNextMessage(toID, preFirstMessageId)
if startMessageErr == nil && startMessage != nil {
payload.Start = &extensions.MAM2MetadataStart{
Id: strconv.FormatInt(startMessage.Id, 10),
Timestamp: extensions.TimestampToRFC3339(int64(startMessage.Date)),
}
}
endMessage, endMessageErr := session.GetPreviousMessage(toID, 0)
if endMessageErr == nil && endMessage != nil {
payload.End = &extensions.MAM2MetadataEnd{
Id: strconv.FormatInt(endMessage.Id, 10),
Timestamp: extensions.TimestampToRFC3339(int64(endMessage.Date)),
}
}
log.Debugf("MAM metadata: %#v", payload)
}
func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryRegister) { func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryRegister) {
component, answer, ok := iqResultStub(s, iq) component, answer, ok := iqResultStub(s, iq)
if !ok { if !ok {
@ -2039,6 +2154,442 @@ func handleSetQueryMucOwner(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer
} }
func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Query) {
component, answer, ok := iqResultStub(s, iq)
if !ok {
return
}
defer gateway.ResumableSend(component, answer)
bare, _, fromOk := gateway.SplitJID(iq.From)
if !fromOk {
iqAnswerSetError(answer, 400)
return
}
session, sessionOk := sessions[bare]
if !sessionOk || !session.Session.MUC {
iqAnswerSetError(answer, 403)
return
}
toID, toOk, toIsGroup := toToID(iq.To)
if !toOk || !toIsGroup {
iqAnswerSetError(answer, 405)
return
}
chat, _, err := session.GetContactByID(toID, nil)
if err != nil || chat == nil || !session.IsGroup(chat) {
iqAnswerSetError(answer, 405)
return
}
var startTime, endTime time.Time
var beforeId, afterId int64
var ids []string
var rsmBefore, rsmAfter int64
var rsmLastPage bool
var rsmLimit int32
var justCount bool
log.Debugf("MAM query to %v: %#v %#v %#v", toID, query, query.Form, query.ResultSet)
if query.Form != nil && query.Form.Type == stanza.FormTypeSubmit {
for _, field := range query.Form.Fields {
if len(field.ValuesList) < 1 {
iqAnswerSetError(answer, 400)
return
}
value := field.ValuesList[0]
log.Debugf("MAM query field: %v %v", field.Var, value)
switch field.Var {
case "FORM_TYPE":
if value != "urn:xmpp:mam:2" {
iqAnswerSetError(answer, 400)
return
}
case "with": // okay, and?
case "start", "end":
timestamp, err := time.Parse(time.RFC3339, value)
if err != nil {
iqAnswerSetError(answer, 400)
return
}
switch field.Var {
case "start":
startTime = timestamp
case "end":
endTime = timestamp
}
case "before-id":
beforeId, ok = parseMessageId(value)
if !ok {
iqAnswerSetError(answer, 400)
return
}
case "after-id":
afterId, ok = parseMessageId(value)
if !ok {
iqAnswerSetError(answer, 400)
return
}
case "ids":
ids = field.ValuesList
default:
iqAnswerSetError(answer, 501)
return
}
}
}
if query.ResultSet != nil {
if query.ResultSet.After != nil {
rsmAfter, ok = parseMessageId(*query.ResultSet.After)
if !ok {
iqAnswerSetError(answer, 400)
return
}
log.Debugf("MAM RSM after: %v", rsmAfter)
}
if query.ResultSet.Before != nil {
before := *query.ResultSet.Before
if before == "" {
rsmLastPage = true
} else {
rsmBefore, ok = parseMessageId(*query.ResultSet.Before)
if !ok {
iqAnswerSetError(answer, 400)
return
}
log.Debugf("MAM RSM after: %v", rsmBefore)
}
}
if query.ResultSet.Max != nil {
rsmLimit = int32(*query.ResultSet.Max)
if rsmLimit == 0 {
justCount = true
}
log.Debugf("MAM RSM max: %v", rsmLimit)
}
if query.ResultSet.First != nil {
iqAnswerSetError(answer, 400)
return
}
if query.ResultSet.Index != nil {
iqAnswerSetError(answer, 501)
return
}
if query.ResultSet.Last != nil {
iqAnswerSetError(answer, 400)
return
}
}
if rsmLimit == 0 && !justCount {
rsmLimit = 100
}
if rsmLastPage {
rsmLimit = -rsmLimit // hacky, I know, and? :P
}
// check for mutual parameter compatibility, there's a lot of them, nah?
if ((!startTime.IsZero() || !endTime.IsZero()) && (beforeId != 0 || afterId != 0 || ids != nil)) ||
((beforeId != 0 || afterId != 0) && (!startTime.IsZero() || !endTime.IsZero() || ids != nil)) ||
(ids != nil && (!startTime.IsZero() || !endTime.IsZero() || beforeId != 0 || afterId != 0)) {
iqAnswerSetError(answer, 501)
return
}
// dummy call to circumvent unexported type
messages, _, err := session.GetMessagesBetween(toID, 0, 0, 0)
quotaTs := time.Now().AddDate(0, 0, -int(gateway.MAMThreshold))
var beyond, complete bool
canBeComplete := true
fromStart := true
var overallyFirstMessageId, overallyLastMessageId int64
if ids != nil {
for _, sId := range ids {
id, ok := parseMessageId(sId)
if !ok {
iqAnswerSetError(answer, 400)
return
}
msg, err := session.GetMessage(toID, id)
if err != nil {
iqAnswerSetError(answer, 404)
return
}
messages = append(messages, msg)
}
messages = telegram.ChronologicallySortMessages(messages)
complete = true
} else if beforeId != 0 || afterId != 0 {
if (beforeId != 0 && afterId != 0) && beforeId > afterId {
iqAnswerSetError(answer, 400)
return
}
// don't allow to fetch far beyond the quota
var replaceWithQuotaTs bool
if afterId != 0 {
afterMessage, afterMessageErr := session.GetMessage(toID, afterId)
if afterMessageErr == nil && afterMessage != nil {
if int64(afterMessage.Date) < quotaTs.Unix() {
replaceWithQuotaTs = true
}
} else {
iqAnswerSetError(answer, 404)
return
}
} else {
replaceWithQuotaTs = true
}
if replaceWithQuotaTs {
preFirstMessage, preFirstMessageErr := session.FindMessageByTime(toID, quotaTs)
if preFirstMessageErr == nil && preFirstMessage != nil {
afterId = preFirstMessage.Id
} else {
// there seem to be no messages older than quota, it's safe to fetch from the very start
afterId = 1 // https://github.com/tdlib/td/issues/195#issuecomment-380836359
}
}
if beforeId != 0 {
beforeMessage, beforeMessageErr := session.GetMessage(toID, beforeId)
if beforeMessageErr != nil || beforeMessage == nil {
iqAnswerSetError(answer, 404)
return
}
}
if rsmAfter != 0 {
if rsmAfter < afterId || (beforeId != 0 && rsmAfter > beforeId) {
iqAnswerSetError(answer, 400)
return
}
if rsmAfter != afterId {
fromStart = false
overallyFirstMessage, overallyFirstMessageErr := session.GetNextMessage(toID, afterId)
if overallyFirstMessageErr == nil && overallyFirstMessage != nil {
overallyFirstMessageId = overallyFirstMessage.Id
}
}
afterId = rsmAfter
}
if rsmBefore != 0 {
if rsmBefore < afterId || (beforeId != 0 && rsmBefore > beforeId) {
iqAnswerSetError(answer, 400)
return
}
if rsmBefore < beforeId {
canBeComplete = false
overallyLastMessageId = beforeId
}
beforeId = rsmBefore
}
var lastMessageId int64
// should be fine even with afterId=0 as it would mean the last as needed
lastMessage, lastMessageErr := session.GetPreviousMessage(toID, afterId)
if lastMessageErr == nil && lastMessage != nil {
lastMessageId = lastMessage.Id
} else {
beyond = true
}
if !beyond {
var newComplete bool
messages, newComplete, err = session.GetMessagesBetween(toID, afterId, lastMessageId, rsmLimit)
if canBeComplete {
complete = newComplete
if !complete {
if lastMessageId != 0 {
overallyLastMessageId = lastMessageId
} else {
newestMessage, newestMessageErr := session.GetPreviousMessage(toID, 0)
if newestMessageErr == nil && newestMessage != nil {
overallyLastMessageId = newestMessage.Id
}
}
}
}
}
} else { // time limit or no limits at all
// don't allow to fetch far beyond the quota
if !endTime.IsZero() && endTime.Before(quotaTs) {
beyond = true
}
if (!startTime.IsZero() && startTime.Before(quotaTs)) || startTime.IsZero() {
startTime = quotaTs
}
if !beyond {
var fromMessageId int64
var lastMessageId int64
fromMessage, fromMessageErr := session.FindMessageByTime(toID, startTime)
if fromMessageErr == nil && fromMessage != nil {
fromMessageId = fromMessage.Id
} else {
// there seem to be no messages older than quota, it's safe to fetch from the very start
fromMessageId = 1 // https://github.com/tdlib/td/issues/195#issuecomment-380836359
}
if !endTime.IsZero() {
endMsg, endMsgErr := session.FindMessageByTime(toID, endTime)
if endMsgErr == nil && endMsg != nil {
lastMessageId = endMsg.Id
} else {
beyond = true
}
}
if rsmAfter != 0 {
rsmAfterMessage, rsmAfterMessageErr := session.GetMessage(toID, rsmAfter)
if rsmAfterMessageErr == nil && rsmAfterMessage != nil {
if int64(rsmAfterMessage.Date) < startTime.Unix() || (!endTime.IsZero() && int64(rsmAfterMessage.Date) > endTime.Unix()) {
iqAnswerSetError(answer, 400)
return
}
if rsmAfterMessage.Id > fromMessageId {
fromStart = false
overallyFirstMessage, overallyFirstMessageErr := session.GetNextMessage(toID, fromMessageId)
if overallyFirstMessageErr == nil && overallyFirstMessage != nil {
overallyFirstMessageId = overallyFirstMessage.Id
}
}
fromMessageId = rsmAfterMessage.Id
} else {
iqAnswerSetError(answer, 404)
return
}
}
if rsmBefore != 0 {
rsmBeforeMessage, rsmBeforeMessageErr := session.GetMessage(toID, rsmBefore)
if rsmBeforeMessageErr == nil && rsmBeforeMessage != nil {
if int64(rsmBeforeMessage.Date) < startTime.Unix() || (!endTime.IsZero() && int64(rsmBeforeMessage.Date) > endTime.Unix()) {
iqAnswerSetError(answer, 400)
return
}
newLastMessage, newLastMessageErr := session.GetPreviousMessage(toID, rsmBeforeMessage.Id)
if newLastMessageErr == nil && newLastMessage != nil {
if lastMessageId == 0 || lastMessageId != newLastMessage.Id {
canBeComplete = false
if lastMessageId != 0 {
overallyLastMessageId = lastMessageId
} else {
newestMessage, newestMessageErr := session.GetPreviousMessage(toID, 0)
if newestMessageErr == nil && newestMessage != nil {
overallyLastMessageId = newestMessage.Id
}
}
lastMessageId = newLastMessage.Id
}
} else {
// nothing?.. not complete, just empty
beyond = true
}
} else {
iqAnswerSetError(answer, 404)
return
}
}
if !beyond { // yes🗿, twice
var newComplete bool
messages, newComplete, err = session.GetMessagesBetween(toID, fromMessageId, lastMessageId, rsmLimit)
if canBeComplete {
complete = newComplete
if !complete {
if lastMessageId != 0 {
overallyLastMessageId = lastMessageId
} else {
newestMessage, newestMessageErr := session.GetPreviousMessage(toID, 0)
if newestMessageErr == nil && newestMessage != nil {
overallyLastMessageId = newestMessage.Id
}
}
}
}
}
}
}
if query.FlipPage != nil {
telegram.ReverseMessagesSlice(messages)
}
log.Debugf("obtained %v messages", len(messages))
for _, message := range messages {
session.SendDelayedMUCMessage(toID, message, iq.From, query.QueryId)
}
rs := stanza.ResultSet{}
answer.Payload = &extensions.MAM2Fin{
ResultSet: &rs,
Complete: complete,
Stable: false,
}
if beyond {
count := 0
rs.Count = &count
} else {
if len(messages) > 0 {
if fromStart {
overallyFirstMessageId = messages[0].Id
}
if complete {
overallyLastMessageId = messages[len(messages)-1].Id
}
}
var firstMsgPositionCount, lastMsgPositionCount int32
// estimate overall count
if overallyFirstMessageId != 0 && overallyLastMessageId != 0 {
firstMsgPosition, firstMsgPositionErr := session.GetChatMessagePosition(toID, overallyFirstMessageId)
if firstMsgPositionErr == nil && firstMsgPosition != nil {
firstMsgPositionCount = firstMsgPosition.Count
}
lastMsgPosition, lastMsgPositionErr := session.GetChatMessagePosition(toID, overallyLastMessageId)
if lastMsgPositionErr == nil && lastMsgPosition != nil {
lastMsgPositionCount = lastMsgPosition.Count
}
if firstMsgPositionCount != 0 && lastMsgPositionCount != 0 {
count := int(lastMsgPositionCount - firstMsgPositionCount + 1)
rs.Count = &count
}
}
if len(messages) > 0 {
rs.First = &stanza.First{
Content: strconv.FormatInt(messages[0].Id, 10),
}
if firstMsgPositionCount != 0 {
rsmFirstMsgPosition, rsmFirstMsgPositionErr := session.GetChatMessagePosition(toID, messages[0].Id)
if rsmFirstMsgPositionErr == nil && rsmFirstMsgPosition != nil {
index := int(rsmFirstMsgPosition.Count - firstMsgPositionCount)
rs.First.Index = &index
}
}
last := strconv.FormatInt(messages[len(messages)-1].Id, 10)
rs.Last = &last
}
}
log.Debugf("MAM fin: %#v", answer.Payload)
}
func iqAnswerSetError(answer *stanza.IQ, code int) { func iqAnswerSetError(answer *stanza.IQ, code int) {
iqAnswerSetErrorInternal(answer, code, false) iqAnswerSetErrorInternal(answer, code, false)
} }
@ -2086,6 +2637,11 @@ func iqAnswerSetErrorInternal(answer *stanza.IQ, code int, registerMode bool) {
Type: stanza.ErrorTypeWait, Type: stanza.ErrorTypeWait,
Reason: "internal-server-error", Reason: "internal-server-error",
} }
case 501:
answer.Error = &stanza.Err{
Type: stanza.ErrorTypeCancel,
Reason: "feature-not-implemented",
}
default: default:
log.Error("Unknown error code, falling back with empty reason") log.Error("Unknown error code, falling back with empty reason")
answer.Error = &stanza.Err{ answer.Error = &stanza.Err{
@ -2251,6 +2807,21 @@ func toToID(to string) (int64, bool, bool) {
return toID, true, isGroup return toID, true, isGroup
} }
func parseMessageId(sId string) (int64, bool) {
if sId[0] == 'e' {
idParts := strings.Split(sId[1:], ":")
if len(idParts) >= 1 {
sId = idParts[0]
}
}
id, err := strconv.ParseInt(sId, 10, 64)
if err != nil {
log.Warn(errors.Wrap(err, "Failed to parse message ID!"))
return 0, false
}
return id, true
}
func makeVCardPayload(typ byte, id string, info telegram.VCardInfo, session *telegram.Client) stanza.IQPayload { func makeVCardPayload(typ byte, id string, info telegram.VCardInfo, session *telegram.Client) stanza.IQPayload {
var base64Photo string var base64Photo string
if info.Photo != nil { if info.Photo != nil {