From 89009dfdacc9f58128ac6ee1224e5248e5472976 Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Mon, 22 Dec 2025 14:11:55 +0000 Subject: [PATCH] Add fields for sticky events sliding sync extension --- synapse/types/handlers/sliding_sync.py | 22 +++++++- synapse/types/rest/client/__init__.py | 71 +++++++++++++++++++++++++- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/synapse/types/handlers/sliding_sync.py b/synapse/types/handlers/sliding_sync.py index 694b3e1645..63f09dca22 100644 --- a/synapse/types/handlers/sliding_sync.py +++ b/synapse/types/handlers/sliding_sync.py @@ -47,7 +47,7 @@ from synapse.types import ( ThreadSubscriptionsToken, UserID, ) -from synapse.types.rest.client import SlidingSyncBody +from synapse.types.rest.client import SlidingSyncBody, SlidingSyncStickyEventsToken from synapse.util.clock import Clock from synapse.util.duration import Duration @@ -396,12 +396,31 @@ class SlidingSyncResult: or bool(self.prev_batch) ) + @attr.s(slots=True, frozen=True, auto_attribs=True) + class StickyEventsExtension: + """The Sticky Events extension (MSC4354) + + Attributes: + room_id_to_sticky_events: map (room_id -> [unexpired_sticky_events]) + The events are ordered by the sticky events stream. + + The events haven't yet been deduplicated to remove + events that also appear in the timeline. + """ + + room_id_to_sticky_events: Mapping[str, list[EventBase]] + next_batch: SlidingSyncStickyEventsToken + + def __bool__(self) -> bool: + return bool(self.room_id_to_sticky_events) + to_device: ToDeviceExtension | None = None e2ee: E2eeExtension | None = None account_data: AccountDataExtension | None = None receipts: ReceiptsExtension | None = None typing: TypingExtension | None = None thread_subscriptions: ThreadSubscriptionsExtension | None = None + sticky_events: StickyEventsExtension | None = None def __bool__(self) -> bool: return bool( @@ -411,6 +430,7 @@ class SlidingSyncResult: or self.receipts or self.typing or self.thread_subscriptions + or self.sticky_events ) next_pos: SlidingSyncStreamToken diff --git a/synapse/types/rest/client/__init__.py b/synapse/types/rest/client/__init__.py index 49782b5234..1bb4724a69 100644 --- a/synapse/types/rest/client/__init__.py +++ b/synapse/types/rest/client/__init__.py @@ -18,9 +18,13 @@ # [This file includes modifications made by New Vector Limited] # # +import re + +import pydantic_core.core_schema from pydantic import ( ConfigDict, Field, + GetCoreSchemaHandler, StrictBool, StrictInt, StrictStr, @@ -28,9 +32,10 @@ from pydantic import ( field_validator, model_validator, ) -from pydantic_core import PydanticCustomError +from pydantic_core import CoreSchema, PydanticCustomError from typing_extensions import Annotated, Self +from synapse.types import Absent, AbsentType, NonNegativeStrictInt from synapse.types.rest import RequestBodyModel from synapse.util.threepids import validate_email @@ -107,6 +112,54 @@ class MsisdnRequestTokenBody(ThreepidRequestTokenBody): phone_number: StrictStr +class SlidingSyncStickyEventsToken: + """ + A token returned by `next_batch` of the MSC4354 Sticky Events extension to Sliding Sync + and then accepted as the `since` parameter in the requests of the same extension. + + Current format: + SlidingSyncStickyEventsToken ::= 'sticky_' DIGIT+ + DIGIT ::= '0'-'9' + + The `sticky_` prefix allows us to make sure it's not swapped for another token + or to evolve the type of token accepted with backwards compatibility in the future. + """ + + PATTERN = re.compile(r"^sticky_([0-9]+)$") + + def __init__(self, *, sticky_events_stream_id: int) -> None: + self.sticky_events_stream_id = sticky_events_stream_id + + @classmethod + def __get_pydantic_core_schema__( + cls, source_type: object, handler: GetCoreSchemaHandler + ) -> CoreSchema: + return pydantic_core.core_schema.no_info_plain_validator_function( + cls._validate, + serialization=pydantic_core.core_schema.plain_serializer_function_ser_schema( + cls.serialise, + info_arg=False, + ), + ) + + @classmethod + def _validate(cls, v: object) -> Self: + if isinstance(v, cls): + return v + if isinstance(v, str): + match = cls.PATTERN.match(v) + if match is None: + raise ValueError(f"Invalid SlidingSyncStickyEventsToken format: {v!r}") + return cls(sticky_events_stream_id=int(match.group(1))) + raise ValueError(f"Cannot parse SlidingSyncStickyEventsToken from {type(v)}") + + def serialise(self) -> str: + return f"sticky_{self.sticky_events_stream_id}" + + def __repr__(self) -> str: + return self.serialise() + + class SlidingSyncBody(RequestBodyModel): """ Sliding Sync API request body. @@ -383,6 +436,19 @@ class SlidingSyncBody(RequestBodyModel): enabled: StrictBool | None = False limit: StrictInt = 100 + class StickyEventsExtension(RequestBodyModel): + """The Sticky Events extension (MSC4354) + + Attributes: + enabled + limit: maximum number of sticky events to return in the extension (default 100) + since: either a string with the Sticky Events since token or absent + """ + + enabled: StrictBool = False + limit: NonNegativeStrictInt = 100 + since: SlidingSyncStickyEventsToken | AbsentType = Absent + to_device: ToDeviceExtension | None = None e2ee: E2eeExtension | None = None account_data: AccountDataExtension | None = None @@ -391,6 +457,9 @@ class SlidingSyncBody(RequestBodyModel): thread_subscriptions: ThreadSubscriptionsExtension | None = Field( None, alias="io.element.msc4308.thread_subscriptions" ) + sticky_events: StickyEventsExtension | AbsentType = Field( + Absent, alias="org.matrix.msc4354.sticky_events" + ) conn_id: StrictStr | None = None lists: (