Skip to content

Commit 3fee32e

Browse files
Order heroes by stream_ordering (as spec'ed) (#17435)
The spec specifically mentions `stream_ordering` but that's a Synapse specific concept. In any case, the essence of the spec is basically the first 5 members of the room which `stream_ordering` accomplishes. Split off from #17419 (comment) ## Spec compliance > This should be the first 5 members of the room, **ordered by stream ordering**, which are joined or invited. The list must never include the client’s own user ID. When no joined or invited members are available, this should consist of the banned and left users. > > *-- https://spec.matrix.org/v1.10/client-server-api/#_matrixclientv3sync_roomsummary* Related to matrix-org/matrix-spec#1334
1 parent 5884f0a commit 3fee32e

File tree

4 files changed

+456
-26
lines changed

4 files changed

+456
-26
lines changed

changelog.d/17435.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Order `heroes` by `stream_ordering` as the Matrix specification states (applies to `/sync`).

synapse/storage/databases/main/roommember.py

+45-12
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,19 @@ def _get_users_in_room_with_profiles(
279279

280280
@cached(max_entries=100000) # type: ignore[synapse-@cached-mutable]
281281
async def get_room_summary(self, room_id: str) -> Mapping[str, MemberSummary]:
282-
"""Get the details of a room roughly suitable for use by the room
282+
"""
283+
Get the details of a room roughly suitable for use by the room
283284
summary extension to /sync. Useful when lazy loading room members.
285+
286+
Returns the total count of members in the room by membership type, and a
287+
truncated list of members (the heroes). This will be the first 6 members of the
288+
room:
289+
- We want 5 heroes plus 1, in case one of them is the
290+
calling user.
291+
- They are ordered by `stream_ordering`, which are joined or
292+
invited. When no joined or invited members are available, this also includes
293+
banned and left users.
294+
284295
Args:
285296
room_id: The room ID to query
286297
Returns:
@@ -308,23 +319,36 @@ def _get_room_summary_txn(
308319
for count, membership in txn:
309320
res.setdefault(membership, MemberSummary([], count))
310321

311-
# we order by membership and then fairly arbitrarily by event_id so
312-
# heroes are consistent
313-
# Note, rejected events will have a null membership field, so
314-
# we we manually filter them out.
322+
# Order by membership (joins -> invites -> leave (former insiders) ->
323+
# everything else (outsiders like bans/knocks), then by `stream_ordering` so
324+
# the first members in the room show up first and to make the sort stable
325+
# (consistent heroes).
326+
#
327+
# Note: rejected events will have a null membership field, so we we manually
328+
# filter them out.
315329
sql = """
316330
SELECT state_key, membership, event_id
317331
FROM current_state_events
318332
WHERE type = 'm.room.member' AND room_id = ?
319333
AND membership IS NOT NULL
320334
ORDER BY
321-
CASE membership WHEN ? THEN 1 WHEN ? THEN 2 ELSE 3 END ASC,
322-
event_id ASC
335+
CASE membership WHEN ? THEN 1 WHEN ? THEN 2 WHEN ? THEN 3 ELSE 4 END ASC,
336+
event_stream_ordering ASC
323337
LIMIT ?
324338
"""
325339

326-
# 6 is 5 (number of heroes) plus 1, in case one of them is the calling user.
327-
txn.execute(sql, (room_id, Membership.JOIN, Membership.INVITE, 6))
340+
txn.execute(
341+
sql,
342+
(
343+
room_id,
344+
# Sort order
345+
Membership.JOIN,
346+
Membership.INVITE,
347+
Membership.LEAVE,
348+
# 6 is 5 (number of heroes) plus 1, in case one of them is the calling user.
349+
6,
350+
),
351+
)
328352
for user_id, membership, event_id in txn:
329353
summary = res[membership]
330354
# we will always have a summary for this membership type at this
@@ -1509,10 +1533,19 @@ def extract_heroes_from_room_summary(
15091533
) -> List[str]:
15101534
"""Determine the users that represent a room, from the perspective of the `me` user.
15111535
1536+
This function expects `MemberSummary.members` to already be sorted by
1537+
`stream_ordering` like the results from `get_room_summary(...)`.
1538+
15121539
The rules which say which users we select are specified in the "Room Summary"
15131540
section of
15141541
https://spec.matrix.org/v1.4/client-server-api/#get_matrixclientv3sync
15151542
1543+
1544+
Args:
1545+
details: Mapping from membership type to member summary. We expect
1546+
`MemberSummary.members` to already be sorted by `stream_ordering`.
1547+
me: The user for whom we are determining the heroes for.
1548+
15161549
Returns a list (possibly empty) of heroes' mxids.
15171550
"""
15181551
empty_ms = MemberSummary([], 0)
@@ -1527,11 +1560,11 @@ def extract_heroes_from_room_summary(
15271560
r[0] for r in details.get(Membership.LEAVE, empty_ms).members if r[0] != me
15281561
] + [r[0] for r in details.get(Membership.BAN, empty_ms).members if r[0] != me]
15291562

1530-
# FIXME: order by stream ordering rather than as returned by SQL
1563+
# We expect `MemberSummary.members` to already be sorted by `stream_ordering`
15311564
if joined_user_ids or invited_user_ids:
1532-
return sorted(joined_user_ids + invited_user_ids)[0:5]
1565+
return (joined_user_ids + invited_user_ids)[0:5]
15331566
else:
1534-
return sorted(gone_user_ids)[0:5]
1567+
return gone_user_ids[0:5]
15351568

15361569

15371570
@attr.s(slots=True, auto_attribs=True)

tests/rest/client/test_sync.py

+9-12
Original file line numberDiff line numberDiff line change
@@ -2160,18 +2160,15 @@ def test_rooms_meta_heroes_max(self) -> None:
21602160

21612161
# Room2 doesn't have a name so we should see `heroes` populated
21622162
self.assertIsNone(channel.json_body["rooms"][room_id1].get("name"))
2163-
# FIXME: Remove this basic assertion and uncomment the better assertion below
2164-
# after https://github.com/element-hq/synapse/pull/17435 merges
2165-
self.assertEqual(len(channel.json_body["rooms"][room_id1].get("heroes", [])), 5)
2166-
# self.assertCountEqual(
2167-
# [
2168-
# hero["user_id"]
2169-
# for hero in channel.json_body["rooms"][room_id1].get("heroes", [])
2170-
# ],
2171-
# # Heroes should be the first 5 users in the room (excluding the user
2172-
# # themselves, we shouldn't see `user1`)
2173-
# [user2_id, user3_id, user4_id, user5_id, user6_id],
2174-
# )
2163+
self.assertCountEqual(
2164+
[
2165+
hero["user_id"]
2166+
for hero in channel.json_body["rooms"][room_id1].get("heroes", [])
2167+
],
2168+
# Heroes should be the first 5 users in the room (excluding the user
2169+
# themselves, we shouldn't see `user1`)
2170+
[user2_id, user3_id, user4_id, user5_id, user6_id],
2171+
)
21752172
self.assertEqual(
21762173
channel.json_body["rooms"][room_id1]["joined_count"],
21772174
7,

0 commit comments

Comments
 (0)