bridgev2: roll back failed room metadata changes

This commit is contained in:
Tulir Asokan 2025-10-27 19:42:17 +02:00
commit 7fd1889444
8 changed files with 63 additions and 14 deletions

View file

@ -77,6 +77,7 @@ type BridgeConfig struct {
DeduplicateMatrixMessages bool `yaml:"deduplicate_matrix_messages"`
CrossRoomReplies bool `yaml:"cross_room_replies"`
OutgoingMessageReID bool `yaml:"outgoing_message_re_id"`
RevertFailedStateChanges bool `yaml:"revert_failed_state_changes"`
CleanupOnLogout CleanupOnLogouts `yaml:"cleanup_on_logout"`
Relay RelayConfig `yaml:"relay"`
Permissions PermissionConfig `yaml:"permissions"`

View file

@ -40,6 +40,7 @@ func doUpgrade(helper up.Helper) {
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", "revert_failed_state_changes")
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")

View file

@ -54,6 +54,7 @@ var (
ErrReactionsNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support reactions")).WithIsCertain(true).WithErrorAsMessage().WithErrorReason(event.MessageStatusUnsupported)
ErrPollsNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support polls")).WithIsCertain(true).WithErrorAsMessage().WithErrorReason(event.MessageStatusUnsupported)
ErrRoomMetadataNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support changing room metadata")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false).WithErrorReason(event.MessageStatusUnsupported)
ErrRoomMetadataNotAllowed error = WrapErrorInStatus(errors.New("changes are not allowed here")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false).WithErrorReason(event.MessageStatusUnsupported)
ErrRedactionsNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support deleting messages")).WithIsCertain(true).WithErrorAsMessage().WithErrorReason(event.MessageStatusUnsupported)
ErrUnexpectedParsedContentType error = WrapErrorInStatus(errors.New("unexpected parsed content type")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true)
ErrInvalidStateKey error = WrapErrorInStatus(errors.New("room metadata state key is unset or non-empty")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(false)

View file

@ -361,6 +361,7 @@ func (br *Connector) ensureConnection(ctx context.Context) {
*br.AS.SpecVersions = *versions
br.Capabilities.AutoJoinInvites = br.SpecVersions.Supports(mautrix.BeeperFeatureAutojoinInvites)
br.Capabilities.BatchSending = br.SpecVersions.Supports(mautrix.BeeperFeatureBatchSending)
br.Capabilities.ArbitraryMemberChange = br.SpecVersions.Supports(mautrix.BeeperFeatureArbitraryMemberChange)
break
}
}

View file

@ -47,6 +47,8 @@ bridge:
# 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
# If a state event fails to bridge, should the bridge revert any state changes made by that event?
revert_failed_state_changes: false
# What should be done to portal rooms when a user logs out or is logged out?
# Permitted values:

View file

@ -23,8 +23,9 @@ import (
)
type MatrixCapabilities struct {
AutoJoinInvites bool
BatchSending bool
AutoJoinInvites bool
BatchSending bool
ArbitraryMemberChange bool
}
type MatrixConnector interface {

View file

@ -645,11 +645,13 @@ func (portal *Portal) handleMatrixEvent(ctx context.Context, sender *User, evt *
if err != nil {
log.Err(err).Msg("Failed to get user login to handle Matrix event")
if errors.Is(err, ErrNotLoggedIn) {
portal.revertRoomMeta(ctx, evt, nil)
shouldSendNotice := evt.Content.AsMessage().MsgType != event.MsgNotice
return EventHandlingResultFailed.WithMSSError(
WrapErrorInStatus(err).WithMessage("You're not logged in").WithIsCertain(true).WithSendNotice(shouldSendNotice),
)
} else {
portal.revertRoomMeta(ctx, evt, nil)
return EventHandlingResultFailed.WithMSSError(
WrapErrorInStatus(err).WithMessage("Failed to get login to handle event").WithIsCertain(true).WithSendNotice(true),
)
@ -1548,14 +1550,20 @@ func handleMatrixRoomMeta[APIType any, ContentType any](
if evt.StateKey == nil || *evt.StateKey != "" {
return EventHandlingResultFailed.WithMSSError(ErrInvalidStateKey)
}
//caps := sender.Client.GetCapabilities(ctx, portal)
//if stateCap, ok := caps.State[evt.Type.Type]; !ok || stateCap.Level <= event.CapLevelUnsupported {
// return EventHandlingResultIgnored.WithMSSError(fmt.Errorf("%s %w", evt.Type.Type, ErrRoomMetadataNotAllowed))
//}
api, ok := sender.Client.(APIType)
if !ok {
return EventHandlingResultIgnored.WithMSSError(ErrRoomMetadataNotSupported)
portal.revertRoomMeta(ctx, evt, sender)
return EventHandlingResultIgnored.WithMSSError(fmt.Errorf("%w of type %s", ErrRoomMetadataNotSupported, evt.Type))
}
log := zerolog.Ctx(ctx)
content, ok := evt.Content.Parsed.(ContentType)
if !ok {
log.Error().Type("content_type", evt.Content.Parsed).Msg("Unexpected parsed content type")
portal.revertRoomMeta(ctx, evt, sender)
return EventHandlingResultFailed.WithMSSError(fmt.Errorf("%w: %T", ErrUnexpectedParsedContentType, evt.Content.Parsed))
}
switch typedContent := evt.Content.Parsed.(type) {
@ -1584,7 +1592,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(), false)
portal.revertRoomMeta(ctx, evt, sender)
return EventHandlingResultFailed.WithMSSError(ErrDisappearingTimerUnsupported)
}
}
@ -1607,9 +1615,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(), false)
}
portal.revertRoomMeta(ctx, evt, sender)
return EventHandlingResultFailed.WithMSSError(err)
}
if changed {
@ -1741,6 +1747,7 @@ func (portal *Portal) handleMatrixMembership(
content, ok := evt.Content.Parsed.(*event.MemberEventContent)
if !ok {
log.Error().Type("content_type", evt.Content.Parsed).Msg("Unexpected parsed content type")
portal.revertRoomMeta(ctx, evt, sender)
return EventHandlingResultFailed.WithMSSError(fmt.Errorf("%w: %T", ErrUnexpectedParsedContentType, evt.Content.Parsed))
}
prevContent := &event.MemberEventContent{Membership: event.MembershipLeave}
@ -1756,6 +1763,7 @@ func (portal *Portal) handleMatrixMembership(
})
api, ok := sender.Client.(MembershipHandlingNetworkAPI)
if !ok {
portal.revertRoomMeta(ctx, evt, sender)
return EventHandlingResultIgnored.WithMSSError(ErrMembershipNotSupported)
}
targetMXID := id.UserID(*evt.StateKey)
@ -1763,6 +1771,7 @@ func (portal *Portal) handleMatrixMembership(
target, err := portal.getTargetUser(ctx, targetMXID)
if err != nil {
log.Err(err).Msg("Failed to get member event target")
portal.revertRoomMeta(ctx, evt, sender)
return EventHandlingResultFailed.WithMSSError(err)
}
@ -1793,6 +1802,7 @@ func (portal *Portal) handleMatrixMembership(
_, err = api.HandleMatrixMembership(ctx, membershipChange)
if err != nil {
log.Err(err).Msg("Failed to handle Matrix membership change")
portal.revertRoomMeta(ctx, evt, sender)
return EventHandlingResultFailed.WithMSSError(err)
}
return EventHandlingResultSuccess.WithMSS()
@ -4085,6 +4095,37 @@ func (portal *Portal) sendRoomMeta(
return true
}
func (portal *Portal) revertRoomMeta(ctx context.Context, evt *event.Event, source *UserLogin) {
if !portal.Bridge.Config.RevertFailedStateChanges {
return
}
if evt.GetStateKey() != "" && evt.Type != event.StateMember {
return
}
switch evt.Type {
case event.StateRoomName:
portal.sendRoomMeta(ctx, nil, time.Time{}, event.StateRoomName, "", &event.RoomNameEventContent{Name: portal.Name}, true)
case event.StateRoomAvatar:
portal.sendRoomMeta(ctx, nil, time.Time{}, event.StateRoomAvatar, "", &event.RoomAvatarEventContent{URL: portal.AvatarMXC}, true)
case event.StateTopic:
portal.sendRoomMeta(ctx, nil, time.Time{}, event.StateTopic, "", &event.TopicEventContent{Topic: portal.Topic}, true)
case event.StateBeeperDisappearingTimer:
portal.sendRoomMeta(ctx, nil, time.Time{}, event.StateBeeperDisappearingTimer, "", portal.Disappear.ToEventContent(), true)
case event.StateMember:
_ = evt.Unsigned.PrevContent.ParseRaw(evt.Type)
prevContent := evt.Unsigned.PrevContent.AsMember()
newContent := evt.Content.AsMember()
if prevContent.Membership == newContent.Membership {
return
}
if portal.Bridge.Matrix.GetCapabilities().ArbitraryMemberChange {
content := *evt.Unsigned.PrevContent
content.Raw["com.beeper.member_rollback"] = true
portal.sendRoomMeta(ctx, nil, time.Time{}, event.StateMember, evt.GetStateKey(), content, true)
}
}
}
func (portal *Portal) getInitialMemberList(ctx context.Context, members *ChatMemberList, source *UserLogin, pl *event.PowerLevelsEventContent) (invite, functional []id.UserID, err error) {
if members == nil {
invite = []id.UserID{source.UserMXID}

View file

@ -70,13 +70,14 @@ var (
FeatureUnstableProfileFields = UnstableFeature{UnstableFlag: "uk.tcpip.msc4133"}
FeatureArbitraryProfileFields = UnstableFeature{UnstableFlag: "uk.tcpip.msc4133.stable", SpecVersion: SpecV116}
BeeperFeatureHungry = UnstableFeature{UnstableFlag: "com.beeper.hungry"}
BeeperFeatureBatchSending = UnstableFeature{UnstableFlag: "com.beeper.batch_sending"}
BeeperFeatureRoomYeeting = UnstableFeature{UnstableFlag: "com.beeper.room_yeeting"}
BeeperFeatureAutojoinInvites = UnstableFeature{UnstableFlag: "com.beeper.room_create_autojoin_invites"}
BeeperFeatureArbitraryProfileMeta = UnstableFeature{UnstableFlag: "com.beeper.arbitrary_profile_meta"}
BeeperFeatureAccountDataMute = UnstableFeature{UnstableFlag: "com.beeper.account_data_mute"}
BeeperFeatureInboxState = UnstableFeature{UnstableFlag: "com.beeper.inbox_state"}
BeeperFeatureHungry = UnstableFeature{UnstableFlag: "com.beeper.hungry"}
BeeperFeatureBatchSending = UnstableFeature{UnstableFlag: "com.beeper.batch_sending"}
BeeperFeatureRoomYeeting = UnstableFeature{UnstableFlag: "com.beeper.room_yeeting"}
BeeperFeatureAutojoinInvites = UnstableFeature{UnstableFlag: "com.beeper.room_create_autojoin_invites"}
BeeperFeatureArbitraryProfileMeta = UnstableFeature{UnstableFlag: "com.beeper.arbitrary_profile_meta"}
BeeperFeatureAccountDataMute = UnstableFeature{UnstableFlag: "com.beeper.account_data_mute"}
BeeperFeatureInboxState = UnstableFeature{UnstableFlag: "com.beeper.inbox_state"}
BeeperFeatureArbitraryMemberChange = UnstableFeature{UnstableFlag: "com.beeper.arbitrary_member_change"}
)
func (versions *RespVersions) Supports(feature UnstableFeature) bool {