bridgev2/portal: add support for implicit read receipts to network
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run

This commit is contained in:
Tulir Asokan 2025-09-19 14:26:21 +03:00
commit b760023dca
2 changed files with 50 additions and 15 deletions

View file

@ -346,6 +346,10 @@ type NetworkGeneralCapabilities struct {
// Should the bridge re-request user info on incoming messages even if the ghost already has info?
// By default, info is only requested for ghosts with no name, and other updating is left to events.
AggressiveUpdateInfo bool
// Should the bridge call HandleMatrixReadReceipt with fake data when receiving a new message?
// This should be enabled if the network requires each message to be marked as read independently,
// and doesn't automatically do it when sending a message.
ImplicitReadReceipts bool
// If the bridge uses the pending message mechanism ([MatrixMessage.AddPendingToSave])
// to handle asynchronous message responses, this field can be set to enable
// automatic timeout errors in case the asynchronous response never arrives.
@ -1361,6 +1365,8 @@ type MatrixReadReceipt struct {
LastRead time.Time
// The receipt metadata.
Receipt event.ReadReceipt
// Whether the receipt is implicit, i.e. triggered by an incoming timeline event rather than an explicit receipt.
Implicit bool
}
type MatrixTyping struct {

View file

@ -587,7 +587,7 @@ func (portal *Portal) handleMatrixEvent(ctx context.Context, sender *User, evt *
// Tombstones aren't bridged so they don't need a login
return portal.handleMatrixTombstone(ctx, evt)
}
login, _, err := portal.FindPreferredLogin(ctx, sender, true)
login, userPortal, err := portal.FindPreferredLogin(ctx, sender, true)
if err != nil {
log.Err(err).Msg("Failed to get user login to handle Matrix event")
if errors.Is(err, ErrNotLoggedIn) {
@ -646,6 +646,21 @@ func (portal *Portal) handleMatrixEvent(ctx context.Context, sender *User, evt *
}
// Copy logger because many of the handlers will use UpdateContext
ctx = log.With().Str("login_id", string(login.ID)).Logger().WithContext(ctx)
if origSender == nil && portal.Bridge.Network.GetCapabilities().ImplicitReadReceipts {
rrLog := log.With().Str("subaction", "implicit read receipt").Logger()
rrCtx := rrLog.WithContext(ctx)
rrLog.Debug().Msg("Sending implicit read receipt for event")
evtTS := time.UnixMilli(evt.Timestamp)
portal.callReadReceiptHandler(rrCtx, login, nil, &MatrixReadReceipt{
Portal: portal,
EventID: evt.ID,
Implicit: true,
ReadUpTo: evtTS,
Receipt: event.ReadReceipt{Timestamp: evtTS},
}, userPortal)
}
switch evt.Type {
case event.EventMessage, event.EventSticker, event.EventUnstablePollStart, event.EventUnstablePollResponse:
return portal.handleMatrixMessage(ctx, login, origSender, evt)
@ -735,15 +750,10 @@ func (portal *Portal) handleMatrixReadReceipt(ctx context.Context, user *User, e
EventID: eventID,
Receipt: receipt,
}
if userPortal == nil {
userPortal = database.UserPortalFor(login.UserLogin, portal.PortalKey)
} else {
evt.LastRead = userPortal.LastRead
userPortal = userPortal.CopyWithoutValues()
}
evt.ExactMessage, err = portal.Bridge.DB.Message.GetPartByMXID(ctx, eventID)
if err != nil {
log.Err(err).Msg("Failed to get exact message from database")
evt.ReadUpTo = receipt.Timestamp
} else if evt.ExactMessage != nil {
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Str("exact_message_id", string(evt.ExactMessage.ID)).Time("exact_message_ts", evt.ExactMessage.Timestamp)
@ -752,19 +762,38 @@ func (portal *Portal) handleMatrixReadReceipt(ctx context.Context, user *User, e
} else {
evt.ReadUpTo = receipt.Timestamp
}
err = rrClient.HandleMatrixReadReceipt(ctx, evt)
portal.callReadReceiptHandler(ctx, login, rrClient, evt, userPortal)
}
func (portal *Portal) callReadReceiptHandler(
ctx context.Context,
login *UserLogin,
rrClient ReadReceiptHandlingNetworkAPI,
evt *MatrixReadReceipt,
userPortal *database.UserPortal,
) {
if rrClient == nil {
var ok bool
rrClient, ok = login.Client.(ReadReceiptHandlingNetworkAPI)
if !ok {
return
}
}
if userPortal == nil {
userPortal = database.UserPortalFor(login.UserLogin, portal.PortalKey)
} else {
evt.LastRead = userPortal.LastRead
userPortal = userPortal.CopyWithoutValues()
}
err := rrClient.HandleMatrixReadReceipt(ctx, evt)
if err != nil {
log.Err(err).Msg("Failed to handle read receipt")
zerolog.Ctx(ctx).Err(err).Msg("Failed to handle read receipt")
return
}
if evt.ExactMessage != nil {
userPortal.LastRead = evt.ExactMessage.Timestamp
} else {
userPortal.LastRead = receipt.Timestamp
}
userPortal.LastRead = evt.ReadUpTo
err = portal.Bridge.DB.UserPortal.Put(ctx, userPortal)
if err != nil {
log.Err(err).Msg("Failed to save user portal metadata")
zerolog.Ctx(ctx).Err(err).Msg("Failed to save user portal metadata")
}
portal.Bridge.DisappearLoop.StartAll(ctx, portal.MXID)
}