feat(membership): room discovery — GET /members/{endpoint}/rooms + ListMyRooms
A peer invited to an encrypted room needs to find it: the control plane is
pull-based (no server push of invitations), so add a discovery endpoint that
lists every room an endpoint belongs to, with the room's metadata and the
endpoint's role.
- store.ListRoomsForEndpoint: JOIN members+rooms, ordered by room id, empty
slice (not error) for an endpoint in no rooms.
- membershipd: GET /members/{endpoint}/rooms returns {room_id, subject, epoch,
policy, role}[].
- client.ListMyRooms + RoomRef: a bot polls this to discover and then Join +
Subscribe rooms it was invited to.
Tests: store-level (owner in N rooms, member in one, unknown endpoint → []) and
client-level e2e through the embedded harness (B discovers a room A invited it
to, without prior knowledge of the room id; owner sees role=owner).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -219,6 +219,42 @@ func (s *Store) ListMembers(roomID string) ([]Member, error) {
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// RoomMembership is a room an endpoint belongs to, with that endpoint's role.
|
||||
// It is the per-endpoint view used for room discovery (a peer asking "which
|
||||
// rooms am I in?") so a freshly-invited member can find and join its rooms.
|
||||
type RoomMembership struct {
|
||||
RoomInfo
|
||||
Role string
|
||||
}
|
||||
|
||||
// ListRoomsForEndpoint returns every room the given endpoint is a member of,
|
||||
// with the room's current metadata and the endpoint's role, ordered by room id.
|
||||
// An endpoint that is in no rooms yields an empty slice (not an error).
|
||||
func (s *Store) ListRoomsForEndpoint(endpoint string) ([]RoomMembership, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT r.room_id, r.subject, r.key_epoch, r.encrypt, r.persist, r.sign_msgs, r.owner_endpoint, m.role
|
||||
FROM members m JOIN rooms r ON r.room_id = m.room_id
|
||||
WHERE m.endpoint = ? ORDER BY r.room_id`,
|
||||
endpoint,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("membership: list rooms for endpoint %q: %w", endpoint, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []RoomMembership
|
||||
for rows.Next() {
|
||||
var rm RoomMembership
|
||||
var enc, per, sgn int
|
||||
if err := rows.Scan(&rm.RoomID, &rm.Subject, &rm.Epoch, &enc, &per, &sgn, &rm.OwnerEndpoint, &rm.Role); err != nil {
|
||||
return nil, fmt.Errorf("membership: scan room membership: %w", err)
|
||||
}
|
||||
rm.Encrypt, rm.Persist, rm.SignMsgs = enc != 0, per != 0, sgn != 0
|
||||
out = append(out, rm)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// GetSealedKey returns the sealed room key for an endpoint at a given epoch.
|
||||
// If epoch <= 0, the latest epoch for that endpoint is returned.
|
||||
func (s *Store) GetSealedKey(roomID, endpoint string, epoch int) (int, []byte, error) {
|
||||
|
||||
Reference in New Issue
Block a user