1
0

Add msc4354_sticky_duration_ttl_ms support

This commit is contained in:
Kegan Dougal
2025-09-29 16:26:32 +01:00
parent 148caefcba
commit de3e9b49ec
4 changed files with 36 additions and 14 deletions

View File

@@ -279,6 +279,8 @@ class EventUnsignedContentFields:
# Requesting user's membership, per MSC4115
MEMBERSHIP: Final = "membership"
STICKY_TTL: Final = "msc4354_sticky_duration_ttl_ms"
class MTextFields:
"""Fields found inside m.text content blocks."""
@@ -369,3 +371,4 @@ class StickyEventField(TypedDict):
class StickyEvent:
QUERY_PARAM_NAME: Final = "msc4354_stick_duration_ms"
FIELD_NAME: Final = "msc4354_sticky"
MAX_DURATION_MS: Final = 3600000 # 1 hour

View File

@@ -41,7 +41,12 @@ from typing import (
import attr
from unpaddedbase64 import encode_base64
from synapse.api.constants import EventContentFields, EventTypes, RelationTypes
from synapse.api.constants import (
EventContentFields,
EventTypes,
RelationTypes,
StickyEvent,
)
from synapse.api.room_versions import EventFormatVersions, RoomVersion, RoomVersions
from synapse.synapse_rust.events import EventInternalMetadata
from synapse.types import (
@@ -323,6 +328,20 @@ class EventBase(metaclass=abc.ABCMeta):
# this will be a no-op if the event dict is already frozen.
self._dict = freeze(self._dict)
def sticky_duration(self) -> Optional[int]:
sticky_obj = self.get_dict().get(StickyEvent.FIELD_NAME, None)
if type(sticky_obj) is not dict:
return None
sticky_duration_ms = sticky_obj.get("duration_ms", None)
# MSC: Valid values are the integer range 0-MAX_DURATION_MS
if (
type(sticky_duration_ms) is int
and sticky_duration_ms >= 0
and sticky_duration_ms <= StickyEvent.MAX_DURATION_MS
):
return sticky_duration_ms
return None
def __str__(self) -> str:
return self.__repr__()

View File

@@ -57,7 +57,6 @@ logger = logging.getLogger(__name__)
# Consumers call 'get_sticky_events_in_rooms' which has `WHERE expires_at > ?`
# to filter out expired sticky events that have yet to be deleted.
DELETE_EXPIRED_STICKY_EVENTS_MS = 60 * 1000 * 60 # 1 hour
MAX_STICKY_DURATION_MS = 3600000 # 1 hour
class StickyEventsWorkerStore(StateGroupWorkerStore, CacheInvalidationWorkerStore):
@@ -297,22 +296,14 @@ class StickyEventsWorkerStore(StateGroupWorkerStore, CacheInvalidationWorkerStor
if ev.rejected_reason is not None:
continue
# MSC: The presence of sticky.duration_ms with a valid value makes the event “sticky”
sticky_obj = ev.get_dict().get(StickyEvent.FIELD_NAME, None)
if type(sticky_obj) is not dict:
continue
sticky_duration_ms = sticky_obj.get("duration_ms", None)
# MSC: Valid values are the integer range 0-MAX_STICKY_DURATION_MS
if (
type(sticky_duration_ms) is int
and sticky_duration_ms >= 0
and sticky_duration_ms <= MAX_STICKY_DURATION_MS
):
sticky_duration = ev.sticky_duration()
if sticky_duration:
# MSC: The start time is min(now, origin_server_ts).
# This ensures that malicious origin timestamps cannot specify start times in the future.
# Calculate the end time as start_time + min(sticky.duration_ms, MAX_STICKY_DURATION_MS).
# Calculate the end time as start_time + min(sticky.duration_ms, MAX_DURATION_MS).
expires_at = min(ev.origin_server_ts, now_ms) + min(
ev.get_dict()[StickyEvent.FIELD_NAME]["duration_ms"],
MAX_STICKY_DURATION_MS,
StickyEvent.MAX_DURATION_MS,
)
# filter out already expired sticky events
if expires_at > now_ms:

View File

@@ -209,6 +209,15 @@ async def filter_events_for_client(
# to the cache!
cloned = clone_event(filtered)
cloned.unsigned[EventUnsignedContentFields.MEMBERSHIP] = user_membership
if storage.main.config.experimental.msc4354_enabled:
sticky_duration = cloned.sticky_duration()
if sticky_duration:
now = storage.main.clock.time_msec()
expires_at = min(cloned.origin_server_ts, now) + sticky_duration
if sticky_duration and expires_at > now:
cloned.unsigned[EventUnsignedContentFields.STICKY_TTL] = (
expires_at - now
)
return cloned