mirror of
https://git.anonymousland.org/anonymousland/synapse-product.git
synced 2024-10-01 08:25:44 -04:00
Stop applying edits to event contents (MSC3925). (#15193)
Enables MSC3925 support by default, which: * Includes the full edit event in the bundled aggregations of an edited event. * Stops modifying the original event's content to return the new content from the edit event. This is a backwards-incompatible change that is considered to be "correct" by the spec.
This commit is contained in:
parent
fd9cadcf53
commit
05e0a4089a
1
changelog.d/15193.bugfix
Normal file
1
changelog.d/15193.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Stop applying edits when bundling aggregations, per [MSC3925](https://github.com/matrix-org/matrix-spec-proposals/pull/3925).
|
@ -166,9 +166,6 @@ class ExperimentalConfig(Config):
|
|||||||
# MSC3391: Removing account data.
|
# MSC3391: Removing account data.
|
||||||
self.msc3391_enabled = experimental.get("msc3391_enabled", False)
|
self.msc3391_enabled = experimental.get("msc3391_enabled", False)
|
||||||
|
|
||||||
# MSC3925: do not replace events with their edits
|
|
||||||
self.msc3925_inhibit_edit = experimental.get("msc3925_inhibit_edit", False)
|
|
||||||
|
|
||||||
# MSC3873: Disambiguate event_match keys.
|
# MSC3873: Disambiguate event_match keys.
|
||||||
self.msc3873_escape_event_match_key = experimental.get(
|
self.msc3873_escape_event_match_key = experimental.get(
|
||||||
"msc3873_escape_event_match_key", False
|
"msc3873_escape_event_match_key", False
|
||||||
|
@ -39,7 +39,6 @@ from synapse.api.constants import (
|
|||||||
from synapse.api.errors import Codes, SynapseError
|
from synapse.api.errors import Codes, SynapseError
|
||||||
from synapse.api.room_versions import RoomVersion
|
from synapse.api.room_versions import RoomVersion
|
||||||
from synapse.types import JsonDict
|
from synapse.types import JsonDict
|
||||||
from synapse.util.frozenutils import unfreeze
|
|
||||||
|
|
||||||
from . import EventBase
|
from . import EventBase
|
||||||
|
|
||||||
@ -403,14 +402,6 @@ class EventClientSerializer:
|
|||||||
clients.
|
clients.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, inhibit_replacement_via_edits: bool = False):
|
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
inhibit_replacement_via_edits: If this is set to True, then events are
|
|
||||||
never replaced by their edits.
|
|
||||||
"""
|
|
||||||
self._inhibit_replacement_via_edits = inhibit_replacement_via_edits
|
|
||||||
|
|
||||||
def serialize_event(
|
def serialize_event(
|
||||||
self,
|
self,
|
||||||
event: Union[JsonDict, EventBase],
|
event: Union[JsonDict, EventBase],
|
||||||
@ -418,7 +409,6 @@ class EventClientSerializer:
|
|||||||
*,
|
*,
|
||||||
config: SerializeEventConfig = _DEFAULT_SERIALIZE_EVENT_CONFIG,
|
config: SerializeEventConfig = _DEFAULT_SERIALIZE_EVENT_CONFIG,
|
||||||
bundle_aggregations: Optional[Dict[str, "BundledAggregations"]] = None,
|
bundle_aggregations: Optional[Dict[str, "BundledAggregations"]] = None,
|
||||||
apply_edits: bool = True,
|
|
||||||
) -> JsonDict:
|
) -> JsonDict:
|
||||||
"""Serializes a single event.
|
"""Serializes a single event.
|
||||||
|
|
||||||
@ -428,10 +418,7 @@ class EventClientSerializer:
|
|||||||
config: Event serialization config
|
config: Event serialization config
|
||||||
bundle_aggregations: A map from event_id to the aggregations to be bundled
|
bundle_aggregations: A map from event_id to the aggregations to be bundled
|
||||||
into the event.
|
into the event.
|
||||||
apply_edits: Whether the content of the event should be modified to reflect
|
|
||||||
any replacement in `bundle_aggregations[<event_id>].replace`.
|
|
||||||
See also the `inhibit_replacement_via_edits` constructor arg: if that is
|
|
||||||
set to True, then this argument is ignored.
|
|
||||||
Returns:
|
Returns:
|
||||||
The serialized event
|
The serialized event
|
||||||
"""
|
"""
|
||||||
@ -450,38 +437,10 @@ class EventClientSerializer:
|
|||||||
config,
|
config,
|
||||||
bundle_aggregations,
|
bundle_aggregations,
|
||||||
serialized_event,
|
serialized_event,
|
||||||
apply_edits=apply_edits,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return serialized_event
|
return serialized_event
|
||||||
|
|
||||||
def _apply_edit(
|
|
||||||
self, orig_event: EventBase, serialized_event: JsonDict, edit: EventBase
|
|
||||||
) -> None:
|
|
||||||
"""Replace the content, preserving existing relations of the serialized event.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
orig_event: The original event.
|
|
||||||
serialized_event: The original event, serialized. This is modified.
|
|
||||||
edit: The event which edits the above.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Ensure we take copies of the edit content, otherwise we risk modifying
|
|
||||||
# the original event.
|
|
||||||
edit_content = edit.content.copy()
|
|
||||||
|
|
||||||
# Unfreeze the event content if necessary, so that we may modify it below
|
|
||||||
edit_content = unfreeze(edit_content)
|
|
||||||
serialized_event["content"] = edit_content.get("m.new_content", {})
|
|
||||||
|
|
||||||
# Check for existing relations
|
|
||||||
relates_to = orig_event.content.get("m.relates_to")
|
|
||||||
if relates_to:
|
|
||||||
# Keep the relations, ensuring we use a dict copy of the original
|
|
||||||
serialized_event["content"]["m.relates_to"] = relates_to.copy()
|
|
||||||
else:
|
|
||||||
serialized_event["content"].pop("m.relates_to", None)
|
|
||||||
|
|
||||||
def _inject_bundled_aggregations(
|
def _inject_bundled_aggregations(
|
||||||
self,
|
self,
|
||||||
event: EventBase,
|
event: EventBase,
|
||||||
@ -489,7 +448,6 @@ class EventClientSerializer:
|
|||||||
config: SerializeEventConfig,
|
config: SerializeEventConfig,
|
||||||
bundled_aggregations: Dict[str, "BundledAggregations"],
|
bundled_aggregations: Dict[str, "BundledAggregations"],
|
||||||
serialized_event: JsonDict,
|
serialized_event: JsonDict,
|
||||||
apply_edits: bool,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Potentially injects bundled aggregations into the unsigned portion of the serialized event.
|
"""Potentially injects bundled aggregations into the unsigned portion of the serialized event.
|
||||||
|
|
||||||
@ -504,9 +462,6 @@ class EventClientSerializer:
|
|||||||
While serializing the bundled aggregations this map may be searched
|
While serializing the bundled aggregations this map may be searched
|
||||||
again for additional events in a recursive manner.
|
again for additional events in a recursive manner.
|
||||||
serialized_event: The serialized event which may be modified.
|
serialized_event: The serialized event which may be modified.
|
||||||
apply_edits: Whether the content of the event should be modified to reflect
|
|
||||||
any replacement in `aggregations.replace` (subject to the
|
|
||||||
`inhibit_replacement_via_edits` constructor arg).
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# We have already checked that aggregations exist for this event.
|
# We have already checked that aggregations exist for this event.
|
||||||
@ -522,11 +477,6 @@ class EventClientSerializer:
|
|||||||
] = event_aggregations.references
|
] = event_aggregations.references
|
||||||
|
|
||||||
if event_aggregations.replace:
|
if event_aggregations.replace:
|
||||||
# If there is an edit, optionally apply it to the event.
|
|
||||||
edit = event_aggregations.replace
|
|
||||||
if apply_edits and not self._inhibit_replacement_via_edits:
|
|
||||||
self._apply_edit(event, serialized_event, edit)
|
|
||||||
|
|
||||||
# Include information about it in the relations dict.
|
# Include information about it in the relations dict.
|
||||||
#
|
#
|
||||||
# Matrix spec v1.5 (https://spec.matrix.org/v1.5/client-server-api/#server-side-aggregation-of-mreplace-relationships)
|
# Matrix spec v1.5 (https://spec.matrix.org/v1.5/client-server-api/#server-side-aggregation-of-mreplace-relationships)
|
||||||
@ -534,10 +484,7 @@ class EventClientSerializer:
|
|||||||
# `sender` of the edit; however MSC3925 proposes extending it to the whole
|
# `sender` of the edit; however MSC3925 proposes extending it to the whole
|
||||||
# of the edit, which is what we do here.
|
# of the edit, which is what we do here.
|
||||||
serialized_aggregations[RelationTypes.REPLACE] = self.serialize_event(
|
serialized_aggregations[RelationTypes.REPLACE] = self.serialize_event(
|
||||||
edit,
|
event_aggregations.replace, time_now, config=config
|
||||||
time_now,
|
|
||||||
config=config,
|
|
||||||
apply_edits=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Include any threaded replies to this event.
|
# Include any threaded replies to this event.
|
||||||
|
@ -818,7 +818,7 @@ class RoomEventServlet(RestServlet):
|
|||||||
# per MSC2676, /rooms/{roomId}/event/{eventId}, should return the
|
# per MSC2676, /rooms/{roomId}/event/{eventId}, should return the
|
||||||
# *original* event, rather than the edited version
|
# *original* event, rather than the edited version
|
||||||
event_dict = self._event_serializer.serialize_event(
|
event_dict = self._event_serializer.serialize_event(
|
||||||
event, time_now, bundle_aggregations=aggregations, apply_edits=False
|
event, time_now, bundle_aggregations=aggregations
|
||||||
)
|
)
|
||||||
return 200, event_dict
|
return 200, event_dict
|
||||||
|
|
||||||
|
@ -743,7 +743,7 @@ class HomeServer(metaclass=abc.ABCMeta):
|
|||||||
|
|
||||||
@cache_in_self
|
@cache_in_self
|
||||||
def get_event_client_serializer(self) -> EventClientSerializer:
|
def get_event_client_serializer(self) -> EventClientSerializer:
|
||||||
return EventClientSerializer(self.config.experimental.msc3925_inhibit_edit)
|
return EventClientSerializer()
|
||||||
|
|
||||||
@cache_in_self
|
@cache_in_self
|
||||||
def get_password_policy_handler(self) -> PasswordPolicyHandler:
|
def get_password_policy_handler(self) -> PasswordPolicyHandler:
|
||||||
|
@ -30,7 +30,6 @@ from tests import unittest
|
|||||||
from tests.server import FakeChannel
|
from tests.server import FakeChannel
|
||||||
from tests.test_utils import make_awaitable
|
from tests.test_utils import make_awaitable
|
||||||
from tests.test_utils.event_injection import inject_event
|
from tests.test_utils.event_injection import inject_event
|
||||||
from tests.unittest import override_config
|
|
||||||
|
|
||||||
|
|
||||||
class BaseRelationsTestCase(unittest.HomeserverTestCase):
|
class BaseRelationsTestCase(unittest.HomeserverTestCase):
|
||||||
@ -403,7 +402,7 @@ class RelationsTestCase(BaseRelationsTestCase):
|
|||||||
|
|
||||||
def test_edit(self) -> None:
|
def test_edit(self) -> None:
|
||||||
"""Test that a simple edit works."""
|
"""Test that a simple edit works."""
|
||||||
|
orig_body = {"body": "Hi!", "msgtype": "m.text"}
|
||||||
new_body = {"msgtype": "m.text", "body": "I've been edited!"}
|
new_body = {"msgtype": "m.text", "body": "I've been edited!"}
|
||||||
edit_event_content = {
|
edit_event_content = {
|
||||||
"msgtype": "m.text",
|
"msgtype": "m.text",
|
||||||
@ -424,9 +423,7 @@ class RelationsTestCase(BaseRelationsTestCase):
|
|||||||
access_token=self.user_token,
|
access_token=self.user_token,
|
||||||
)
|
)
|
||||||
self.assertEqual(200, channel.code, channel.json_body)
|
self.assertEqual(200, channel.code, channel.json_body)
|
||||||
self.assertEqual(
|
self.assertEqual(channel.json_body["content"], orig_body)
|
||||||
channel.json_body["content"], {"body": "Hi!", "msgtype": "m.text"}
|
|
||||||
)
|
|
||||||
self._assert_edit_bundle(channel.json_body, edit_event_id, edit_event_content)
|
self._assert_edit_bundle(channel.json_body, edit_event_id, edit_event_content)
|
||||||
|
|
||||||
# Request the room messages.
|
# Request the room messages.
|
||||||
@ -443,7 +440,7 @@ class RelationsTestCase(BaseRelationsTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Request the room context.
|
# Request the room context.
|
||||||
# /context should return the edited event.
|
# /context should return the event.
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"GET",
|
"GET",
|
||||||
f"/rooms/{self.room}/context/{self.parent_id}",
|
f"/rooms/{self.room}/context/{self.parent_id}",
|
||||||
@ -453,7 +450,7 @@ class RelationsTestCase(BaseRelationsTestCase):
|
|||||||
self._assert_edit_bundle(
|
self._assert_edit_bundle(
|
||||||
channel.json_body["event"], edit_event_id, edit_event_content
|
channel.json_body["event"], edit_event_id, edit_event_content
|
||||||
)
|
)
|
||||||
self.assertEqual(channel.json_body["event"]["content"], new_body)
|
self.assertEqual(channel.json_body["event"]["content"], orig_body)
|
||||||
|
|
||||||
# Request sync, but limit the timeline so it becomes limited (and includes
|
# Request sync, but limit the timeline so it becomes limited (and includes
|
||||||
# bundled aggregations).
|
# bundled aggregations).
|
||||||
@ -491,45 +488,11 @@ class RelationsTestCase(BaseRelationsTestCase):
|
|||||||
edit_event_content,
|
edit_event_content,
|
||||||
)
|
)
|
||||||
|
|
||||||
@override_config({"experimental_features": {"msc3925_inhibit_edit": True}})
|
|
||||||
def test_edit_inhibit_replace(self) -> None:
|
|
||||||
"""
|
|
||||||
If msc3925_inhibit_edit is enabled, then the original event should not be
|
|
||||||
replaced.
|
|
||||||
"""
|
|
||||||
|
|
||||||
new_body = {"msgtype": "m.text", "body": "I've been edited!"}
|
|
||||||
edit_event_content = {
|
|
||||||
"msgtype": "m.text",
|
|
||||||
"body": "foo",
|
|
||||||
"m.new_content": new_body,
|
|
||||||
}
|
|
||||||
channel = self._send_relation(
|
|
||||||
RelationTypes.REPLACE,
|
|
||||||
"m.room.message",
|
|
||||||
content=edit_event_content,
|
|
||||||
)
|
|
||||||
edit_event_id = channel.json_body["event_id"]
|
|
||||||
|
|
||||||
# /context should return the *original* event.
|
|
||||||
channel = self.make_request(
|
|
||||||
"GET",
|
|
||||||
f"/rooms/{self.room}/context/{self.parent_id}",
|
|
||||||
access_token=self.user_token,
|
|
||||||
)
|
|
||||||
self.assertEqual(200, channel.code, channel.json_body)
|
|
||||||
self.assertEqual(
|
|
||||||
channel.json_body["event"]["content"], {"body": "Hi!", "msgtype": "m.text"}
|
|
||||||
)
|
|
||||||
self._assert_edit_bundle(
|
|
||||||
channel.json_body["event"], edit_event_id, edit_event_content
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_multi_edit(self) -> None:
|
def test_multi_edit(self) -> None:
|
||||||
"""Test that multiple edits, including attempts by people who
|
"""Test that multiple edits, including attempts by people who
|
||||||
shouldn't be allowed, are correctly handled.
|
shouldn't be allowed, are correctly handled.
|
||||||
"""
|
"""
|
||||||
|
orig_body = orig_body = {"body": "Hi!", "msgtype": "m.text"}
|
||||||
self._send_relation(
|
self._send_relation(
|
||||||
RelationTypes.REPLACE,
|
RelationTypes.REPLACE,
|
||||||
"m.room.message",
|
"m.room.message",
|
||||||
@ -570,7 +533,7 @@ class RelationsTestCase(BaseRelationsTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(200, channel.code, channel.json_body)
|
self.assertEqual(200, channel.code, channel.json_body)
|
||||||
|
|
||||||
self.assertEqual(channel.json_body["event"]["content"], new_body)
|
self.assertEqual(channel.json_body["event"]["content"], orig_body)
|
||||||
self._assert_edit_bundle(
|
self._assert_edit_bundle(
|
||||||
channel.json_body["event"], edit_event_id, edit_event_content
|
channel.json_body["event"], edit_event_id, edit_event_content
|
||||||
)
|
)
|
||||||
@ -642,6 +605,7 @@ class RelationsTestCase(BaseRelationsTestCase):
|
|||||||
|
|
||||||
def test_edit_edit(self) -> None:
|
def test_edit_edit(self) -> None:
|
||||||
"""Test that an edit cannot be edited."""
|
"""Test that an edit cannot be edited."""
|
||||||
|
orig_body = {"body": "Hi!", "msgtype": "m.text"}
|
||||||
new_body = {"msgtype": "m.text", "body": "Initial edit"}
|
new_body = {"msgtype": "m.text", "body": "Initial edit"}
|
||||||
edit_event_content = {
|
edit_event_content = {
|
||||||
"msgtype": "m.text",
|
"msgtype": "m.text",
|
||||||
@ -675,14 +639,12 @@ class RelationsTestCase(BaseRelationsTestCase):
|
|||||||
access_token=self.user_token,
|
access_token=self.user_token,
|
||||||
)
|
)
|
||||||
self.assertEqual(200, channel.code, channel.json_body)
|
self.assertEqual(200, channel.code, channel.json_body)
|
||||||
self.assertEqual(
|
self.assertEqual(channel.json_body["content"], orig_body)
|
||||||
channel.json_body["content"], {"body": "Hi!", "msgtype": "m.text"}
|
|
||||||
)
|
|
||||||
|
|
||||||
# The relations information should not include the edit to the edit.
|
# The relations information should not include the edit to the edit.
|
||||||
self._assert_edit_bundle(channel.json_body, edit_event_id, edit_event_content)
|
self._assert_edit_bundle(channel.json_body, edit_event_id, edit_event_content)
|
||||||
|
|
||||||
# /context should return the event updated for the *first* edit
|
# /context should return the bundled edit for the *first* edit
|
||||||
# (The edit to the edit should be ignored.)
|
# (The edit to the edit should be ignored.)
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"GET",
|
"GET",
|
||||||
@ -690,7 +652,7 @@ class RelationsTestCase(BaseRelationsTestCase):
|
|||||||
access_token=self.user_token,
|
access_token=self.user_token,
|
||||||
)
|
)
|
||||||
self.assertEqual(200, channel.code, channel.json_body)
|
self.assertEqual(200, channel.code, channel.json_body)
|
||||||
self.assertEqual(channel.json_body["event"]["content"], new_body)
|
self.assertEqual(channel.json_body["event"]["content"], orig_body)
|
||||||
self._assert_edit_bundle(
|
self._assert_edit_bundle(
|
||||||
channel.json_body["event"], edit_event_id, edit_event_content
|
channel.json_body["event"], edit_event_id, edit_event_content
|
||||||
)
|
)
|
||||||
@ -1287,7 +1249,6 @@ class BundledAggregationsTestCase(BaseRelationsTestCase):
|
|||||||
thread_summary = relations_dict[RelationTypes.THREAD]
|
thread_summary = relations_dict[RelationTypes.THREAD]
|
||||||
self.assertIn("latest_event", thread_summary)
|
self.assertIn("latest_event", thread_summary)
|
||||||
latest_event_in_thread = thread_summary["latest_event"]
|
latest_event_in_thread = thread_summary["latest_event"]
|
||||||
self.assertEqual(latest_event_in_thread["content"]["body"], "I've been edited!")
|
|
||||||
# The latest event in the thread should have the edit appear under the
|
# The latest event in the thread should have the edit appear under the
|
||||||
# bundled aggregations.
|
# bundled aggregations.
|
||||||
self.assertDictContainsSubset(
|
self.assertDictContainsSubset(
|
||||||
|
Loading…
Reference in New Issue
Block a user