bridgev2: implement re-ID'ing portals properly

This commit is contained in:
Tulir Asokan 2024-06-19 20:55:38 +03:00
commit 28d81a2b60
5 changed files with 211 additions and 21 deletions

View file

@ -89,7 +89,11 @@ const (
name_set=$11, avatar_set=$12, topic_set=$13, in_space=$14, metadata=$15
WHERE bridge_id=$1 AND id=$2 AND receiver=$3
`
reIDPortalQuery = `UPDATE portal SET id=$3 WHERE bridge_id=$1 AND id=$2`
deletePortalQuery = `
DELETE FROM portal
WHERE bridge_id=$1 AND id=$2 AND receiver=$3
`
reIDPortalQuery = `UPDATE portal SET id=$4, receiver=$5 WHERE bridge_id=$1 AND id=$2 AND receiver=$3`
)
func (pq *PortalQuery) GetByID(ctx context.Context, key networkid.PortalKey) (*Portal, error) {
@ -108,8 +112,8 @@ func (pq *PortalQuery) GetChildren(ctx context.Context, parentID networkid.Porta
return pq.QueryMany(ctx, getChildPortalsQuery, pq.BridgeID, parentID)
}
func (pq *PortalQuery) ReID(ctx context.Context, oldID, newID networkid.PortalID) error {
return pq.Exec(ctx, reIDPortalQuery, pq.BridgeID, oldID, newID)
func (pq *PortalQuery) ReID(ctx context.Context, oldID, newID networkid.PortalKey) error {
return pq.Exec(ctx, reIDPortalQuery, pq.BridgeID, oldID.ID, oldID.Receiver, newID.ID, newID.Receiver)
}
func (pq *PortalQuery) Insert(ctx context.Context, p *Portal) error {
@ -122,6 +126,10 @@ func (pq *PortalQuery) Update(ctx context.Context, p *Portal) error {
return pq.Exec(ctx, updatePortalQuery, p.sqlVariables()...)
}
func (pq *PortalQuery) Delete(ctx context.Context, key networkid.PortalKey) error {
return pq.Exec(ctx, deletePortalQuery, pq.BridgeID, key.ID, key.Receiver)
}
func (p *Portal) Scan(row dbutil.Scannable) (*Portal, error) {
var mxid, parentID sql.NullString
var avatarHash string

View file

@ -11,6 +11,8 @@ import (
"fmt"
"time"
"github.com/rs/zerolog"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridgev2"
@ -216,7 +218,34 @@ func (as *ASIntent) CreateRoom(ctx context.Context, req *mautrix.ReqCreateRoom)
return resp.RoomID, nil
}
func (as *ASIntent) DeleteRoom(ctx context.Context, roomID id.RoomID) error {
// TODO implement non-beeper delete
return as.Matrix.BeeperDeleteRoom(ctx, roomID)
func (as *ASIntent) DeleteRoom(ctx context.Context, roomID id.RoomID, puppetsOnly bool) error {
if as.Connector.SpecVersions.Supports(mautrix.BeeperFeatureRoomYeeting) {
return as.Matrix.BeeperDeleteRoom(ctx, roomID)
}
members, err := as.Matrix.JoinedMembers(ctx, roomID)
if err != nil {
return fmt.Errorf("failed to get portal members for cleanup: %w", err)
}
for member := range members.Joined {
if member == as.Matrix.UserID {
continue
}
_, isGhost := as.Connector.ParseGhostMXID(member)
if isGhost {
_, err = as.Connector.AS.Intent(member).LeaveRoom(ctx, roomID)
if err != nil {
zerolog.Ctx(ctx).Err(err).Stringer("user_id", member).Msg("Failed to leave room while cleaning up portal")
}
} else if !puppetsOnly {
_, err = as.Matrix.KickUser(ctx, roomID, &mautrix.ReqKickUser{UserID: member, Reason: "Deleting portal"})
if err != nil {
zerolog.Ctx(ctx).Err(err).Stringer("user_id", member).Msg("Failed to kick user while cleaning up portal")
}
}
}
_, err = as.Matrix.LeaveRoom(ctx, roomID)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to leave room while cleaning up portal")
}
return nil
}

View file

@ -55,7 +55,7 @@ type MatrixAPI interface {
SetExtraProfileMeta(ctx context.Context, data any) error
CreateRoom(ctx context.Context, req *mautrix.ReqCreateRoom) (id.RoomID, error)
DeleteRoom(ctx context.Context, roomID id.RoomID) error
DeleteRoom(ctx context.Context, roomID id.RoomID, puppetsOnly bool) error
InviteUser(ctx context.Context, roomID id.RoomID, userID id.UserID) error
EnsureJoined(ctx context.Context, roomID id.RoomID) error
}

View file

@ -778,7 +778,7 @@ func (portal *Portal) handleRemoteEvent(source *UserLogin, evt RemoteEvent) {
if !ok || !mcp.ShouldCreatePortal() {
return
}
err := portal.CreateMatrixRoom(ctx, source)
err := portal.CreateMatrixRoom(ctx, source, nil)
if err != nil {
log.Err(err).Msg("Failed to create portal to handle event")
// TODO error
@ -1441,7 +1441,7 @@ func (portal *Portal) SyncParticipants(ctx context.Context, members []networkid.
return expectedUserIDs, extraFunctionalMembers, nil
}
func (portal *Portal) UpdateInfo(ctx context.Context, info *PortalInfo, sender *Ghost, ts time.Time) {
func (portal *Portal) UpdateInfo(ctx context.Context, info *PortalInfo, source *UserLogin, sender *Ghost, ts time.Time) {
changed := false
if info.Name != nil {
changed = portal.UpdateName(ctx, *info.Name, sender, ts) || changed
@ -1452,12 +1452,13 @@ func (portal *Portal) UpdateInfo(ctx context.Context, info *PortalInfo, sender *
if info.Avatar != nil {
changed = portal.UpdateAvatar(ctx, info.Avatar, sender, ts) || changed
}
//if info.Members != nil && portal.MXID != "" {
// _, err := portal.SyncParticipants(ctx, info.Members, source)
// if err != nil {
// zerolog.Ctx(ctx).Err(err).Msg("Failed to sync room members")
// }
//}
if info.Members != nil && portal.MXID != "" && source != nil {
_, _, err := portal.SyncParticipants(ctx, info.Members, source)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to sync room members")
}
// TODO detect changes to functional members list?
}
if changed {
portal.UpdateBridgeInfo(ctx)
err := portal.Save(ctx)
@ -1467,7 +1468,7 @@ func (portal *Portal) UpdateInfo(ctx context.Context, info *PortalInfo, sender *
}
}
func (portal *Portal) CreateMatrixRoom(ctx context.Context, source *UserLogin) error {
func (portal *Portal) CreateMatrixRoom(ctx context.Context, source *UserLogin, info *PortalInfo) error {
portal.roomCreateLock.Lock()
defer portal.roomCreateLock.Unlock()
if portal.MXID != "" {
@ -1479,12 +1480,15 @@ func (portal *Portal) CreateMatrixRoom(ctx context.Context, source *UserLogin) e
ctx = log.WithContext(ctx)
log.Info().Msg("Creating Matrix room")
info, err := source.Client.GetChatInfo(ctx, portal)
if err != nil {
log.Err(err).Msg("Failed to update portal info for creation")
return err
var err error
if info == nil {
info, err = source.Client.GetChatInfo(ctx, portal)
if err != nil {
log.Err(err).Msg("Failed to update portal info for creation")
return err
}
}
portal.UpdateInfo(ctx, info, nil, time.Time{})
portal.UpdateInfo(ctx, info, source, nil, time.Time{})
initialMembers, extraFunctionalMembers, err := portal.SyncParticipants(ctx, info.Members, source)
if err != nil {
log.Err(err).Msg("Failed to process participant list for portal creation")
@ -1583,6 +1587,34 @@ func (portal *Portal) CreateMatrixRoom(ctx context.Context, source *UserLogin) e
return nil
}
func (portal *Portal) Delete(ctx context.Context) error {
err := portal.Bridge.DB.Portal.Delete(ctx, portal.PortalKey)
if err != nil {
return err
}
portal.Bridge.cacheLock.Lock()
defer portal.Bridge.cacheLock.Unlock()
portal.unlockedDeleteCache()
return nil
}
func (portal *Portal) unlockedDelete(ctx context.Context) error {
// TODO delete child portals?
err := portal.Bridge.DB.Portal.Delete(ctx, portal.PortalKey)
if err != nil {
return err
}
portal.unlockedDeleteCache()
return nil
}
func (portal *Portal) unlockedDeleteCache() {
delete(portal.Bridge.portalsByKey, portal.PortalKey)
if portal.MXID != "" {
delete(portal.Bridge.portalsByMXID, portal.MXID)
}
}
func (portal *Portal) Save(ctx context.Context) error {
return portal.Bridge.DB.Portal.Update(ctx, portal.Portal)
}

121
bridgev2/portalreid.go Normal file
View file

@ -0,0 +1,121 @@
// Copyright (c) 2024 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package bridgev2
import (
"context"
"fmt"
"time"
"github.com/rs/zerolog"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
)
type ReIDResult int
const (
ReIDResultError ReIDResult = iota
ReIDResultNoOp
ReIDResultSourceDeleted
ReIDResultSourceReIDd
ReIDResultTargetDeletedAndSourceReIDd
ReIDResultSourceTombstonedIntoTarget
)
func (br *Bridge) ReIDPortal(ctx context.Context, source, target networkid.PortalKey) (ReIDResult, *Portal, error) {
log := zerolog.Ctx(ctx)
log.Debug().Msg("Re-ID'ing portal")
defer func() {
log.Debug().Msg("Finished handling portal re-ID")
}()
br.cacheLock.Lock()
defer br.cacheLock.Unlock()
sourcePortal, err := br.unlockedGetPortalByID(ctx, source, true)
if err != nil {
return ReIDResultError, nil, fmt.Errorf("failed to get source portal: %w", err)
} else if sourcePortal == nil {
log.Debug().Msg("Source portal not found, re-ID is no-op")
return ReIDResultNoOp, nil, nil
}
sourcePortal.roomCreateLock.Lock()
defer sourcePortal.roomCreateLock.Unlock()
if sourcePortal.MXID == "" {
log.Info().Msg("Source portal doesn't have Matrix room, deleting row")
err = sourcePortal.unlockedDelete(ctx)
if err != nil {
return ReIDResultError, nil, fmt.Errorf("failed to delete source portal: %w", err)
}
return ReIDResultSourceDeleted, nil, nil
}
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Stringer("source_portal_mxid", sourcePortal.MXID)
})
targetPortal, err := br.unlockedGetPortalByID(ctx, target, true)
if err != nil {
return ReIDResultError, nil, fmt.Errorf("failed to get target portal: %w", err)
}
if targetPortal == nil {
log.Info().Msg("Target portal doesn't exist, re-ID'ing source portal")
err = sourcePortal.unlockedReID(ctx, target)
if err != nil {
return ReIDResultError, nil, fmt.Errorf("failed to re-ID source portal: %w", err)
}
return ReIDResultSourceReIDd, sourcePortal, nil
}
targetPortal.roomCreateLock.Lock()
defer targetPortal.roomCreateLock.Unlock()
if targetPortal.MXID == "" {
log.Info().Msg("Target portal row exists, but doesn't have a Matrix room. Deleting target portal row and re-ID'ing source portal")
err = targetPortal.unlockedDelete(ctx)
if err != nil {
return ReIDResultError, nil, fmt.Errorf("failed to delete target portal: %w", err)
}
err = sourcePortal.unlockedReID(ctx, target)
if err != nil {
return ReIDResultError, nil, fmt.Errorf("failed to re-ID source portal after deleting target: %w", err)
}
return ReIDResultTargetDeletedAndSourceReIDd, sourcePortal, nil
} else {
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Stringer("target_portal_mxid", targetPortal.MXID)
})
log.Info().Msg("Both target and source portals have Matrix rooms, tombstoning source portal")
err = sourcePortal.unlockedDelete(ctx)
if err != nil {
return ReIDResultError, nil, fmt.Errorf("failed to delete source portal row: %w", err)
}
go func() {
_, err := br.Bot.SendState(ctx, sourcePortal.MXID, event.StateTombstone, "", &event.Content{
Parsed: &event.TombstoneEventContent{
Body: fmt.Sprintf("This room has been merged"),
ReplacementRoom: targetPortal.MXID,
},
}, time.Now())
if err != nil {
log.Err(err).Msg("Failed to send tombstone to source portal room")
}
err = br.Bot.DeleteRoom(ctx, sourcePortal.MXID, err == nil)
if err != nil {
log.Err(err).Msg("Failed to delete source portal room")
}
}()
return ReIDResultSourceTombstonedIntoTarget, targetPortal, nil
}
}
func (portal *Portal) unlockedReID(ctx context.Context, target networkid.PortalKey) error {
err := portal.Bridge.DB.Portal.ReID(ctx, portal.PortalKey, target)
if err != nil {
return err
}
delete(portal.Bridge.portalsByKey, portal.PortalKey)
portal.Bridge.portalsByKey[target] = portal
portal.PortalKey = target
return nil
}