1
0

Add spaces filtering to Sliding Sync /sync

Based on [MSC3575](https://github.com/matrix-org/matrix-spec-proposals/pull/3575): Sliding Sync
This commit is contained in:
Eric Eastwood
2024-05-30 15:45:52 -05:00
parent 6b1eba4fee
commit d477a63df6
4 changed files with 234 additions and 11 deletions

View File

@@ -452,11 +452,15 @@ class RoomSummaryHandler:
return _RoomEntry(room_id, room_entry)
# Otherwise, look for child rooms/spaces.
child_events = await self._get_child_events(room_id)
space_child_events = await self._get_space_child_events(room_id)
# Sort the results for stability.
space_child_events = sorted(
space_child_events, key=_child_events_comparison_key
)
if suggested_only:
# we only care about suggested children
child_events = filter(_is_suggested_child_event, child_events)
space_child_events = filter(_is_suggested_child_event, space_child_events)
stripped_events: List[JsonDict] = [
{
@@ -466,7 +470,7 @@ class RoomSummaryHandler:
"sender": e.sender,
"origin_server_ts": e.origin_server_ts,
}
for e in child_events
for e in space_child_events
]
return _RoomEntry(room_id, room_entry, stripped_events)
@@ -763,11 +767,9 @@ class RoomSummaryHandler:
return room_entry
async def _get_child_events(self, room_id: str) -> Iterable[EventBase]:
async def _get_space_child_events(self, room_id: str) -> Iterable[EventBase]:
"""
Get the child events for a given room.
The returned results are sorted for stability.
Get the space child events for a given room.
Args:
room_id: The room id to get the children of.
@@ -791,7 +793,9 @@ class RoomSummaryHandler:
# filter out any events without a "via" (which implies it has been redacted),
# and order to ensure we return stable results.
return sorted(filter(_has_valid_via, events), key=_child_events_comparison_key)
filtered_events = filter(_has_valid_via, events)
return filtered_events
async def get_room_summary(
self,

View File

@@ -184,11 +184,13 @@ class SlidingSyncResult:
class SlidingSyncHandler:
def __init__(self, hs: "HomeServer"):
self.hs_config = hs.config
self.rooms_to_exclude_globally = hs.config.server.rooms_to_exclude_from_sync
self.store = hs.get_datastores().main
self.storage_controllers = hs.get_storage_controllers()
self.auth_blocking = hs.get_auth_blocking()
self.notifier = hs.get_notifier()
self.event_sources = hs.get_event_sources()
self.rooms_to_exclude_globally = hs.config.server.rooms_to_exclude_from_sync
self.room_summary_handler = hs.get_room_summary_handler()
async def wait_for_sync_for_user(
self,
@@ -535,8 +537,42 @@ class SlidingSyncHandler:
# Only non-DM rooms please
filtered_room_id_set = filtered_room_id_set.difference(dm_room_id_set)
# Filter the room based on the space they belong to according to `m.space.child`
# state events. If multiple spaces are present, a room can be part of any one of
# the listed spaces (OR'd).
if filters.spaces:
raise NotImplementedError()
# Only use spaces that we're joined to to avoid leaking private space
# information that the user is not part of. We could probably allow
# public spaces here but the spec says "joined" only.
joined_space_room_ids = set()
for space_room_id in set(filters.spaces):
# TODO: Is there a good method to look up all space rooms at once? (N+1 query problem)
is_user_in_room = await self.store.check_local_user_in_room(
user_id=user.to_string(), room_id=space_room_id
)
if is_user_in_room:
joined_space_room_ids.add(space_room_id)
# Flatten the rooms in the spaces
space_child_room_ids = set()
for space_room_id in joined_space_room_ids:
space_child_events = (
await self.room_summary_handler._get_space_child_events(
space_room_id
)
)
space_child_room_ids.update(
event.state_key for event in space_child_events
)
# TODO: The spec says that if the child room has a `m.room.tombstone`
# event, we should recursively navigate until we find the latest room
# and include those IDs (although this point is under scrutiny).
# Only rooms in the spaces please
filtered_room_id_set = filtered_room_id_set.intersection(
space_child_room_ids
)
if filters.is_encrypted:
raise NotImplementedError()

View File

@@ -238,6 +238,53 @@ class SlidingSyncBody(RequestBodyModel):
"""
class Filters(RequestBodyModel):
"""
All fields are applied with AND operators, hence if `is_dm: True` and
`is_encrypted: True` then only Encrypted DM rooms will be returned. The absence
of fields implies no filter on that criteria: it does NOT imply `False`.
These fields may be expanded through use of extensions.
Attributes:
is_dm: Flag which only returns rooms present (or not) in the DM section
of account data. If unset, both DM rooms and non-DM rooms are returned.
If False, only non-DM rooms are returned. If True, only DM rooms are
returned.
spaces: Filter the room based on the space they belong to according to
`m.space.child` state events. If multiple spaces are present, a room can
be part of any one of the listed spaces (OR'd). The server will inspect
the `m.space.child` state events for the JOINED space room IDs given.
Servers MUST NOT navigate subspaces. It is up to the client to give a
complete list of spaces to navigate. Only rooms directly mentioned as
`m.space.child` events in these spaces will be returned. Unknown spaces
or spaces the user is not joined to will be ignored.
is_encrypted: Flag which only returns rooms which have an
`m.room.encryption` state event. If unset, both encrypted and
unencrypted rooms are returned. If `False`, only unencrypted rooms are
returned. If `True`, only encrypted rooms are returned.
is_invite: Flag which only returns rooms the user is currently invited
to. If unset, both invited and joined rooms are returned. If `False`, no
invited rooms are returned. If `True`, only invited rooms are returned.
room_types: If specified, only rooms where the `m.room.create` event has
a `type` matching one of the strings in this array will be returned. If
this field is unset, all rooms are returned regardless of type. This can
be used to get the initial set of spaces for an account. For rooms which
do not have a room type, use `null`/`None` to include them.
not_room_types: Same as `room_types` but inverted. This can be used to
filter out spaces from the room list. If a type is in both `room_types`
and `not_room_types`, then `not_room_types` wins and they are not included
in the result.
room_name_like: Filter the room name. Case-insensitive partial matching
e.g 'foo' matches 'abFooab'. The term 'like' is inspired by SQL 'LIKE',
and the text here is similar to '%foo%'.
tags: Filter the room based on its room tags. If multiple tags are
present, a room can have any one of the listed tags (OR'd).
not_tags: Filter the room based on its room tags. Takes priority over
`tags`. For example, a room with tags A and B with filters `tags: [A]`
`not_tags: [B]` would NOT be included because `not_tags` takes priority over
`tags`. This filter is useful if your rooms list does NOT include the
list of favourite rooms again.
"""
is_dm: Optional[StrictBool] = None
spaces: Optional[List[StrictStr]] = None
is_encrypted: Optional[StrictBool] = None

View File

@@ -1,8 +1,15 @@
from typing import List, Optional
import logging
from twisted.test.proto_helpers import MemoryReactor
from synapse.api.constants import AccountDataTypes, EventTypes, JoinRules
from synapse.api.constants import (
AccountDataTypes,
EventContentFields,
EventTypes,
JoinRules,
RoomTypes,
)
from synapse.api.room_versions import RoomVersions
from synapse.rest import admin
from synapse.rest.client import knock, login, room
@@ -607,6 +614,32 @@ class FilterRoomsTestCase(HomeserverTestCase):
return room_id
def _add_space_child(
self,
space_id: str,
room_id: str,
token: str,
order: Optional[str] = None,
via: Optional[List[str]] = None,
) -> None:
"""
Helper to add a child room to a space.
"""
if via is None:
via = [self.hs.hostname]
content: JsonDict = {"via": via}
if order is not None:
content["order"] = order
self.helper.send_state(
space_id,
event_type=EventTypes.SpaceChild,
body=content,
tok=token,
state_key=room_id,
)
def test_filter_dm_rooms(self) -> None:
"""
Test filter for DM rooms
@@ -688,3 +721,106 @@ class FilterRoomsTestCase(HomeserverTestCase):
)
self.assertEqual(filtered_room_ids, {room_id})
def test_filter_space_rooms(self) -> None:
"""
Test filter for non-DM rooms
"""
user1_id = self.register_user("user1", "pass")
user1_tok = self.login(user1_id, "pass")
space_a = self.helper.create_room_as(
user1_id,
tok=user1_tok,
extra_content={
"creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE}
},
)
space_b = self.helper.create_room_as(
user1_id,
tok=user1_tok,
extra_content={
"creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE}
},
)
space_c = self.helper.create_room_as(
user1_id,
tok=user1_tok,
extra_content={
"creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE}
},
)
room_id1 = self.helper.create_room_as(
user1_id,
is_public=False,
tok=user1_tok,
)
# Add to space_a
self._add_space_child(space_a, room_id1, user1_tok)
room_id2 = self.helper.create_room_as(
user1_id,
is_public=False,
tok=user1_tok,
)
# Add to space_a and space b
self._add_space_child(space_a, room_id2, user1_tok)
self._add_space_child(space_c, room_id2, user1_tok)
room_id3 = self.helper.create_room_as(
user1_id,
is_public=False,
tok=user1_tok,
)
# Add to all spaces
self._add_space_child(space_a, room_id3, user1_tok)
self._add_space_child(space_b, room_id3, user1_tok)
self._add_space_child(space_c, room_id3, user1_tok)
room_id4 = self.helper.create_room_as(
user1_id,
is_public=False,
tok=user1_tok,
)
# Add to space_c
self._add_space_child(space_c, room_id3, user1_tok)
room_not_in_space1 = self.helper.create_room_as(
user1_id,
is_public=False,
tok=user1_tok,
)
# TODO: Better way to avoid the circular import? (see
# https://github.com/element-hq/synapse/pull/17187#discussion_r1619492779)
from synapse.handlers.sliding_sync import SlidingSyncConfig
filters = SlidingSyncConfig.SlidingSyncList.Filters(
spaces=[
space_a,
space_b,
],
)
# Try filtering the rooms
filtered_room_ids = self.get_success(
self.sliding_sync_handler.filter_rooms(
UserID.from_string(user1_id),
{
# a
room_id1,
# a, c
room_id2,
# a, b, c
room_id3,
# c
room_id4,
# not in any space
room_not_in_space1,
},
filters,
)
)
self.assertEqual(filtered_room_ids, {room_id1, room_id2, room_id3})