From 728231b57b6b8585483542f2cc08c26ac4916fb2 Mon Sep 17 00:00:00 2001 From: SpiritCroc Date: Thu, 4 Sep 2025 17:09:43 +0200 Subject: [PATCH 1/6] Fix sending redactions on rooms < v11 using the /send endpoint While there is a dedicated API endpoint for redactions, being able to send redactions using the normal send endpoint is useful when using MSC-4140 for sending delayed redactions to replicate expiring messages. Currently this would only work on rooms >= v11 but fail with an internal server error on older room versions. --- synapse/handlers/message.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index cb64df2d0..520e6d69f 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -1014,7 +1014,14 @@ async def create_and_send_nonmember_event( if room_version.updated_redaction_rules: redacts = event_dict["content"].get("redacts") else: - redacts = event_dict.get("redacts") + # Legacy room versions need the "redacts" field outside of the event's + # content. However clients may still send it within the content, so copy + # the field if necessary for compatibility. + redacts = event_dict.get("redacts") or event_dict["content"].get( + "redacts" + ) + if redacts and "redacts" not in event_dict: + event_dict["redacts"] = redacts is_admin_redaction = await self.is_admin_redaction( event_type=event_dict["type"], From 01f35fef5cb2bbee92ec825a5d9da11a7fe4a6ed Mon Sep 17 00:00:00 2001 From: SpiritCroc Date: Tue, 9 Sep 2025 09:38:00 +0200 Subject: [PATCH 2/6] Move rather than copy "redacts" field Co-authored-by: Tulir Asokan --- synapse/handlers/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 520e6d69f..cf2f046c7 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -1017,7 +1017,7 @@ async def create_and_send_nonmember_event( # Legacy room versions need the "redacts" field outside of the event's # content. However clients may still send it within the content, so copy # the field if necessary for compatibility. - redacts = event_dict.get("redacts") or event_dict["content"].get( + redacts = event_dict.get("redacts") or event_dict["content"].pop( "redacts" ) if redacts and "redacts" not in event_dict: From a3dd1db1c8fc0c1336d7d762f5368dacef5e63ca Mon Sep 17 00:00:00 2001 From: SpiritCroc Date: Tue, 9 Sep 2025 09:56:13 +0200 Subject: [PATCH 3/6] Fix sending messages that do not redact --- synapse/handlers/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index cf2f046c7..31121d6f7 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -1018,7 +1018,7 @@ async def create_and_send_nonmember_event( # content. However clients may still send it within the content, so copy # the field if necessary for compatibility. redacts = event_dict.get("redacts") or event_dict["content"].pop( - "redacts" + "redacts", None ) if redacts and "redacts" not in event_dict: event_dict["redacts"] = redacts From 28d83f61e2c5e102ec8070f01ba5311c7a19e9b1 Mon Sep 17 00:00:00 2001 From: SpiritCroc Date: Tue, 9 Sep 2025 09:47:31 +0200 Subject: [PATCH 4/6] Make changes an opt-in to MSC-4169 --- changelog.d/18898.feature | 1 + synapse/config/experimental.py | 3 +++ synapse/handlers/message.py | 4 +++- synapse/rest/client/versions.py | 2 ++ 4 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 changelog.d/18898.feature diff --git a/changelog.d/18898.feature b/changelog.d/18898.feature new file mode 100644 index 000000000..bf31dd55d --- /dev/null +++ b/changelog.d/18898.feature @@ -0,0 +1 @@ +Support [MSC4169](https://github.com/matrix-org/matrix-spec-proposals/pull/4169) for backwards-compatible redaction sending using the `/send` endpoint. Contributed by @SpiritCroc @ Beeper. diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 5f2749886..b45e96b0f 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -552,6 +552,9 @@ def read_config( # MSC4133: Custom profile fields self.msc4133_enabled: bool = experimental.get("msc4133_enabled", False) + # MSC4169: Backwards-compatible redaction sending using `/send` + self.msc4169_enabled: bool = experimental.get("msc4169_enabled", False) + # MSC4210: Remove legacy mentions self.msc4210_enabled: bool = experimental.get("msc4210_enabled", False) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 31121d6f7..c6f00e23b 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -1013,7 +1013,7 @@ async def create_and_send_nonmember_event( if room_version.updated_redaction_rules: redacts = event_dict["content"].get("redacts") - else: + elif self.hs.config.experimental.msc4169_enabled: # Legacy room versions need the "redacts" field outside of the event's # content. However clients may still send it within the content, so copy # the field if necessary for compatibility. @@ -1022,6 +1022,8 @@ async def create_and_send_nonmember_event( ) if redacts and "redacts" not in event_dict: event_dict["redacts"] = redacts + else: + redacts = event_dict.get("redacts") is_admin_redaction = await self.is_admin_redaction( event_type=event_dict["type"], diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index fa39eb9e6..86a2bc1c3 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -177,6 +177,8 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: "uk.tcpip.msc4133": self.config.experimental.msc4133_enabled, # MSC4155: Invite filtering "org.matrix.msc4155": self.config.experimental.msc4155_enabled, + # MSC4169: Backwards-compatible redaction sending useing `/send` + "com.beeper.msc4169": self.config.experimental.msc4169_enabled, }, }, ) From 5423673b24090b7af5698dbf48518ca4c43d8c92 Mon Sep 17 00:00:00 2001 From: SpiritCroc Date: Tue, 9 Sep 2025 10:31:00 +0200 Subject: [PATCH 5/6] Fix typo in comment --- synapse/rest/client/versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 86a2bc1c3..fed56976d 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -177,7 +177,7 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: "uk.tcpip.msc4133": self.config.experimental.msc4133_enabled, # MSC4155: Invite filtering "org.matrix.msc4155": self.config.experimental.msc4155_enabled, - # MSC4169: Backwards-compatible redaction sending useing `/send` + # MSC4169: Backwards-compatible redaction sending using `/send` "com.beeper.msc4169": self.config.experimental.msc4169_enabled, }, }, From e496ac2bdd1f391d13e34dfbb0f3ffedd8080737 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 9 Sep 2025 14:51:26 +0300 Subject: [PATCH 6/6] Move redacts moving outside ratelimit check --- synapse/handlers/message.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index c6f00e23b..2931f3d9d 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -1003,25 +1003,40 @@ async def create_and_send_nonmember_event( await self.clock.sleep(random.randint(1, 10)) raise ShadowBanError() - if ratelimit: + room_version = None + + if ( + event_dict["type"] == EventTypes.Redaction + and "redacts" in event_dict["content"] + and self.hs.config.experimental.msc4169_enabled + ): + room_id = event_dict["room_id"] try: room_version = await self.store.get_room_version(room_id) except NotFoundError: - # The room doesn't exist. raise AuthError(403, f"User {requester.user} not in room {room_id}") - if room_version.updated_redaction_rules: - redacts = event_dict["content"].get("redacts") - elif self.hs.config.experimental.msc4169_enabled: + if not room_version.updated_redaction_rules: # Legacy room versions need the "redacts" field outside of the event's # content. However clients may still send it within the content, so copy # the field if necessary for compatibility. redacts = event_dict.get("redacts") or event_dict["content"].pop( "redacts", None ) - if redacts and "redacts" not in event_dict: + if redacts is not None and "redacts" not in event_dict: event_dict["redacts"] = redacts + + if ratelimit: + if room_version is None: + room_id = event_dict["room_id"] + try: + room_version = await self.store.get_room_version(room_id) + except NotFoundError: + raise AuthError(403, f"User {requester.user} not in room {room_id}") + + if room_version.updated_redaction_rules: + redacts = event_dict["content"].get("redacts") else: redacts = event_dict.get("redacts")