Skip to content

Commit 2b620e0

Browse files
Sliding Sync: Add typing notification extension (MSC3961) (#17505)
[MSC3961](matrix-org/matrix-spec-proposals#3961): Sliding Sync Extension: Typing Notifications Based on [MSC3575](matrix-org/matrix-spec-proposals#3575): Sliding Sync
1 parent 39731bb commit 2b620e0

File tree

9 files changed

+640
-7
lines changed

9 files changed

+640
-7
lines changed

changelog.d/17505.feature

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

synapse/handlers/receipts.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,10 @@ async def get_new_events(
288288
explicit_room_id: Optional[str] = None,
289289
to_key: Optional[MultiWriterStreamToken] = None,
290290
) -> Tuple[List[JsonMapping], MultiWriterStreamToken]:
291+
"""
292+
Find read receipts for given rooms (> `from_token` and <= `to_token`)
293+
"""
294+
291295
if to_key is None:
292296
to_key = self.get_current_key()
293297

synapse/handlers/sliding_sync.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2284,11 +2284,24 @@ async def get_extensions_response(
22842284
from_token=from_token,
22852285
)
22862286

2287+
typing_response = None
2288+
if sync_config.extensions.typing is not None:
2289+
typing_response = await self.get_typing_extension_response(
2290+
sync_config=sync_config,
2291+
actual_lists=actual_lists,
2292+
actual_room_ids=actual_room_ids,
2293+
actual_room_response_map=actual_room_response_map,
2294+
typing_request=sync_config.extensions.typing,
2295+
to_token=to_token,
2296+
from_token=from_token,
2297+
)
2298+
22872299
return SlidingSyncResult.Extensions(
22882300
to_device=to_device_response,
22892301
e2ee=e2ee_response,
22902302
account_data=account_data_response,
22912303
receipts=receipts_response,
2304+
typing=typing_response,
22922305
)
22932306

22942307
def find_relevant_room_ids_for_extension(
@@ -2615,6 +2628,8 @@ async def get_receipts_extension_response(
26152628

26162629
room_id_to_receipt_map: Dict[str, JsonMapping] = {}
26172630
if len(relevant_room_ids) > 0:
2631+
# TODO: Take connection tracking into account so that when a room comes back
2632+
# into range we can send the receipts that were missed.
26182633
receipt_source = self.event_sources.sources.receipt
26192634
receipts, _ = await receipt_source.get_new_events(
26202635
user=sync_config.user,
@@ -2636,6 +2651,8 @@ async def get_receipts_extension_response(
26362651
type = receipt["type"]
26372652
content = receipt["content"]
26382653

2654+
# For `inital: True` rooms, we only want to include receipts for events
2655+
# in the timeline.
26392656
room_result = actual_room_response_map.get(room_id)
26402657
if room_result is not None:
26412658
if room_result.initial:
@@ -2659,6 +2676,70 @@ async def get_receipts_extension_response(
26592676
room_id_to_receipt_map=room_id_to_receipt_map,
26602677
)
26612678

2679+
async def get_typing_extension_response(
2680+
self,
2681+
sync_config: SlidingSyncConfig,
2682+
actual_lists: Dict[str, SlidingSyncResult.SlidingWindowList],
2683+
actual_room_ids: Set[str],
2684+
actual_room_response_map: Dict[str, SlidingSyncResult.RoomResult],
2685+
typing_request: SlidingSyncConfig.Extensions.TypingExtension,
2686+
to_token: StreamToken,
2687+
from_token: Optional[SlidingSyncStreamToken],
2688+
) -> Optional[SlidingSyncResult.Extensions.TypingExtension]:
2689+
"""Handle Typing Notification extension (MSC3961)
2690+
2691+
Args:
2692+
sync_config: Sync configuration
2693+
actual_lists: Sliding window API. A map of list key to list results in the
2694+
Sliding Sync response.
2695+
actual_room_ids: The actual room IDs in the the Sliding Sync response.
2696+
actual_room_response_map: A map of room ID to room results in the the
2697+
Sliding Sync response.
2698+
account_data_request: The account_data extension from the request
2699+
to_token: The point in the stream to sync up to.
2700+
from_token: The point in the stream to sync from.
2701+
"""
2702+
# Skip if the extension is not enabled
2703+
if not typing_request.enabled:
2704+
return None
2705+
2706+
relevant_room_ids = self.find_relevant_room_ids_for_extension(
2707+
requested_lists=typing_request.lists,
2708+
requested_room_ids=typing_request.rooms,
2709+
actual_lists=actual_lists,
2710+
actual_room_ids=actual_room_ids,
2711+
)
2712+
2713+
room_id_to_typing_map: Dict[str, JsonMapping] = {}
2714+
if len(relevant_room_ids) > 0:
2715+
# Note: We don't need to take connection tracking into account for typing
2716+
# notifications because they'll get anything still relevant and hasn't timed
2717+
# out when the room comes into range. We consider the gap where the room
2718+
# fell out of range, as long enough for any typing notifications to have
2719+
# timed out (it's not worth the 30 seconds of data we may have missed).
2720+
typing_source = self.event_sources.sources.typing
2721+
typing_notifications, _ = await typing_source.get_new_events(
2722+
user=sync_config.user,
2723+
from_key=(from_token.stream_token.typing_key if from_token else 0),
2724+
to_key=to_token.typing_key,
2725+
# This is a dummy value and isn't used in the function
2726+
limit=0,
2727+
room_ids=relevant_room_ids,
2728+
is_guest=False,
2729+
)
2730+
2731+
for typing_notification in typing_notifications:
2732+
# These fields should exist for every typing notification
2733+
room_id = typing_notification["room_id"]
2734+
type = typing_notification["type"]
2735+
content = typing_notification["content"]
2736+
2737+
room_id_to_typing_map[room_id] = {"type": type, "content": content}
2738+
2739+
return SlidingSyncResult.Extensions.TypingExtension(
2740+
room_id_to_typing_map=room_id_to_typing_map,
2741+
)
2742+
26622743

26632744
class HaveSentRoomFlag(Enum):
26642745
"""Flag for whether we have sent the room down a sliding sync connection.

synapse/handlers/typing.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,12 @@ async def get_new_events(
565565
room_ids: Iterable[str],
566566
is_guest: bool,
567567
explicit_room_id: Optional[str] = None,
568+
to_key: Optional[int] = None,
568569
) -> Tuple[List[JsonMapping], int]:
570+
"""
571+
Find typing notifications for given rooms (> `from_token` and <= `to_token`)
572+
"""
573+
569574
with Measure(self.clock, "typing.get_new_events"):
570575
from_key = int(from_key)
571576
handler = self.get_typing_handler()
@@ -574,7 +579,9 @@ async def get_new_events(
574579
for room_id in room_ids:
575580
if room_id not in handler._room_serials:
576581
continue
577-
if handler._room_serials[room_id] <= from_key:
582+
if handler._room_serials[room_id] <= from_key or (
583+
to_key is not None and handler._room_serials[room_id] > to_key
584+
):
578585
continue
579586

580587
events.append(self._make_event_for(room_id))

synapse/rest/client/sync.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1152,10 +1152,14 @@ async def encode_extensions(
11521152

11531153
if extensions.receipts is not None:
11541154
serialized_extensions["receipts"] = {
1155-
# Same as the the top-level `account_data.events` field in Sync v2.
11561155
"rooms": extensions.receipts.room_id_to_receipt_map,
11571156
}
11581157

1158+
if extensions.typing is not None:
1159+
serialized_extensions["typing"] = {
1160+
"rooms": extensions.typing.room_id_to_typing_map,
1161+
}
1162+
11591163
return serialized_extensions
11601164

11611165

synapse/types/handlers/__init__.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,22 +366,42 @@ class ReceiptsExtension:
366366
"""The Receipts extension (MSC3960)
367367
368368
Attributes:
369-
room_id_to_receipt_map: Mapping from room_id to `m.receipt` event (type, content)
369+
room_id_to_receipt_map: Mapping from room_id to `m.receipt` ephemeral
370+
event (type, content)
370371
"""
371372

372373
room_id_to_receipt_map: Mapping[str, JsonMapping]
373374

374375
def __bool__(self) -> bool:
375376
return bool(self.room_id_to_receipt_map)
376377

378+
@attr.s(slots=True, frozen=True, auto_attribs=True)
379+
class TypingExtension:
380+
"""The Typing Notification extension (MSC3961)
381+
382+
Attributes:
383+
room_id_to_typing_map: Mapping from room_id to `m.typing` ephemeral
384+
event (type, content)
385+
"""
386+
387+
room_id_to_typing_map: Mapping[str, JsonMapping]
388+
389+
def __bool__(self) -> bool:
390+
return bool(self.room_id_to_typing_map)
391+
377392
to_device: Optional[ToDeviceExtension] = None
378393
e2ee: Optional[E2eeExtension] = None
379394
account_data: Optional[AccountDataExtension] = None
380395
receipts: Optional[ReceiptsExtension] = None
396+
typing: Optional[TypingExtension] = None
381397

382398
def __bool__(self) -> bool:
383399
return bool(
384-
self.to_device or self.e2ee or self.account_data or self.receipts
400+
self.to_device
401+
or self.e2ee
402+
or self.account_data
403+
or self.receipts
404+
or self.typing
385405
)
386406

387407
next_pos: SlidingSyncStreamToken

synapse/types/rest/client/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,10 +359,28 @@ class ReceiptsExtension(RequestBodyModel):
359359
# Process all room subscriptions defined in the Room Subscription API. (This is the default.)
360360
rooms: Optional[List[StrictStr]] = ["*"]
361361

362+
class TypingExtension(RequestBodyModel):
363+
"""The Typing Notification extension (MSC3961)
364+
365+
Attributes:
366+
enabled
367+
lists: List of list keys (from the Sliding Window API) to apply this
368+
extension to.
369+
rooms: List of room IDs (from the Room Subscription API) to apply this
370+
extension to.
371+
"""
372+
373+
enabled: Optional[StrictBool] = False
374+
# Process all lists defined in the Sliding Window API. (This is the default.)
375+
lists: Optional[List[StrictStr]] = ["*"]
376+
# Process all room subscriptions defined in the Room Subscription API. (This is the default.)
377+
rooms: Optional[List[StrictStr]] = ["*"]
378+
362379
to_device: Optional[ToDeviceExtension] = None
363380
e2ee: Optional[E2eeExtension] = None
364381
account_data: Optional[AccountDataExtension] = None
365382
receipts: Optional[ReceiptsExtension] = None
383+
typing: Optional[TypingExtension] = None
366384

367385
conn_id: Optional[str]
368386

0 commit comments

Comments
 (0)