Skip to content

Conversation

SpiritCroc
Copy link
Contributor

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 when setting the redacts field in the content, since older rooms would require that field to be outside of content. We can address this by copying it over if necessary.

Upstream PR: element-hq/synapse#18898

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.
Copy link

coderabbitai bot commented Sep 8, 2025

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Adds experimental support for MSC4169: backwards-compatible redaction sending via the /send endpoint.
    • Client versions endpoint now advertises an unstable feature flag for MSC4169 when enabled.
    • Improved redaction handling ensures compatibility across legacy and newer room versions.
  • Documentation

    • Changelog entry announcing MSC4169 support, with credits.
  • Configuration

    • New experimental feature flag to enable MSC4169 support, disabled by default.

Walkthrough

Adds MSC4169-aware handling for redactions in create_and_send_nonmember_event (promotes or sources redacts depending on room version and experimental flag), introduces msc4169_enabled experimental config, exposes "com.beeper.msc4169" in client versions, and adds a changelog entry.

Changes

Cohort / File(s) Summary
Message handler redaction sourcing
synapse/handlers/message.py
Add MSC4169-aware redaction handling in create_and_send_nonmember_event: load room version when needed, promote redacts from content to top-level for legacy room versions, and choose redacts source based on room_version.updated_redaction_rules when computing is_admin_redaction. Existing control flow and error paths preserved.
Experimental config flag
synapse/config/experimental.py
Add experimental_features.msc4169_enabled and set self.msc4169_enabled: bool = experimental.get("msc4169_enabled", False) in ExperimentalConfig.read_config.
Client versions capability
synapse/rest/client/versions.py
Add unstable feature flag "com.beeper.msc4169" to the /versions response, sourced from self.config.experimental.msc4169_enabled.
Changelog entry
changelog.d/18898.feature
Add changelog entry announcing MSC4169 support for backwards-compatible redaction sending via /send, with contributor credits.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant MessageHandler
  participant AdminCheck as is_admin_redaction
  Note over MessageHandler: MSC4169 experimental flag may be enabled

  Client->>MessageHandler: create_and_send_nonmember_event(event_dict, ...)
  alt MSC4169 enabled AND legacy redaction rules
    MessageHandler->>MessageHandler: load room_version if needed\nredacts = event_dict.get("redacts") or event_dict["content"].pop("redacts", None)
    MessageHandler->>AdminCheck: is_admin_redaction(redacts)
    AdminCheck-->>MessageHandler: result
  else MSC4169 disabled OR updated redaction rules
    MessageHandler->>MessageHandler: redacts = event_dict["content"].get("redacts")
    MessageHandler->>AdminCheck: is_admin_redaction(redacts)
    AdminCheck-->>MessageHandler: result
  end
  MessageHandler-->>Client: continue with event processing/sending
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly summarizes the primary change by stating that MSC-4169 is implemented to enable backwards-compatible redaction sending via the /send endpoint for rooms below version 11, accurately reflecting the core purpose of the pull request without extraneous detail.
Description Check ✅ Passed The description clearly explains the motivation for supporting redactions through the standard send API, the current limitation on rooms earlier than version 11, and how copying the redacts field addresses it, directly matching the implemented changes.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (2)
  • MSC-4169: Entity not found: Issue - Could not find referenced Issue.
  • MSC-4140: Entity not found: Issue - Could not find referenced Issue.
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch tobias/inf-2094-redaction-send

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
synapse/handlers/message.py (1)

656-663: Suspended-user redaction path assumes content.redacts only; mirror legacy fallback

This block fetches the target via event_dict["content"]["redacts"] and will KeyError for legacy rooms when clients send top-level redacts. Align with the new fallback used below.

Suggested change:

-                    event = await self.store.get_event(
-                        event_dict["content"]["redacts"], allow_none=True
-                    )
+                    redacts = event_dict.get("content", {}).get("redacts") or event_dict.get("redacts")
+                    if not redacts:
+                        raise SynapseError(400, "Missing redacts field", Codes.INVALID_PARAM)
+                    event = await self.store.get_event(redacts, allow_none=True)

Please add/adjust tests:

  • /send redaction in rooms v1–v10 with redacts only in content → succeeds.
  • Same, but only top-level redacts → still succeeds (suspended-user path included).
  • v11+ continues to accept content.redacts and ignores top-level.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 557272b and 728231b.

📒 Files selected for processing (1)
  • synapse/handlers/message.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
synapse/handlers/message.py (1)
synapse/events/__init__.py (2)
  • redacts (238-242)
  • get (257-258)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: test-complement
  • GitHub Check: test-trial
  • GitHub Check: test-sytest
  • GitHub Check: lint-style
  • GitHub Check: lint-types

