diff --git a/ROADMAP.md b/ROADMAP.md index c734d27..48c52af 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -9,10 +9,10 @@ * [x] Message redactions * [x] Reactions * [x] Unicode emojis - * [ ] Custom emojis + * [ ] Custom emojis (re-reacting with custom emojis sent from Discord already works) * [ ] Presence * [ ] Typing notifications - * [ ] Own read status + * [x] Own read status * [ ] Power level * [ ] Membership actions * [ ] Invite @@ -40,6 +40,7 @@ * [x] Avatars * [ ] Presence * [ ] Typing notifications + * [x] Own read status * [ ] Membership actions * [ ] Invite * [ ] Join diff --git a/database/message.go b/database/message.go index d4a375a..51ebb46 100644 --- a/database/message.go +++ b/database/message.go @@ -56,6 +56,16 @@ func (mq *MessageQuery) GetFirstByDiscordID(key PortalKey, discordID string) *Me return mq.New().Scan(mq.db.QueryRow(query, key.ChannelID, key.Receiver, discordID)) } +func (mq *MessageQuery) GetLastByDiscordID(key PortalKey, discordID string) *Message { + query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND dcid=$3 AND dc_edit_index=0 ORDER BY dc_attachment_id DESC LIMIT 1" + return mq.New().Scan(mq.db.QueryRow(query, key.ChannelID, key.Receiver, discordID)) +} + +func (mq *MessageQuery) GetClosestBefore(key PortalKey, ts time.Time) *Message { + query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND timestamp<=$3 ORDER BY timestamp DESC, dc_attachment_id DESC LIMIT 1" + return mq.New().Scan(mq.db.QueryRow(query, key.ChannelID, key.Receiver, ts.UnixMilli())) +} + func (mq *MessageQuery) GetLastInThread(key PortalKey, threadID string) *Message { query := messageSelect + " WHERE dc_chan_id=$1 AND dc_chan_receiver=$2 AND dc_thread_id=$3 AND dc_edit_index=0 ORDER BY timestamp DESC, dc_attachment_id DESC LIMIT 1" return mq.New().Scan(mq.db.QueryRow(query, key.ChannelID, key.Receiver, threadID)) diff --git a/go.mod b/go.mod index 993993c..5bf39ea 100644 --- a/go.mod +++ b/go.mod @@ -26,4 +26,4 @@ require ( maunium.net/go/mauflag v1.0.0 // indirect ) -replace github.com/bwmarrin/discordgo => gitlab.com/beeper/discordgo v0.23.3-0.20220703095519-7b2c44e4bc2f +replace github.com/bwmarrin/discordgo => gitlab.com/beeper/discordgo v0.23.3-0.20220708085215-5150e3de5797 diff --git a/go.sum b/go.sum index 8d3ebc7..a464947 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/tidwall/sjson v1.2.4 h1:cuiLzLnaMeBhRmEv00Lpk3tkYrcxpmbU81tAY4Dw0tc= github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM= github.com/yuin/goldmark v1.4.12 h1:6hffw6vALvEDqJ19dOJvJKOoAOKe4NDaTqvd2sktGN0= github.com/yuin/goldmark v1.4.12/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -gitlab.com/beeper/discordgo v0.23.3-0.20220703095519-7b2c44e4bc2f h1:Ag8rA+k9IRnEYxd0z671a7auMKoQ7DGw5FMtLpykFsA= -gitlab.com/beeper/discordgo v0.23.3-0.20220703095519-7b2c44e4bc2f/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +gitlab.com/beeper/discordgo v0.23.3-0.20220708085215-5150e3de5797 h1:G+6sujr5CBD1+GyDq9Vobj/2XhaRejXzyOVDNNWlM/E= +gitlab.com/beeper/discordgo v0.23.3-0.20220708085215-5150e3de5797/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= diff --git a/portal.go b/portal.go index 2003c09..edc055d 100644 --- a/portal.go +++ b/portal.go @@ -1451,6 +1451,30 @@ func (portal *Portal) handleMatrixRedaction(sender *User, evt *event.Event) { go portal.sendMessageMetrics(evt, errTargetNotFound, "Ignoring") } +func (portal *Portal) HandleMatrixReadReceipt(brUser bridge.User, eventID id.EventID, receiptTimestamp time.Time) { + sender := brUser.(*User) + if sender.Session == nil { + return + } + msg := portal.bridge.DB.Message.GetByMXID(portal.Key, eventID) + if msg == nil { + msg = portal.bridge.DB.Message.GetClosestBefore(portal.Key, receiptTimestamp) + if msg == nil { + portal.log.Debugfln("Dropping Matrix read receipt from %s for %s: no messages found", sender.MXID, eventID) + } else { + portal.log.Debugfln("Matrix read receipt target %s from %s not found, using closest message %s", eventID, sender.MXID, msg.MXID) + } + } + resp, err := sender.Session.ChannelMessageAckNoToken(msg.DiscordProtoChannelID(), msg.DiscordID) + if err != nil { + portal.log.Warnfln("Failed to handle read receipt for %s/%s from %s: %v", msg.MXID, msg.DiscordID, sender.MXID) + } else if resp.Token != nil { + portal.log.Debugfln("Marked %s/%s as read by %s (and got unexpected non-nil token %s)", msg.MXID, msg.DiscordID, sender.MXID, *resp.Token) + } else { + portal.log.Debugfln("Marked %s/%s as read by %s", msg.MXID, msg.DiscordID, sender.MXID) + } +} + func (portal *Portal) UpdateName(name string) bool { if portal.Name == name && portal.NameSet { return false diff --git a/user.go b/user.go index c802dfe..dd3a9cd 100644 --- a/user.go +++ b/user.go @@ -479,6 +479,7 @@ func (user *User) Connect() error { user.Session.AddHandler(user.messageUpdateHandler) user.Session.AddHandler(user.reactionAddHandler) user.Session.AddHandler(user.reactionRemoveHandler) + user.Session.AddHandler(user.messageAckHandler) user.Session.Identify.Presence.Status = "online" @@ -717,6 +718,52 @@ func (user *User) reactionRemoveHandler(_ *discordgo.Session, m *discordgo.Messa user.pushPortalMessage(m, "reaction remove", m.ChannelID, m.GuildID) } +type CustomReadReceipt struct { + Timestamp int64 `json:"ts,omitempty"` + DoublePuppetSource string `json:"fi.mau.double_puppet_source,omitempty"` +} + +type CustomReadMarkers struct { + mautrix.ReqSetReadMarkers + ReadExtra CustomReadReceipt `json:"com.beeper.read.extra"` + FullyReadExtra CustomReadReceipt `json:"com.beeper.fully_read.extra"` +} + +func (user *User) makeReadMarkerContent(eventID id.EventID) *CustomReadMarkers { + var extra CustomReadReceipt + extra.DoublePuppetSource = user.bridge.Name + return &CustomReadMarkers{ + ReqSetReadMarkers: mautrix.ReqSetReadMarkers{ + Read: eventID, + FullyRead: eventID, + }, + ReadExtra: extra, + FullyReadExtra: extra, + } +} + +func (user *User) messageAckHandler(_ *discordgo.Session, m *discordgo.MessageAck) { + portal := user.GetExistingPortalByID(m.ChannelID) + if portal == nil || portal.MXID == "" { + return + } + dp := user.GetIDoublePuppet() + if dp == nil { + return + } + msg := user.bridge.DB.Message.GetLastByDiscordID(portal.Key, m.MessageID) + if msg == nil { + user.log.Debugfln("Dropping message ack event for unknown message %s/%s", m.ChannelID, m.MessageID) + return + } + err := dp.CustomIntent().SetReadMarkers(portal.MXID, user.makeReadMarkerContent(msg.MXID)) + if err != nil { + user.log.Warnfln("Failed to mark %s/%s as read: %v", msg.MXID, msg.DiscordID, err) + } else { + user.log.Debugfln("Marked %s/%s as read after Discord message ack event", msg.MXID, msg.DiscordID) + } +} + func (user *User) ensureInvited(intent *appservice.IntentAPI, roomID id.RoomID, isDirect bool) bool { if intent == nil { intent = user.bridge.Bot