From 07567f6f96d6dc420e37f561a3474c3bcdf98b1e Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 8 Jun 2025 00:12:53 +0300 Subject: [PATCH] bridgev2/portal: include room id in cross-room replies --- bridgev2/bridgeconfig/config.go | 1 + bridgev2/bridgeconfig/upgrade.go | 1 + bridgev2/matrix/mxmain/example-config.yaml | 3 +++ bridgev2/portal.go | 23 +++++++++++++++++++--- bridgev2/portalbackfill.go | 2 +- bridgev2/portalinternal.go | 20 +++++++++++++++++-- 6 files changed, 44 insertions(+), 6 deletions(-) diff --git a/bridgev2/bridgeconfig/config.go b/bridgev2/bridgeconfig/config.go index 37517818..bd6f53c3 100644 --- a/bridgev2/bridgeconfig/config.go +++ b/bridgev2/bridgeconfig/config.go @@ -72,6 +72,7 @@ type BridgeConfig struct { OnlyBridgeTags []event.RoomTag `yaml:"only_bridge_tags"` MuteOnlyOnCreate bool `yaml:"mute_only_on_create"` DeduplicateMatrixMessages bool `yaml:"deduplicate_matrix_messages"` + CrossRoomReplies bool `yaml:"cross_room_replies"` OutgoingMessageReID bool `yaml:"outgoing_message_re_id"` CleanupOnLogout CleanupOnLogouts `yaml:"cleanup_on_logout"` Relay RelayConfig `yaml:"relay"` diff --git a/bridgev2/bridgeconfig/upgrade.go b/bridgev2/bridgeconfig/upgrade.go index 3e19bf8f..fa4b4493 100644 --- a/bridgev2/bridgeconfig/upgrade.go +++ b/bridgev2/bridgeconfig/upgrade.go @@ -38,6 +38,7 @@ func doUpgrade(helper up.Helper) { helper.Copy(up.List, "bridge", "only_bridge_tags") helper.Copy(up.Bool, "bridge", "mute_only_on_create") helper.Copy(up.Bool, "bridge", "deduplicate_matrix_messages") + helper.Copy(up.Bool, "bridge", "cross_room_replies") helper.Copy(up.Bool, "bridge", "cleanup_on_logout", "enabled") helper.Copy(up.Str, "bridge", "cleanup_on_logout", "manual", "private") helper.Copy(up.Str, "bridge", "cleanup_on_logout", "manual", "relayed") diff --git a/bridgev2/matrix/mxmain/example-config.yaml b/bridgev2/matrix/mxmain/example-config.yaml index a9d05fd1..dad3f8a8 100644 --- a/bridgev2/matrix/mxmain/example-config.yaml +++ b/bridgev2/matrix/mxmain/example-config.yaml @@ -40,6 +40,9 @@ bridge: mute_only_on_create: true # Should the bridge check the db to ensure that incoming events haven't been handled before deduplicate_matrix_messages: false + # Should cross-room reply metadata be bridged? + # Most Matrix clients don't support this and servers may reject such messages too. + cross_room_replies: false # What should be done to portal rooms when a user logs out or is logged out? # Permitted values: diff --git a/bridgev2/portal.go b/bridgev2/portal.go index 5cc7bb4a..333c1889 100644 --- a/bridgev2/portal.go +++ b/bridgev2/portal.go @@ -1943,7 +1943,7 @@ func (portal *Portal) getRelationMeta(ctx context.Context, currentMsg networkid. return } -func (portal *Portal) applyRelationMeta(content *event.MessageEventContent, replyTo, threadRoot, prevThreadEvent *database.Message) { +func (portal *Portal) applyRelationMeta(ctx context.Context, content *event.MessageEventContent, replyTo, threadRoot, prevThreadEvent *database.Message) { if content.Mentions == nil { content.Mentions = &event.Mentions{} } @@ -1951,7 +1951,24 @@ func (portal *Portal) applyRelationMeta(content *event.MessageEventContent, repl content.GetRelatesTo().SetThread(threadRoot.MXID, prevThreadEvent.MXID) } if replyTo != nil { - content.GetRelatesTo().SetReplyTo(replyTo.MXID) + crossRoom := replyTo.Room != portal.PortalKey + if !crossRoom || portal.Bridge.Config.CrossRoomReplies { + content.GetRelatesTo().SetReplyTo(replyTo.MXID) + } + if crossRoom && portal.Bridge.Config.CrossRoomReplies { + targetPortal, err := portal.Bridge.GetExistingPortalByKey(ctx, replyTo.Room) + if err != nil { + zerolog.Ctx(ctx).Err(err). + Object("target_portal_key", replyTo.Room). + Msg("Failed to get cross-room reply portal") + } else if targetPortal == nil || targetPortal.MXID == "" { + zerolog.Ctx(ctx).Warn(). + Object("target_portal_key", replyTo.Room). + Msg("Cross-room reply portal not found") + } else { + content.RelatesTo.InReplyTo.UnstableRoomID = targetPortal.MXID + } + } content.Mentions.Add(replyTo.SenderMXID) } } @@ -1975,7 +1992,7 @@ func (portal *Portal) sendConvertedMessage( replyTo, threadRoot, prevThreadEvent := portal.getRelationMeta(ctx, id, converted.ReplyTo, converted.ThreadRoot, false) output := make([]*database.Message, 0, len(converted.Parts)) for i, part := range converted.Parts { - portal.applyRelationMeta(part.Content, replyTo, threadRoot, prevThreadEvent) + portal.applyRelationMeta(ctx, part.Content, replyTo, threadRoot, prevThreadEvent) dbMessage := &database.Message{ ID: id, PartID: part.ID, diff --git a/bridgev2/portalbackfill.go b/bridgev2/portalbackfill.go index a5dfb42a..3953a043 100644 --- a/bridgev2/portalbackfill.go +++ b/bridgev2/portalbackfill.go @@ -333,7 +333,7 @@ func (portal *Portal) compileBatchMessage(ctx context.Context, source *UserLogin var firstPart *database.Message for i, part := range msg.Parts { partIDs = append(partIDs, part.ID) - portal.applyRelationMeta(part.Content, replyTo, threadRoot, prevThreadEvent) + portal.applyRelationMeta(ctx, part.Content, replyTo, threadRoot, prevThreadEvent) evtID := portal.Bridge.Matrix.GenerateDeterministicEventID(portal.MXID, portal.PortalKey, msg.ID, part.ID) dbMessage := &database.Message{ ID: msg.ID, diff --git a/bridgev2/portalinternal.go b/bridgev2/portalinternal.go index e0f4ee5a..fd6724f4 100644 --- a/bridgev2/portalinternal.go +++ b/bridgev2/portalinternal.go @@ -89,10 +89,22 @@ func (portal *PortalInternals) CheckMessageContentCaps(ctx context.Context, caps return (*Portal)(portal).checkMessageContentCaps(ctx, caps, content, evt) } +func (portal *PortalInternals) ParseInputTransactionID(origSender *OrigSender, evt *event.Event) networkid.RawTransactionID { + return (*Portal)(portal).parseInputTransactionID(origSender, evt) +} + func (portal *PortalInternals) HandleMatrixMessage(ctx context.Context, sender *UserLogin, origSender *OrigSender, evt *event.Event) { (*Portal)(portal).handleMatrixMessage(ctx, sender, origSender, evt) } +func (portal *PortalInternals) PendingMessageTimeoutLoop(ctx context.Context, cfg *OutgoingTimeoutConfig) { + (*Portal)(portal).pendingMessageTimeoutLoop(ctx, cfg) +} + +func (portal *PortalInternals) CheckPendingMessages(ctx context.Context, cfg *OutgoingTimeoutConfig) { + (*Portal)(portal).checkPendingMessages(ctx, cfg) +} + func (portal *PortalInternals) HandleMatrixEdit(ctx context.Context, sender *UserLogin, origSender *OrigSender, evt *event.Event, content *event.MessageEventContent, caps *event.RoomFeatures) { (*Portal)(portal).handleMatrixEdit(ctx, sender, origSender, evt, content, caps) } @@ -129,8 +141,8 @@ func (portal *PortalInternals) GetRelationMeta(ctx context.Context, currentMsg n return (*Portal)(portal).getRelationMeta(ctx, currentMsg, replyToPtr, threadRootPtr, isBatchSend) } -func (portal *PortalInternals) ApplyRelationMeta(content *event.MessageEventContent, replyTo, threadRoot, prevThreadEvent *database.Message) { - (*Portal)(portal).applyRelationMeta(content, replyTo, threadRoot, prevThreadEvent) +func (portal *PortalInternals) ApplyRelationMeta(ctx context.Context, content *event.MessageEventContent, replyTo, threadRoot, prevThreadEvent *database.Message) { + (*Portal)(portal).applyRelationMeta(ctx, content, replyTo, threadRoot, prevThreadEvent) } func (portal *PortalInternals) SendConvertedMessage(ctx context.Context, id networkid.MessageID, intent MatrixAPI, senderID networkid.UserID, converted *ConvertedMessage, ts time.Time, streamOrder int64, logContext func(*zerolog.Event) *zerolog.Event) []*database.Message { @@ -241,6 +253,10 @@ func (portal *PortalInternals) UpdateAvatar(ctx context.Context, avatar *Avatar, return (*Portal)(portal).updateAvatar(ctx, avatar, sender, ts) } +func (portal *PortalInternals) GetBridgeInfoStateKey() string { + return (*Portal)(portal).getBridgeInfoStateKey() +} + func (portal *PortalInternals) GetBridgeInfo() (string, event.BridgeEventContent) { return (*Portal)(portal).getBridgeInfo() }