mirror of https://github.com/mautrix/go.git
345 lines
12 KiB
Go
345 lines
12 KiB
Go
// Copyright (c) 2023 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 mautrix
|
|
|
|
import (
|
|
"context"
|
|
"maps"
|
|
"sync"
|
|
|
|
"github.com/rs/zerolog"
|
|
"go.mau.fi/util/exerrors"
|
|
|
|
"maunium.net/go/mautrix/event"
|
|
"maunium.net/go/mautrix/id"
|
|
)
|
|
|
|
// StateStore is an interface for storing basic room state information.
|
|
type StateStore interface {
|
|
IsInRoom(ctx context.Context, roomID id.RoomID, userID id.UserID) bool
|
|
IsInvited(ctx context.Context, roomID id.RoomID, userID id.UserID) bool
|
|
IsMembership(ctx context.Context, roomID id.RoomID, userID id.UserID, allowedMemberships ...event.Membership) bool
|
|
GetMember(ctx context.Context, roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, error)
|
|
TryGetMember(ctx context.Context, roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, error)
|
|
SetMembership(ctx context.Context, roomID id.RoomID, userID id.UserID, membership event.Membership) error
|
|
SetMember(ctx context.Context, roomID id.RoomID, userID id.UserID, member *event.MemberEventContent) error
|
|
IsConfusableName(ctx context.Context, roomID id.RoomID, currentUser id.UserID, name string) ([]id.UserID, error)
|
|
ClearCachedMembers(ctx context.Context, roomID id.RoomID, memberships ...event.Membership) error
|
|
ReplaceCachedMembers(ctx context.Context, roomID id.RoomID, evts []*event.Event, onlyMemberships ...event.Membership) error
|
|
|
|
SetPowerLevels(ctx context.Context, roomID id.RoomID, levels *event.PowerLevelsEventContent) error
|
|
GetPowerLevels(ctx context.Context, roomID id.RoomID) (*event.PowerLevelsEventContent, 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)
|
|
|
|
SetEncryptionEvent(ctx context.Context, roomID id.RoomID, content *event.EncryptionEventContent) error
|
|
IsEncrypted(ctx context.Context, roomID id.RoomID) (bool, error)
|
|
|
|
GetRoomJoinedOrInvitedMembers(ctx context.Context, roomID id.RoomID) ([]id.UserID, error)
|
|
}
|
|
|
|
type StateStoreUpdater interface {
|
|
UpdateState(ctx context.Context, evt *event.Event)
|
|
}
|
|
|
|
func UpdateStateStore(ctx context.Context, store StateStore, evt *event.Event) {
|
|
if store == nil || evt == nil || evt.StateKey == nil {
|
|
return
|
|
}
|
|
if directUpdater, ok := store.(StateStoreUpdater); ok {
|
|
directUpdater.UpdateState(ctx, evt)
|
|
return
|
|
}
|
|
// We only care about events without a state key (power levels, encryption) or member events with state key
|
|
if evt.Type != event.StateMember && evt.GetStateKey() != "" {
|
|
return
|
|
}
|
|
var err error
|
|
switch content := evt.Content.Parsed.(type) {
|
|
case *event.MemberEventContent:
|
|
err = store.SetMember(ctx, evt.RoomID, id.UserID(evt.GetStateKey()), content)
|
|
case *event.PowerLevelsEventContent:
|
|
err = store.SetPowerLevels(ctx, evt.RoomID, content)
|
|
case *event.EncryptionEventContent:
|
|
err = store.SetEncryptionEvent(ctx, evt.RoomID, content)
|
|
default:
|
|
switch evt.Type {
|
|
case event.StateMember, event.StatePowerLevels, event.StateEncryption:
|
|
zerolog.Ctx(ctx).Warn().
|
|
Stringer("event_id", evt.ID).
|
|
Str("event_type", evt.Type.Type).
|
|
Type("content_type", evt.Content.Parsed).
|
|
Msg("Got known event type with unknown content type in UpdateStateStore")
|
|
}
|
|
}
|
|
if err != nil {
|
|
zerolog.Ctx(ctx).Warn().Err(err).
|
|
Stringer("event_id", evt.ID).
|
|
Str("event_type", evt.Type.Type).
|
|
Msg("Failed to update state store")
|
|
}
|
|
}
|
|
|
|
// StateStoreSyncHandler can be added as an event handler in the syncer to update the state store automatically.
|
|
//
|
|
// client.Syncer.(mautrix.ExtensibleSyncer).OnEvent(client.StateStoreSyncHandler)
|
|
//
|
|
// DefaultSyncer.ParseEventContent must also be true for this to work (which it is by default).
|
|
func (cli *Client) StateStoreSyncHandler(ctx context.Context, evt *event.Event) {
|
|
UpdateStateStore(ctx, cli.StateStore, evt)
|
|
}
|
|
|
|
type MemoryStateStore struct {
|
|
Registrations map[id.UserID]bool `json:"registrations"`
|
|
Members map[id.RoomID]map[id.UserID]*event.MemberEventContent `json:"memberships"`
|
|
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"`
|
|
|
|
registrationsLock sync.RWMutex
|
|
membersLock sync.RWMutex
|
|
powerLevelsLock sync.RWMutex
|
|
encryptionLock sync.RWMutex
|
|
}
|
|
|
|
func NewMemoryStateStore() StateStore {
|
|
return &MemoryStateStore{
|
|
Registrations: make(map[id.UserID]bool),
|
|
Members: make(map[id.RoomID]map[id.UserID]*event.MemberEventContent),
|
|
MembersFetched: make(map[id.RoomID]bool),
|
|
PowerLevels: make(map[id.RoomID]*event.PowerLevelsEventContent),
|
|
Encryption: make(map[id.RoomID]*event.EncryptionEventContent),
|
|
}
|
|
}
|
|
|
|
func (store *MemoryStateStore) IsRegistered(_ context.Context, userID id.UserID) (bool, error) {
|
|
store.registrationsLock.RLock()
|
|
defer store.registrationsLock.RUnlock()
|
|
registered, ok := store.Registrations[userID]
|
|
return ok && registered, nil
|
|
}
|
|
|
|
func (store *MemoryStateStore) MarkRegistered(_ context.Context, userID id.UserID) error {
|
|
store.registrationsLock.Lock()
|
|
defer store.registrationsLock.Unlock()
|
|
store.Registrations[userID] = true
|
|
return nil
|
|
}
|
|
|
|
func (store *MemoryStateStore) GetRoomMembers(_ context.Context, roomID id.RoomID) (map[id.UserID]*event.MemberEventContent, error) {
|
|
store.membersLock.RLock()
|
|
members, ok := store.Members[roomID]
|
|
store.membersLock.RUnlock()
|
|
if !ok {
|
|
members = make(map[id.UserID]*event.MemberEventContent)
|
|
store.membersLock.Lock()
|
|
store.Members[roomID] = members
|
|
store.membersLock.Unlock()
|
|
}
|
|
return members, nil
|
|
}
|
|
|
|
func (store *MemoryStateStore) GetRoomJoinedOrInvitedMembers(ctx context.Context, roomID id.RoomID) ([]id.UserID, error) {
|
|
members, err := store.GetRoomMembers(ctx, roomID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ids := make([]id.UserID, 0, len(members))
|
|
for id := range members {
|
|
ids = append(ids, id)
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
func (store *MemoryStateStore) GetMembership(ctx context.Context, roomID id.RoomID, userID id.UserID) (event.Membership, error) {
|
|
return exerrors.Must(store.GetMember(ctx, roomID, userID)).Membership, nil
|
|
}
|
|
|
|
func (store *MemoryStateStore) GetMember(ctx context.Context, roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, error) {
|
|
member, err := store.TryGetMember(ctx, roomID, userID)
|
|
if member == nil && err == nil {
|
|
member = &event.MemberEventContent{Membership: event.MembershipLeave}
|
|
}
|
|
return member, err
|
|
}
|
|
|
|
func (store *MemoryStateStore) IsConfusableName(ctx context.Context, roomID id.RoomID, currentUser id.UserID, name string) ([]id.UserID, error) {
|
|
// TODO implement?
|
|
return nil, nil
|
|
}
|
|
|
|
func (store *MemoryStateStore) TryGetMember(_ context.Context, roomID id.RoomID, userID id.UserID) (member *event.MemberEventContent, err error) {
|
|
store.membersLock.RLock()
|
|
defer store.membersLock.RUnlock()
|
|
members, membersOk := store.Members[roomID]
|
|
if !membersOk {
|
|
return
|
|
}
|
|
member = members[userID]
|
|
return
|
|
}
|
|
|
|
func (store *MemoryStateStore) IsInRoom(ctx context.Context, roomID id.RoomID, userID id.UserID) bool {
|
|
return store.IsMembership(ctx, roomID, userID, "join")
|
|
}
|
|
|
|
func (store *MemoryStateStore) IsInvited(ctx context.Context, roomID id.RoomID, userID id.UserID) bool {
|
|
return store.IsMembership(ctx, roomID, userID, "join", "invite")
|
|
}
|
|
|
|
func (store *MemoryStateStore) IsMembership(ctx context.Context, roomID id.RoomID, userID id.UserID, allowedMemberships ...event.Membership) bool {
|
|
membership := exerrors.Must(store.GetMembership(ctx, roomID, userID))
|
|
for _, allowedMembership := range allowedMemberships {
|
|
if allowedMembership == membership {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (store *MemoryStateStore) SetMembership(_ context.Context, roomID id.RoomID, userID id.UserID, membership event.Membership) error {
|
|
store.membersLock.Lock()
|
|
members, ok := store.Members[roomID]
|
|
if !ok {
|
|
members = map[id.UserID]*event.MemberEventContent{
|
|
userID: {Membership: membership},
|
|
}
|
|
} else {
|
|
member, ok := members[userID]
|
|
if !ok {
|
|
members[userID] = &event.MemberEventContent{Membership: membership}
|
|
} else {
|
|
member.Membership = membership
|
|
members[userID] = member
|
|
}
|
|
}
|
|
store.Members[roomID] = members
|
|
store.membersLock.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (store *MemoryStateStore) SetMember(_ context.Context, roomID id.RoomID, userID id.UserID, member *event.MemberEventContent) error {
|
|
store.membersLock.Lock()
|
|
members, ok := store.Members[roomID]
|
|
if !ok {
|
|
members = map[id.UserID]*event.MemberEventContent{
|
|
userID: member,
|
|
}
|
|
} else {
|
|
members[userID] = member
|
|
}
|
|
store.Members[roomID] = members
|
|
store.membersLock.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (store *MemoryStateStore) ClearCachedMembers(_ context.Context, roomID id.RoomID, memberships ...event.Membership) error {
|
|
store.membersLock.Lock()
|
|
defer store.membersLock.Unlock()
|
|
members, ok := store.Members[roomID]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
for userID, member := range members {
|
|
for _, membership := range memberships {
|
|
if membership == member.Membership {
|
|
delete(members, userID)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
store.MembersFetched[roomID] = false
|
|
return nil
|
|
}
|
|
|
|
func (store *MemoryStateStore) HasFetchedMembers(ctx context.Context, roomID id.RoomID) (bool, error) {
|
|
store.membersLock.RLock()
|
|
defer store.membersLock.RUnlock()
|
|
return store.MembersFetched[roomID], nil
|
|
}
|
|
|
|
func (store *MemoryStateStore) MarkMembersFetched(ctx context.Context, roomID id.RoomID) error {
|
|
store.membersLock.Lock()
|
|
defer store.membersLock.Unlock()
|
|
store.MembersFetched[roomID] = true
|
|
return nil
|
|
}
|
|
|
|
func (store *MemoryStateStore) ReplaceCachedMembers(ctx context.Context, roomID id.RoomID, evts []*event.Event, onlyMemberships ...event.Membership) error {
|
|
_ = store.ClearCachedMembers(ctx, roomID, onlyMemberships...)
|
|
for _, evt := range evts {
|
|
UpdateStateStore(ctx, store, evt)
|
|
}
|
|
if len(onlyMemberships) == 0 {
|
|
_ = store.MarkMembersFetched(ctx, roomID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (store *MemoryStateStore) GetAllMembers(ctx context.Context, roomID id.RoomID) (map[id.UserID]*event.MemberEventContent, error) {
|
|
store.membersLock.RLock()
|
|
defer store.membersLock.RUnlock()
|
|
return maps.Clone(store.Members[roomID]), nil
|
|
}
|
|
|
|
func (store *MemoryStateStore) SetPowerLevels(_ context.Context, roomID id.RoomID, levels *event.PowerLevelsEventContent) error {
|
|
store.powerLevelsLock.Lock()
|
|
store.PowerLevels[roomID] = levels
|
|
store.powerLevelsLock.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (store *MemoryStateStore) GetPowerLevels(_ context.Context, roomID id.RoomID) (levels *event.PowerLevelsEventContent, err error) {
|
|
store.powerLevelsLock.RLock()
|
|
levels = store.PowerLevels[roomID]
|
|
store.powerLevelsLock.RUnlock()
|
|
return
|
|
}
|
|
|
|
func (store *MemoryStateStore) GetPowerLevel(ctx context.Context, roomID id.RoomID, userID id.UserID) (int, error) {
|
|
return exerrors.Must(store.GetPowerLevels(ctx, roomID)).GetUserLevel(userID), nil
|
|
}
|
|
|
|
func (store *MemoryStateStore) GetPowerLevelRequirement(ctx context.Context, roomID id.RoomID, eventType event.Type) (int, error) {
|
|
return exerrors.Must(store.GetPowerLevels(ctx, roomID)).GetEventLevel(eventType), nil
|
|
}
|
|
|
|
func (store *MemoryStateStore) HasPowerLevel(ctx context.Context, roomID id.RoomID, userID id.UserID, eventType event.Type) (bool, error) {
|
|
return exerrors.Must(store.GetPowerLevel(ctx, roomID, userID)) >= exerrors.Must(store.GetPowerLevelRequirement(ctx, roomID, eventType)), nil
|
|
}
|
|
|
|
func (store *MemoryStateStore) SetEncryptionEvent(_ context.Context, roomID id.RoomID, content *event.EncryptionEventContent) error {
|
|
store.encryptionLock.Lock()
|
|
store.Encryption[roomID] = content
|
|
store.encryptionLock.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (store *MemoryStateStore) GetEncryptionEvent(_ context.Context, roomID id.RoomID) (*event.EncryptionEventContent, error) {
|
|
store.encryptionLock.RLock()
|
|
defer store.encryptionLock.RUnlock()
|
|
return store.Encryption[roomID], nil
|
|
}
|
|
|
|
func (store *MemoryStateStore) IsEncrypted(ctx context.Context, roomID id.RoomID) (bool, error) {
|
|
cfg, err := store.GetEncryptionEvent(ctx, roomID)
|
|
return cfg != nil && cfg.Algorithm == id.AlgorithmMegolmV1, err
|
|
}
|
|
|
|
func (store *MemoryStateStore) FindSharedRooms(ctx context.Context, userID id.UserID) (rooms []id.RoomID, err error) {
|
|
store.membersLock.RLock()
|
|
defer store.membersLock.RUnlock()
|
|
for roomID, members := range store.Members {
|
|
if _, ok := members[userID]; ok {
|
|
rooms = append(rooms, roomID)
|
|
}
|
|
}
|
|
return rooms, nil
|
|
}
|