mirror of
https://mau.dev/mautrix/go.git
synced 2026-03-14 14:25:53 +01:00
bridgev2: roll back failed room metadata changes (#425)
This commit is contained in:
parent
be9bbf8d09
commit
2ece053b2b
10 changed files with 86 additions and 27 deletions
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,8 +90,8 @@ func (as *ASIntent) SendMessage(ctx context.Context, roomID id.RoomID, eventType
|
|||
}
|
||||
|
||||
func (as *ASIntent) fillMemberEvent(ctx context.Context, roomID id.RoomID, userID id.UserID, content *event.Content) {
|
||||
targetContent := content.Parsed.(*event.MemberEventContent)
|
||||
if targetContent.Displayname != "" || targetContent.AvatarURL != "" {
|
||||
targetContent, ok := content.Parsed.(*event.MemberEventContent)
|
||||
if !ok || targetContent.Displayname != "" || targetContent.AvatarURL != "" {
|
||||
return
|
||||
}
|
||||
memberContent, err := as.Matrix.StateStore.TryGetMember(ctx, roomID, userID)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@ import (
|
|||
)
|
||||
|
||||
type MatrixCapabilities struct {
|
||||
AutoJoinInvites bool
|
||||
BatchSending bool
|
||||
AutoJoinInvites bool
|
||||
BatchSending bool
|
||||
ArbitraryMemberChange bool
|
||||
}
|
||||
|
||||
type MatrixConnector interface {
|
||||
|
|
|
|||
|
|
@ -520,6 +520,9 @@ func (portal *Portal) handleSingleEvent(ctx context.Context, rawEvt any, doneCal
|
|||
portal.sendSuccessStatus(ctx, evt.evt, 0, "")
|
||||
}
|
||||
}
|
||||
if res.Error != nil && evt.evt.StateKey != nil {
|
||||
portal.revertRoomMeta(ctx, evt.evt)
|
||||
}
|
||||
case *portalRemoteEvent:
|
||||
res = portal.handleRemoteEvent(ctx, evt.source, evt.evtType, evt.evt)
|
||||
case *portalCreateEvent:
|
||||
|
|
@ -1562,9 +1565,13 @@ 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)
|
||||
return EventHandlingResultIgnored.WithMSSError(fmt.Errorf("%w of type %s", ErrRoomMetadataNotSupported, evt.Type))
|
||||
}
|
||||
log := zerolog.Ctx(ctx)
|
||||
content, ok := evt.Content.Parsed.(ContentType)
|
||||
|
|
@ -1598,7 +1605,6 @@ 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)
|
||||
return EventHandlingResultFailed.WithMSSError(ErrDisappearingTimerUnsupported)
|
||||
}
|
||||
}
|
||||
|
|
@ -1621,9 +1627,6 @@ 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)
|
||||
}
|
||||
return EventHandlingResultFailed.WithMSSError(err)
|
||||
}
|
||||
if changed {
|
||||
|
|
@ -3891,7 +3894,7 @@ func (portal *Portal) updateName(
|
|||
}
|
||||
portal.Name = name
|
||||
portal.NameSet = portal.sendRoomMeta(
|
||||
ctx, sender, ts, event.StateRoomName, "", &event.RoomNameEventContent{Name: name}, excludeFromTimeline,
|
||||
ctx, sender, ts, event.StateRoomName, "", &event.RoomNameEventContent{Name: name}, excludeFromTimeline, nil,
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
|
@ -3904,7 +3907,7 @@ func (portal *Portal) updateTopic(
|
|||
}
|
||||
portal.Topic = topic
|
||||
portal.TopicSet = portal.sendRoomMeta(
|
||||
ctx, sender, ts, event.StateTopic, "", &event.TopicEventContent{Topic: topic}, excludeFromTimeline,
|
||||
ctx, sender, ts, event.StateTopic, "", &event.TopicEventContent{Topic: topic}, excludeFromTimeline, nil,
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
|
@ -3935,7 +3938,7 @@ func (portal *Portal) updateAvatar(
|
|||
portal.AvatarHash = newHash
|
||||
}
|
||||
portal.AvatarSet = portal.sendRoomMeta(
|
||||
ctx, sender, ts, event.StateRoomAvatar, "", &event.RoomAvatarEventContent{URL: portal.AvatarMXC}, excludeFromTimeline,
|
||||
ctx, sender, ts, event.StateRoomAvatar, "", &event.RoomAvatarEventContent{URL: portal.AvatarMXC}, excludeFromTimeline, nil,
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
|
@ -4003,8 +4006,8 @@ func (portal *Portal) UpdateBridgeInfo(ctx context.Context) {
|
|||
return
|
||||
}
|
||||
stateKey, bridgeInfo := portal.getBridgeInfo()
|
||||
portal.sendRoomMeta(ctx, nil, time.Now(), event.StateBridge, stateKey, &bridgeInfo, false)
|
||||
portal.sendRoomMeta(ctx, nil, time.Now(), event.StateHalfShotBridge, stateKey, &bridgeInfo, false)
|
||||
portal.sendRoomMeta(ctx, nil, time.Now(), event.StateBridge, stateKey, &bridgeInfo, false, nil)
|
||||
portal.sendRoomMeta(ctx, nil, time.Now(), event.StateHalfShotBridge, stateKey, &bridgeInfo, false, nil)
|
||||
}
|
||||
|
||||
func (portal *Portal) UpdateCapabilities(ctx context.Context, source *UserLogin, implicit bool) bool {
|
||||
|
|
@ -4026,7 +4029,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, false)
|
||||
success := portal.sendRoomMeta(ctx, nil, time.Now(), event.StateBeeperRoomFeatures, portal.getBridgeInfoStateKey(), caps, false, nil)
|
||||
if !success {
|
||||
return false
|
||||
}
|
||||
|
|
@ -4037,7 +4040,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(), true)
|
||||
success = portal.sendRoomMeta(ctx, nil, time.Now(), event.StateBeeperDisappearingTimer, "", portal.Disappear.ToEventContent(), true, nil)
|
||||
if !success {
|
||||
return false
|
||||
}
|
||||
|
|
@ -4076,11 +4079,14 @@ func (portal *Portal) sendRoomMeta(
|
|||
stateKey string,
|
||||
content any,
|
||||
excludeFromTimeline bool,
|
||||
extra map[string]any,
|
||||
) bool {
|
||||
if portal.MXID == "" {
|
||||
return false
|
||||
}
|
||||
extra := make(map[string]any)
|
||||
if extra == nil {
|
||||
extra = make(map[string]any)
|
||||
}
|
||||
if excludeFromTimeline {
|
||||
extra["com.beeper.exclude_from_timeline"] = true
|
||||
}
|
||||
|
|
@ -4106,6 +4112,46 @@ func (portal *Portal) sendRoomMeta(
|
|||
return true
|
||||
}
|
||||
|
||||
func (portal *Portal) revertRoomMeta(ctx context.Context, evt *event.Event) {
|
||||
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, nil)
|
||||
case event.StateRoomAvatar:
|
||||
portal.sendRoomMeta(ctx, nil, time.Time{}, event.StateRoomAvatar, "", &event.RoomAvatarEventContent{URL: portal.AvatarMXC}, true, nil)
|
||||
case event.StateTopic:
|
||||
portal.sendRoomMeta(ctx, nil, time.Time{}, event.StateTopic, "", &event.TopicEventContent{Topic: portal.Topic}, true, nil)
|
||||
case event.StateBeeperDisappearingTimer:
|
||||
portal.sendRoomMeta(ctx, nil, time.Time{}, event.StateBeeperDisappearingTimer, "", portal.Disappear.ToEventContent(), true, nil)
|
||||
case event.StateMember:
|
||||
var prevContent *event.MemberEventContent
|
||||
var extra map[string]any
|
||||
if evt.Unsigned.PrevContent != nil {
|
||||
_ = evt.Unsigned.PrevContent.ParseRaw(evt.Type)
|
||||
prevContent = evt.Unsigned.PrevContent.AsMember()
|
||||
newContent := evt.Content.AsMember()
|
||||
if prevContent.Membership == newContent.Membership {
|
||||
return
|
||||
}
|
||||
extra = evt.Unsigned.PrevContent.Raw
|
||||
} else {
|
||||
prevContent = &event.MemberEventContent{Membership: event.MembershipLeave}
|
||||
}
|
||||
if portal.Bridge.Matrix.GetCapabilities().ArbitraryMemberChange {
|
||||
if extra == nil {
|
||||
extra = make(map[string]any)
|
||||
}
|
||||
extra["com.beeper.member_rollback"] = true
|
||||
portal.sendRoomMeta(ctx, nil, time.Time{}, event.StateMember, evt.GetStateKey(), prevContent, true, extra)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
|
|
@ -4490,6 +4536,7 @@ func (portal *Portal) UpdateDisappearingSetting(
|
|||
"",
|
||||
setting.ToEventContent(),
|
||||
opts.ExcludeFromTimeline,
|
||||
nil,
|
||||
)
|
||||
|
||||
if !opts.SendNotice {
|
||||
|
|
@ -4618,7 +4665,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, info.ExcludeChangesFromTimeline)
|
||||
portal.sendRoomMeta(ctx, sender, ts, event.StateJoinRules, "", info.JoinRule, info.ExcludeChangesFromTimeline, nil)
|
||||
}
|
||||
if info.Type != nil && portal.RoomType != *info.Type {
|
||||
if portal.MXID != "" && (*info.Type == database.RoomTypeSpace || portal.RoomType == database.RoomTypeSpace) {
|
||||
|
|
|
|||
|
|
@ -289,8 +289,12 @@ 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, excludeFromTimeline bool) bool {
|
||||
return (*Portal)(portal).sendRoomMeta(ctx, sender, ts, eventType, stateKey, content, excludeFromTimeline)
|
||||
func (portal *PortalInternals) SendRoomMeta(ctx context.Context, sender MatrixAPI, ts time.Time, eventType event.Type, stateKey string, content any, excludeFromTimeline bool, extra map[string]any) bool {
|
||||
return (*Portal)(portal).sendRoomMeta(ctx, sender, ts, eventType, stateKey, content, excludeFromTimeline, extra)
|
||||
}
|
||||
|
||||
func (portal *PortalInternals) RevertRoomMeta(ctx context.Context, evt *event.Event) {
|
||||
(*Portal)(portal).revertRoomMeta(ctx, evt)
|
||||
}
|
||||
|
||||
func (portal *PortalInternals) GetInitialMemberList(ctx context.Context, members *ChatMemberList, source *UserLogin, pl *event.PowerLevelsEventContent) (invite, functional []id.UserID, err error) {
|
||||
|
|
|
|||
15
versions.go
15
versions.go
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue