Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 06ab64f

Browse files
authored
Implement MSC3925: changes to bundling of edits (#14811)
Two parts to this: * Bundle the whole of the replacement with any edited events. This is backwards-compatible so I haven't put it behind a flag. * Optionally, inhibit server-side replacement of edited events. This has scope to break things, so it is currently disabled by default.
1 parent f417fb8 commit 06ab64f

File tree

5 files changed

+159
-63
lines changed

5 files changed

+159
-63
lines changed

changelog.d/14811.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Per [MSC3925](https://github.com/matrix-org/matrix-spec-proposals/pull/3925), bundle the whole of the replacement with any edited events, and optionally inhibit server-side replacement.

synapse/config/experimental.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,6 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
139139

140140
# MSC3391: Removing account data.
141141
self.msc3391_enabled = experimental.get("msc3391_enabled", False)
142+
143+
# MSC3925: do not replace events with their edits
144+
self.msc3925_inhibit_edit = experimental.get("msc3925_inhibit_edit", False)

synapse/events/utils.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,14 @@ class EventClientSerializer:
403403
clients.
404404
"""
405405

406+
def __init__(self, inhibit_replacement_via_edits: bool = False):
407+
"""
408+
Args:
409+
inhibit_replacement_via_edits: If this is set to True, then events are
410+
never replaced by their edits.
411+
"""
412+
self._inhibit_replacement_via_edits = inhibit_replacement_via_edits
413+
406414
def serialize_event(
407415
self,
408416
event: Union[JsonDict, EventBase],
@@ -422,6 +430,8 @@ def serialize_event(
422430
into the event.
423431
apply_edits: Whether the content of the event should be modified to reflect
424432
any replacement in `bundle_aggregations[<event_id>].replace`.
433+
See also the `inhibit_replacement_via_edits` constructor arg: if that is
434+
set to True, then this argument is ignored.
425435
Returns:
426436
The serialized event
427437
"""
@@ -495,7 +505,8 @@ def _inject_bundled_aggregations(
495505
again for additional events in a recursive manner.
496506
serialized_event: The serialized event which may be modified.
497507
apply_edits: Whether the content of the event should be modified to reflect
498-
any replacement in `aggregations.replace`.
508+
any replacement in `aggregations.replace` (subject to the
509+
`inhibit_replacement_via_edits` constructor arg).
499510
"""
500511

501512
# We have already checked that aggregations exist for this event.
@@ -518,15 +529,21 @@ def _inject_bundled_aggregations(
518529
if event_aggregations.replace:
519530
# If there is an edit, optionally apply it to the event.
520531
edit = event_aggregations.replace
521-
if apply_edits:
532+
if apply_edits and not self._inhibit_replacement_via_edits:
522533
self._apply_edit(event, serialized_event, edit)
523534

524535
# Include information about it in the relations dict.
525-
serialized_aggregations[RelationTypes.REPLACE] = {
526-
"event_id": edit.event_id,
527-
"origin_server_ts": edit.origin_server_ts,
528-
"sender": edit.sender,
529-
}
536+
#
537+
# Matrix spec v1.5 (https://spec.matrix.org/v1.5/client-server-api/#server-side-aggregation-of-mreplace-relationships)
538+
# said that we should only include the `event_id`, `origin_server_ts` and
539+
# `sender` of the edit; however MSC3925 proposes extending it to the whole
540+
# of the edit, which is what we do here.
541+
serialized_aggregations[RelationTypes.REPLACE] = self.serialize_event(
542+
edit,
543+
time_now,
544+
config=config,
545+
apply_edits=False,
546+
)
530547

531548
# Include any threaded replies to this event.
532549
if event_aggregations.thread:

synapse/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -743,7 +743,7 @@ def get_oidc_handler(self) -> "OidcHandler":
743743

744744
@cache_in_self
745745
def get_event_client_serializer(self) -> EventClientSerializer:
746-
return EventClientSerializer()
746+
return EventClientSerializer(self.config.experimental.msc3925_inhibit_edit)
747747

748748
@cache_in_self
749749
def get_password_policy_handler(self) -> PasswordPolicyHandler:

tests/rest/client/test_relations.py

Lines changed: 130 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from tests.server import FakeChannel
3131
from tests.test_utils import make_awaitable
3232
from tests.test_utils.event_injection import inject_event
33+
from tests.unittest import override_config
3334

3435

3536
class BaseRelationsTestCase(unittest.HomeserverTestCase):
@@ -355,30 +356,67 @@ def test_ignore_invalid_room(self) -> None:
355356
self.assertEqual(200, channel.code, channel.json_body)
356357
self.assertNotIn("m.relations", channel.json_body["unsigned"])
357358

359+
def _assert_edit_bundle(
360+
self, event_json: JsonDict, edit_event_id: str, edit_event_content: JsonDict
361+
) -> None:
362+
"""
363+
Assert that the given event has a correctly-serialised edit event in its
364+
bundled aggregations
365+
366+
Args:
367+
event_json: the serialised event to be checked
368+
edit_event_id: the ID of the edit event that we expect to be bundled
369+
edit_event_content: the content of that event, excluding the 'm.relates_to`
370+
property
371+
"""
372+
relations_dict = event_json["unsigned"].get("m.relations")
373+
self.assertIn(RelationTypes.REPLACE, relations_dict)
374+
375+
m_replace_dict = relations_dict[RelationTypes.REPLACE]
376+
for key in [
377+
"event_id",
378+
"sender",
379+
"origin_server_ts",
380+
"content",
381+
"type",
382+
"unsigned",
383+
]:
384+
self.assertIn(key, m_replace_dict)
385+
386+
expected_edit_content = {
387+
"m.relates_to": {
388+
"event_id": event_json["event_id"],
389+
"rel_type": "m.replace",
390+
}
391+
}
392+
expected_edit_content.update(edit_event_content)
393+
394+
self.assert_dict(
395+
{
396+
"event_id": edit_event_id,
397+
"sender": self.user_id,
398+
"content": expected_edit_content,
399+
"type": "m.room.message",
400+
},
401+
m_replace_dict,
402+
)
403+
358404
def test_edit(self) -> None:
359405
"""Test that a simple edit works."""
360406

361407
new_body = {"msgtype": "m.text", "body": "I've been edited!"}
408+
edit_event_content = {
409+
"msgtype": "m.text",
410+
"body": "foo",
411+
"m.new_content": new_body,
412+
}
362413
channel = self._send_relation(
363414
RelationTypes.REPLACE,
364415
"m.room.message",
365-
content={"msgtype": "m.text", "body": "foo", "m.new_content": new_body},
416+
content=edit_event_content,
366417
)
367418
edit_event_id = channel.json_body["event_id"]
368419

369-
def assert_bundle(event_json: JsonDict) -> None:
370-
"""Assert the expected values of the bundled aggregations."""
371-
relations_dict = event_json["unsigned"].get("m.relations")
372-
self.assertIn(RelationTypes.REPLACE, relations_dict)
373-
374-
m_replace_dict = relations_dict[RelationTypes.REPLACE]
375-
for key in ["event_id", "sender", "origin_server_ts"]:
376-
self.assertIn(key, m_replace_dict)
377-
378-
self.assert_dict(
379-
{"event_id": edit_event_id, "sender": self.user_id}, m_replace_dict
380-
)
381-
382420
# /event should return the *original* event
383421
channel = self.make_request(
384422
"GET",
@@ -389,7 +427,7 @@ def assert_bundle(event_json: JsonDict) -> None:
389427
self.assertEqual(
390428
channel.json_body["content"], {"body": "Hi!", "msgtype": "m.text"}
391429
)
392-
assert_bundle(channel.json_body)
430+
self._assert_edit_bundle(channel.json_body, edit_event_id, edit_event_content)
393431

394432
# Request the room messages.
395433
channel = self.make_request(
@@ -398,7 +436,11 @@ def assert_bundle(event_json: JsonDict) -> None:
398436
access_token=self.user_token,
399437
)
400438
self.assertEqual(200, channel.code, channel.json_body)
401-
assert_bundle(self._find_event_in_chunk(channel.json_body["chunk"]))
439+
self._assert_edit_bundle(
440+
self._find_event_in_chunk(channel.json_body["chunk"]),
441+
edit_event_id,
442+
edit_event_content,
443+
)
402444

403445
# Request the room context.
404446
# /context should return the edited event.
@@ -408,7 +450,9 @@ def assert_bundle(event_json: JsonDict) -> None:
408450
access_token=self.user_token,
409451
)
410452
self.assertEqual(200, channel.code, channel.json_body)
411-
assert_bundle(channel.json_body["event"])
453+
self._assert_edit_bundle(
454+
channel.json_body["event"], edit_event_id, edit_event_content
455+
)
412456
self.assertEqual(channel.json_body["event"]["content"], new_body)
413457

414458
# Request sync, but limit the timeline so it becomes limited (and includes
@@ -420,7 +464,11 @@ def assert_bundle(event_json: JsonDict) -> None:
420464
self.assertEqual(200, channel.code, channel.json_body)
421465
room_timeline = channel.json_body["rooms"]["join"][self.room]["timeline"]
422466
self.assertTrue(room_timeline["limited"])
423-
assert_bundle(self._find_event_in_chunk(room_timeline["events"]))
467+
self._assert_edit_bundle(
468+
self._find_event_in_chunk(room_timeline["events"]),
469+
edit_event_id,
470+
edit_event_content,
471+
)
424472

425473
# Request search.
426474
channel = self.make_request(
@@ -437,7 +485,45 @@ def assert_bundle(event_json: JsonDict) -> None:
437485
"results"
438486
]
439487
]
440-
assert_bundle(self._find_event_in_chunk(chunk))
488+
self._assert_edit_bundle(
489+
self._find_event_in_chunk(chunk),
490+
edit_event_id,
491+
edit_event_content,
492+
)
493+
494+
@override_config({"experimental_features": {"msc3925_inhibit_edit": True}})
495+
def test_edit_inhibit_replace(self) -> None:
496+
"""
497+
If msc3925_inhibit_edit is enabled, then the original event should not be
498+
replaced.
499+
"""
500+
501+
new_body = {"msgtype": "m.text", "body": "I've been edited!"}
502+
edit_event_content = {
503+
"msgtype": "m.text",
504+
"body": "foo",
505+
"m.new_content": new_body,
506+
}
507+
channel = self._send_relation(
508+
RelationTypes.REPLACE,
509+
"m.room.message",
510+
content=edit_event_content,
511+
)
512+
edit_event_id = channel.json_body["event_id"]
513+
514+
# /context should return the *original* event.
515+
channel = self.make_request(
516+
"GET",
517+
f"/rooms/{self.room}/context/{self.parent_id}",
518+
access_token=self.user_token,
519+
)
520+
self.assertEqual(200, channel.code, channel.json_body)
521+
self.assertEqual(
522+
channel.json_body["event"]["content"], {"body": "Hi!", "msgtype": "m.text"}
523+
)
524+
self._assert_edit_bundle(
525+
channel.json_body["event"], edit_event_id, edit_event_content
526+
)
441527

442528
def test_multi_edit(self) -> None:
443529
"""Test that multiple edits, including attempts by people who
@@ -455,10 +541,15 @@ def test_multi_edit(self) -> None:
455541
)
456542

457543
new_body = {"msgtype": "m.text", "body": "I've been edited!"}
544+
edit_event_content = {
545+
"msgtype": "m.text",
546+
"body": "foo",
547+
"m.new_content": new_body,
548+
}
458549
channel = self._send_relation(
459550
RelationTypes.REPLACE,
460551
"m.room.message",
461-
content={"msgtype": "m.text", "body": "foo", "m.new_content": new_body},
552+
content=edit_event_content,
462553
)
463554
edit_event_id = channel.json_body["event_id"]
464555

@@ -480,16 +571,8 @@ def test_multi_edit(self) -> None:
480571
self.assertEqual(200, channel.code, channel.json_body)
481572

482573
self.assertEqual(channel.json_body["event"]["content"], new_body)
483-
484-
relations_dict = channel.json_body["event"]["unsigned"].get("m.relations")
485-
self.assertIn(RelationTypes.REPLACE, relations_dict)
486-
487-
m_replace_dict = relations_dict[RelationTypes.REPLACE]
488-
for key in ["event_id", "sender", "origin_server_ts"]:
489-
self.assertIn(key, m_replace_dict)
490-
491-
self.assert_dict(
492-
{"event_id": edit_event_id, "sender": self.user_id}, m_replace_dict
574+
self._assert_edit_bundle(
575+
channel.json_body["event"], edit_event_id, edit_event_content
493576
)
494577

495578
def test_edit_reply(self) -> None:
@@ -502,11 +585,15 @@ def test_edit_reply(self) -> None:
502585
)
503586
reply = channel.json_body["event_id"]
504587

505-
new_body = {"msgtype": "m.text", "body": "I've been edited!"}
588+
edit_event_content = {
589+
"msgtype": "m.text",
590+
"body": "foo",
591+
"m.new_content": {"msgtype": "m.text", "body": "I've been edited!"},
592+
}
506593
channel = self._send_relation(
507594
RelationTypes.REPLACE,
508595
"m.room.message",
509-
content={"msgtype": "m.text", "body": "foo", "m.new_content": new_body},
596+
content=edit_event_content,
510597
parent_id=reply,
511598
)
512599
edit_event_id = channel.json_body["event_id"]
@@ -549,28 +636,22 @@ def test_edit_reply(self) -> None:
549636

550637
# We expect that the edit relation appears in the unsigned relations
551638
# section.
552-
relations_dict = result_event_dict["unsigned"].get("m.relations")
553-
self.assertIn(RelationTypes.REPLACE, relations_dict, desc)
554-
555-
m_replace_dict = relations_dict[RelationTypes.REPLACE]
556-
for key in ["event_id", "sender", "origin_server_ts"]:
557-
self.assertIn(key, m_replace_dict, desc)
558-
559-
self.assert_dict(
560-
{"event_id": edit_event_id, "sender": self.user_id}, m_replace_dict
639+
self._assert_edit_bundle(
640+
result_event_dict, edit_event_id, edit_event_content
561641
)
562642

563643
def test_edit_edit(self) -> None:
564644
"""Test that an edit cannot be edited."""
565645
new_body = {"msgtype": "m.text", "body": "Initial edit"}
646+
edit_event_content = {
647+
"msgtype": "m.text",
648+
"body": "Wibble",
649+
"m.new_content": new_body,
650+
}
566651
channel = self._send_relation(
567652
RelationTypes.REPLACE,
568653
"m.room.message",
569-
content={
570-
"msgtype": "m.text",
571-
"body": "Wibble",
572-
"m.new_content": new_body,
573-
},
654+
content=edit_event_content,
574655
)
575656
edit_event_id = channel.json_body["event_id"]
576657

@@ -599,8 +680,7 @@ def test_edit_edit(self) -> None:
599680
)
600681

601682
# The relations information should not include the edit to the edit.
602-
relations_dict = channel.json_body["unsigned"].get("m.relations")
603-
self.assertIn(RelationTypes.REPLACE, relations_dict)
683+
self._assert_edit_bundle(channel.json_body, edit_event_id, edit_event_content)
604684

605685
# /context should return the event updated for the *first* edit
606686
# (The edit to the edit should be ignored.)
@@ -611,13 +691,8 @@ def test_edit_edit(self) -> None:
611691
)
612692
self.assertEqual(200, channel.code, channel.json_body)
613693
self.assertEqual(channel.json_body["event"]["content"], new_body)
614-
615-
m_replace_dict = relations_dict[RelationTypes.REPLACE]
616-
for key in ["event_id", "sender", "origin_server_ts"]:
617-
self.assertIn(key, m_replace_dict)
618-
619-
self.assert_dict(
620-
{"event_id": edit_event_id, "sender": self.user_id}, m_replace_dict
694+
self._assert_edit_bundle(
695+
channel.json_body["event"], edit_event_id, edit_event_content
621696
)
622697

623698
# Directly requesting the edit should not have the edit to the edit applied.

0 commit comments

Comments
 (0)