From dd778ae0cdaf0c147dc106403de64d25780a0b60 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 1 Oct 2025 14:55:35 +0300 Subject: [PATCH] bridgev2/portal: add option to exclude metadata changes from timeline --- bridgev2/portal.go | 101 +++++++++++++++++++++++++------------ bridgev2/portalinternal.go | 24 ++++++--- 2 files changed, 86 insertions(+), 39 deletions(-) diff --git a/bridgev2/portal.go b/bridgev2/portal.go index 817b3144..51d6a294 100644 --- a/bridgev2/portal.go +++ b/bridgev2/portal.go @@ -1537,7 +1537,7 @@ func handleMatrixRoomMeta[APIType any, ContentType any]( return EventHandlingResultIgnored } if !sender.Client.GetCapabilities(ctx, portal).DisappearingTimer.Supports(typedContent) { - portal.sendRoomMeta(ctx, nil, time.Now(), event.StateBeeperDisappearingTimer, "", portal.Disappear.ToEventContent()) + portal.sendRoomMeta(ctx, nil, time.Now(), event.StateBeeperDisappearingTimer, "", portal.Disappear.ToEventContent(), false) return EventHandlingResultFailed.WithMSSError(ErrDisappearingTimerUnsupported) } } @@ -1561,7 +1561,7 @@ func handleMatrixRoomMeta[APIType any, ContentType any]( if err != nil { log.Err(err).Msg("Failed to handle Matrix room metadata") if evt.Type == event.StateBeeperDisappearingTimer { - portal.sendRoomMeta(ctx, nil, time.Now(), event.StateBeeperDisappearingTimer, "", portal.Disappear.ToEventContent()) + portal.sendRoomMeta(ctx, nil, time.Now(), event.StateBeeperDisappearingTimer, "", portal.Disappear.ToEventContent(), false) } return EventHandlingResultFailed.WithMSSError(err) } @@ -3712,6 +3712,8 @@ type ChatInfo struct { CanBackfill bool + ExcludeChangesFromTimeline bool + ExtraUpdates ExtraUpdater[*Portal] } @@ -3744,25 +3746,35 @@ type UserLocalPortalInfo struct { Tag *event.RoomTag } -func (portal *Portal) updateName(ctx context.Context, name string, sender MatrixAPI, ts time.Time) bool { +func (portal *Portal) updateName( + ctx context.Context, name string, sender MatrixAPI, ts time.Time, excludeFromTimeline bool, +) bool { if portal.Name == name && (portal.NameSet || portal.MXID == "") { return false } portal.Name = name - portal.NameSet = portal.sendRoomMeta(ctx, sender, ts, event.StateRoomName, "", &event.RoomNameEventContent{Name: name}) + portal.NameSet = portal.sendRoomMeta( + ctx, sender, ts, event.StateRoomName, "", &event.RoomNameEventContent{Name: name}, excludeFromTimeline, + ) return true } -func (portal *Portal) updateTopic(ctx context.Context, topic string, sender MatrixAPI, ts time.Time) bool { +func (portal *Portal) updateTopic( + ctx context.Context, topic string, sender MatrixAPI, ts time.Time, excludeFromTimeline bool, +) bool { if portal.Topic == topic && (portal.TopicSet || portal.MXID == "") { return false } portal.Topic = topic - portal.TopicSet = portal.sendRoomMeta(ctx, sender, ts, event.StateTopic, "", &event.TopicEventContent{Topic: topic}) + portal.TopicSet = portal.sendRoomMeta( + ctx, sender, ts, event.StateTopic, "", &event.TopicEventContent{Topic: topic}, excludeFromTimeline, + ) return true } -func (portal *Portal) updateAvatar(ctx context.Context, avatar *Avatar, sender MatrixAPI, ts time.Time) bool { +func (portal *Portal) updateAvatar( + ctx context.Context, avatar *Avatar, sender MatrixAPI, ts time.Time, excludeFromTimeline bool, +) bool { if portal.AvatarID == avatar.ID && (avatar.Remove || portal.AvatarMXC != "") && (portal.AvatarSet || portal.MXID == "") { return false } @@ -3785,7 +3797,9 @@ func (portal *Portal) updateAvatar(ctx context.Context, avatar *Avatar, sender M portal.AvatarMXC = newMXC portal.AvatarHash = newHash } - portal.AvatarSet = portal.sendRoomMeta(ctx, sender, ts, event.StateRoomAvatar, "", &event.RoomAvatarEventContent{URL: portal.AvatarMXC}) + portal.AvatarSet = portal.sendRoomMeta( + ctx, sender, ts, event.StateRoomAvatar, "", &event.RoomAvatarEventContent{URL: portal.AvatarMXC}, excludeFromTimeline, + ) return true } @@ -3851,8 +3865,8 @@ func (portal *Portal) UpdateBridgeInfo(ctx context.Context) { return } stateKey, bridgeInfo := portal.getBridgeInfo() - portal.sendRoomMeta(ctx, nil, time.Now(), event.StateBridge, stateKey, &bridgeInfo) - portal.sendRoomMeta(ctx, nil, time.Now(), event.StateHalfShotBridge, stateKey, &bridgeInfo) + portal.sendRoomMeta(ctx, nil, time.Now(), event.StateBridge, stateKey, &bridgeInfo, false) + portal.sendRoomMeta(ctx, nil, time.Now(), event.StateHalfShotBridge, stateKey, &bridgeInfo, false) } func (portal *Portal) UpdateCapabilities(ctx context.Context, source *UserLogin, implicit bool) bool { @@ -3874,7 +3888,7 @@ func (portal *Portal) UpdateCapabilities(ctx context.Context, source *UserLogin, Str("old_id", portal.CapState.ID). Str("new_id", capID). Msg("Sending new room capability event") - success := portal.sendRoomMeta(ctx, nil, time.Now(), event.StateBeeperRoomFeatures, portal.getBridgeInfoStateKey(), caps) + success := portal.sendRoomMeta(ctx, nil, time.Now(), event.StateBeeperRoomFeatures, portal.getBridgeInfoStateKey(), caps, false) if !success { return false } @@ -3885,7 +3899,7 @@ func (portal *Portal) UpdateCapabilities(ctx context.Context, source *UserLogin, } if caps.DisappearingTimer != nil && !portal.CapState.Flags.Has(database.CapStateFlagDisappearingTimerSet) { zerolog.Ctx(ctx).Debug().Msg("Disappearing timer capability was added, sending disappearing timer state event") - success = portal.sendRoomMeta(ctx, nil, time.Now(), event.StateBeeperDisappearingTimer, "", portal.Disappear.ToEventContent()) + success = portal.sendRoomMeta(ctx, nil, time.Now(), event.StateBeeperDisappearingTimer, "", portal.Disappear.ToEventContent(), true) if !success { return false } @@ -3916,15 +3930,24 @@ func (portal *Portal) sendStateWithIntentOrBot(ctx context.Context, sender Matri return } -func (portal *Portal) sendRoomMeta(ctx context.Context, sender MatrixAPI, ts time.Time, eventType event.Type, stateKey string, content any) bool { +func (portal *Portal) sendRoomMeta( + ctx context.Context, + sender MatrixAPI, + ts time.Time, + eventType event.Type, + stateKey string, + content any, + excludeFromTimeline bool, +) bool { if portal.MXID == "" { return false } - var extra map[string]any + extra := make(map[string]any) + if excludeFromTimeline { + extra["com.beeper.exclude_from_timeline"] = true + } if !portal.NameIsCustom && (eventType == event.StateRoomName || eventType == event.StateRoomAvatar) { - extra = map[string]any{ - "fi.mau.implicit_name": true, - } + extra["fi.mau.implicit_name"] = true } _, err := portal.sendStateWithIntentOrBot(ctx, sender, eventType, stateKey, &event.Content{ Parsed: content, @@ -4281,9 +4304,15 @@ type UpdateDisappearingSettingOpts struct { Implicit bool Save bool SendNotice bool + + ExcludeFromTimeline bool } -func (portal *Portal) UpdateDisappearingSetting(ctx context.Context, setting database.DisappearingSetting, opts UpdateDisappearingSettingOpts) bool { +func (portal *Portal) UpdateDisappearingSetting( + ctx context.Context, + setting database.DisappearingSetting, + opts UpdateDisappearingSettingOpts, +) bool { setting = setting.Normalize() if portal.Disappear.Timer == setting.Timer && portal.Disappear.Type == setting.Type { return false @@ -4306,7 +4335,15 @@ func (portal *Portal) UpdateDisappearingSetting(ctx context.Context, setting dat if opts.Timestamp.IsZero() { opts.Timestamp = time.Now() } - portal.sendRoomMeta(ctx, opts.Sender, opts.Timestamp, event.StateBeeperDisappearingTimer, "", setting.ToEventContent()) + portal.sendRoomMeta( + ctx, + opts.Sender, + opts.Timestamp, + event.StateBeeperDisappearingTimer, + "", + setting.ToEventContent(), + opts.ExcludeFromTimeline, + ) if !opts.SendNotice { return true @@ -4390,13 +4427,13 @@ func (portal *Portal) UpdateInfoFromGhost(ctx context.Context, ghost *Ghost) (ch return } } - changed = portal.updateName(ctx, ghost.Name, nil, time.Time{}) || changed + changed = portal.updateName(ctx, ghost.Name, nil, time.Time{}, false) || changed changed = portal.updateAvatar(ctx, &Avatar{ ID: ghost.AvatarID, MXC: ghost.AvatarMXC, Hash: ghost.AvatarHash, Remove: ghost.AvatarID == "", - }, nil, time.Time{}) || changed + }, nil, time.Time{}, false) || changed return } @@ -4405,26 +4442,28 @@ func (portal *Portal) UpdateInfo(ctx context.Context, info *ChatInfo, source *Us if info.Name == DefaultChatName { if portal.NameIsCustom { portal.NameIsCustom = false - changed = portal.updateName(ctx, "", sender, ts) || changed + changed = portal.updateName(ctx, "", sender, ts, info.ExcludeChangesFromTimeline) || changed } } else if info.Name != nil { portal.NameIsCustom = true - changed = portal.updateName(ctx, *info.Name, sender, ts) || changed + changed = portal.updateName(ctx, *info.Name, sender, ts, info.ExcludeChangesFromTimeline) || changed } if info.Topic != nil { - changed = portal.updateTopic(ctx, *info.Topic, sender, ts) || changed + changed = portal.updateTopic(ctx, *info.Topic, sender, ts, info.ExcludeChangesFromTimeline) || changed } if info.Avatar != nil { portal.NameIsCustom = true - changed = portal.updateAvatar(ctx, info.Avatar, sender, ts) || changed + changed = portal.updateAvatar(ctx, info.Avatar, sender, ts, info.ExcludeChangesFromTimeline) || changed } if info.Disappear != nil { changed = portal.UpdateDisappearingSetting(ctx, *info.Disappear, UpdateDisappearingSettingOpts{ - Sender: sender, - Timestamp: ts, - Implicit: false, - Save: false, - SendNotice: true, + Sender: sender, + Timestamp: ts, + Implicit: false, + Save: false, + + SendNotice: !info.ExcludeChangesFromTimeline, + ExcludeFromTimeline: info.ExcludeChangesFromTimeline, }) || changed } if info.ParentID != nil { @@ -4432,7 +4471,7 @@ func (portal *Portal) UpdateInfo(ctx context.Context, info *ChatInfo, source *Us } if info.JoinRule != nil { // TODO change detection instead of spamming this every time? - portal.sendRoomMeta(ctx, sender, ts, event.StateJoinRules, "", info.JoinRule) + portal.sendRoomMeta(ctx, sender, ts, event.StateJoinRules, "", info.JoinRule, info.ExcludeChangesFromTimeline) } if info.Type != nil && portal.RoomType != *info.Type { if portal.MXID != "" && (*info.Type == database.RoomTypeSpace || portal.RoomType == database.RoomTypeSpace) { diff --git a/bridgev2/portalinternal.go b/bridgev2/portalinternal.go index ddbadc76..d9373eb6 100644 --- a/bridgev2/portalinternal.go +++ b/bridgev2/portalinternal.go @@ -121,6 +121,10 @@ func (portal *PortalInternals) GetTargetUser(ctx context.Context, userID id.User return (*Portal)(portal).getTargetUser(ctx, userID) } +func (portal *PortalInternals) HandleMatrixDeleteChat(ctx context.Context, sender *UserLogin, origSender *OrigSender, evt *event.Event) EventHandlingResult { + return (*Portal)(portal).handleMatrixDeleteChat(ctx, sender, origSender, evt) +} + func (portal *PortalInternals) HandleMatrixMembership(ctx context.Context, sender *UserLogin, origSender *OrigSender, evt *event.Event) EventHandlingResult { return (*Portal)(portal).handleMatrixMembership(ctx, sender, origSender, evt) } @@ -249,6 +253,10 @@ func (portal *PortalInternals) HandleRemoteChatResync(ctx context.Context, sourc return (*Portal)(portal).handleRemoteChatResync(ctx, source, evt) } +func (portal *PortalInternals) FindOtherLogins(ctx context.Context, source *UserLogin) (ownUP *database.UserPortal, others []*database.UserPortal, err error) { + return (*Portal)(portal).findOtherLogins(ctx, source) +} + func (portal *PortalInternals) HandleRemoteChatDelete(ctx context.Context, source *UserLogin, evt RemoteChatDelete) EventHandlingResult { return (*Portal)(portal).handleRemoteChatDelete(ctx, source, evt) } @@ -257,16 +265,16 @@ func (portal *PortalInternals) HandleRemoteBackfill(ctx context.Context, source return (*Portal)(portal).handleRemoteBackfill(ctx, source, backfill) } -func (portal *PortalInternals) UpdateName(ctx context.Context, name string, sender MatrixAPI, ts time.Time) bool { - return (*Portal)(portal).updateName(ctx, name, sender, ts) +func (portal *PortalInternals) UpdateName(ctx context.Context, name string, sender MatrixAPI, ts time.Time, excludeFromTimeline bool) bool { + return (*Portal)(portal).updateName(ctx, name, sender, ts, excludeFromTimeline) } -func (portal *PortalInternals) UpdateTopic(ctx context.Context, topic string, sender MatrixAPI, ts time.Time) bool { - return (*Portal)(portal).updateTopic(ctx, topic, sender, ts) +func (portal *PortalInternals) UpdateTopic(ctx context.Context, topic string, sender MatrixAPI, ts time.Time, excludeFromTimeline bool) bool { + return (*Portal)(portal).updateTopic(ctx, topic, sender, ts, excludeFromTimeline) } -func (portal *PortalInternals) UpdateAvatar(ctx context.Context, avatar *Avatar, sender MatrixAPI, ts time.Time) bool { - return (*Portal)(portal).updateAvatar(ctx, avatar, sender, ts) +func (portal *PortalInternals) UpdateAvatar(ctx context.Context, avatar *Avatar, sender MatrixAPI, ts time.Time, excludeFromTimeline bool) bool { + return (*Portal)(portal).updateAvatar(ctx, avatar, sender, ts, excludeFromTimeline) } func (portal *PortalInternals) GetBridgeInfoStateKey() string { @@ -281,8 +289,8 @@ func (portal *PortalInternals) SendStateWithIntentOrBot(ctx context.Context, sen return (*Portal)(portal).sendStateWithIntentOrBot(ctx, sender, eventType, stateKey, content, ts) } -func (portal *PortalInternals) SendRoomMeta(ctx context.Context, sender MatrixAPI, ts time.Time, eventType event.Type, stateKey string, content any) bool { - return (*Portal)(portal).sendRoomMeta(ctx, sender, ts, eventType, stateKey, content) +func (portal *PortalInternals) SendRoomMeta(ctx context.Context, sender MatrixAPI, ts time.Time, eventType event.Type, stateKey string, content any, excludeFromTimeline bool) bool { + return (*Portal)(portal).sendRoomMeta(ctx, sender, ts, eventType, stateKey, content, excludeFromTimeline) } func (portal *PortalInternals) GetInitialMemberList(ctx context.Context, members *ChatMemberList, source *UserLogin, pl *event.PowerLevelsEventContent) (invite, functional []id.UserID, err error) {