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:
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user