diff --git a/config.yml.example b/config.yml.example index b8de1dd..260dc46 100644 --- a/config.yml.example +++ b/config.yml.example @@ -7,6 +7,7 @@ :user: 'www-data' # owner of content files :quota: '256MB' # maximum storage size :tdlib_verbosity: 1 + :mam_threshold: 7 # in days :tdlib: :datadir: './sessions/' :client: diff --git a/config/config.go b/config/config.go index 7c685fb..b214495 100644 --- a/config/config.go +++ b/config/config.go @@ -27,10 +27,11 @@ type XMPPConfig struct { // TelegramConfig is for :telegram: subtree type TelegramConfig struct { - Loglevel string `yaml:":loglevel"` - Content TelegramContentConfig `yaml:":content"` - Verbosity uint8 `yaml:":tdlib_verbosity"` - Tdlib TelegramTdlibConfig `yaml:":tdlib"` + Loglevel string `yaml:":loglevel"` + Content TelegramContentConfig `yaml:":content"` + Verbosity uint8 `yaml:":tdlib_verbosity"` + MAMThreshold uint32 `yaml:":mam_threshold"` + Tdlib TelegramTdlibConfig `yaml:":tdlib"` } // TelegramContentConfig is for :content: subtree diff --git a/config_schema.json b/config_schema.json index ab25307..a58da77 100644 --- a/config_schema.json +++ b/config_schema.json @@ -33,6 +33,9 @@ ":tdlib_verbosity": { "type": "integer" }, + ":mam_threshold": { + "type": "integer" + }, ":tdlib": { "required": [":client"], "type": "object", diff --git a/telegram/handlers.go b/telegram/handlers.go index 1185f2f..0170d5e 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -374,7 +374,7 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { } for _, jid := range jids { 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 { gateway.SendMUCAnnouncement(jid, from, text.String(), nickname, id, c.xmpp) } diff --git a/telegram/utils.go b/telegram/utils.go index 367fda8..004e9ef 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -13,6 +13,7 @@ import ( osUser "os/user" "path/filepath" "regexp" + "sort" "strconv" "strings" "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) if safeToSend { - c.SendMessageToGateway(chatId, message, "", false, groupChatFrom, groupChatTos) + c.SendMessageToGateway(chatId, message, "", false, groupChatFrom, groupChatTos, "") } else { mucJID := gateway.MUCJID(chatId) 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 -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 jids []string var isGroupchat bool @@ -1998,15 +1999,52 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id 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 { - 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 != "" { - 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) } +// 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 func (c *Client) MarkAsRead(chatId, messageId int64) { c.client.ViewMessages(&client.ViewMessagesRequest{ @@ -2330,6 +2368,73 @@ func (c *Client) getNLastMessages(chatID int64, limit *MessageLimit) ([]*client. 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 func (c *Client) GetFile(id int32) (*client.File, error) { return c.client.GetFile(&client.GetFileRequest{ @@ -2751,18 +2856,13 @@ func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, p false, originalFrom, "", + "", + "", + nil, ) } } else { - msgId, _ := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, chatID, message.Id) - c.SendMessageToGateway( - chatID, - message, - msgId, - true, - mucJid + "/" + c.GetMUCNickname(c.getMessageSenderId(message)), - []string{toJid}, - ) + c.SendDelayedMUCMessage(chatID, message, toJid, "") } } } @@ -3072,6 +3172,22 @@ func (c *Client) DeleteChat(chatID int64) error { 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 func CloneChatPermissions(permissions *client.ChatPermissions) *client.ChatPermissions { return &client.ChatPermissions{ @@ -3219,6 +3335,46 @@ func (c *Client) GetMUCNicknameByUsername(username string) (string, error) { 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 func GetErrorCode(err error) (int32, bool) { responseError, ok := err.(client.ResponseError) @@ -3227,3 +3383,20 @@ func GetErrorCode(err error) (int32, bool) { } 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] + } +} diff --git a/test/bad_config.yml b/test/bad_config.yml index 094faf4..0b554df 100644 --- a/test/bad_config.yml +++ b/test/bad_config.yml @@ -5,6 +5,7 @@ :link: 'http://tlgrm.localhost/content' # webserver public address :upload: 'https:///xmppfiles.localhost' # xmpp http upload address :tdlib_verbosity: 1 + :mam_threshold: 7 # in days :tdlib: :client: :api_id: '17349' diff --git a/test/good_config.yml b/test/good_config.yml index ac68438..547eb7e 100644 --- a/test/good_config.yml +++ b/test/good_config.yml @@ -5,6 +5,7 @@ :link: '' # webserver public address :upload: '' # xmpp http upload address :tdlib_verbosity: 1 + :tdlib_verbosity: 7 # in days :tdlib: :client: :api_id: '17349' diff --git a/xmpp/component.go b/xmpp/component.go index f0c481d..45c5440 100644 --- a/xmpp/component.go +++ b/xmpp/component.go @@ -65,6 +65,8 @@ func NewComponent(conf config.XMPPConfig, tc config.TelegramConfig, idsPath stri } } + gateway.MAMThreshold = tc.MAMThreshold + options := xmpp.ComponentOptions{ TransportConfiguration: xmpp.TransportConfiguration{ Address: conf.Host + ":" + conf.Port, diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 04d13d2..ba03643 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -3,6 +3,7 @@ package extensions import ( "encoding/xml" "strconv" + "time" "gosrc.io/xmpp/stanza" ) @@ -224,6 +225,7 @@ type MessageXMucUserExtension struct { XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"` Invite *MessageXMucUserInvite `xml:"invite,omitempty"` Status *MessageXMucUserStatus `xml:"status,omitempty"` + Item PresenceXMucUserItem `xml:"item,omitempty"` Password string `xml:"password,omitempty"` } @@ -273,10 +275,21 @@ type PresenceXMucUserStatus struct { // MessageDelay is from XEP-0203 type MessageDelay struct { XMLName xml.Name `xml:"urn:xmpp:delay delay"` - From string `xml:"from,attr"` + From string `xml:"from,attr,omitempty"` 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 type MessageDelayLegacy struct { XMLName xml.Name `xml:"jabber:x:delay x"` @@ -284,6 +297,13 @@ type MessageDelayLegacy struct { 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 type MessageAddresses struct { XMLName xml.Name `xml:"http://jabber.org/protocol/address addresses"` @@ -347,6 +367,65 @@ type MucDestroy struct { 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! func (c PresenceNickExtension) Namespace() string { return c.XMLName.Space @@ -452,6 +531,36 @@ func (c QueryMucOwner) GetSet() *stanza.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 func NewReplyFallback(start uint64, end uint64) Fallback { return Fallback{ @@ -585,4 +694,28 @@ func init() { "http://jabber.org/protocol/muc#owner", "query", }, 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{}) } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index c37c0ba..4657a2f 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -6,7 +6,6 @@ import ( "strconv" "strings" "sync" - "time" "dev.narayana.im/narayana/telegabber/badger" "dev.narayana.im/narayana/telegabber/xmpp/extensions" @@ -38,6 +37,12 @@ type marker struct { Id string } +type MUCUserItem struct { + Affiliation string + Jid string + Role string +} + const NSNick string = "http://jabber.org/protocol/nick" const NodeVCard4 string = "urn:xmpp:vcard4" 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 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 func CHATNODE(chatId int64) string { return strconv.FormatInt(chatId, 10) @@ -114,8 +122,8 @@ func ResourcePrep(resource string) (string, error) { } // 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) { - sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, "", replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "", stanzaId, 0) +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, mamQueryId, mucJID, mucUserItem) } // 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 { 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 @@ -133,7 +141,7 @@ func SendTextMessage(to, from, body string, component *xmpp.Component, isGroupch if uuid, err := uuid.NewRandom(); err == nil { 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 @@ -158,7 +166,7 @@ func SendMUCAnnouncement(to, from, body, nickname, id string, component *xmpp.Co 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( component, @@ -173,22 +181,22 @@ func SendMUCAnnouncement(to, from, body, nickname, id string, component *xmpp.Co // SendErrorMessage creates and sends an error message stanza 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 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 -func SendMessageWithOOB(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom, stanzaId string) { - sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, oob, replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "", stanzaId, 0) +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, mamQueryId, mucJID, mucUserItem) } // SendSubjectMessage creates and sends a MUC subject 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 @@ -196,20 +204,20 @@ func SendMessageMarker(to string, from string, component *xmpp.Component, marker sendMessageWrapper(to, from, "", "", "", "", component, nil, &marker{ Type: markerType, 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 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 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) if err != nil { log.WithFields(log.Fields{ @@ -247,6 +255,8 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component if isCarbon { messageTo = messageFrom messageFrom = bareTo + "/" + Jid.Resource + } else if mucJID != "" { + messageTo = mucJID } else { messageTo = to } @@ -322,19 +332,13 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component if !isGroupchat && !isCarbon && toJid.Resource != "" && inviteFrom == "" { message.Extensions = append(message.Extensions, stanza.HintNoCopy{}) } - if timestamp != 0 { + if timestamp != 0 && mamQueryId == "" { var delayFrom string if isGroupchat { delayFrom = bareFrom } - message.Extensions = append(message.Extensions, extensions.MessageDelay{ - From: 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"), - }) + message.Extensions = append(message.Extensions, extensions.NewMessageDelay(timestamp, delayFrom)) + message.Extensions = append(message.Extensions, extensions.NewMessageDelayLegacy(timestamp, delayFrom)) } if originalFrom != "" { 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), } } - 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) } if stanzaId != "" { @@ -425,6 +436,26 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, 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 { sendMessage(&message, component) } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 15fcd10..5875367 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -83,6 +83,16 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { go handleGetQueryMucOwner(s, iq) 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 { queryRegister, ok := iq.Payload.(*extensions.QueryRegister) if ok { @@ -104,6 +114,11 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { go handleSetQueryMucOwner(s, iq, queryMucOwner) return } + queryMAM2, ok := iq.Payload.(*extensions.MAM2Query) + if ok { + go handleSetQueryMAM2(s, iq, queryMAM2) + return + } } else if iq.Type == stanza.IQTypeResult { discoInfo, ok := iq.Payload.(*stanza.DiscoInfo) if ok { @@ -202,17 +217,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { log.Debugf("replace tg: %#v %#v", chatId, msgId) } } else { - id := 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!")) - } + replyId, _ = parseMessageId(reply.Id) } 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, msg.To + "/" + session.GetMUCNickname(session.GetSenderId(tgMessage.SenderId)), []string{msg.From}, + "", ) } } else if isCommand && isGroupchat && session.Session.MUC { // 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 { /* // 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", "http://jabber.org/protocol/muc#stable_id", "jabber:iq:register", + "urn:xmpp:mam:2", + "urn:xmpp:mam:2#extended", "urn:xmpp:sid:0", "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) { component, answer, ok := iqResultStub(s, iq) 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) { iqAnswerSetErrorInternal(answer, code, false) } @@ -2086,6 +2637,11 @@ func iqAnswerSetErrorInternal(answer *stanza.IQ, code int, registerMode bool) { Type: stanza.ErrorTypeWait, Reason: "internal-server-error", } + case 501: + answer.Error = &stanza.Err{ + Type: stanza.ErrorTypeCancel, + Reason: "feature-not-implemented", + } default: log.Error("Unknown error code, falling back with empty reason") answer.Error = &stanza.Err{ @@ -2251,6 +2807,21 @@ func toToID(to string) (int64, bool, bool) { 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 { var base64Photo string if info.Photo != nil {