Skip to content

Commit 3fef535

Browse files
Add rooms.bump_stamp to Sliding Sync /sync for easier client-side sorting (#17395)
`bump_stamp` corresponds to the `stream_ordering` of the latest `DEFAULT_BUMP_EVENT_TYPES` in the room. This helps clients sort more readily without them needing to pull in a bunch of the timeline to determine the last activity. `bump_event_types` is a thing because for example, we don't want display name changes to mark the room as unread and bump it to the top. For encrypted rooms, we just have to consider any activity as a bump because we can't see the content and the client has to figure it out for themselves. Outside of Synapse, `bump_stamp` is just a free-form counter so other implementations could use `received_ts`or `origin_server_ts` (see the [*Security considerations* section in MSC3575 about the potential pitfalls of using `origin_server_ts`](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/sync-v3/proposals/3575-sync.md#security-considerations)). It doesn't have any guarantee about always going up. In the Synapse case, it could go down if an event was redacted/removed (or purged in cases of retention policies). In the future, we could add `bump_event_types` as [MSC3575](matrix-org/matrix-spec-proposals#3575) mentions if people need to customize the event types. --- In the Sliding Sync proxy, a similar [`timestamp` field was added](matrix-org/sliding-sync#247) for the same purpose but the name is not obvious what it pertains to or what it's for. The `timestamp` field was also added to Ruma in ruma/ruma#1622
1 parent 62134dc commit 3fef535

File tree

9 files changed

+295
-34
lines changed

9 files changed

+295
-34
lines changed

changelog.d/17395.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `rooms.bump_stamp` for easier client-side sorting in experimental [MSC3575](https://github.com/matrix-org/matrix-spec-proposals/pull/3575) Sliding Sync `/sync` endpoint.

synapse/api/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,13 @@ class EventTypes:
128128
SpaceParent: Final = "m.space.parent"
129129

130130
Reaction: Final = "m.reaction"
131+
Sticker: Final = "m.sticker"
132+
LiveLocationShareStart: Final = "m.beacon_info"
131133

132134
CallInvite: Final = "m.call.invite"
133135

136+
PollStart: Final = "m.poll.start"
137+
134138

135139
class ToDeviceEventTypes:
136140
RoomKeyRequest: Final = "m.room_key_request"

synapse/handlers/sliding_sync.py

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@
5454
logger = logging.getLogger(__name__)
5555

5656

57+
# The event types that clients should consider as new activity.
58+
DEFAULT_BUMP_EVENT_TYPES = {
59+
EventTypes.Message,
60+
EventTypes.Encrypted,
61+
EventTypes.Sticker,
62+
EventTypes.CallInvite,
63+
EventTypes.PollStart,
64+
EventTypes.LiveLocationShareStart,
65+
}
66+
67+
5768
def filter_membership_for_sync(
5869
*, membership: str, user_id: str, sender: Optional[str]
5970
) -> bool:
@@ -285,6 +296,7 @@ class _RoomMembershipForUser:
285296
range
286297
"""
287298

299+
room_id: str
288300
event_id: Optional[str]
289301
event_pos: PersistedEventPosition
290302
membership: str
@@ -469,7 +481,9 @@ async def current_sync_for_user(
469481
#
470482
# Both sides of range are inclusive so we `+ 1`
471483
max_num_rooms = range[1] - range[0] + 1
472-
for room_id, _ in sorted_room_info[range[0] :]:
484+
for room_membership in sorted_room_info[range[0] :]:
485+
room_id = room_membership.room_id
486+
473487
if len(room_ids_in_list) >= max_num_rooms:
474488
break
475489

@@ -519,7 +533,7 @@ async def current_sync_for_user(
519533
user=sync_config.user,
520534
room_id=room_id,
521535
room_sync_config=room_sync_config,
522-
rooms_membership_for_user_at_to_token=sync_room_map[room_id],
536+
room_membership_for_user_at_to_token=sync_room_map[room_id],
523537
from_token=from_token,
524538
to_token=to_token,
525539
)
@@ -591,6 +605,7 @@ async def get_sync_room_ids_for_user(
591605
# (below) because they are potentially from the current snapshot time
592606
# instead from the time of the `to_token`.
593607
room_for_user.room_id: _RoomMembershipForUser(
608+
room_id=room_for_user.room_id,
594609
event_id=room_for_user.event_id,
595610
event_pos=room_for_user.event_pos,
596611
membership=room_for_user.membership,
@@ -691,6 +706,7 @@ async def get_sync_room_ids_for_user(
691706
is not None
692707
):
693708
sync_room_id_set[room_id] = _RoomMembershipForUser(
709+
room_id=room_id,
694710
event_id=first_membership_change_after_to_token.prev_event_id,
695711
event_pos=first_membership_change_after_to_token.prev_event_pos,
696712
membership=first_membership_change_after_to_token.prev_membership,
@@ -785,6 +801,7 @@ async def get_sync_room_ids_for_user(
785801
# is their own leave event
786802
if last_membership_change_in_from_to_range.membership == Membership.LEAVE:
787803
filtered_sync_room_id_set[room_id] = _RoomMembershipForUser(
804+
room_id=room_id,
788805
event_id=last_membership_change_in_from_to_range.event_id,
789806
event_pos=last_membership_change_in_from_to_range.event_pos,
790807
membership=last_membership_change_in_from_to_range.membership,
@@ -969,7 +986,7 @@ async def sort_rooms(
969986
self,
970987
sync_room_map: Dict[str, _RoomMembershipForUser],
971988
to_token: StreamToken,
972-
) -> List[Tuple[str, _RoomMembershipForUser]]:
989+
) -> List[_RoomMembershipForUser]:
973990
"""
974991
Sort by `stream_ordering` of the last event that the user should see in the
975992
room. `stream_ordering` is unique so we get a stable sort.
@@ -1007,12 +1024,17 @@ async def sort_rooms(
10071024
else:
10081025
# Otherwise, if the user has left/been invited/knocked/been banned from
10091026
# a room, they shouldn't see anything past that point.
1027+
#
1028+
# FIXME: It's possible that people should see beyond this point in
1029+
# invited/knocked cases if for example the room has
1030+
# `invite`/`world_readable` history visibility, see
1031+
# https://github.com/matrix-org/matrix-spec-proposals/pull/3575#discussion_r1653045932
10101032
last_activity_in_room_map[room_id] = room_for_user.event_pos.stream
10111033

10121034
return sorted(
1013-
sync_room_map.items(),
1035+
sync_room_map.values(),
10141036
# Sort by the last activity (stream_ordering) in the room
1015-
key=lambda room_info: last_activity_in_room_map[room_info[0]],
1037+
key=lambda room_info: last_activity_in_room_map[room_info.room_id],
10161038
# We want descending order
10171039
reverse=True,
10181040
)
@@ -1022,7 +1044,7 @@ async def get_room_sync_data(
10221044
user: UserID,
10231045
room_id: str,
10241046
room_sync_config: RoomSyncConfig,
1025-
rooms_membership_for_user_at_to_token: _RoomMembershipForUser,
1047+
room_membership_for_user_at_to_token: _RoomMembershipForUser,
10261048
from_token: Optional[StreamToken],
10271049
to_token: StreamToken,
10281050
) -> SlidingSyncResult.RoomResult:
@@ -1036,7 +1058,7 @@ async def get_room_sync_data(
10361058
room_id: The room ID to fetch data for
10371059
room_sync_config: Config for what data we should fetch for a room in the
10381060
sync response.
1039-
rooms_membership_for_user_at_to_token: Membership information for the user
1061+
room_membership_for_user_at_to_token: Membership information for the user
10401062
in the room at the time of `to_token`.
10411063
from_token: The point in the stream to sync from.
10421064
to_token: The point in the stream to sync up to.
@@ -1056,7 +1078,7 @@ async def get_room_sync_data(
10561078
if (
10571079
room_sync_config.timeline_limit > 0
10581080
# No timeline for invite/knock rooms (just `stripped_state`)
1059-
and rooms_membership_for_user_at_to_token.membership
1081+
and room_membership_for_user_at_to_token.membership
10601082
not in (Membership.INVITE, Membership.KNOCK)
10611083
):
10621084
limited = False
@@ -1069,12 +1091,12 @@ async def get_room_sync_data(
10691091
# We're going to paginate backwards from the `to_token`
10701092
from_bound = to_token.room_key
10711093
# People shouldn't see past their leave/ban event
1072-
if rooms_membership_for_user_at_to_token.membership in (
1094+
if room_membership_for_user_at_to_token.membership in (
10731095
Membership.LEAVE,
10741096
Membership.BAN,
10751097
):
10761098
from_bound = (
1077-
rooms_membership_for_user_at_to_token.event_pos.to_room_stream_token()
1099+
room_membership_for_user_at_to_token.event_pos.to_room_stream_token()
10781100
)
10791101

10801102
# Determine whether we should limit the timeline to the token range.
@@ -1089,7 +1111,7 @@ async def get_room_sync_data(
10891111
to_bound = (
10901112
from_token.room_key
10911113
if from_token is not None
1092-
and not rooms_membership_for_user_at_to_token.newly_joined
1114+
and not room_membership_for_user_at_to_token.newly_joined
10931115
else None
10941116
)
10951117

@@ -1126,7 +1148,7 @@ async def get_room_sync_data(
11261148
self.storage_controllers,
11271149
user.to_string(),
11281150
timeline_events,
1129-
is_peeking=rooms_membership_for_user_at_to_token.membership
1151+
is_peeking=room_membership_for_user_at_to_token.membership
11301152
!= Membership.JOIN,
11311153
filter_send_to_client=True,
11321154
)
@@ -1181,16 +1203,16 @@ async def get_room_sync_data(
11811203
# Figure out any stripped state events for invite/knocks. This allows the
11821204
# potential joiner to identify the room.
11831205
stripped_state: List[JsonDict] = []
1184-
if rooms_membership_for_user_at_to_token.membership in (
1206+
if room_membership_for_user_at_to_token.membership in (
11851207
Membership.INVITE,
11861208
Membership.KNOCK,
11871209
):
11881210
# This should never happen. If someone is invited/knocked on room, then
11891211
# there should be an event for it.
1190-
assert rooms_membership_for_user_at_to_token.event_id is not None
1212+
assert room_membership_for_user_at_to_token.event_id is not None
11911213

11921214
invite_or_knock_event = await self.store.get_event(
1193-
rooms_membership_for_user_at_to_token.event_id
1215+
room_membership_for_user_at_to_token.event_id
11941216
)
11951217

11961218
stripped_state = []
@@ -1206,7 +1228,7 @@ async def get_room_sync_data(
12061228
stripped_state.append(strip_event(invite_or_knock_event))
12071229

12081230
# TODO: Handle state resets. For example, if we see
1209-
# `rooms_membership_for_user_at_to_token.membership = Membership.LEAVE` but
1231+
# `room_membership_for_user_at_to_token.membership = Membership.LEAVE` but
12101232
# `required_state` doesn't include it, we should indicate to the client that a
12111233
# state reset happened. Perhaps we should indicate this by setting `initial:
12121234
# True` and empty `required_state`.
@@ -1226,7 +1248,7 @@ async def get_room_sync_data(
12261248
# `invite`/`knock` rooms only have `stripped_state`. See
12271249
# https://github.com/matrix-org/matrix-spec-proposals/pull/3575#discussion_r1653045932
12281250
room_state: Optional[StateMap[EventBase]] = None
1229-
if rooms_membership_for_user_at_to_token.membership not in (
1251+
if room_membership_for_user_at_to_token.membership not in (
12301252
Membership.INVITE,
12311253
Membership.KNOCK,
12321254
):
@@ -1303,15 +1325,15 @@ async def get_room_sync_data(
13031325
# initial sync
13041326
if initial:
13051327
# People shouldn't see past their leave/ban event
1306-
if rooms_membership_for_user_at_to_token.membership in (
1328+
if room_membership_for_user_at_to_token.membership in (
13071329
Membership.LEAVE,
13081330
Membership.BAN,
13091331
):
13101332
room_state = await self.storage_controllers.state.get_state_at(
13111333
room_id,
13121334
stream_position=to_token.copy_and_replace(
13131335
StreamKeyType.ROOM,
1314-
rooms_membership_for_user_at_to_token.event_pos.to_room_stream_token(),
1336+
room_membership_for_user_at_to_token.event_pos.to_room_stream_token(),
13151337
),
13161338
state_filter=state_filter,
13171339
# Partially-stated rooms should have all state events except for
@@ -1341,6 +1363,20 @@ async def get_room_sync_data(
13411363
# we can return updates instead of the full required state.
13421364
raise NotImplementedError()
13431365

1366+
# Figure out the last bump event in the room
1367+
last_bump_event_result = (
1368+
await self.store.get_last_event_pos_in_room_before_stream_ordering(
1369+
room_id, to_token.room_key, event_types=DEFAULT_BUMP_EVENT_TYPES
1370+
)
1371+
)
1372+
1373+
# By default, just choose the membership event position
1374+
bump_stamp = room_membership_for_user_at_to_token.event_pos.stream
1375+
# But if we found a bump event, use that instead
1376+
if last_bump_event_result is not None:
1377+
_, bump_event_pos = last_bump_event_result
1378+
bump_stamp = bump_event_pos.stream
1379+
13441380
return SlidingSyncResult.RoomResult(
13451381
# TODO: Dummy value
13461382
name=None,
@@ -1358,6 +1394,7 @@ async def get_room_sync_data(
13581394
prev_batch=prev_batch_token,
13591395
limited=limited,
13601396
num_live=num_live,
1397+
bump_stamp=bump_stamp,
13611398
# TODO: Dummy values
13621399
joined_count=0,
13631400
invited_count=0,

synapse/rest/client/sync.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,7 @@ async def encode_rooms(
982982
serialized_rooms: Dict[str, JsonDict] = {}
983983
for room_id, room_result in rooms.items():
984984
serialized_rooms[room_id] = {
985+
"bump_stamp": room_result.bump_stamp,
985986
"joined_count": room_result.joined_count,
986987
"invited_count": room_result.invited_count,
987988
"notification_count": room_result.notification_count,

synapse/storage/databases/main/stream.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,6 +1178,7 @@ async def get_last_event_pos_in_room_before_stream_ordering(
11781178
self,
11791179
room_id: str,
11801180
end_token: RoomStreamToken,
1181+
event_types: Optional[Collection[str]] = None,
11811182
) -> Optional[Tuple[str, PersistedEventPosition]]:
11821183
"""
11831184
Returns the ID and event position of the last event in a room at or before a
@@ -1186,6 +1187,7 @@ async def get_last_event_pos_in_room_before_stream_ordering(
11861187
Args:
11871188
room_id
11881189
end_token: The token used to stream from
1190+
event_types: Optional allowlist of event types to filter by
11891191
11901192
Returns:
11911193
The ID of the most recent event and it's position, or None if there are no
@@ -1207,9 +1209,17 @@ def get_last_event_pos_in_room_before_stream_ordering_txn(
12071209
min_stream = end_token.stream
12081210
max_stream = end_token.get_max_stream_pos()
12091211

1210-
# We use `union all` because we don't need any of the deduplication logic
1211-
# (`union` is really a union + distinct). `UNION ALL` does preserve the
1212-
# ordering of the operand queries but there is no actual gurantee that it
1212+
event_type_clause = ""
1213+
event_type_args: List[str] = []
1214+
if event_types is not None and len(event_types) > 0:
1215+
event_type_clause, event_type_args = make_in_list_sql_clause(
1216+
txn.database_engine, "type", event_types
1217+
)
1218+
event_type_clause = f"AND {event_type_clause}"
1219+
1220+
# We use `UNION ALL` because we don't need any of the deduplication logic
1221+
# (`UNION` is really a `UNION` + `DISTINCT`). `UNION ALL` does preserve the
1222+
# ordering of the operand queries but there is no actual guarantee that it
12131223
# has this behavior in all scenarios so we need the extra `ORDER BY` at the
12141224
# bottom.
12151225
sql = """
@@ -1218,6 +1228,7 @@ def get_last_event_pos_in_room_before_stream_ordering_txn(
12181228
FROM events
12191229
LEFT JOIN rejections USING (event_id)
12201230
WHERE room_id = ?
1231+
%s
12211232
AND ? < stream_ordering AND stream_ordering <= ?
12221233
AND NOT outlier
12231234
AND rejections.event_id IS NULL
@@ -1229,23 +1240,25 @@ def get_last_event_pos_in_room_before_stream_ordering_txn(
12291240
FROM events
12301241
LEFT JOIN rejections USING (event_id)
12311242
WHERE room_id = ?
1243+
%s
12321244
AND stream_ordering <= ?
12331245
AND NOT outlier
12341246
AND rejections.event_id IS NULL
12351247
ORDER BY stream_ordering DESC
12361248
LIMIT 1
12371249
) AS b
12381250
ORDER BY stream_ordering DESC
1239-
"""
1251+
""" % (
1252+
event_type_clause,
1253+
event_type_clause,
1254+
)
12401255
txn.execute(
12411256
sql,
1242-
(
1243-
room_id,
1244-
min_stream,
1245-
max_stream,
1246-
room_id,
1247-
min_stream,
1248-
),
1257+
[room_id]
1258+
+ event_type_args
1259+
+ [min_stream, max_stream, room_id]
1260+
+ event_type_args
1261+
+ [min_stream],
12491262
)
12501263

12511264
for instance_name, stream_ordering, topological_ordering, event_id in txn:

synapse/types/handlers/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,13 @@ class RoomResult:
183183
events because if a room not in the sliding window bumps into the window because
184184
of an @mention it will have `initial: true` yet contain a single live event
185185
(with potentially other old events in the timeline).
186+
bump_stamp: The `stream_ordering` of the last event according to the
187+
`bump_event_types`. This helps clients sort more readily without them
188+
needing to pull in a bunch of the timeline to determine the last activity.
189+
`bump_event_types` is a thing because for example, we don't want display
190+
name changes to mark the room as unread and bump it to the top. For
191+
encrypted rooms, we just have to consider any activity as a bump because we
192+
can't see the content and the client has to figure it out for themselves.
186193
joined_count: The number of users with membership of join, including the client's
187194
own user ID. (same as sync `v2 m.joined_member_count`)
188195
invited_count: The number of users with membership of invite. (same as sync v2
@@ -211,6 +218,7 @@ class RoomResult:
211218
limited: Optional[bool]
212219
# Only optional because it won't be included for invite/knock rooms with `stripped_state`
213220
num_live: Optional[int]
221+
bump_stamp: int
214222
joined_count: int
215223
invited_count: int
216224
notification_count: int

0 commit comments

Comments
 (0)