Add rooms name and avatar to Sliding Sync /sync (#17418)
Some checks are pending
Build docker images / build (push) Waiting to run
Deploy the documentation / Calculate variables for GitHub Pages deployment (push) Waiting to run
Deploy the documentation / GitHub Pages (push) Blocked by required conditions
Build release artifacts / Calculate list of debian distros (push) Waiting to run
Build release artifacts / Build .deb packages (push) Blocked by required conditions
Build release artifacts / Build wheels on ${{ matrix.os }} for ${{ matrix.arch }} (aarch64, ${{ startsWith(github.ref, 'refs/pull/') }}, ubuntu-20.04) (push) Waiting to run
Build release artifacts / Build wheels on ${{ matrix.os }} for ${{ matrix.arch }} (x86_64, ${{ startsWith(github.ref, 'refs/pull/') }}, macos-12) (push) Waiting to run
Build release artifacts / Build wheels on ${{ matrix.os }} for ${{ matrix.arch }} (x86_64, ${{ startsWith(github.ref, 'refs/pull/') }}, ubuntu-20.04) (push) Waiting to run
Build release artifacts / Build sdist (push) Waiting to run
Build release artifacts / Attach assets to release (push) Blocked by required conditions
Tests / changes (push) Waiting to run
Tests / check-sampleconfig (push) Blocked by required conditions
Tests / check-schema-delta (push) Blocked by required conditions
Tests / check-lockfile (push) Waiting to run
Tests / lint (push) Blocked by required conditions
Tests / Typechecking (push) Blocked by required conditions
Tests / lint-crlf (push) Waiting to run
Tests / lint-newsfile (push) Waiting to run
Tests / lint-pydantic (push) Blocked by required conditions
Tests / lint-clippy (push) Blocked by required conditions
Tests / lint-clippy-nightly (push) Blocked by required conditions
Tests / lint-rustfmt (push) Blocked by required conditions
Tests / lint-readme (push) Blocked by required conditions
Tests / linting-done (push) Blocked by required conditions
Tests / calculate-test-jobs (push) Blocked by required conditions
Tests / trial (push) Blocked by required conditions
Tests / trial-olddeps (push) Blocked by required conditions
Tests / trial-pypy (all, pypy-3.8) (push) Blocked by required conditions
Tests / sytest (push) Blocked by required conditions
Tests / export-data (push) Blocked by required conditions
Tests / portdb (11, 3.8) (push) Blocked by required conditions
Tests / portdb (15, 3.11) (push) Blocked by required conditions
Tests / complement (monolith, Postgres) (push) Blocked by required conditions
Tests / complement (monolith, SQLite) (push) Blocked by required conditions
Tests / complement (workers, Postgres) (push) Blocked by required conditions
Tests / cargo-test (push) Blocked by required conditions
Tests / cargo-bench (push) Blocked by required conditions
Tests / tests-done (push) Blocked by required conditions

Based on [MSC3575](https://github.com/matrix-org/matrix-spec-proposals/pull/3575): Sliding Sync
This commit is contained in:
Eric Eastwood 2024-07-09 12:26:45 -05:00 committed by GitHub
parent d48061b7e6
commit 1cf3ff6b40
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 304 additions and 54 deletions

View file

@ -0,0 +1 @@
Populate `name`/`avatar` fields in experimental [MSC3575](https://github.com/matrix-org/matrix-spec-proposals/pull/3575) Sliding Sync `/sync` endpoint.

View file

@ -18,6 +18,7 @@
# #
# #
import logging import logging
from itertools import chain
from typing import TYPE_CHECKING, Any, Dict, Final, List, Optional, Set, Tuple from typing import TYPE_CHECKING, Any, Dict, Final, List, Optional, Set, Tuple
import attr import attr
@ -464,6 +465,7 @@ class SlidingSyncHandler:
membership_state_keys = room_sync_config.required_state_map.get( membership_state_keys = room_sync_config.required_state_map.get(
EventTypes.Member EventTypes.Member
) )
# Also see `StateFilter.must_await_full_state(...)` for comparison
lazy_loading = ( lazy_loading = (
membership_state_keys is not None membership_state_keys is not None
and len(membership_state_keys) == 1 and len(membership_state_keys) == 1
@ -1202,7 +1204,7 @@ class SlidingSyncHandler:
# Figure out any stripped state events for invite/knocks. This allows the # Figure out any stripped state events for invite/knocks. This allows the
# potential joiner to identify the room. # potential joiner to identify the room.
stripped_state: List[JsonDict] = [] stripped_state: Optional[List[JsonDict]] = None
if room_membership_for_user_at_to_token.membership in ( if room_membership_for_user_at_to_token.membership in (
Membership.INVITE, Membership.INVITE,
Membership.KNOCK, Membership.KNOCK,
@ -1239,7 +1241,7 @@ class SlidingSyncHandler:
# updates. # updates.
initial = True initial = True
# Fetch the required state for the room # Fetch the `required_state` for the room
# #
# No `required_state` for invite/knock rooms (just `stripped_state`) # No `required_state` for invite/knock rooms (just `stripped_state`)
# #
@ -1247,13 +1249,15 @@ class SlidingSyncHandler:
# of membership. Currently, we have to make this optional because # of membership. Currently, we have to make this optional because
# `invite`/`knock` rooms only have `stripped_state`. See # `invite`/`knock` rooms only have `stripped_state`. See
# https://github.com/matrix-org/matrix-spec-proposals/pull/3575#discussion_r1653045932 # https://github.com/matrix-org/matrix-spec-proposals/pull/3575#discussion_r1653045932
#
# Calculate the `StateFilter` based on the `required_state` for the room
room_state: Optional[StateMap[EventBase]] = None room_state: Optional[StateMap[EventBase]] = None
required_room_state: Optional[StateMap[EventBase]] = None
if room_membership_for_user_at_to_token.membership not in ( if room_membership_for_user_at_to_token.membership not in (
Membership.INVITE, Membership.INVITE,
Membership.KNOCK, Membership.KNOCK,
): ):
# Calculate the `StateFilter` based on the `required_state` for the room required_state_filter = StateFilter.none()
state_filter: Optional[StateFilter] = StateFilter.none()
# If we have a double wildcard ("*", "*") in the `required_state`, we need # If we have a double wildcard ("*", "*") in the `required_state`, we need
# to fetch all state for the room # to fetch all state for the room
# #
@ -1276,7 +1280,7 @@ class SlidingSyncHandler:
if StateValues.WILDCARD in room_sync_config.required_state_map.get( if StateValues.WILDCARD in room_sync_config.required_state_map.get(
StateValues.WILDCARD, set() StateValues.WILDCARD, set()
): ):
state_filter = StateFilter.all() required_state_filter = StateFilter.all()
# TODO: `StateFilter` currently doesn't support wildcard event types. We're # TODO: `StateFilter` currently doesn't support wildcard event types. We're
# currently working around this by returning all state to the client but it # currently working around this by returning all state to the client but it
# would be nice to fetch less from the database and return just what the # would be nice to fetch less from the database and return just what the
@ -1285,7 +1289,7 @@ class SlidingSyncHandler:
room_sync_config.required_state_map.get(StateValues.WILDCARD) room_sync_config.required_state_map.get(StateValues.WILDCARD)
is not None is not None
): ):
state_filter = StateFilter.all() required_state_filter = StateFilter.all()
else: else:
required_state_types: List[Tuple[str, Optional[str]]] = [] required_state_types: List[Tuple[str, Optional[str]]] = []
for ( for (
@ -1317,51 +1321,88 @@ class SlidingSyncHandler:
else: else:
required_state_types.append((state_type, state_key)) required_state_types.append((state_type, state_key))
state_filter = StateFilter.from_types(required_state_types) required_state_filter = StateFilter.from_types(required_state_types)
# We can skip fetching state if we don't need any # We need this base set of info for the response so let's just fetch it along
if state_filter != StateFilter.none(): # with the `required_state` for the room
# We can return all of the state that was requested if we're doing an META_ROOM_STATE = [(EventTypes.Name, ""), (EventTypes.RoomAvatar, "")]
# initial sync state_filter = StateFilter(
if initial: types=StateFilter.from_types(
# People shouldn't see past their leave/ban event chain(META_ROOM_STATE, required_state_filter.to_types())
if room_membership_for_user_at_to_token.membership in ( ).types,
Membership.LEAVE, include_others=required_state_filter.include_others,
Membership.BAN, )
):
room_state = await self.storage_controllers.state.get_state_at( # We can return all of the state that was requested if this was the first
room_id, # time we've sent the room down this connection.
stream_position=to_token.copy_and_replace( if initial:
StreamKeyType.ROOM, # People shouldn't see past their leave/ban event
room_membership_for_user_at_to_token.event_pos.to_room_stream_token(), if room_membership_for_user_at_to_token.membership in (
), Membership.LEAVE,
state_filter=state_filter, Membership.BAN,
# Partially-stated rooms should have all state events except for ):
# the membership events and since we've already excluded room_state = await self.storage_controllers.state.get_state_at(
# partially-stated rooms unless `required_state` only has room_id,
# `["m.room.member", "$LAZY"]` for membership, we should be able stream_position=to_token.copy_and_replace(
# to retrieve everything requested. Plus we don't want to block StreamKeyType.ROOM,
# the whole sync waiting for this one room. room_membership_for_user_at_to_token.event_pos.to_room_stream_token(),
await_full_state=False, ),
) state_filter=state_filter,
# Otherwise, we can get the latest current state in the room # Partially-stated rooms should have all state events except for
else: # remote membership events. Since we've already excluded
room_state = await self.storage_controllers.state.get_current_state( # partially-stated rooms unless `required_state` only has
room_id, # `["m.room.member", "$LAZY"]` for membership, we should be able to
state_filter, # retrieve everything requested. When we're lazy-loading, if there
# Partially-stated rooms should have all state events except for # are some remote senders in the timeline, we should also have their
# the membership events and since we've already excluded # membership event because we had to auth that timeline event. Plus
# partially-stated rooms unless `required_state` only has # we don't want to block the whole sync waiting for this one room.
# `["m.room.member", "$LAZY"]` for membership, we should be able await_full_state=False,
# to retrieve everything requested. Plus we don't want to block )
# the whole sync waiting for this one room. # Otherwise, we can get the latest current state in the room
await_full_state=False,
)
# TODO: Query `current_state_delta_stream` and reverse/rewind back to the `to_token`
else: else:
# TODO: Once we can figure out if we've sent a room down this connection before, room_state = await self.storage_controllers.state.get_current_state(
# we can return updates instead of the full required state. room_id,
raise NotImplementedError() state_filter,
# Partially-stated rooms should have all state events except for
# remote membership events. Since we've already excluded
# partially-stated rooms unless `required_state` only has
# `["m.room.member", "$LAZY"]` for membership, we should be able to
# retrieve everything requested. When we're lazy-loading, if there
# are some remote senders in the timeline, we should also have their
# membership event because we had to auth that timeline event. Plus
# we don't want to block the whole sync waiting for this one room.
await_full_state=False,
)
# TODO: Query `current_state_delta_stream` and reverse/rewind back to the `to_token`
else:
# TODO: Once we can figure out if we've sent a room down this connection before,
# we can return updates instead of the full required state.
raise NotImplementedError()
if required_state_filter != StateFilter.none():
required_room_state = required_state_filter.filter_state(room_state)
# Find the room name and avatar from the state
room_name: Optional[str] = None
room_avatar: Optional[str] = None
if room_state is not None:
name_event = room_state.get((EventTypes.Name, ""))
if name_event is not None:
room_name = name_event.content.get("name")
avatar_event = room_state.get((EventTypes.RoomAvatar, ""))
if avatar_event is not None:
room_avatar = avatar_event.content.get("url")
elif stripped_state is not None:
for event in stripped_state:
if event["type"] == EventTypes.Name:
room_name = event.get("content", {}).get("name")
elif event["type"] == EventTypes.RoomAvatar:
room_avatar = event.get("content", {}).get("url")
# Found everything so we can stop looking
if room_name is not None and room_avatar is not None:
break
# Figure out the last bump event in the room # Figure out the last bump event in the room
last_bump_event_result = ( last_bump_event_result = (
@ -1378,16 +1419,16 @@ class SlidingSyncHandler:
bump_stamp = bump_event_pos.stream bump_stamp = bump_event_pos.stream
return SlidingSyncResult.RoomResult( return SlidingSyncResult.RoomResult(
# TODO: Dummy value name=room_name,
name=None, avatar=room_avatar,
# TODO: Dummy value
avatar=None,
# TODO: Dummy value # TODO: Dummy value
heroes=None, heroes=None,
# TODO: Dummy value # TODO: Dummy value
is_dm=False, is_dm=False,
initial=initial, initial=initial,
required_state=list(room_state.values()) if room_state else None, required_state=(
list(required_room_state.values()) if required_room_state else None
),
timeline_events=timeline_events, timeline_events=timeline_events,
bundled_aggregations=bundled_aggregations, bundled_aggregations=bundled_aggregations,
stripped_state=stripped_state, stripped_state=stripped_state,

View file

@ -1802,6 +1802,206 @@ class SlidingSyncTestCase(unittest.HomeserverTestCase):
channel.json_body["lists"]["foo-list"], channel.json_body["lists"]["foo-list"],
) )
def test_rooms_meta_when_joined(self) -> None:
"""
Test that the `rooms` `name` and `avatar` (soon to test `heroes`) are included
in the response when the user is joined to the room.
"""
user1_id = self.register_user("user1", "pass")
user1_tok = self.login(user1_id, "pass")
user2_id = self.register_user("user2", "pass")
user2_tok = self.login(user2_id, "pass")
room_id1 = self.helper.create_room_as(
user2_id,
tok=user2_tok,
extra_content={
"name": "my super room",
},
)
# Set the room avatar URL
self.helper.send_state(
room_id1,
EventTypes.RoomAvatar,
{"url": "mxc://DUMMY_MEDIA_ID"},
tok=user2_tok,
)
self.helper.join(room_id1, user1_id, tok=user1_tok)
# Make the Sliding Sync request
channel = self.make_request(
"POST",
self.sync_endpoint,
{
"lists": {
"foo-list": {
"ranges": [[0, 1]],
"required_state": [],
"timeline_limit": 0,
}
}
},
access_token=user1_tok,
)
self.assertEqual(channel.code, 200, channel.json_body)
# Reflect the current state of the room
self.assertEqual(
channel.json_body["rooms"][room_id1]["name"],
"my super room",
channel.json_body["rooms"][room_id1],
)
self.assertEqual(
channel.json_body["rooms"][room_id1]["avatar"],
"mxc://DUMMY_MEDIA_ID",
channel.json_body["rooms"][room_id1],
)
def test_rooms_meta_when_invited(self) -> None:
"""
Test that the `rooms` `name` and `avatar` (soon to test `heroes`) are included
in the response when the user is invited to the room.
"""
user1_id = self.register_user("user1", "pass")
user1_tok = self.login(user1_id, "pass")
user2_id = self.register_user("user2", "pass")
user2_tok = self.login(user2_id, "pass")
room_id1 = self.helper.create_room_as(
user2_id,
tok=user2_tok,
extra_content={
"name": "my super room",
},
)
# Set the room avatar URL
self.helper.send_state(
room_id1,
EventTypes.RoomAvatar,
{"url": "mxc://DUMMY_MEDIA_ID"},
tok=user2_tok,
)
self.helper.join(room_id1, user1_id, tok=user1_tok)
# Update the room name after user1 has left
self.helper.send_state(
room_id1,
EventTypes.Name,
{"name": "my super duper room"},
tok=user2_tok,
)
# Update the room avatar URL after user1 has left
self.helper.send_state(
room_id1,
EventTypes.RoomAvatar,
{"url": "mxc://UPDATED_DUMMY_MEDIA_ID"},
tok=user2_tok,
)
# Make the Sliding Sync request
channel = self.make_request(
"POST",
self.sync_endpoint,
{
"lists": {
"foo-list": {
"ranges": [[0, 1]],
"required_state": [],
"timeline_limit": 0,
}
}
},
access_token=user1_tok,
)
self.assertEqual(channel.code, 200, channel.json_body)
# This should still reflect the current state of the room even when the user is
# invited.
self.assertEqual(
channel.json_body["rooms"][room_id1]["name"],
"my super duper room",
channel.json_body["rooms"][room_id1],
)
self.assertEqual(
channel.json_body["rooms"][room_id1]["avatar"],
"mxc://UPDATED_DUMMY_MEDIA_ID",
channel.json_body["rooms"][room_id1],
)
def test_rooms_meta_when_banned(self) -> None:
"""
Test that the `rooms` `name` and `avatar` (soon to test `heroes`) reflect the
state of the room when the user was banned (do not leak current state).
"""
user1_id = self.register_user("user1", "pass")
user1_tok = self.login(user1_id, "pass")
user2_id = self.register_user("user2", "pass")
user2_tok = self.login(user2_id, "pass")
room_id1 = self.helper.create_room_as(
user2_id,
tok=user2_tok,
extra_content={
"name": "my super room",
},
)
# Set the room avatar URL
self.helper.send_state(
room_id1,
EventTypes.RoomAvatar,
{"url": "mxc://DUMMY_MEDIA_ID"},
tok=user2_tok,
)
self.helper.join(room_id1, user1_id, tok=user1_tok)
self.helper.ban(room_id1, src=user2_id, targ=user1_id, tok=user2_tok)
# Update the room name after user1 has left
self.helper.send_state(
room_id1,
EventTypes.Name,
{"name": "my super duper room"},
tok=user2_tok,
)
# Update the room avatar URL after user1 has left
self.helper.send_state(
room_id1,
EventTypes.RoomAvatar,
{"url": "mxc://UPDATED_DUMMY_MEDIA_ID"},
tok=user2_tok,
)
# Make the Sliding Sync request
channel = self.make_request(
"POST",
self.sync_endpoint,
{
"lists": {
"foo-list": {
"ranges": [[0, 1]],
"required_state": [],
"timeline_limit": 0,
}
}
},
access_token=user1_tok,
)
self.assertEqual(channel.code, 200, channel.json_body)
# Reflect the state of the room at the time of leaving
self.assertEqual(
channel.json_body["rooms"][room_id1]["name"],
"my super room",
channel.json_body["rooms"][room_id1],
)
self.assertEqual(
channel.json_body["rooms"][room_id1]["avatar"],
"mxc://DUMMY_MEDIA_ID",
channel.json_body["rooms"][room_id1],
)
def test_rooms_limited_initial_sync(self) -> None: def test_rooms_limited_initial_sync(self) -> None:
""" """
Test that we mark `rooms` as `limited=True` when we saturate the `timeline_limit` Test that we mark `rooms` as `limited=True` when we saturate the `timeline_limit`
@ -2973,6 +3173,7 @@ class SlidingSyncTestCase(unittest.HomeserverTestCase):
}, },
exact=True, exact=True,
) )
self.assertIsNone(channel.json_body["rooms"][room_id1].get("invite_state"))
def test_rooms_required_state_incremental_sync(self) -> None: def test_rooms_required_state_incremental_sync(self) -> None:
""" """
@ -3027,6 +3228,7 @@ class SlidingSyncTestCase(unittest.HomeserverTestCase):
}, },
exact=True, exact=True,
) )
self.assertIsNone(channel.json_body["rooms"][room_id1].get("invite_state"))
def test_rooms_required_state_wildcard(self) -> None: def test_rooms_required_state_wildcard(self) -> None:
""" """
@ -3084,6 +3286,7 @@ class SlidingSyncTestCase(unittest.HomeserverTestCase):
state_map.values(), state_map.values(),
exact=True, exact=True,
) )
self.assertIsNone(channel.json_body["rooms"][room_id1].get("invite_state"))
def test_rooms_required_state_wildcard_event_type(self) -> None: def test_rooms_required_state_wildcard_event_type(self) -> None:
""" """
@ -3147,6 +3350,7 @@ class SlidingSyncTestCase(unittest.HomeserverTestCase):
# events when the `event_type` is a wildcard. # events when the `event_type` is a wildcard.
exact=False, exact=False,
) )
self.assertIsNone(channel.json_body["rooms"][room_id1].get("invite_state"))
def test_rooms_required_state_wildcard_state_key(self) -> None: def test_rooms_required_state_wildcard_state_key(self) -> None:
""" """
@ -3192,6 +3396,7 @@ class SlidingSyncTestCase(unittest.HomeserverTestCase):
}, },
exact=True, exact=True,
) )
self.assertIsNone(channel.json_body["rooms"][room_id1].get("invite_state"))
def test_rooms_required_state_lazy_loading_room_members(self) -> None: def test_rooms_required_state_lazy_loading_room_members(self) -> None:
""" """
@ -3247,6 +3452,7 @@ class SlidingSyncTestCase(unittest.HomeserverTestCase):
}, },
exact=True, exact=True,
) )
self.assertIsNone(channel.json_body["rooms"][room_id1].get("invite_state"))
@parameterized.expand([(Membership.LEAVE,), (Membership.BAN,)]) @parameterized.expand([(Membership.LEAVE,), (Membership.BAN,)])
def test_rooms_required_state_leave_ban(self, stop_membership: str) -> None: def test_rooms_required_state_leave_ban(self, stop_membership: str) -> None:
@ -3329,6 +3535,7 @@ class SlidingSyncTestCase(unittest.HomeserverTestCase):
}, },
exact=True, exact=True,
) )
self.assertIsNone(channel.json_body["rooms"][room_id1].get("invite_state"))
def test_rooms_required_state_combine_superset(self) -> None: def test_rooms_required_state_combine_superset(self) -> None:
""" """
@ -3401,6 +3608,7 @@ class SlidingSyncTestCase(unittest.HomeserverTestCase):
}, },
exact=True, exact=True,
) )
self.assertIsNone(channel.json_body["rooms"][room_id1].get("invite_state"))
def test_rooms_required_state_partial_state(self) -> None: def test_rooms_required_state_partial_state(self) -> None:
""" """