@SpiritCroc SpiritCroc requested a review from tulir September 8, 2025 11:37
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 728231b and 28d83f6.

📒 Files selected for processing (4)
  • changelog.d/18898.feature (1 hunks)
  • synapse/config/experimental.py (1 hunks)
  • synapse/handlers/message.py (1 hunks)
  • synapse/rest/client/versions.py (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • changelog.d/18898.feature
🚧 Files skipped from review as they are similar to previous changes (1)
  • synapse/handlers/message.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: test-trial
  • GitHub Check: lint-style
  • GitHub Check: lint-types
  • GitHub Check: test-sytest
  • GitHub Check: test-complement
🔇 Additional comments (1)
synapse/config/experimental.py (1)

555-557: LGTM: cleanly adds MSC4169 feature flag.

Boolean default, naming, and placement are consistent with the rest of ExperimentalConfig.

@SpiritCroc SpiritCroc changed the title Fix sending redactions on rooms < v11 using the /send endpoint Implement MSC-4169: backwards-compatible send of redactions for rooms < v11 using the /send endpoint Sep 9, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
synapse/handlers/message.py (1)

1008-1012: Avoid KeyError on missing content; use .get("content", {}) consistently

Safer and simpler; also aligns with earlier feedback.

Apply:

-        if (
-            event_dict["type"] == EventTypes.Redaction
-            and "redacts" in event_dict["content"]
-            and self.hs.config.experimental.msc4169_enabled
-        ):
+        if (
+            event_dict["type"] == EventTypes.Redaction
+            and "redacts" in event_dict.get("content", {})
+            and self.hs.config.experimental.msc4169_enabled
+        ):
@@
-                redacts = event_dict.get("redacts") or event_dict["content"].pop(
-                    "redacts", None
-                )
+                redacts = event_dict.get("redacts") or event_dict.get("content", {}).pop(
+                    "redacts", None
+                )
@@
-            if room_version.updated_redaction_rules:
-                redacts = event_dict["content"].get("redacts")
+            if room_version.updated_redaction_rules:
+                redacts = event_dict.get("content", {}).get("redacts")

Also applies to: 1024-1026, 1039-1041

🧹 Nitpick comments (2)
synapse/handlers/message.py (2)

1020-1028: Always strip duplicate redacts from content for legacy rooms

Prevents legacy validation surprises if both top-level and content copies exist.

-            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 is not None and "redacts" not in event_dict:
-                    event_dict["redacts"] = redacts
+            if not room_version.updated_redaction_rules:
+                # Legacy room versions need top-level `redacts`. Promote if needed and
+                # remove the duplicate from content.
+                content = event_dict.get("content", {})
+                redacts_in_content = content.get("redacts")
+                top_level_redacts = event_dict.get("redacts")
+                redacts = top_level_redacts or redacts_in_content
+                if redacts is not None and "redacts" not in event_dict:
+                    event_dict["redacts"] = redacts
+                # Avoid having both copies in legacy rooms.
+                if "redacts" in content:
+                    content.pop("redacts", None)

1030-1047: Compute redacts only for redaction events in the ratelimit path

Minor clarity/perf win and avoids unnecessary dict lookups for other event types.

-            if room_version.updated_redaction_rules:
-                redacts = event_dict.get("content", {}).get("redacts")
-            else:
-                redacts = event_dict.get("redacts")
+            if event_dict["type"] == EventTypes.Redaction:
+                if room_version.updated_redaction_rules:
+                    redacts = event_dict.get("content", {}).get("redacts")
+                else:
+                    redacts = event_dict.get("redacts")
+            else:
+                redacts = None
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5423673 and e496ac2.

📒 Files selected for processing (1)
  • synapse/handlers/message.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
synapse/handlers/message.py (4)
synapse/api/constants.py (1)
  • EventTypes (107-145)
synapse/events/__init__.py (6)
  • room_id (230-231)
  • room_id (396-397)
  • room_id (457-458)
  • room_id (521-540)
  • redacts (238-242)
  • get (257-258)
synapse/storage/databases/main/state.py (5)
  • get_room_version (136-145)
  • get (994-994)
  • get (996-996)
  • get (998-998)
  • get (1000-1005)
synapse/api/errors.py (2)
  • NotFoundError (373-377)
  • AuthError (380-392)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: test-trial
  • GitHub Check: test-sytest
  • GitHub Check: test-complement
  • GitHub Check: lint-types
  • GitHub Check: lint-style

@tulir tulir merged commit 322fd41 into beeper Sep 9, 2025
5 of 7 checks passed
@tulir tulir deleted the tobias/inf-2094-redaction-send branch September 9, 2025 12:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants