Skip to content

Commit 5a97bbd

Browse files
Add heroes and room summary fields to Sliding Sync /sync (#17419)
Additional room summary fields: `joined_count`, `invited_count` Based on [MSC3575](matrix-org/matrix-spec-proposals#3575): Sliding Sync
1 parent 606da39 commit 5a97bbd

File tree

6 files changed

+529
-110
lines changed

6 files changed

+529
-110
lines changed

changelog.d/17419.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Populate `heroes` and room summary fields (`joined_count`, `invited_count`) in experimental [MSC3575](https://github.com/matrix-org/matrix-spec-proposals/pull/3575) Sliding Sync `/sync` endpoint.

synapse/handlers/sliding_sync.py

Lines changed: 197 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
#
2020
import logging
2121
from itertools import chain
22-
from typing import TYPE_CHECKING, Any, Dict, Final, List, Optional, Set, Tuple
22+
from typing import TYPE_CHECKING, Any, Dict, Final, List, Mapping, Optional, Set, Tuple
2323

2424
import attr
2525
from immutabledict import immutabledict
@@ -28,7 +28,9 @@
2828
from synapse.events import EventBase
2929
from synapse.events.utils import strip_event
3030
from synapse.handlers.relations import BundledAggregations
31+
from synapse.storage.databases.main.roommember import extract_heroes_from_room_summary
3132
from synapse.storage.databases.main.stream import CurrentStateDeltaMembership
33+
from synapse.storage.roommember import MemberSummary
3234
from synapse.types import (
3335
JsonDict,
3436
PersistedEventPosition,
@@ -1043,6 +1045,103 @@ async def sort_rooms(
10431045
reverse=True,
10441046
)
10451047

1048+
async def get_current_state_ids_at(
1049+
self,
1050+
room_id: str,
1051+
room_membership_for_user_at_to_token: _RoomMembershipForUser,
1052+
state_filter: StateFilter,
1053+
to_token: StreamToken,
1054+
) -> StateMap[str]:
1055+
"""
1056+
Get current state IDs for the user in the room according to their membership. This
1057+
will be the current state at the time of their LEAVE/BAN, otherwise will be the
1058+
current state <= to_token.
1059+
1060+
Args:
1061+
room_id: The room ID to fetch data for
1062+
room_membership_for_user_at_token: Membership information for the user
1063+
in the room at the time of `to_token`.
1064+
to_token: The point in the stream to sync up to.
1065+
"""
1066+
1067+
room_state_ids: StateMap[str]
1068+
# People shouldn't see past their leave/ban event
1069+
if room_membership_for_user_at_to_token.membership in (
1070+
Membership.LEAVE,
1071+
Membership.BAN,
1072+
):
1073+
# TODO: `get_state_ids_at(...)` doesn't take into account the "current state"
1074+
room_state_ids = await self.storage_controllers.state.get_state_ids_at(
1075+
room_id,
1076+
stream_position=to_token.copy_and_replace(
1077+
StreamKeyType.ROOM,
1078+
room_membership_for_user_at_to_token.event_pos.to_room_stream_token(),
1079+
),
1080+
state_filter=state_filter,
1081+
# Partially-stated rooms should have all state events except for
1082+
# remote membership events. Since we've already excluded
1083+
# partially-stated rooms unless `required_state` only has
1084+
# `["m.room.member", "$LAZY"]` for membership, we should be able to
1085+
# retrieve everything requested. When we're lazy-loading, if there
1086+
# are some remote senders in the timeline, we should also have their
1087+
# membership event because we had to auth that timeline event. Plus
1088+
# we don't want to block the whole sync waiting for this one room.
1089+
await_full_state=False,
1090+
)
1091+
# Otherwise, we can get the latest current state in the room
1092+
else:
1093+
room_state_ids = await self.storage_controllers.state.get_current_state_ids(
1094+
room_id,
1095+
state_filter,
1096+
# Partially-stated rooms should have all state events except for
1097+
# remote membership events. Since we've already excluded
1098+
# partially-stated rooms unless `required_state` only has
1099+
# `["m.room.member", "$LAZY"]` for membership, we should be able to
1100+
# retrieve everything requested. When we're lazy-loading, if there
1101+
# are some remote senders in the timeline, we should also have their
1102+
# membership event because we had to auth that timeline event. Plus
1103+
# we don't want to block the whole sync waiting for this one room.
1104+
await_full_state=False,
1105+
)
1106+
# TODO: Query `current_state_delta_stream` and reverse/rewind back to the `to_token`
1107+
1108+
return room_state_ids
1109+
1110+
async def get_current_state_at(
1111+
self,
1112+
room_id: str,
1113+
room_membership_for_user_at_to_token: _RoomMembershipForUser,
1114+
state_filter: StateFilter,
1115+
to_token: StreamToken,
1116+
) -> StateMap[EventBase]:
1117+
"""
1118+
Get current state for the user in the room according to their membership. This
1119+
will be the current state at the time of their LEAVE/BAN, otherwise will be the
1120+
current state <= to_token.
1121+
1122+
Args:
1123+
room_id: The room ID to fetch data for
1124+
room_membership_for_user_at_token: Membership information for the user
1125+
in the room at the time of `to_token`.
1126+
to_token: The point in the stream to sync up to.
1127+
"""
1128+
room_state_ids = await self.get_current_state_ids_at(
1129+
room_id=room_id,
1130+
room_membership_for_user_at_to_token=room_membership_for_user_at_to_token,
1131+
state_filter=state_filter,
1132+
to_token=to_token,
1133+
)
1134+
1135+
event_map = await self.store.get_events(list(room_state_ids.values()))
1136+
1137+
state_map = {}
1138+
for key, event_id in room_state_ids.items():
1139+
event = event_map.get(event_id)
1140+
if event:
1141+
state_map[key] = event
1142+
1143+
return state_map
1144+
10461145
async def get_room_sync_data(
10471146
self,
10481147
user: UserID,
@@ -1074,7 +1173,7 @@ async def get_room_sync_data(
10741173
# membership. Currently, we have to make all of these optional because
10751174
# `invite`/`knock` rooms only have `stripped_state`. See
10761175
# https://github.com/matrix-org/matrix-spec-proposals/pull/3575#discussion_r1653045932
1077-
timeline_events: Optional[List[EventBase]] = None
1176+
timeline_events: List[EventBase] = []
10781177
bundled_aggregations: Optional[Dict[str, BundledAggregations]] = None
10791178
limited: Optional[bool] = None
10801179
prev_batch_token: Optional[StreamToken] = None
@@ -1206,7 +1305,7 @@ async def get_room_sync_data(
12061305

12071306
# Figure out any stripped state events for invite/knocks. This allows the
12081307
# potential joiner to identify the room.
1209-
stripped_state: Optional[List[JsonDict]] = None
1308+
stripped_state: List[JsonDict] = []
12101309
if room_membership_for_user_at_to_token.membership in (
12111310
Membership.INVITE,
12121311
Membership.KNOCK,
@@ -1243,6 +1342,44 @@ async def get_room_sync_data(
12431342
# updates.
12441343
initial = True
12451344

1345+
# Check whether the room has a name set
1346+
name_state_ids = await self.get_current_state_ids_at(
1347+
room_id=room_id,
1348+
room_membership_for_user_at_to_token=room_membership_for_user_at_to_token,
1349+
state_filter=StateFilter.from_types([(EventTypes.Name, "")]),
1350+
to_token=to_token,
1351+
)
1352+
name_event_id = name_state_ids.get((EventTypes.Name, ""))
1353+
1354+
room_membership_summary: Mapping[str, MemberSummary]
1355+
empty_membership_summary = MemberSummary([], 0)
1356+
if room_membership_for_user_at_to_token.membership in (
1357+
Membership.LEAVE,
1358+
Membership.BAN,
1359+
):
1360+
# TODO: Figure out how to get the membership summary for left/banned rooms
1361+
room_membership_summary = {}
1362+
else:
1363+
room_membership_summary = await self.store.get_room_summary(room_id)
1364+
# TODO: Reverse/rewind back to the `to_token`
1365+
1366+
# `heroes` are required if the room name is not set.
1367+
#
1368+
# Note: When you're the first one on your server to be invited to a new room
1369+
# over federation, we only have access to some stripped state in
1370+
# `event.unsigned.invite_room_state` which currently doesn't include `heroes`,
1371+
# see https://github.com/matrix-org/matrix-spec/issues/380. This means that
1372+
# clients won't be able to calculate the room name when necessary and just a
1373+
# pitfall we have to deal with until that spec issue is resolved.
1374+
hero_user_ids: List[str] = []
1375+
# TODO: Should we also check for `EventTypes.CanonicalAlias`
1376+
# (`m.room.canonical_alias`) as a fallback for the room name? see
1377+
# https://github.com/matrix-org/matrix-spec-proposals/pull/3575#discussion_r1671260153
1378+
if name_event_id is None:
1379+
hero_user_ids = extract_heroes_from_room_summary(
1380+
room_membership_summary, me=user.to_string()
1381+
)
1382+
12461383
# Fetch the `required_state` for the room
12471384
#
12481385
# No `required_state` for invite/knock rooms (just `stripped_state`)
@@ -1253,13 +1390,11 @@ async def get_room_sync_data(
12531390
# https://github.com/matrix-org/matrix-spec-proposals/pull/3575#discussion_r1653045932
12541391
#
12551392
# Calculate the `StateFilter` based on the `required_state` for the room
1256-
room_state: Optional[StateMap[EventBase]] = None
1257-
required_room_state: Optional[StateMap[EventBase]] = None
1393+
required_state_filter = StateFilter.none()
12581394
if room_membership_for_user_at_to_token.membership not in (
12591395
Membership.INVITE,
12601396
Membership.KNOCK,
12611397
):
1262-
required_state_filter = StateFilter.none()
12631398
# If we have a double wildcard ("*", "*") in the `required_state`, we need
12641399
# to fetch all state for the room
12651400
#
@@ -1325,86 +1460,65 @@ async def get_room_sync_data(
13251460

13261461
required_state_filter = StateFilter.from_types(required_state_types)
13271462

1328-
# We need this base set of info for the response so let's just fetch it along
1329-
# with the `required_state` for the room
1330-
META_ROOM_STATE = [(EventTypes.Name, ""), (EventTypes.RoomAvatar, "")]
1463+
# We need this base set of info for the response so let's just fetch it along
1464+
# with the `required_state` for the room
1465+
meta_room_state = [(EventTypes.Name, ""), (EventTypes.RoomAvatar, "")] + [
1466+
(EventTypes.Member, hero_user_id) for hero_user_id in hero_user_ids
1467+
]
1468+
state_filter = StateFilter.all()
1469+
if required_state_filter != StateFilter.all():
13311470
state_filter = StateFilter(
13321471
types=StateFilter.from_types(
1333-
chain(META_ROOM_STATE, required_state_filter.to_types())
1472+
chain(meta_room_state, required_state_filter.to_types())
13341473
).types,
13351474
include_others=required_state_filter.include_others,
13361475
)
13371476

1338-
# We can return all of the state that was requested if this was the first
1339-
# time we've sent the room down this connection.
1340-
if initial:
1341-
# People shouldn't see past their leave/ban event
1342-
if room_membership_for_user_at_to_token.membership in (
1343-
Membership.LEAVE,
1344-
Membership.BAN,
1345-
):
1346-
room_state = await self.storage_controllers.state.get_state_at(
1347-
room_id,
1348-
stream_position=to_token.copy_and_replace(
1349-
StreamKeyType.ROOM,
1350-
room_membership_for_user_at_to_token.event_pos.to_room_stream_token(),
1351-
),
1352-
state_filter=state_filter,
1353-
# Partially-stated rooms should have all state events except for
1354-
# remote membership events. Since we've already excluded
1355-
# partially-stated rooms unless `required_state` only has
1356-
# `["m.room.member", "$LAZY"]` for membership, we should be able to
1357-
# retrieve everything requested. When we're lazy-loading, if there
1358-
# are some remote senders in the timeline, we should also have their
1359-
# membership event because we had to auth that timeline event. Plus
1360-
# we don't want to block the whole sync waiting for this one room.
1361-
await_full_state=False,
1362-
)
1363-
# Otherwise, we can get the latest current state in the room
1364-
else:
1365-
room_state = await self.storage_controllers.state.get_current_state(
1366-
room_id,
1367-
state_filter,
1368-
# Partially-stated rooms should have all state events except for
1369-
# remote membership events. Since we've already excluded
1370-
# partially-stated rooms unless `required_state` only has
1371-
# `["m.room.member", "$LAZY"]` for membership, we should be able to
1372-
# retrieve everything requested. When we're lazy-loading, if there
1373-
# are some remote senders in the timeline, we should also have their
1374-
# membership event because we had to auth that timeline event. Plus
1375-
# we don't want to block the whole sync waiting for this one room.
1376-
await_full_state=False,
1377-
)
1378-
# TODO: Query `current_state_delta_stream` and reverse/rewind back to the `to_token`
1379-
else:
1380-
# TODO: Once we can figure out if we've sent a room down this connection before,
1381-
# we can return updates instead of the full required state.
1382-
raise NotImplementedError()
1477+
# We can return all of the state that was requested if this was the first
1478+
# time we've sent the room down this connection.
1479+
room_state: StateMap[EventBase] = {}
1480+
if initial:
1481+
room_state = await self.get_current_state_at(
1482+
room_id=room_id,
1483+
room_membership_for_user_at_to_token=room_membership_for_user_at_to_token,
1484+
state_filter=state_filter,
1485+
to_token=to_token,
1486+
)
1487+
else:
1488+
# TODO: Once we can figure out if we've sent a room down this connection before,
1489+
# we can return updates instead of the full required state.
1490+
raise NotImplementedError()
13831491

1384-
if required_state_filter != StateFilter.none():
1385-
required_room_state = required_state_filter.filter_state(room_state)
1492+
required_room_state: StateMap[EventBase] = {}
1493+
if required_state_filter != StateFilter.none():
1494+
required_room_state = required_state_filter.filter_state(room_state)
13861495

13871496
# Find the room name and avatar from the state
13881497
room_name: Optional[str] = None
1498+
# TODO: Should we also check for `EventTypes.CanonicalAlias`
1499+
# (`m.room.canonical_alias`) as a fallback for the room name? see
1500+
# https://github.com/matrix-org/matrix-spec-proposals/pull/3575#discussion_r1671260153
1501+
name_event = room_state.get((EventTypes.Name, ""))
1502+
if name_event is not None:
1503+
room_name = name_event.content.get("name")
1504+
13891505
room_avatar: Optional[str] = None
1390-
if room_state is not None:
1391-
name_event = room_state.get((EventTypes.Name, ""))
1392-
if name_event is not None:
1393-
room_name = name_event.content.get("name")
1394-
1395-
avatar_event = room_state.get((EventTypes.RoomAvatar, ""))
1396-
if avatar_event is not None:
1397-
room_avatar = avatar_event.content.get("url")
1398-
elif stripped_state is not None:
1399-
for event in stripped_state:
1400-
if event["type"] == EventTypes.Name:
1401-
room_name = event.get("content", {}).get("name")
1402-
elif event["type"] == EventTypes.RoomAvatar:
1403-
room_avatar = event.get("content", {}).get("url")
1404-
1405-
# Found everything so we can stop looking
1406-
if room_name is not None and room_avatar is not None:
1407-
break
1506+
avatar_event = room_state.get((EventTypes.RoomAvatar, ""))
1507+
if avatar_event is not None:
1508+
room_avatar = avatar_event.content.get("url")
1509+
1510+
# Assemble heroes: extract the info from the state we just fetched
1511+
heroes: List[SlidingSyncResult.RoomResult.StrippedHero] = []
1512+
for hero_user_id in hero_user_ids:
1513+
member_event = room_state.get((EventTypes.Member, hero_user_id))
1514+
if member_event is not None:
1515+
heroes.append(
1516+
SlidingSyncResult.RoomResult.StrippedHero(
1517+
user_id=hero_user_id,
1518+
display_name=member_event.content.get("displayname"),
1519+
avatar_url=member_event.content.get("avatar_url"),
1520+
)
1521+
)
14081522

14091523
# Figure out the last bump event in the room
14101524
last_bump_event_result = (
@@ -1423,24 +1537,24 @@ async def get_room_sync_data(
14231537
return SlidingSyncResult.RoomResult(
14241538
name=room_name,
14251539
avatar=room_avatar,
1426-
# TODO: Dummy value
1427-
heroes=None,
1540+
heroes=heroes,
14281541
# TODO: Dummy value
14291542
is_dm=False,
14301543
initial=initial,
1431-
required_state=(
1432-
list(required_room_state.values()) if required_room_state else None
1433-
),
1544+
required_state=list(required_room_state.values()),
14341545
timeline_events=timeline_events,
14351546
bundled_aggregations=bundled_aggregations,
14361547
stripped_state=stripped_state,
14371548
prev_batch=prev_batch_token,
14381549
limited=limited,
14391550
num_live=num_live,
14401551
bump_stamp=bump_stamp,
1441-
# TODO: Dummy values
1442-
joined_count=0,
1443-
invited_count=0,
1552+
joined_count=room_membership_summary.get(
1553+
Membership.JOIN, empty_membership_summary
1554+
).count,
1555+
invited_count=room_membership_summary.get(
1556+
Membership.INVITE, empty_membership_summary
1557+
).count,
14441558
# TODO: These are just dummy values. We could potentially just remove these
14451559
# since notifications can only really be done correctly on the client anyway
14461560
# (encrypted rooms).

0 commit comments

Comments
 (0)