Repository: matrix-org/synapse

Issue:
Synapse does not apply edits to events other than `m.room.message`
MSC2676 [says](https://github.com/uhoreg/matrix-doc/blob/aggregations-edits/proposals/2676-message-editing.md#server-behaviour):

> Whenever a homeserver would return an event via the Client-Server API, it should check for any valid, applicable `m.replace` event, and if one is found, it should first modify the content of the original event according to the `m.new_content` of the most recent edit.

It also [gives](https://github.com/uhoreg/matrix-doc/blob/aggregations-edits/proposals/2676-message-editing.md#validity-of-message-edit-events) a comprehensive list of what comprises a "valid" edit event.

Synapse does this, but only where the events are of type `m.room.message`. This is incorrect: it should also do so for other event types. Examples include polls ([`org.matrix.msc3381.poll.start`](https://github.com/matrix-org/matrix-spec-proposals/pull/3381)), which can be edited via the Element UI, and stickers ([`m.sticker`](https://spec.matrix.org/v1.2/client-server-api/#sticker-messages)), which cannot be edited via the UI but can via the API.

This is closely related to #12503, but that concerns aggregations, whereas this issue is about updating the `content` of the original event.
Synapse does not bundle `m.replace` aggregations for events other than `m.room.message`
Per MSC2675, when the server returns an event, it is supposed to bundle the aggregation of any related events into the response.

This includes `m.replace` relations: for an `m.room.message` event, `/context/$original_event_id` might return:

```json5
{
  "event": {
    ...
    "event_id": "$original_event_id",
    "type": "m.room.message",
    "unsigned": {
      "m.relations": {
        "m.replace": {
          "event_id": "$replacement_event_id",
          "origin_server_ts": 1650390324643,
          "sender": "@richvdh:matrix.org"
        }
      }
    },
    "user_id": "@richvdh:matrix.org"
  }
}
```

However, for other event types, no `m.replace` aggregation is returned.

It's not as if `m.room.message` is the only event type you can edit (consider, for example, encrypted events), so this seems odd.


Discussion:

Presumably this means that if you forward-paginate through a room with `/messages` and get as far as the updated event, you'll see the old content rather than the edited content.
> Presumably this means that if you forward-paginate through a room with `/messages` and get as far as the updated event, you'll see the old content rather than the edited content.

Another situation this can impact clients is when viewing a thread (which uses the `/relations` API). @t3chguy mentioned to me that clients essentially assume all encrypted events have an edit and attempt to re-fetch them from the server to see if this is accurate. (matrix-js-sdk is using `/relations` to see if there are edits, see https://github.com/matrix-org/matrix-js-sdk/blob/v17.1.0-rc.1/src/models/thread.ts#L240.)

See also vector-im/element-web#21447 and vector-im/element-web#21445
> @t3chguy mentioned to me that clients essentially assume all encrypted events have an edit and attempt to re-fetch them from the server to see if this is accurate.

so to be clear: that is a workaround for this bug, and if we fixed this bug, clients could remove that workaround?
>> @t3chguy mentioned to me that clients essentially assume all encrypted events have an edit and attempt to re-fetch them from the server to see if this is accurate.
>
> so to be clear: that is a workaround for this bug, and if we fixed this bug, clients could remove that workaround?

This is my understanding, yes. 👍 
Yes, once enough server uptake EW will gladly remove the check which asks the server for /relations on each and every thread response

Resolving patch:
diff --git a/synapse/storage/databases/main/relations.py b/synapse/storage/databases/main/relations.py
--- a/synapse/storage/databases/main/relations.py
+++ b/synapse/storage/databases/main/relations.py
@@ -384,12 +384,11 @@ async def get_applicable_edits(
             the event will map to None.
         """
 
-        # We only allow edits for `m.room.message` events that have the same sender
-        # and event type. We can't assert these things during regular event auth so
-        # we have to do the checks post hoc.
+        # We only allow edits for events that have the same sender and event type.
+        # We can't assert these things during regular event auth so we have to do
+        # the checks post hoc.
 
-        # Fetches latest edit that has the same type and sender as the
-        # original, and is an `m.room.message`.
+        # Fetches latest edit that has the same type and sender as the original.
         if isinstance(self.database_engine, PostgresEngine):
             # The `DISTINCT ON` clause will pick the *first* row it encounters,
             # so ordering by origin server ts + event ID desc will ensure we get
@@ -405,7 +404,6 @@ async def get_applicable_edits(
                 WHERE
                     %s
                     AND relation_type = ?
-                    AND edit.type = 'm.room.message'
                 ORDER by original.event_id DESC, edit.origin_server_ts DESC, edit.event_id DESC
             """
         else:
@@ -424,7 +422,6 @@ async def get_applicable_edits(
                 WHERE
                     %s
                     AND relation_type = ?
-                    AND edit.type = 'm.room.message'
                 ORDER by edit.origin_server_ts, edit.event_id
             """
 


Test patch: