appservice: keep the crypto method set on the wrapped state store

The lazy store embedded appservice.StateStore, but the client's store is
also type-asserted to crypto.StateStore -- a wider interface -- and
NewCryptoHelper refuses to start when that assertion fails. Embedding the
narrower interface dropped GetEncryptionEvent and FindSharedRooms from the
concrete type, so the bot died on startup with "the client state store
must implement crypto.StateStore".

Embed both, assert both at compile time, and resolve GetEncryptionEvent
lazily like IsEncrypted since the crypto machine reads megolm rotation
settings through it.
This commit is contained in:
prosolis
2026-07-12 17:31:19 -07:00
parent 2482433896
commit a533e106c2
3 changed files with 61 additions and 12 deletions

View File

@@ -8,6 +8,7 @@ import (
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/crypto"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
@@ -30,7 +31,12 @@ import (
// error, which aborts the send. A failed message is recoverable; a plaintext one
// that has already landed in an encrypted room is not.
type lazyStateStore struct {
appservice.StateStore
// Embeds both interfaces the store is consumed through: the appservice
// dispatches state updates through the first, and the crypto machine type-
// asserts the client's store to the second (cryptohelper.NewCryptoHelper
// rejects a store that fails the assertion). Embedding only the appservice
// interface silently drops crypto's extra methods from the concrete type.
innerStateStore
client *mautrix.Client
@@ -42,9 +48,22 @@ type lazyStateStore struct {
encryptionResolved map[id.RoomID]bool
}
func newLazyStateStore(inner appservice.StateStore) *lazyStateStore {
// innerStateStore is the full method set the wrapped store must provide.
type innerStateStore interface {
appservice.StateStore
crypto.StateStore
}
// The wrapper is consumed through both, and losing either one is a startup
// failure rather than a build failure without these.
var (
_ appservice.StateStore = (*lazyStateStore)(nil)
_ crypto.StateStore = (*lazyStateStore)(nil)
)
func newLazyStateStore(inner innerStateStore) *lazyStateStore {
return &lazyStateStore{
StateStore: inner,
innerStateStore: inner,
locks: make(map[id.RoomID]*sync.Mutex),
encryptionResolved: make(map[id.RoomID]bool),
}
@@ -90,7 +109,7 @@ func (s *lazyStateStore) IsEncrypted(ctx context.Context, roomID id.RoomID) (boo
switch {
case err == nil:
if enc.Algorithm != "" {
if err := s.StateStore.SetEncryptionEvent(ctx, roomID, &enc); err != nil {
if err := s.innerStateStore.SetEncryptionEvent(ctx, roomID, &enc); err != nil {
return false, fmt.Errorf("cache encryption state for %s: %w", roomID, err)
}
}
@@ -103,14 +122,24 @@ func (s *lazyStateStore) IsEncrypted(ctx context.Context, roomID id.RoomID) (boo
s.markEncryptionResolved(roomID)
}
}
return s.StateStore.IsEncrypted(ctx, roomID)
return s.innerStateStore.IsEncrypted(ctx, roomID)
}
// GetEncryptionEvent is how the crypto machine reads a room's megolm settings
// (session rotation period, etc). It needs the same resolution as IsEncrypted,
// since an unresolved room would otherwise report no encryption event.
func (s *lazyStateStore) GetEncryptionEvent(ctx context.Context, roomID id.RoomID) (*event.EncryptionEventContent, error) {
if _, err := s.IsEncrypted(ctx, roomID); err != nil {
return nil, err
}
return s.innerStateStore.GetEncryptionEvent(ctx, roomID)
}
// GetRoomJoinedOrInvitedMembers backs megolm session sharing. Without a sync
// there is no member backfill, so an unfetched room would report zero members
// and the bot would encrypt to an audience of nobody.
func (s *lazyStateStore) GetRoomJoinedOrInvitedMembers(ctx context.Context, roomID id.RoomID) ([]id.UserID, error) {
fetched, err := s.StateStore.HasFetchedMembers(ctx, roomID)
fetched, err := s.innerStateStore.HasFetchedMembers(ctx, roomID)
if err != nil {
return nil, fmt.Errorf("check member cache for %s: %w", roomID, err)
}
@@ -118,7 +147,7 @@ func (s *lazyStateStore) GetRoomJoinedOrInvitedMembers(ctx context.Context, room
unlock := s.lockRoom(roomID)
defer unlock()
if fetched, err := s.StateStore.HasFetchedMembers(ctx, roomID); err != nil {
if fetched, err := s.innerStateStore.HasFetchedMembers(ctx, roomID); err != nil {
return nil, fmt.Errorf("check member cache for %s: %w", roomID, err)
} else if !fetched {
members, err := s.client.Members(ctx, roomID)
@@ -126,10 +155,10 @@ func (s *lazyStateStore) GetRoomJoinedOrInvitedMembers(ctx context.Context, room
return nil, fmt.Errorf("fetch members of %s: %w", roomID, err)
}
// No membership filter, so this also marks the room as fetched.
if err := s.StateStore.ReplaceCachedMembers(ctx, roomID, members.Chunk); err != nil {
if err := s.innerStateStore.ReplaceCachedMembers(ctx, roomID, members.Chunk); err != nil {
return nil, fmt.Errorf("cache members of %s: %w", roomID, err)
}
}
}
return s.StateStore.GetRoomJoinedOrInvitedMembers(ctx, roomID)
return s.innerStateStore.GetRoomJoinedOrInvitedMembers(ctx, roomID)
}