From 0b62253d3b48ec0ea3540af0568d74fd889f9ecc Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 11 Jul 2025 12:55:44 +0300 Subject: [PATCH] all: add support for creator power --- appservice/intent.go | 27 +++++- bridgev2/matrix/connector.go | 8 ++ bridgev2/matrixinterface.go | 1 + bridgev2/portal.go | 8 ++ client.go | 18 +++- event/powerlevels.go | 30 ++++++- event/state.go | 14 ++++ sqlstatestore/statestore.go | 116 +++++++++++--------------- sqlstatestore/v00-latest-revision.sql | 3 +- sqlstatestore/v08-create-event.sql | 2 + statestore.go | 29 ++++++- 11 files changed, 178 insertions(+), 78 deletions(-) create mode 100644 sqlstatestore/v08-create-event.sql diff --git a/appservice/intent.go b/appservice/intent.go index 194057f7..a1245d74 100644 --- a/appservice/intent.go +++ b/appservice/intent.go @@ -375,6 +375,24 @@ func (intent *IntentAPI) Member(ctx context.Context, roomID id.RoomID, userID id return member } +func (intent *IntentAPI) FillPowerLevelCreateEvent(ctx context.Context, roomID id.RoomID, pl *event.PowerLevelsEventContent) error { + if pl.CreateEvent != nil { + return nil + } + var err error + pl.CreateEvent, err = intent.StateStore.GetCreate(ctx, roomID) + if err != nil { + return fmt.Errorf("failed to get create event from cache: %w", err) + } else if pl.CreateEvent != nil { + return nil + } + pl.CreateEvent, err = intent.FullStateEvent(ctx, roomID, event.StateCreate, "") + if err != nil { + return fmt.Errorf("failed to get create event from server: %w", err) + } + return nil +} + func (intent *IntentAPI) PowerLevels(ctx context.Context, roomID id.RoomID) (pl *event.PowerLevelsEventContent, err error) { pl, err = intent.as.StateStore.GetPowerLevels(ctx, roomID) if err != nil { @@ -384,6 +402,12 @@ func (intent *IntentAPI) PowerLevels(ctx context.Context, roomID id.RoomID) (pl if pl == nil { pl = &event.PowerLevelsEventContent{} err = intent.StateEvent(ctx, roomID, event.StatePowerLevels, "", pl) + if err != nil { + return + } + } + if pl.CreateEvent == nil { + pl.CreateEvent, err = intent.FullStateEvent(ctx, roomID, event.StateCreate, "") } return } @@ -398,8 +422,7 @@ func (intent *IntentAPI) SetPowerLevel(ctx context.Context, roomID id.RoomID, us return nil, err } - if pl.GetUserLevel(userID) != level { - pl.SetUserLevel(userID, level) + if pl.EnsureUserLevelAs(intent.UserID, userID, level) { return intent.SendStateEvent(ctx, roomID, event.StatePowerLevels, "", &pl) } return nil, nil diff --git a/bridgev2/matrix/connector.go b/bridgev2/matrix/connector.go index 7075a1aa..978f666f 100644 --- a/bridgev2/matrix/connector.go +++ b/bridgev2/matrix/connector.go @@ -534,6 +534,14 @@ func (br *Connector) GetPowerLevels(ctx context.Context, roomID id.RoomID) (*eve return br.Bot.PowerLevels(ctx, roomID) } +func (br *Connector) GetCreateEvent(ctx context.Context, roomID id.RoomID) (*event.Event, error) { + createEvt, err := br.Bot.StateStore.GetCreate(ctx, roomID) + if err != nil || createEvt != nil { + return createEvt, err + } + return br.Bot.FullStateEvent(ctx, roomID, event.StateCreate, "") +} + func (br *Connector) GetMembers(ctx context.Context, roomID id.RoomID) (map[id.UserID]*event.MemberEventContent, error) { fetched, err := br.Bot.StateStore.HasFetchedMembers(ctx, roomID) if err != nil { diff --git a/bridgev2/matrixinterface.go b/bridgev2/matrixinterface.go index c1bd69b8..5d0cb014 100644 --- a/bridgev2/matrixinterface.go +++ b/bridgev2/matrixinterface.go @@ -47,6 +47,7 @@ type MatrixConnector interface { GenerateContentURI(ctx context.Context, mediaID networkid.MediaID) (id.ContentURIString, error) GetPowerLevels(ctx context.Context, roomID id.RoomID) (*event.PowerLevelsEventContent, error) + GetCreateEvent(ctx context.Context, roomID id.RoomID) (*event.Event, error) GetMembers(ctx context.Context, roomID id.RoomID) (map[id.UserID]*event.MemberEventContent, error) GetMemberInfo(ctx context.Context, roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, error) diff --git a/bridgev2/portal.go b/bridgev2/portal.go index b9ea3385..c91523ef 100644 --- a/bridgev2/portal.go +++ b/bridgev2/portal.go @@ -1654,6 +1654,13 @@ func (portal *Portal) handleMatrixPowerLevels( log.Error().Type("content_type", evt.Content.Parsed).Msg("Unexpected parsed content type") return EventHandlingResultFailed.WithMSSError(fmt.Errorf("%w: %T", ErrUnexpectedParsedContentType, evt.Content.Parsed)) } + if content.CreateEvent == nil { + var err error + content.CreateEvent, err = portal.Bridge.Matrix.GetCreateEvent(ctx, portal.MXID) + if err != nil { + return EventHandlingResultFailed.WithMSSError(fmt.Errorf("failed to get create event for power levels: %w", err)) + } + } api, ok := sender.Client.(PowerLevelHandlingNetworkAPI) if !ok { return EventHandlingResultIgnored.WithMSSError(ErrPowerLevelsNotSupported) @@ -1662,6 +1669,7 @@ func (portal *Portal) handleMatrixPowerLevels( if evt.Unsigned.PrevContent != nil { _ = evt.Unsigned.PrevContent.ParseRaw(evt.Type) prevContent, _ = evt.Unsigned.PrevContent.Parsed.(*event.PowerLevelsEventContent) + prevContent.CreateEvent = content.CreateEvent } plChange := &MatrixPowerLevelChange{ diff --git a/client.go b/client.go index 7a83619f..886dbb63 100644 --- a/client.go +++ b/client.go @@ -1548,12 +1548,15 @@ func (cli *Client) FullStateEvent(ctx context.Context, roomID id.RoomID, eventTy "format": "event", }) _, err = cli.MakeRequest(ctx, http.MethodGet, u, nil, &evt) - if err == nil && cli.StateStore != nil { - UpdateStateStore(ctx, cli.StateStore, evt) - } if evt != nil { evt.Type.Class = event.StateEventType _ = evt.Content.ParseRaw(evt.Type) + if evt.RoomID == "" { + evt.RoomID = roomID + } + } + if err == nil && cli.StateStore != nil { + UpdateStateStore(ctx, cli.StateStore, evt) } return } @@ -1606,12 +1609,21 @@ func (cli *Client) State(ctx context.Context, roomID id.RoomID) (stateMap RoomSt ResponseJSON: &stateMap, Handler: parseRoomStateArray, }) + if stateMap != nil { + pls, ok := stateMap[event.StatePowerLevels][""] + if ok { + pls.Content.AsPowerLevels().CreateEvent = stateMap[event.StateCreate][""] + } + } if err == nil && cli.StateStore != nil { for evtType, evts := range stateMap { if evtType == event.StateMember { continue } for _, evt := range evts { + if evt.RoomID == "" { + evt.RoomID = roomID + } UpdateStateStore(ctx, cli.StateStore, evt) } } diff --git a/event/powerlevels.go b/event/powerlevels.go index 2f4d4573..79dbd1f3 100644 --- a/event/powerlevels.go +++ b/event/powerlevels.go @@ -7,6 +7,8 @@ package event import ( + "math" + "slices" "sync" "go.mau.fi/util/ptr" @@ -34,6 +36,10 @@ type PowerLevelsEventContent struct { KickPtr *int `json:"kick,omitempty"` BanPtr *int `json:"ban,omitempty"` RedactPtr *int `json:"redact,omitempty"` + + // This is not a part of power levels, it's added by mautrix-go internally in certain places + // in order to detect creator power accurately. + CreateEvent *Event `json:"-,omitempty"` } func (pl *PowerLevelsEventContent) Clone() *PowerLevelsEventContent { @@ -53,6 +59,8 @@ func (pl *PowerLevelsEventContent) Clone() *PowerLevelsEventContent { KickPtr: ptr.Clone(pl.KickPtr), BanPtr: ptr.Clone(pl.BanPtr), RedactPtr: ptr.Clone(pl.RedactPtr), + + CreateEvent: pl.CreateEvent, } } @@ -112,6 +120,9 @@ func (pl *PowerLevelsEventContent) StateDefault() int { } func (pl *PowerLevelsEventContent) GetUserLevel(userID id.UserID) int { + if pl.isCreator(userID) { + return math.MaxInt + } pl.usersLock.RLock() defer pl.usersLock.RUnlock() level, ok := pl.Users[userID] @@ -138,9 +149,24 @@ func (pl *PowerLevelsEventContent) EnsureUserLevel(target id.UserID, level int) return pl.EnsureUserLevelAs("", target, level) } +func (pl *PowerLevelsEventContent) createContent() *CreateEventContent { + if pl.CreateEvent == nil { + return &CreateEventContent{} + } + return pl.CreateEvent.Content.AsCreate() +} + +func (pl *PowerLevelsEventContent) isCreator(userID id.UserID) bool { + cc := pl.createContent() + return cc.SupportsCreatorPower() && (userID == pl.CreateEvent.Sender || slices.Contains(cc.AdditionalCreators, userID)) +} + func (pl *PowerLevelsEventContent) EnsureUserLevelAs(actor, target id.UserID, level int) bool { + if pl.isCreator(target) { + return false + } existingLevel := pl.GetUserLevel(target) - if actor != "" { + if actor != "" && !pl.isCreator(actor) { actorLevel := pl.GetUserLevel(actor) if actorLevel <= existingLevel || actorLevel < level { return false @@ -185,7 +211,7 @@ func (pl *PowerLevelsEventContent) EnsureEventLevel(eventType Type, level int) b func (pl *PowerLevelsEventContent) EnsureEventLevelAs(actor id.UserID, eventType Type, level int) bool { existingLevel := pl.GetEventLevel(eventType) - if actor != "" { + if actor != "" && !pl.isCreator(actor) { actorLevel := pl.GetUserLevel(actor) if existingLevel > actorLevel || level > actorLevel { return false diff --git a/event/state.go b/event/state.go index 028691e1..ff6dabfa 100644 --- a/event/state.go +++ b/event/state.go @@ -88,6 +88,7 @@ const ( RoomV9 RoomVersion = "9" RoomV10 RoomVersion = "10" RoomV11 RoomVersion = "11" + RoomV12 RoomVersion = "12" ) // CreateEventContent represents the content of a m.room.create state event. @@ -98,10 +99,23 @@ type CreateEventContent struct { RoomVersion RoomVersion `json:"room_version,omitempty"` Predecessor *Predecessor `json:"predecessor,omitempty"` + // Room v12+ only + AdditionalCreators []id.UserID `json:"additional_creators,omitempty"` + // Deprecated: use the event sender instead Creator id.UserID `json:"creator,omitempty"` } +func (cec *CreateEventContent) SupportsCreatorPower() bool { + switch cec.RoomVersion { + case "", RoomV1, RoomV2, RoomV3, RoomV4, RoomV5, RoomV6, RoomV7, RoomV8, RoomV9, RoomV10, RoomV11: + return false + default: + // Assume anything except known old versions supports creator power. + return true + } +} + // JoinRule specifies how open a room is to new members. // https://spec.matrix.org/v1.2/client-server-api/#mroomjoin_rules type JoinRule string diff --git a/sqlstatestore/statestore.go b/sqlstatestore/statestore.go index 4a220a2b..f9a7e421 100644 --- a/sqlstatestore/statestore.go +++ b/sqlstatestore/statestore.go @@ -379,89 +379,67 @@ func (store *SQLStateStore) SetPowerLevels(ctx context.Context, roomID id.RoomID } func (store *SQLStateStore) GetPowerLevels(ctx context.Context, roomID id.RoomID) (levels *event.PowerLevelsEventContent, err error) { + levels = &event.PowerLevelsEventContent{} err = store. - QueryRow(ctx, "SELECT power_levels FROM mx_room_state WHERE room_id=$1", roomID). - Scan(&dbutil.JSON{Data: &levels}) + QueryRow(ctx, "SELECT power_levels, create_event FROM mx_room_state WHERE room_id=$1", roomID). + Scan(&dbutil.JSON{Data: &levels}, &dbutil.JSON{Data: &levels.CreateEvent}) if errors.Is(err, sql.ErrNoRows) { - err = nil + return nil, nil + } else if err != nil { + return nil, err + } + if levels.CreateEvent != nil { + err = levels.CreateEvent.Content.ParseRaw(event.StateCreate) } return } func (store *SQLStateStore) GetPowerLevel(ctx context.Context, roomID id.RoomID, userID id.UserID) (int, error) { - if store.Dialect == dbutil.Postgres { - var powerLevel int - err := store. - QueryRow(ctx, ` - SELECT COALESCE((power_levels->'users'->$2)::int, (power_levels->'users_default')::int, 0) - FROM mx_room_state WHERE room_id=$1 - `, roomID, userID). - Scan(&powerLevel) - return powerLevel, err - } else { - levels, err := store.GetPowerLevels(ctx, roomID) - if err != nil { - return 0, err - } - return levels.GetUserLevel(userID), nil + levels, err := store.GetPowerLevels(ctx, roomID) + if err != nil { + return 0, err } + return levels.GetUserLevel(userID), nil } func (store *SQLStateStore) GetPowerLevelRequirement(ctx context.Context, roomID id.RoomID, eventType event.Type) (int, error) { - if store.Dialect == dbutil.Postgres { - defaultType := "events_default" - defaultValue := 0 - if eventType.IsState() { - defaultType = "state_default" - defaultValue = 50 - } - var powerLevel int - err := store. - QueryRow(ctx, ` - SELECT COALESCE((power_levels->'events'->$2)::int, (power_levels->'$3')::int, $4) - FROM mx_room_state WHERE room_id=$1 - `, roomID, eventType.Type, defaultType, defaultValue). - Scan(&powerLevel) - if errors.Is(err, sql.ErrNoRows) { - err = nil - powerLevel = defaultValue - } - return powerLevel, err - } else { - levels, err := store.GetPowerLevels(ctx, roomID) - if err != nil { - return 0, err - } - return levels.GetEventLevel(eventType), nil + levels, err := store.GetPowerLevels(ctx, roomID) + if err != nil { + return 0, err } + return levels.GetEventLevel(eventType), nil } func (store *SQLStateStore) HasPowerLevel(ctx context.Context, roomID id.RoomID, userID id.UserID, eventType event.Type) (bool, error) { - if store.Dialect == dbutil.Postgres { - defaultType := "events_default" - defaultValue := 0 - if eventType.IsState() { - defaultType = "state_default" - defaultValue = 50 - } - var hasPower bool - err := store. - QueryRow(ctx, `SELECT - COALESCE((power_levels->'users'->$2)::int, (power_levels->'users_default')::int, 0) - >= - COALESCE((power_levels->'events'->$3)::int, (power_levels->'$4')::int, $5) - FROM mx_room_state WHERE room_id=$1`, roomID, userID, eventType.Type, defaultType, defaultValue). - Scan(&hasPower) - if errors.Is(err, sql.ErrNoRows) { - err = nil - hasPower = defaultValue == 0 - } - return hasPower, err - } else { - levels, err := store.GetPowerLevels(ctx, roomID) - if err != nil { - return false, err - } - return levels.GetUserLevel(userID) >= levels.GetEventLevel(eventType), nil + levels, err := store.GetPowerLevels(ctx, roomID) + if err != nil { + return false, err } + return levels.GetUserLevel(userID) >= levels.GetEventLevel(eventType), nil +} + +func (store *SQLStateStore) SetCreate(ctx context.Context, evt *event.Event) error { + if evt.Type != event.StateCreate { + return fmt.Errorf("invalid event type for create event: %s", evt.Type) + } + _, err := store.Exec(ctx, ` + INSERT INTO mx_room_state (room_id, create_event) VALUES ($1, $2) + ON CONFLICT (room_id) DO UPDATE SET create_event=excluded.create_event + `, evt.RoomID, dbutil.JSON{Data: evt}) + return err +} + +func (store *SQLStateStore) GetCreate(ctx context.Context, roomID id.RoomID) (evt *event.Event, err error) { + err = store. + QueryRow(ctx, "SELECT create_event FROM mx_room_state WHERE room_id=$1", roomID). + Scan(&dbutil.JSON{Data: &evt}) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } else if err != nil { + return nil, err + } + if evt != nil { + err = evt.Content.ParseRaw(event.StateCreate) + } + return } diff --git a/sqlstatestore/v00-latest-revision.sql b/sqlstatestore/v00-latest-revision.sql index a58cc56a..132ed1ab 100644 --- a/sqlstatestore/v00-latest-revision.sql +++ b/sqlstatestore/v00-latest-revision.sql @@ -1,4 +1,4 @@ --- v0 -> v7 (compatible with v3+): Latest revision +-- v0 -> v8 (compatible with v3+): Latest revision CREATE TABLE mx_registrations ( user_id TEXT PRIMARY KEY @@ -26,5 +26,6 @@ CREATE TABLE mx_room_state ( room_id TEXT PRIMARY KEY, power_levels jsonb, encryption jsonb, + create_event jsonb, members_fetched BOOLEAN NOT NULL DEFAULT false ); diff --git a/sqlstatestore/v08-create-event.sql b/sqlstatestore/v08-create-event.sql new file mode 100644 index 00000000..9f1b55c9 --- /dev/null +++ b/sqlstatestore/v08-create-event.sql @@ -0,0 +1,2 @@ +-- v8 (compatible with v3+): Add create event to room state table +ALTER TABLE mx_room_state ADD COLUMN create_event jsonb; diff --git a/statestore.go b/statestore.go index e728b885..1933ab95 100644 --- a/statestore.go +++ b/statestore.go @@ -34,6 +34,9 @@ type StateStore interface { SetPowerLevels(ctx context.Context, roomID id.RoomID, levels *event.PowerLevelsEventContent) error GetPowerLevels(ctx context.Context, roomID id.RoomID) (*event.PowerLevelsEventContent, error) + SetCreate(ctx context.Context, evt *event.Event) error + GetCreate(ctx context.Context, roomID id.RoomID) (*event.Event, error) + HasFetchedMembers(ctx context.Context, roomID id.RoomID) (bool, error) MarkMembersFetched(ctx context.Context, roomID id.RoomID) error GetAllMembers(ctx context.Context, roomID id.RoomID) (map[id.UserID]*event.MemberEventContent, error) @@ -68,9 +71,11 @@ func UpdateStateStore(ctx context.Context, store StateStore, evt *event.Event) { err = store.SetPowerLevels(ctx, evt.RoomID, content) case *event.EncryptionEventContent: err = store.SetEncryptionEvent(ctx, evt.RoomID, content) + case *event.CreateEventContent: + err = store.SetCreate(ctx, evt) default: switch evt.Type { - case event.StateMember, event.StatePowerLevels, event.StateEncryption: + case event.StateMember, event.StatePowerLevels, event.StateEncryption, event.StateCreate: zerolog.Ctx(ctx).Warn(). Stringer("event_id", evt.ID). Str("event_type", evt.Type.Type). @@ -101,6 +106,7 @@ type MemoryStateStore struct { MembersFetched map[id.RoomID]bool `json:"members_fetched"` PowerLevels map[id.RoomID]*event.PowerLevelsEventContent `json:"power_levels"` Encryption map[id.RoomID]*event.EncryptionEventContent `json:"encryption"` + Create map[id.RoomID]*event.Event `json:"create"` registrationsLock sync.RWMutex membersLock sync.RWMutex @@ -115,6 +121,7 @@ func NewMemoryStateStore() StateStore { MembersFetched: make(map[id.RoomID]bool), PowerLevels: make(map[id.RoomID]*event.PowerLevelsEventContent), Encryption: make(map[id.RoomID]*event.EncryptionEventContent), + Create: make(map[id.RoomID]*event.Event), } } @@ -298,6 +305,9 @@ func (store *MemoryStateStore) SetPowerLevels(_ context.Context, roomID id.RoomI func (store *MemoryStateStore) GetPowerLevels(_ context.Context, roomID id.RoomID) (levels *event.PowerLevelsEventContent, err error) { store.powerLevelsLock.RLock() levels = store.PowerLevels[roomID] + if levels != nil && levels.CreateEvent == nil { + levels.CreateEvent = store.Create[roomID] + } store.powerLevelsLock.RUnlock() return } @@ -314,6 +324,23 @@ func (store *MemoryStateStore) HasPowerLevel(ctx context.Context, roomID id.Room return exerrors.Must(store.GetPowerLevel(ctx, roomID, userID)) >= exerrors.Must(store.GetPowerLevelRequirement(ctx, roomID, eventType)), nil } +func (store *MemoryStateStore) SetCreate(ctx context.Context, evt *event.Event) error { + store.powerLevelsLock.Lock() + store.Create[evt.RoomID] = evt + if pls, ok := store.PowerLevels[evt.RoomID]; ok && pls.CreateEvent == nil { + pls.CreateEvent = evt + } + store.powerLevelsLock.Unlock() + return nil +} + +func (store *MemoryStateStore) GetCreate(ctx context.Context, roomID id.RoomID) (*event.Event, error) { + store.powerLevelsLock.RLock() + evt := store.Create[roomID] + store.powerLevelsLock.RUnlock() + return evt, nil +} + func (store *MemoryStateStore) SetEncryptionEvent(_ context.Context, roomID id.RoomID, content *event.EncryptionEventContent) error { store.encryptionLock.Lock() store.Encryption[roomID] = content