From eee277e36eb9b6383e937fb86fcfb9684df447be Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 31 Oct 2025 13:16:56 -0400 Subject: [PATCH] Track updated message ids for more reliable message edit check --- telegram/client.go | 39 +++++++++++ telegram/handlers.go | 154 ++++++++++++++++++++++++++++++------------- xmpp/component.go | 12 ++++ 3 files changed, 158 insertions(+), 47 deletions(-) diff --git a/telegram/client.go b/telegram/client.go index 005ec2f..4ef78c9 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -27,6 +27,41 @@ type HashedAvatar struct { File int32 } +// NewId stores message ids and timestamps of their additions so old ones can be truncated to save memory +type newId struct { + Id int64 + Ts int64 + lock sync.Mutex + ownLock sync.Mutex + locked bool + fired bool +} + +func newNewId() *newId { + return &newId{Ts: time.Now().Unix()} +} + +func (i *newId) Lock() { + i.ownLock.Lock() + if i.fired { + i.ownLock.Unlock() + return + } + i.locked = true + i.ownLock.Unlock() + i.lock.Lock() +} + +func (i *newId) Unlock() { + i.ownLock.Lock() + if i.locked { + i.lock.Unlock() + i.locked = false + i.fired = true + } + i.ownLock.Unlock() +} + // Client stores the metadata for lazily invoked TDlib instance type Client struct { client *client.Client @@ -64,6 +99,9 @@ type Client struct { AvatarHashes map[int64]*HashedAvatar AvatarHashesLock sync.Mutex + MessageIdChanges map[int64]map[int64]*newId + MessageIdChangesLock sync.Mutex + locks clientLocks SendMessageLock sync.Mutex } @@ -149,6 +187,7 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component lastMsgIds: make(map[int64]string), XmppClientFeatures: make(map[string]*[]string), AvatarHashes: make(map[int64]*HashedAvatar), + MessageIdChanges: make(map[int64]map[int64]*newId), locks: clientLocks{ chatMessageLocks: make(map[int64]*sync.Mutex), }, diff --git a/telegram/handlers.go b/telegram/handlers.go index a9cdb06..d7d0823 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -312,67 +312,99 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { sId := strconv.FormatInt(update.MessageId, 10) var isCarbon bool - message, messageErr := c.client.GetMessage(&client.GetMessageRequest{ - ChatId: update.ChatId, - MessageId: update.MessageId, - }) - var prefix string - if messageErr == nil { - if message.EditDate == 0 { - return + go func() { + message, messageErr := c.client.GetMessage(&client.GetMessageRequest{ + ChatId: update.ChatId, + MessageId: update.MessageId, + }) + if messageErr != nil { + // odnako za vremya puti + // sobaka mogla podrasti + c.MessageIdChangesLock.Lock() + idsMap, idsMapOk := c.MessageIdChanges[update.ChatId] + hadNoId := false + if idsMapOk { + newId, newIdOk := idsMap[update.MessageId] + if newIdOk { + if newId.Id == 0 { + hadNoId = true + c.MessageIdChangesLock.Unlock() + newId.Lock() + } + log.Infof("falling back to updated message id: %v/%v->%v", update.ChatId, update.MessageId, newId.Id) + message, messageErr = c.client.GetMessage(&client.GetMessageRequest{ + ChatId: update.ChatId, + MessageId: newId.Id, + }) + } + } + if !hadNoId { + c.MessageIdChangesLock.Unlock() + } } + var prefix string + if messageErr == nil { + if message.EditDate == 0 { + return + } + log.Debugf("editDate: %v", message.EditDate) - isCarbon = c.isCarbonsEnabled() && message.IsOutgoing - // reply correction support in clients is suboptimal yet, so cut them out for now - prefix, _ = c.messageToPrefix(message, "", "", true) - } else { - log.Errorf("No message %v/%v found, cannot reliably determine if it is a carbon and if it is edited", update.ChatId, update.MessageId) - } - - // use XEP-0308 edits only if the last message is edited for sure, fallback otherwise - if c.Session.NativeEdits { - lastXmppId, ok := c.getLastChatMessageId(update.ChatId) - if xmppIdErr != nil { - xmppId = sId - } - if ok && lastXmppId == xmppId { - replaceId = xmppId + isCarbon = c.isCarbonsEnabled() && message.IsOutgoing + // reply correction support in clients is suboptimal yet, so cut them out for now + prefix, _ = c.messageToPrefix(message, "", "", true) } else { - log.Infof("Mismatching message ids: %v %v, falling back to separate edit message", lastXmppId, xmppId) + log.Errorf("No message %v/%v found, cannot reliably determine if it is a carbon and if it is edited: %v", update.ChatId, update.MessageId, messageErr.Error()) } - } - var text strings.Builder - - if replaceId == "" { - var editChar string - if c.Session.AsciiArrows { - editChar = "e" - } else { - editChar = "✎" + // use XEP-0308 edits only if the last message is edited for sure, fallback otherwise + if c.Session.NativeEdits { + lastXmppId, ok := c.getLastChatMessageId(update.ChatId) + if xmppIdErr != nil { + xmppId = sId + } + if ok && lastXmppId == xmppId { + replaceId = xmppId + } else { + log.Infof("Mismatching message ids: %v %v, falling back to separate edit message", lastXmppId, xmppId) + } } - text.WriteString(fmt.Sprintf("%s %v | ", editChar, update.MessageId)) - } else if prefix != "" { - text.WriteString(prefix) - text.WriteString(c.getPrefixSeparator(update.ChatId)) - } - text.WriteString(formatter.Format( - textContent.Text.Text, - textContent.Text.Entities, - markupFunction, - )) + var text strings.Builder - sChatId := strconv.FormatInt(update.ChatId, 10) - for _, jid := range jids { - gateway.SendMessage(jid, sChatId, text.String(), "e"+sId, c.xmpp, nil, replaceId, isCarbon, false) - } + if replaceId == "" { + var editChar string + if c.Session.AsciiArrows { + editChar = "e" + } else { + editChar = "✎" + } + text.WriteString(fmt.Sprintf("%s %v | ", editChar, update.MessageId)) + } else if prefix != "" { + text.WriteString(prefix) + text.WriteString(c.getPrefixSeparator(update.ChatId)) + } + + text.WriteString(formatter.Format( + textContent.Text.Text, + textContent.Text.Entities, + markupFunction, + )) + + sChatId := strconv.FormatInt(update.ChatId, 10) + for _, jid := range jids { + gateway.SendMessage(jid, sChatId, text.String(), "e"+sId, c.xmpp, nil, replaceId, isCarbon, false) + } + }() } } // message(s) deleted func (c *Client) updateDeleteMessages(update *client.UpdateDeleteMessages) { if update.IsPermanent { + for _, deleteId := range update.MessageIds { + c.tryUnlockMessageId(update.ChatId, deleteId) + } + if c.Session.IsChatIgnored(update.ChatId) { return } @@ -411,6 +443,20 @@ func (c *Client) updateMessageSendSucceeded(update *client.UpdateMessageSendSucc log.Errorf("failed to replace %v with %v: %v", update.OldMessageId, update.Message.Id, err.Error()) } + c.MessageIdChangesLock.Lock() + idsMap, ok := c.MessageIdChanges[update.Message.ChatId] + if !ok { + idsMap = make(map[int64]*newId) + c.MessageIdChanges[update.Message.ChatId] = idsMap + } + id, ok := idsMap[update.OldMessageId] + if !ok { + id = newNewId() + idsMap[update.OldMessageId] = id + } + id.Id = update.Message.Id + c.MessageIdChangesLock.Unlock() + c.sendMarker(update.Message.ChatId, update.Message.Id, gateway.MarkerTypeReceived) // clean uploaded files @@ -420,6 +466,8 @@ func (c *Client) updateMessageSendSucceeded(update *client.UpdateMessageSendSucc } } func (c *Client) updateMessageSendFailed(update *client.UpdateMessageSendFailed) { + c.tryUnlockMessageId(update.Message.ChatId, update.OldMessageId) + // clean uploaded files file, _ := c.contentToFile(update.Message.Content) if file != nil && file.Local != nil { @@ -446,3 +494,15 @@ func (c *Client) updateChatTitle(update *client.UpdateChatTitle) { func (c *Client) updateChatReadOutbox(update *client.UpdateChatReadOutbox) { c.sendMarker(update.ChatId, update.LastReadOutboxMessageId, gateway.MarkerTypeDisplayed) } + +func (c *Client) tryUnlockMessageId(chatId, messageId int64) { + c.MessageIdChangesLock.Lock() + idsMap, ok := c.MessageIdChanges[chatId] + if ok { + id, ok := idsMap[messageId] + if ok { + id.Unlock() + } + } + c.MessageIdChangesLock.Unlock() +} diff --git a/xmpp/component.go b/xmpp/component.go index 6a0ae98..784b25f 100644 --- a/xmpp/component.go +++ b/xmpp/component.go @@ -148,6 +148,18 @@ func heartbeat(component *xmpp.Component) { } } session.DelayedStatusesLock.Unlock() + + // shrink message id maps + session.MessageIdChangesLock.Lock() + for _, idsMap := range session.MessageIdChanges { + for oldMessageId, newId := range idsMap { + if newId.Ts < now - 60 { + newId.Unlock() + delete(idsMap, oldMessageId) + } + } + } + session.MessageIdChangesLock.Unlock() } sessionLock.Unlock()