Skip to content

Commit 4a7c586

Browse files
Add Sliding Sync /sync endpoint (initial implementation) (#17187)
Based on [MSC3575](matrix-org/matrix-spec-proposals#3575): Sliding Sync This iteration only focuses on returning the list of room IDs in the sliding window API (without sorting/filtering). Rooms appear in the Sliding sync response based on: - `invite`, `join`, `knock`, `ban` membership events - Kicks (`leave` membership events where `sender` is different from the `user_id`/`state_key`) - `newly_left` (rooms that were left during the given token range, > `from_token` and <= `to_token`) - In order for bans/kicks to not show up, you need to `/forget` those rooms. This doesn't modify the event itself though and only adds the `forgotten` flag to `room_memberships` in Synapse. There isn't a way to tell when a room was forgotten at the moment so we can't factor it into the from/to range. ### Example request `POST http://localhost:8008/_matrix/client/unstable/org.matrix.msc3575/sync` ```json { "lists": { "foo-list": { "ranges": [ [0, 99] ], "sort": [ "by_notification_level", "by_recency", "by_name" ], "required_state": [ ["m.room.join_rules", ""], ["m.room.history_visibility", ""], ["m.space.child", "*"] ], "timeline_limit": 100 } } } ``` Response: ```json { "next_pos": "s58_224_0_13_10_1_1_16_0_1", "lists": { "foo-list": { "count": 1, "ops": [ { "op": "SYNC", "range": [0, 99], "room_ids": [ "!MmgikIyFzsuvtnbvVG:my.synapse.linux.server" ] } ] } }, "rooms": {}, "extensions": {} } ```
1 parent ce93858 commit 4a7c586

File tree

11 files changed

+2302
-15
lines changed

11 files changed

+2302
-15
lines changed

changelog.d/17187.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add initial implementation of an experimental [MSC3575](https://github.com/matrix-org/matrix-spec-proposals/pull/3575) Sliding Sync `/sync` endpoint.

synapse/api/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class Membership:
5050
KNOCK: Final = "knock"
5151
LEAVE: Final = "leave"
5252
BAN: Final = "ban"
53-
LIST: Final = (INVITE, JOIN, KNOCK, LEAVE, BAN)
53+
LIST: Final = {INVITE, JOIN, KNOCK, LEAVE, BAN}
5454

5555

5656
class PresenceState:

synapse/handlers/sliding_sync.py

Lines changed: 610 additions & 0 deletions
Large diffs are not rendered by default.

synapse/handlers/sync.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2002,7 +2002,7 @@ async def get_sync_result_builder(
20022002
"""
20032003
user_id = sync_config.user.to_string()
20042004

2005-
# Note: we get the users room list *before* we get the current token, this
2005+
# Note: we get the users room list *before* we get the `now_token`, this
20062006
# avoids checking back in history if rooms are joined after the token is fetched.
20072007
token_before_rooms = self.event_sources.get_current_token()
20082008
mutable_joined_room_ids = set(await self.store.get_rooms_for_user(user_id))
@@ -2014,10 +2014,10 @@ async def get_sync_result_builder(
20142014
now_token = self.event_sources.get_current_token()
20152015
log_kv({"now_token": now_token})
20162016

2017-
# Since we fetched the users room list before the token, there's a small window
2018-
# during which membership events may have been persisted, so we fetch these now
2019-
# and modify the joined room list for any changes between the get_rooms_for_user
2020-
# call and the get_current_token call.
2017+
# Since we fetched the users room list before calculating the `now_token` (see
2018+
# above), there's a small window during which membership events may have been
2019+
# persisted, so we fetch these now and modify the joined room list for any
2020+
# changes between the get_rooms_for_user call and the get_current_token call.
20212021
membership_change_events = []
20222022
if since_token:
20232023
membership_change_events = await self.store.get_membership_changes_for_user(
@@ -2027,16 +2027,19 @@ async def get_sync_result_builder(
20272027
self.rooms_to_exclude_globally,
20282028
)
20292029

2030-
mem_last_change_by_room_id: Dict[str, EventBase] = {}
2030+
last_membership_change_by_room_id: Dict[str, EventBase] = {}
20312031
for event in membership_change_events:
2032-
mem_last_change_by_room_id[event.room_id] = event
2032+
last_membership_change_by_room_id[event.room_id] = event
20332033

20342034
# For the latest membership event in each room found, add/remove the room ID
20352035
# from the joined room list accordingly. In this case we only care if the
20362036
# latest change is JOIN.
20372037

2038-
for room_id, event in mem_last_change_by_room_id.items():
2038+
for room_id, event in last_membership_change_by_room_id.items():
20392039
assert event.internal_metadata.stream_ordering
2040+
# As a shortcut, skip any events that happened before we got our
2041+
# `get_rooms_for_user()` snapshot (any changes are already represented
2042+
# in that list).
20402043
if (
20412044
event.internal_metadata.stream_ordering
20422045
< token_before_rooms.room_key.stream

synapse/rest/client/models.py

Lines changed: 188 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,30 @@
1818
# [This file includes modifications made by New Vector Limited]
1919
#
2020
#
21-
from typing import TYPE_CHECKING, Dict, Optional
21+
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
2222

2323
from synapse._pydantic_compat import HAS_PYDANTIC_V2
2424

2525
if TYPE_CHECKING or HAS_PYDANTIC_V2:
26-
from pydantic.v1 import Extra, StrictInt, StrictStr, constr, validator
26+
from pydantic.v1 import (
27+
Extra,
28+
StrictBool,
29+
StrictInt,
30+
StrictStr,
31+
conint,
32+
constr,
33+
validator,
34+
)
2735
else:
28-
from pydantic import Extra, StrictInt, StrictStr, constr, validator
36+
from pydantic import (
37+
Extra,
38+
StrictBool,
39+
StrictInt,
40+
StrictStr,
41+
conint,
42+
constr,
43+
validator,
44+
)
2945

3046
from synapse.rest.models import RequestBodyModel
3147
from synapse.util.threepids import validate_email
@@ -97,3 +113,172 @@ class EmailRequestTokenBody(ThreepidRequestTokenBody):
97113
class MsisdnRequestTokenBody(ThreepidRequestTokenBody):
98114
country: ISO3116_1_Alpha_2
99115
phone_number: StrictStr
116+
117+
118+
class SlidingSyncBody(RequestBodyModel):
119+
"""
120+
Sliding Sync API request body.
121+
122+
Attributes:
123+
lists: Sliding window API. A map of list key to list information
124+
(:class:`SlidingSyncList`). Max lists: 100. The list keys should be
125+
arbitrary strings which the client is using to refer to the list. Keep this
126+
small as it needs to be sent a lot. Max length: 64 bytes.
127+
room_subscriptions: Room subscription API. A map of room ID to room subscription
128+
information. Used to subscribe to a specific room. Sometimes clients know
129+
exactly which room they want to get information about e.g by following a
130+
permalink or by refreshing a webapp currently viewing a specific room. The
131+
sliding window API alone is insufficient for this use case because there's
132+
no way to say "please track this room explicitly".
133+
extensions: Extensions API. A map of extension key to extension config.
134+
"""
135+
136+
class CommonRoomParameters(RequestBodyModel):
137+
"""
138+
Common parameters shared between the sliding window and room subscription APIs.
139+
140+
Attributes:
141+
required_state: Required state for each room returned. An array of event
142+
type and state key tuples. Elements in this array are ORd together to
143+
produce the final set of state events to return. One unique exception is
144+
when you request all state events via `["*", "*"]`. When used, all state
145+
events are returned by default, and additional entries FILTER OUT the
146+
returned set of state events. These additional entries cannot use `*`
147+
themselves. For example, `["*", "*"], ["m.room.member",
148+
"@alice:example.com"]` will *exclude* every `m.room.member` event
149+
*except* for `@alice:example.com`, and include every other state event.
150+
In addition, `["*", "*"], ["m.space.child", "*"]` is an error, the
151+
`m.space.child` filter is not required as it would have been returned
152+
anyway.
153+
timeline_limit: The maximum number of timeline events to return per response.
154+
(Max 1000 messages)
155+
include_old_rooms: Determines if `predecessor` rooms are included in the
156+
`rooms` response. The user MUST be joined to old rooms for them to show up
157+
in the response.
158+
"""
159+
160+
class IncludeOldRooms(RequestBodyModel):
161+
timeline_limit: StrictInt
162+
required_state: List[Tuple[StrictStr, StrictStr]]
163+
164+
required_state: List[Tuple[StrictStr, StrictStr]]
165+
# mypy workaround via https://github.com/pydantic/pydantic/issues/156#issuecomment-1130883884
166+
if TYPE_CHECKING:
167+
timeline_limit: int
168+
else:
169+
timeline_limit: conint(le=1000, strict=True) # type: ignore[valid-type]
170+
include_old_rooms: Optional[IncludeOldRooms] = None
171+
172+
class SlidingSyncList(CommonRoomParameters):
173+
"""
174+
Attributes:
175+
ranges: Sliding window ranges. If this field is missing, no sliding window
176+
is used and all rooms are returned in this list. Integers are
177+
*inclusive*.
178+
sort: How the list should be sorted on the server. The first value is
179+
applied first, then tiebreaks are performed with each subsequent sort
180+
listed.
181+
182+
FIXME: Furthermore, it's not currently defined how servers should behave
183+
if they encounter a filter or sort operation they do not recognise. If
184+
the server rejects the request with an HTTP 400 then that will break
185+
backwards compatibility with new clients vs old servers. However, the
186+
client would be otherwise unaware that only some of the sort/filter
187+
operations have taken effect. We may need to include a "warnings"
188+
section to indicate which sort/filter operations are unrecognised,
189+
allowing for some form of graceful degradation of service.
190+
-- https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/sync-v3/proposals/3575-sync.md#filter-and-sort-extensions
191+
192+
slow_get_all_rooms: Just get all rooms (for clients that don't want to deal with
193+
sliding windows). When true, the `ranges` and `sort` fields are ignored.
194+
required_state: Required state for each room returned. An array of event
195+
type and state key tuples. Elements in this array are ORd together to
196+
produce the final set of state events to return.
197+
198+
One unique exception is when you request all state events via `["*",
199+
"*"]`. When used, all state events are returned by default, and
200+
additional entries FILTER OUT the returned set of state events. These
201+
additional entries cannot use `*` themselves. For example, `["*", "*"],
202+
["m.room.member", "@alice:example.com"]` will *exclude* every
203+
`m.room.member` event *except* for `@alice:example.com`, and include
204+
every other state event. In addition, `["*", "*"], ["m.space.child",
205+
"*"]` is an error, the `m.space.child` filter is not required as it
206+
would have been returned anyway.
207+
208+
Room members can be lazily-loaded by using the special `$LAZY` state key
209+
(`["m.room.member", "$LAZY"]`). Typically, when you view a room, you
210+
want to retrieve all state events except for m.room.member events which
211+
you want to lazily load. To get this behaviour, clients can send the
212+
following::
213+
214+
{
215+
"required_state": [
216+
// activate lazy loading
217+
["m.room.member", "$LAZY"],
218+
// request all state events _except_ for m.room.member
219+
events which are lazily loaded
220+
["*", "*"]
221+
]
222+
}
223+
224+
timeline_limit: The maximum number of timeline events to return per response.
225+
include_old_rooms: Determines if `predecessor` rooms are included in the
226+
`rooms` response. The user MUST be joined to old rooms for them to show up
227+
in the response.
228+
include_heroes: Return a stripped variant of membership events (containing
229+
`user_id` and optionally `avatar_url` and `displayname`) for the users used
230+
to calculate the room name.
231+
filters: Filters to apply to the list before sorting.
232+
bump_event_types: Allowlist of event types which should be considered recent activity
233+
when sorting `by_recency`. By omitting event types from this field,
234+
clients can ensure that uninteresting events (e.g. a profile rename) do
235+
not cause a room to jump to the top of its list(s). Empty or omitted
236+
`bump_event_types` have no effect—all events in a room will be
237+
considered recent activity.
238+
"""
239+
240+
class Filters(RequestBodyModel):
241+
is_dm: Optional[StrictBool] = None
242+
spaces: Optional[List[StrictStr]] = None
243+
is_encrypted: Optional[StrictBool] = None
244+
is_invite: Optional[StrictBool] = None
245+
room_types: Optional[List[Union[StrictStr, None]]] = None
246+
not_room_types: Optional[List[StrictStr]] = None
247+
room_name_like: Optional[StrictStr] = None
248+
tags: Optional[List[StrictStr]] = None
249+
not_tags: Optional[List[StrictStr]] = None
250+
251+
# mypy workaround via https://github.com/pydantic/pydantic/issues/156#issuecomment-1130883884
252+
if TYPE_CHECKING:
253+
ranges: Optional[List[Tuple[int, int]]] = None
254+
else:
255+
ranges: Optional[List[Tuple[conint(ge=0, strict=True), conint(ge=0, strict=True)]]] = None # type: ignore[valid-type]
256+
sort: Optional[List[StrictStr]] = None
257+
slow_get_all_rooms: Optional[StrictBool] = False
258+
include_heroes: Optional[StrictBool] = False
259+
filters: Optional[Filters] = None
260+
bump_event_types: Optional[List[StrictStr]] = None
261+
262+
class RoomSubscription(CommonRoomParameters):
263+
pass
264+
265+
class Extension(RequestBodyModel):
266+
enabled: Optional[StrictBool] = False
267+
lists: Optional[List[StrictStr]] = None
268+
rooms: Optional[List[StrictStr]] = None
269+
270+
# mypy workaround via https://github.com/pydantic/pydantic/issues/156#issuecomment-1130883884
271+
if TYPE_CHECKING:
272+
lists: Optional[Dict[str, SlidingSyncList]] = None
273+
else:
274+
lists: Optional[Dict[constr(max_length=64, strict=True), SlidingSyncList]] = None # type: ignore[valid-type]
275+
room_subscriptions: Optional[Dict[StrictStr, RoomSubscription]] = None
276+
extensions: Optional[Dict[StrictStr, Extension]] = None
277+
278+
@validator("lists")
279+
def lists_length_check(
280+
cls, value: Optional[Dict[str, SlidingSyncList]]
281+
) -> Optional[Dict[str, SlidingSyncList]]:
282+
if value is not None:
283+
assert len(value) <= 100, f"Max lists: 100 but saw {len(value)}"
284+
return value

synapse/rest/client/room.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,9 @@ async def on_PUT(
292292
try:
293293
if event_type == EventTypes.Member:
294294
membership = content.get("membership", None)
295+
if not isinstance(membership, str):
296+
raise SynapseError(400, "Invalid membership (must be a string)")
297+
295298
event_id, _ = await self.room_member_handler.update_membership(
296299
requester,
297300
target=UserID.from_string(state_key),

0 commit comments

Comments
 (0)