Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/19663.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support [MSC4446](https://github.com/matrix-org/matrix-spec-proposals/pull/4446) for moving fully read markers backwards. Contributed by @SpiritCroc @ Beeper.
1 change: 1 addition & 0 deletions rust/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,5 @@ pub struct ExperimentalConfig {
pub msc4222_enabled: bool,
pub msc4491_enabled: bool,
pub msc4143_enabled: bool,
pub msc4446_enabled: bool,
}
4 changes: 4 additions & 0 deletions rust/src/handlers/versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,9 @@ pub struct UnstableFeatureMap {
/// MSC4143: Matrix RTC transports (LiveKit backend)
#[serde(rename = "org.matrix.msc4143")]
msc4143_enabled: bool,
/// MSC4446: Allow moving the fully read marker backwards.
#[serde(rename = "com.beeper.msc4446")]
msc4446_enabled: bool,

// Whether new rooms will be set to encrypted or not (based on presets).
#[serde(rename = "io.element.e2ee_forced.public")]
Expand Down Expand Up @@ -319,6 +322,7 @@ pub fn synapse_config_to_global_unstable_feature_map(
msc4445_initial_sync_timeline_topological_ordering: true,
msc4491_enabled: config.experimental.msc4491_enabled,
msc4143_enabled: config.experimental.msc4143_enabled,
msc4446_enabled: config.experimental.msc4446_enabled,
e2ee_forced_public: config
.room
.encryption_enabled_by_default_for_room_presets
Expand Down
3 changes: 3 additions & 0 deletions synapse/config/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,9 @@ def read_config(
# (and MSC4308: Thread Subscriptions extension to Sliding Sync)
self.msc4306_enabled: bool = experimental.get("msc4306_enabled", False)

# MSC4446: Allow moving the fully read marker backwards.
self.msc4446_enabled: bool = experimental.get("msc4446_enabled", False)
Comment on lines +290 to +291

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you create an experimental feature issue so we can track this implementation going forwards?


# MSC4354: Sticky Events
# Tracked in: https://github.com/element-hq/synapse/issues/19409
# Note that sticky events persisted before this feature is enabled will not be
Expand Down
8 changes: 6 additions & 2 deletions synapse/handlers/read_marker.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ def __init__(self, hs: "HomeServer"):
)

async def received_client_read_marker(
self, room_id: str, user_id: str, event_id: str
self,
room_id: str,
user_id: str,
event_id: str,
allow_backward: bool = False,
) -> None:
"""Updates the read marker for a given user in a given room if the event ID given
is ahead in the stream relative to the current read marker.
Expand All @@ -59,7 +63,7 @@ async def received_client_read_marker(
# Get event ordering, this also ensures we know about the event
event_ordering = await self.store.get_event_ordering(event_id, room_id)

if existing_read_marker:
if existing_read_marker and not allow_backward:
try:
old_event_ordering = await self.store.get_event_ordering(
existing_read_marker["event_id"], room_id
Expand Down
17 changes: 17 additions & 0 deletions synapse/rest/client/read_marker.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from typing import TYPE_CHECKING

from synapse.api.constants import ReceiptTypes
from synapse.api.errors import Codes, SynapseError
from synapse.http.server import HttpServer
from synapse.http.servlet import RestServlet, parse_json_object_from_request
from synapse.http.site import SynapseRequest
Expand Down Expand Up @@ -66,6 +67,21 @@ async def on_POST(
body = parse_json_object_from_request(request)

unrecognized_types = set(body.keys()) - self._known_receipt_types

if self.config.experimental.msc4446_enabled:
allow_backward = body.get("com.beeper.allow_backward", False)
if not isinstance(allow_backward, bool):
raise SynapseError(
400,
"com.beeper.allow_backward must be a boolean.",
Codes.INVALID_PARAM,
)

# Prevent considering the `allow_backward` field as a receipt type.
unrecognized_types -= {"com.beeper.allow_backward"}
Comment thread
SpiritCroc marked this conversation as resolved.
else:
allow_backward = False

if unrecognized_types:
# It's fine if there are unrecognized receipt types, but let's log
# it to help debug clients that have typoed the receipt type.
Expand All @@ -86,6 +102,7 @@ async def on_POST(
room_id,
user_id=requester.user.to_string(),
event_id=event_id,
allow_backward=allow_backward,
)
else:
await self.receipts_handler.received_client_receipt(
Expand Down
22 changes: 22 additions & 0 deletions synapse/rest/client/receipts.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#

import logging
from http import HTTPStatus
from typing import TYPE_CHECKING

from synapse.api.constants import MAIN_TIMELINE, ReceiptTypes
Expand Down Expand Up @@ -50,6 +51,7 @@ def __init__(self, hs: "HomeServer"):
self.read_marker_handler = hs.get_read_marker_handler()
self.presence_handler = hs.get_presence_handler()
self._main_store = hs.get_datastores().main
self._msc4446_enabled = hs.config.experimental.msc4446_enabled

self._known_receipt_types = {
ReceiptTypes.READ,
Expand All @@ -73,6 +75,25 @@ async def on_POST(

body = parse_json_object_from_request(request)

if self._msc4446_enabled:
allow_backward = body.get("com.beeper.allow_backward", False)
if not isinstance(allow_backward, bool):
raise SynapseError(
HTTPStatus.BAD_REQUEST,
"com.beeper.allow_backward must be a boolean.",
Codes.INVALID_PARAM,
)

if allow_backward and receipt_type != ReceiptTypes.FULLY_READ:
raise SynapseError(
HTTPStatus.BAD_REQUEST,
"com.beeper.allow_backward is only allowed to be true for "
f"{ReceiptTypes.FULLY_READ}.",
Codes.INVALID_PARAM,
)
else:
allow_backward = False

# Pull the thread ID, if one exists.
thread_id = None
if "thread_id" in body:
Expand Down Expand Up @@ -108,6 +129,7 @@ async def on_POST(
room_id,
user_id=requester.user.to_string(),
event_id=event_id,
allow_backward=allow_backward,
)
else:
await self.receipts_handler.received_client_receipt(
Expand Down
130 changes: 130 additions & 0 deletions tests/rest/client/test_read_marker.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
self.store = self.hs.get_datastores().main
self.clock = self.hs.get_clock()

def _get_fully_read_marker(self, room_id: str) -> str | None:
content = self.get_success(
self.store.get_account_data_for_room_and_type(
self.owner,
room_id,
"m.fully_read",
)
)
if content is None:
return None

return content.get("event_id")

def test_send_read_marker(self) -> None:
room_id = self.helper.create_room_as(self.owner, tok=self.owner_tok)

Expand Down Expand Up @@ -98,6 +111,123 @@ def send_message() -> str:
)
self.assertEqual(channel.code, 200, channel.result)

def test_send_read_marker_does_not_move_backwards_by_default(self) -> None:
Comment thread
SpiritCroc marked this conversation as resolved.
room_id = self.helper.create_room_as(self.owner, tok=self.owner_tok)

older_event_id = self.helper.send(
room_id=room_id, body="1", tok=self.owner_tok
)["event_id"]
newer_event_id = self.helper.send(
room_id=room_id, body="2", tok=self.owner_tok
)["event_id"]

channel = self.make_request(
"POST",
f"/rooms/{room_id}/read_markers",
content={"m.fully_read": newer_event_id},
access_token=self.owner_tok,
)
self.assertEqual(channel.code, 200, channel.result)
self.assertEqual(self._get_fully_read_marker(room_id), newer_event_id)

# Expected to be a no-op.
channel = self.make_request(
Comment thread
SpiritCroc marked this conversation as resolved.
"POST",
f"/rooms/{room_id}/read_markers",
content={"m.fully_read": older_event_id},
access_token=self.owner_tok,
)
self.assertEqual(channel.code, 200, channel.result)
self.assertEqual(self._get_fully_read_marker(room_id), newer_event_id)

@unittest.override_config({"experimental_features": {"msc4446_enabled": True}})
def test_send_read_marker_can_move_backwards_with_opt_in(self) -> None:
room_id = self.helper.create_room_as(self.owner, tok=self.owner_tok)

older_event_id = self.helper.send(
room_id=room_id, body="1", tok=self.owner_tok
)["event_id"]
newer_event_id = self.helper.send(
room_id=room_id, body="2", tok=self.owner_tok
)["event_id"]

channel = self.make_request(
"POST",
f"/rooms/{room_id}/read_markers",
content={"m.fully_read": newer_event_id},
access_token=self.owner_tok,
)
self.assertEqual(channel.code, 200, channel.result)

channel = self.make_request(
"POST",
f"/rooms/{room_id}/read_markers",
content={"m.fully_read": older_event_id, "com.beeper.allow_backward": True},
access_token=self.owner_tok,
)
self.assertEqual(channel.code, 200, channel.result)
self.assertEqual(self._get_fully_read_marker(room_id), older_event_id)

@unittest.override_config({"experimental_features": {"msc4446_enabled": True}})
def test_send_read_marker_does_not_move_backwards_with_explicit_opt_out(
self,
) -> None:
room_id = self.helper.create_room_as(self.owner, tok=self.owner_tok)

older_event_id = self.helper.send(
room_id=room_id, body="1", tok=self.owner_tok
)["event_id"]
newer_event_id = self.helper.send(
room_id=room_id, body="2", tok=self.owner_tok
)["event_id"]

channel = self.make_request(
"POST",
f"/rooms/{room_id}/read_markers",
content={"m.fully_read": newer_event_id},
access_token=self.owner_tok,
)
self.assertEqual(channel.code, 200, channel.result)

# Expected to be a no-op.
channel = self.make_request(
"POST",
f"/rooms/{room_id}/read_markers",
content={
"m.fully_read": older_event_id,
"com.beeper.allow_backward": False,
},
access_token=self.owner_tok,
)
self.assertEqual(channel.code, 200, channel.result)
self.assertEqual(self._get_fully_read_marker(room_id), newer_event_id)

def test_send_read_marker_ignores_opt_in_when_feature_disabled(self) -> None:
room_id = self.helper.create_room_as(self.owner, tok=self.owner_tok)
older_event_id = self.helper.send(
room_id=room_id, body="1", tok=self.owner_tok
)["event_id"]
newer_event_id = self.helper.send(
room_id=room_id, body="2", tok=self.owner_tok
)["event_id"]

channel = self.make_request(
"POST",
f"/rooms/{room_id}/read_markers",
content={"m.fully_read": newer_event_id},
access_token=self.owner_tok,
)
self.assertEqual(channel.code, 200, channel.result)

channel = self.make_request(
"POST",
f"/rooms/{room_id}/read_markers",
content={"m.fully_read": older_event_id, "com.beeper.allow_backward": True},
access_token=self.owner_tok,
)
self.assertEqual(channel.code, 200, channel.result)
self.assertEqual(self._get_fully_read_marker(room_id), newer_event_id)

def test_send_read_marker_missing_previous_event(self) -> None:
"""
Test moving a read marker from an event that previously existed but was
Expand Down
Loading
Loading