bridgev2: add interface for deleting chats from Matrix

Closes #408
This commit is contained in:
Tulir Asokan 2025-09-22 16:05:42 +03:00
commit a9ff1443f7
4 changed files with 84 additions and 11 deletions

View file

@ -44,6 +44,7 @@ var (
ErrNoPortal error = WrapErrorInStatus(errors.New("room is not a portal")).WithIsCertain(true).WithSendNotice(false)
ErrIgnoringReactionFromRelayedUser error = WrapErrorInStatus(errors.New("ignoring reaction event from relayed user")).WithIsCertain(true).WithSendNotice(false)
ErrIgnoringPollFromRelayedUser error = WrapErrorInStatus(errors.New("ignoring poll event from relayed user")).WithIsCertain(true).WithSendNotice(false)
ErrIgnoringDeleteChatRelayedUser error = WrapErrorInStatus(errors.New("ignoring delete chat event from relayed user")).WithIsCertain(true).WithSendNotice(false)
ErrEditsNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support edits")).WithIsCertain(true).WithErrorAsMessage()
ErrEditsNotSupportedInPortal error = WrapErrorInStatus(errors.New("edits are not allowed in this chat")).WithIsCertain(true).WithErrorAsMessage()
ErrCaptionsNotAllowed error = WrapErrorInStatus(errors.New("captions are not supported here")).WithIsCertain(true).WithErrorAsMessage()
@ -65,6 +66,7 @@ var (
ErrMediaReuploadFailed error = WrapErrorInStatus(errors.New("failed to reupload media")).WithMessage("failed to reupload media").WithIsCertain(true).WithSendNotice(true)
ErrMediaConvertFailed error = WrapErrorInStatus(errors.New("failed to convert media")).WithMessage("failed to convert media").WithIsCertain(true).WithSendNotice(true)
ErrMembershipNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support changing group membership")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false)
ErrDeleteChatNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support deleting chats")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false)
ErrPowerLevelsNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support changing group power levels")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false)
ErrRemoteEchoTimeout = WrapErrorInStatus(errors.New("remote echo timed out")).WithIsCertain(false).WithSendNotice(true).WithErrorReason(event.MessageStatusTooOld)
ErrRemoteAckTimeout = WrapErrorInStatus(errors.New("remote ack timed out")).WithIsCertain(false).WithSendNotice(true).WithErrorReason(event.MessageStatusTooOld)

View file

@ -148,6 +148,7 @@ func (br *Connector) Init(bridge *bridgev2.Bridge) {
br.EventProcessor.On(event.StateTopic, br.handleRoomEvent)
br.EventProcessor.On(event.StateTombstone, br.handleRoomEvent)
br.EventProcessor.On(event.StateBeeperDisappearingTimer, br.handleRoomEvent)
br.EventProcessor.On(event.BeeperDeleteChat, br.handleRoomEvent)
br.EventProcessor.On(event.EphemeralEventReceipt, br.handleEphemeralEvent)
br.EventProcessor.On(event.EphemeralEventTyping, br.handleEphemeralEvent)
br.Bot = br.AS.BotIntent()

View file

@ -697,6 +697,14 @@ type DisappearTimerChangingNetworkAPI interface {
HandleMatrixDisappearingTimer(ctx context.Context, msg *MatrixDisappearingTimer) (bool, error)
}
// DeleteChatHandlingNetworkAPI is an optional interface that network connectors
// can implement to delete a chat from the remote network.
type DeleteChatHandlingNetworkAPI interface {
NetworkAPI
// HandleMatrixDeleteChat is called when the user explicitly deletes a chat.
HandleMatrixDeleteChat(ctx context.Context, msg *MatrixDeleteChat) error
}
type ResolveIdentifierResponse struct {
// Ghost is the ghost of the user that the identifier resolves to.
// This field should be set whenever possible. However, it is not required,
@ -1380,6 +1388,7 @@ type MatrixViewingChat struct {
Portal *Portal
}
type MatrixDeleteChat = MatrixEventBase[*event.BeeperChatDeleteEventContent]
type MatrixMarkedUnread = MatrixRoomMeta[*event.MarkedUnreadEventContent]
type MatrixMute = MatrixRoomMeta[*event.BeeperMuteEventContent]
type MatrixRoomTag = MatrixRoomMeta[*event.TagEventContent]

View file

@ -706,6 +706,8 @@ func (portal *Portal) handleMatrixEvent(ctx context.Context, sender *User, evt *
return portal.handleMatrixMembership(ctx, login, origSender, evt)
case event.StatePowerLevels:
return portal.handleMatrixPowerLevels(ctx, login, origSender, evt)
case event.BeeperDeleteChat:
return portal.handleMatrixDeleteChat(ctx, login, origSender, evt)
default:
return EventHandlingResultIgnored
}
@ -1622,6 +1624,58 @@ func (portal *Portal) getTargetUser(ctx context.Context, userID id.UserID) (Ghos
}
}
func (portal *Portal) handleMatrixDeleteChat(
ctx context.Context,
sender *UserLogin,
origSender *OrigSender,
evt *event.Event,
) EventHandlingResult {
if origSender != nil {
return EventHandlingResultFailed.WithMSSError(ErrIgnoringDeleteChatRelayedUser)
}
log := zerolog.Ctx(ctx)
content, ok := evt.Content.Parsed.(*event.BeeperChatDeleteEventContent)
if !ok {
log.Error().Type("content_type", evt.Content.Parsed).Msg("Unexpected parsed content type")
return EventHandlingResultFailed.WithMSSError(fmt.Errorf("%w: %T", ErrUnexpectedParsedContentType, evt.Content.Parsed))
}
api, ok := sender.Client.(DeleteChatHandlingNetworkAPI)
if !ok {
return EventHandlingResultIgnored.WithMSSError(ErrDeleteChatNotSupported)
}
err := api.HandleMatrixDeleteChat(ctx, &MatrixDeleteChat{
Event: evt,
Content: content,
Portal: portal,
})
if err != nil {
log.Err(err).Msg("Failed to handle Matrix chat delete")
return EventHandlingResultFailed.WithMSSError(err)
}
if portal.Receiver == "" {
_, others, err := portal.findOtherLogins(ctx, sender)
if err != nil {
log.Err(err).Msg("Failed to check if portal has other logins")
return EventHandlingResultFailed.WithError(err)
} else if len(others) > 0 {
log.Debug().Msg("Not deleting portal after chat delete as other logins are present")
return EventHandlingResultSuccess
}
}
err = portal.Delete(ctx)
if err != nil {
log.Err(err).Msg("Failed to delete portal from database")
return EventHandlingResultFailed.WithMSSError(err)
}
err = portal.Bridge.Bot.DeleteRoom(ctx, portal.MXID, false)
if err != nil {
log.Err(err).Msg("Failed to delete Matrix room")
return EventHandlingResultFailed.WithMSSError(err)
}
// No MSS here as the portal was deleted
return EventHandlingResultSuccess
}
func (portal *Portal) handleMatrixMembership(
ctx context.Context,
sender *UserLogin,
@ -3160,11 +3214,11 @@ func (portal *Portal) handleRemoteMessageRemove(ctx context.Context, source *Use
onlyForMeProvider, ok := evt.(RemoteDeleteOnlyForMe)
onlyForMe := ok && onlyForMeProvider.DeleteOnlyForMe()
if onlyForMe && portal.Receiver == "" {
logins, err := portal.Bridge.DB.UserPortal.GetAllInPortal(ctx, portal.PortalKey)
_, others, err := portal.findOtherLogins(ctx, source)
if err != nil {
log.Err(err).Msg("Failed to check if portal has other logins")
return EventHandlingResultFailed.WithError(err)
} else if len(logins) > 1 {
} else if len(others) > 0 {
log.Debug().Msg("Ignoring delete for me event in portal with multiple logins")
return EventHandlingResultIgnored
}
@ -3413,22 +3467,29 @@ func (portal *Portal) handleRemoteChatResync(ctx context.Context, source *UserLo
return EventHandlingResultSuccess
}
func (portal *Portal) findOtherLogins(ctx context.Context, source *UserLogin) (ownUP *database.UserPortal, others []*database.UserPortal, err error) {
others, err = portal.Bridge.DB.UserPortal.GetAllInPortal(ctx, portal.PortalKey)
if err != nil {
return
}
others = slices.DeleteFunc(others, func(up *database.UserPortal) bool {
if up.LoginID == source.ID {
ownUP = up
return true
}
return false
})
return
}
func (portal *Portal) handleRemoteChatDelete(ctx context.Context, source *UserLogin, evt RemoteChatDelete) EventHandlingResult {
log := zerolog.Ctx(ctx)
if portal.Receiver == "" && evt.DeleteOnlyForMe() {
logins, err := portal.Bridge.DB.UserPortal.GetAllInPortal(ctx, portal.PortalKey)
ownUP, logins, err := portal.findOtherLogins(ctx, source)
if err != nil {
log.Err(err).Msg("Failed to check if portal has other logins")
return EventHandlingResultFailed.WithError(err)
}
var ownUP *database.UserPortal
logins = slices.DeleteFunc(logins, func(up *database.UserPortal) bool {
if up.LoginID == source.ID {
ownUP = up
return true
}
return false
})
if len(logins) > 0 {
log.Debug().Msg("Not deleting portal with other logins in remote chat delete event")
if ownUP != nil {