diff --git a/changelogs/client_server/newsfragments/1211.feature b/changelogs/client_server/newsfragments/1211.feature new file mode 100644 index 00000000..92f1bf1b --- /dev/null +++ b/changelogs/client_server/newsfragments/1211.feature @@ -0,0 +1 @@ +Add `m.replace` relations (event edits), as per [MSC2676](https://github.com/matrix-org/matrix-spec-proposals/pull/2676). diff --git a/content/client-server-api/_index.md b/content/client-server-api/_index.md index 3d80c849..b1b55159 100644 --- a/content/client-server-api/_index.md +++ b/content/client-server-api/_index.md @@ -1955,16 +1955,6 @@ rooms, or the relationship missing properties required by the schema below. Clie handling such invalid relationships should show the events independently of each other, optionally with an error message. -{{% boxes/note %}} -While this specification describes an `m.relates_to` object containing a `rel_type`, there -is not currently any relationship type which uses this structure. Replies, described below, -form their relationship outside of the `rel_type` as a legacy type of relationship. Future -versions of the specification might change replies to better match the relationship structures. - -Custom `rel_type`s can, and should, still use the schema described above for relevant -behaviour. -{{% /boxes/note %}} - `m.relates_to` is defined as follows: {{% definition path="api/client-server/definitions/m.relates_to" %}} @@ -1974,6 +1964,7 @@ behaviour. This specification describes the following relationship types: * [Rich replies](#rich-replies) (**Note**: does not use `rel_type`). +* [Event replacements](#event-replacements). #### Aggregations @@ -2643,3 +2634,4 @@ systems. {{< cs-module name="server_notices" >}} {{< cs-module name="moderation_policies" >}} {{< cs-module name="spaces" >}} +{{< cs-module name="event_replacements" >}} \ No newline at end of file diff --git a/content/client-server-api/modules/event_replacements.md b/content/client-server-api/modules/event_replacements.md new file mode 100644 index 00000000..52597abd --- /dev/null +++ b/content/client-server-api/modules/event_replacements.md @@ -0,0 +1,330 @@ +--- +type: module +--- + +### Event replacements + +{{% added-in v="1.4" %}} + +Event replacements, or "message edit events", are events that use an [event +relationship](#forming-relationships-between-events) +with a `rel_type` of `m.replace`, which indicates that the original event is +intended to be replaced. + +An example of a message edit event might look like this: + +```json +{ + "type": "m.room.message", + "content": { + "body": "* Hello! My name is bar", + "msgtype": "m.text", + "m.new_content": { + "body": "Hello! My name is bar", + "msgtype": "m.text" + }, + "m.relates_to": { + "rel_type": "m.replace", + "event_id": "$some_event_id" + } + }, + // ... other fields required by events +} +``` + +The `content` of the replacement must contain a `m.new_content` property which +defines the replacement `content`. The normal `content` properties (`body`, +`msgtype` etc.) provide a fallback for clients which do not understand +replacement events. + +`m.new_content` can include any properties that would normally be found in +an event's content property, such as `formatted_body` (see [`m.room.message` +`msgtypes`](#mroommessage-msgtypes)). + +#### Validity of replacement events + +There are a number of requirements on replacement events, which must be satisfied for the replacement to be considered valid: + + * As with all event relationships, the original event and replacement event + must have the same `room_id` (i.e. you cannot send an event in + one room and then an edited version in a different room). + + * The original event and replacement event must have the same `sender` + (i.e. you cannot edit someone else's messages). + + * The replacement and original events must have the same `type` (i.e. you + cannot change the original event's type). + + * The replacement and original events must not have a `state_key` property + (i.e. you cannot edit state events at all). + + * The original event must not, itself, have a `rel_type` of `m.replace` + (i.e. you cannot edit an edit — though you can send multiple edits for a + single original event). + + * The replacement event (once decrypted, if appropriate) must have an + `m.new_content` property. + +If any of these criteria are not satisfied, implementations should ignore the +replacement event (the content of the original should not be replaced, and the +edit should not be included in the server-side aggregation). + +Note that the [`msgtype`](#mroommessage-msgtypes) property of replacement +`m.room.message` events does *not* need to be the same as in the original event. For +example, it is legitimate to replace an `m.text` event with an `m.emote`. + +#### Editing encrypted events + +If the original event was [encrypted](#end-to-end-encryption), the replacement +should be too. In that case, `m.new_content` is placed in the content of the +encrypted payload. As with all event relationships, the `m.relates_to` property +must be sent in the unencrypted (cleartext) part of the event. + +For example, a replacement for an encrypted event might look like this: + +```json +{ + "type": "m.room.encrypted", + "content": { + "m.relates_to": { + "rel_type": "m.replace", + "event_id": "$some_event_id" + }, + "algorithm": "m.megolm.v1.aes-sha2", + "sender_key": "", + "device_id": "", + "session_id": "", + "ciphertext": "" + } + // irrelevant fields not shown +} +``` + +... and, once decrypted, the payload might look like this: + +```json +{ + "type": "m.room.", + "room_id": "!some_room_id", + "content": { + "body": "* Hello! My name is bar", + "msgtype": "m.text", + "m.new_content": { + "body": "Hello! My name is bar", + "msgtype": "m.text" + } + } +} +``` + +Note that: + + * There is no `m.relates_to` property in the encrypted payload. If there was, it would be ignored. + * There is no `m.new_content` property in the cleartext content of the `m.room.encrypted` event. As above, if there was then it would be ignored. + +{{% boxes/note %}} +The payload of an encrypted replacement event must be encrypted as normal, including +ratcheting any [Megolm](#mmegolmv1aes-sha2) session as normal. The original Megolm +ratchet entry should **not** be re-used. +{{% /boxes/note %}} + + +#### Applying `m.new_content` + +When applying a replacement, the `content` of the original event is treated as +being overwritten entirely by `m.new_content`, with the exception of `m.relates_to`, +which is left *unchanged*. Any `m.relates_to` property within `m.new_content` +is ignored. + +{{% boxes/note %}} +Note that server implementations must not *actually* overwrite +the original event's `content`: instead the server presents it as being overwritten +when it is served over the client-server API. See [Server-side replacement of content](#server-side-replacement-of-content) +below. +{{% /boxes/note %}} + +For example, given a pair of events: + +```json +{ + "event_id": "$original_event", + "type": "m.room.message", + "content": { + "body": "I really like cake", + "msgtype": "m.text", + "formatted_body": "I really like cake", + } +} +``` + +```json +{ + "event_id": "$edit_event", + "type": "m.room.message", + "content": { + "body": "* I really like *chocolate* cake", + "msgtype": "m.text", + "m.new_content": { + "body": "I really like *chocolate* cake", + "msgtype": "m.text", + "com.example.extension_property": "chocolate" + }, + "m.relates_to": { + "rel_type": "m.replace", + "event_id": "$original_event_id" + } + } +} +``` + +... then the end result is an event as shown below: + +```json +{ + "event_id": "$original_event", + "type": "m.room.message", + "content": { + "body": "I really like *chocolate* cake", + "msgtype": "m.text", + "com.example.extension_property": "chocolate" + } +} +``` + +Note that `formatted_body` is now absent, because it was absent in the +replacement event. + +#### Server behaviour + +##### Server-side aggregation of `m.replace` relationships + +Note that there can be multiple events with an `m.replace` relationship to a +given event (for example, if an event is edited multiple times). These should +be [aggregated](#aggregations) by the homeserver. + +The aggregation format of `m.replace` relationships gives the `event_id`, +`origin_server_ts`, and `sender` of the **most recent** replacement event. The +most recent event is determined by comparing `origin_server_ts`; if two or more +replacement events have identical `origin_server_ts`, the event with the +lexicographically largest `event_id` is treated as more recent. + +This aggregation is bundled under the `unsigned` property as `m.relations` for any +event that is the target of an `m.replace` relationship. For example: + +```json +{ + "event_id": "$original_event_id", + // irrelevant fields not shown + "unsigned": { + "m.relations": { + "m.replace": { + "event_id": "$latest_edit_event_id", + "origin_server_ts": 1649772304313, + "sender": "@editing_user:localhost" + } + } + } +} +``` + +If the original event is +[redacted](#redactions), any +`m.replace` relationship should **not** be bundled with it (whether or not any +subsequent replacements are themselves redacted). Note that this behaviour is +specific to the `m.replace` relationship. See also [redactions of edited +events](#redactions-of-edited-events) below. + +##### Server-side replacement of content + +Whenever an `m.replace` is to be bundled with an event as above, the server +should also modify the content of the original event according to the +`m.new_content` of the most recent replacement event (determined as above). + +An exception applies to [`GET /_matrix/client/v3/rooms/{roomId}/event/{eventId}`](#get_matrixclientv3roomsroomideventeventid), +which should return the unmodified event (though the relationship should still +be bundled, as described above). + +#### Client behaviour + +Clients can often ignore `m.replace` events, because any events returned +by the server to the client will be updated by the server to account for +subsequent edits. + +However, clients should apply the replacement themselves when the server is +unable to do so. This happens in the following situations: + + * The client has already received and stored the original event before the + message edit event arrives. + + * The original event (and hence its replacement) are encrypted. + +Client authors are reminded to take note of the requirements for [Validity of +message edit events](#validity-of-message-edit-events), and to ignore any +invalid edit events that are received. + +##### Permalinks + +When creating [links](/appendices/#uris) to events (also known as permalinks), +clients build links which reference the event that the creator of the permalink +is viewing at that point (which might be a message edit event). + +The client viewing the permalink should resolve this reference to the original +event, and then display the most recent version of that event. + +#### Redactions of edited events + +When an event using a `rel_type` of `m.replace` is [redacted](#redactions), it +removes that edit revision. This has little effect if there were subsequent +edits. However, if it was the most recent edit, the event is in effect +reverted to its content before the redacted edit. + +Redacting the *original* message in effect removes the message, including all +subsequent edits, from the visible timeline. In this situation, homeservers +will return an empty `content` for the original event as with any other +redacted event, and as +[above](#server-side-aggregation-of-mreplace-relationships) the replacement +events will not be bundled with the original event. Note that the subsequent edits are +not actually redacted themselves: they simply serve no purpose within the visible timeline. + +#### Edits of replies + +Some particular constraints apply to events which replace a +[reply](#rich-replies). In particular: + + * In contrast to the original reply, there should be no `m.in_reply_to` + property in the the `m.relates_to` object, since it would be redundant (see + [Applying `m.new_content`](#applying-mnew_content) above, which notes that + the original event's `m.relates_to` is preserved), as well as being contrary + to the spirit of the event relationships mechanism which expects only one + "parent" per event. + + * `m.new_content` should **not** contain any [reply + fallback](#fallbacks-for-rich-replies), + since it is assumed that any client which can handle edits can also display + replies natively. However, the `content` of the replacement event should provide + fallback content for clients which support neither rich replies nor edits. + +An example of an edit to a reply is as follows: + +```json +{ + "type": "m.room.message", + // irrelevant fields not shown + "content": { + "body": "> <@alice:example.org> question\n\n* reply", + "msgtype": "m.text", + "format": "org.matrix.custom.html", + "formatted_body": "
In reply to @alice:example.org
question
* reply", + "m.new_content": { + "body": "reply", + "msgtype": "m.text", + "format": "org.matrix.custom.html", + "formatted_body": "reply" + }, + "m.relates_to": { + "rel_type": "m.replace", + "event_id": "$original_reply_event" + } + } +} +```