From 71f675926bb59f57381c4991902ba1f446d742cb Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 16 Aug 2022 19:11:01 +0100 Subject: [PATCH 1/5] Event edits --- .../client_server/newsfragments/1211.feature | 1 + content/client-server-api/_index.md | 18 +- .../modules/event_replacements.md | 319 ++++++++++++++++++ 3 files changed, 325 insertions(+), 13 deletions(-) create mode 100644 changelogs/client_server/newsfragments/1211.feature create mode 100644 content/client-server-api/modules/event_replacements.md diff --git a/changelogs/client_server/newsfragments/1211.feature b/changelogs/client_server/newsfragments/1211.feature new file mode 100644 index 000000000..92f1bf1ba --- /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 a814144c6..4a5d8f358 100644 --- a/content/client-server-api/_index.md +++ b/content/client-server-api/_index.md @@ -1938,14 +1938,14 @@ parent event, for example. {{% /boxes/note %}} To allow the server to aggregate and find child events for a parent, the `m.relates_to` -key of an event MUST be included in the plaintext copy of the event. It cannot be +key of an event MUST be included in the cleartext (unencrypted) part of the event. It cannot be exclusively recorded in the encrypted payload as the server cannot decrypt the event for processing. {{% boxes/warning %}} If an encrypted event contains an `m.relates_to` in its payload, it should be -ignored and instead favour the plaintext `m.relates_to` copy (including when there -is no plaintext copy). This is to ensure the client's behaviour matches the server's +ignored and instead favour the cleartext `m.relates_to` copy (including when there +is no cleartext copy). This is to ensure the client's behaviour matches the server's capability to handle relationships. {{% /boxes/warning %}} @@ -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" >}} 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 000000000..2547e5d71 --- /dev/null +++ b/content/client-server-api/modules/event_replacements.md @@ -0,0 +1,319 @@ +--- +type: module +--- + +### Event replacements + +{{% added-in v="1.4" %}} + +Event replacements, or "message edit events", are events that use an [event +relationship](http://localhost:1313/client-server-api/#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 message edit events + +There are a number of requirements on message edit 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. it is not possible to 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. editing + an event cannot change its type). + + * Neither the replacement nor original events may have a `state_key` property + (i.e. it is not possible to edit a state event). + + * 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 need *not* 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: + +For example, an encrypted replacement 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": "" + } + // ... +} +``` + +... 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. (Any such property would be ignored.) + * There is no `m.new_content` property in the cleartext content of the `m.room.encrypted` event. (Again, any such property would be ignored.) + +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. + + +#### Applying `m.new_content` + +When applying a replacement, the `content` of the original event is replaced +entirely by the `m.new_content` from the replacement event, with the exception +of `m.relates_to`, which is left *unchanged*. (Any `m.relates_to` property +within `m.new_content` should be ignored.) + +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 into the `unsigned/m.relations` property of any +event that is the target of an `m.replace` relationship. For example: + +```json +{ + "event_id": "$original_event_id", + // ... + "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](http://localhost:1313/client-server-api/#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, since any events the server +returns via the C-S API 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 may be 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. + +#### 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](https://spec.matrix.org/v1.3/client-server-api/#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", + "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" + } + } +} +``` From 091d1f032c02f6d00cb8fb93179f41582ec3fd0e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 23 Aug 2022 15:27:28 +0100 Subject: [PATCH 2/5] Apply suggestions from code review Co-authored-by: Travis Ralston --- .../modules/event_replacements.md | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/content/client-server-api/modules/event_replacements.md b/content/client-server-api/modules/event_replacements.md index 2547e5d71..9eb267f5b 100644 --- a/content/client-server-api/modules/event_replacements.md +++ b/content/client-server-api/modules/event_replacements.md @@ -7,7 +7,7 @@ type: module {{% added-in v="1.4" %}} Event replacements, or "message edit events", are events that use an [event -relationship](http://localhost:1313/client-server-api/#forming-relationships-between-events) +relationship](#forming-relationships-between-events) with a `rel_type` of `m.replace`, which indicates that the original event is intended to be replaced. @@ -46,17 +46,17 @@ an event's content property, such as `formatted_body` (see [`m.room.message` There are a number of requirements on message edit 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. it is not possible to send an event in + 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. editing - an event cannot change its type). + * The replacement and original events must have the same `type` (i.e. you + cannot change the original event's type). - * Neither the replacement nor original events may have a `state_key` property - (i.e. it is not possible to edit a state event). + * 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 @@ -70,7 +70,7 @@ 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 need *not* be the same as in the original event. For +`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 @@ -82,8 +82,6 @@ must be sent in the unencrypted (cleartext) part of the event. For example, a replacement for an encrypted event might look like this: -For example, an encrypted replacement event might look like this: - ```json { "type": "m.room.encrypted", @@ -98,7 +96,7 @@ For example, an encrypted replacement event might look like this: "session_id": "", "ciphertext": "" } - // ... + // irrelevant fields not shown } ``` @@ -121,10 +119,14 @@ For example, an encrypted replacement event might look like this: Note that: - * There is no `m.relates_to` property in the encrypted payload. (Any such property would be ignored.) - * There is no `m.new_content` property in the cleartext content of the `m.room.encrypted` event. (Again, any such property would be ignored.) + * 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. -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 %}} +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` @@ -199,13 +201,13 @@ 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 into the `unsigned/m.relations` property of any +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": { @@ -219,13 +221,13 @@ event that is the target of an `m.replace` relationship. For example: ``` If the original event is -[redacted](http://localhost:1313/client-server-api/#redactions), any +[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 +##### 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 @@ -237,9 +239,9 @@ be bundled, as described above). #### Client behaviour -Clients can often ignore `m.replace` events, since any events the server -returns via the C-S API will be updated by the server to account for subsequent -edits. +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: @@ -251,7 +253,7 @@ unable to do so. This happens in the following situations: 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 may be received. +invalid edit events that are received. ##### Permalinks @@ -274,7 +276,8 @@ 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. +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 @@ -283,13 +286,13 @@ Some particular constraints apply to events which replace a * 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 + [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](https://spec.matrix.org/v1.3/client-server-api/#fallbacks-for-rich-replies), + 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. @@ -299,6 +302,7 @@ 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", From 943b5f72f4b921a0e2feb2dcb8e2b59065322456 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 23 Aug 2022 15:27:47 +0100 Subject: [PATCH 3/5] Update content/client-server-api/modules/event_replacements.md --- .../client-server-api/modules/event_replacements.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/content/client-server-api/modules/event_replacements.md b/content/client-server-api/modules/event_replacements.md index 9eb267f5b..cac34fe80 100644 --- a/content/client-server-api/modules/event_replacements.md +++ b/content/client-server-api/modules/event_replacements.md @@ -131,10 +131,13 @@ ratchet entry should **not** be re-used. #### Applying `m.new_content` -When applying a replacement, the `content` of the original event is replaced -entirely by the `m.new_content` from the replacement event, with the exception -of `m.relates_to`, which is left *unchanged*. (Any `m.relates_to` property -within `m.new_content` should be ignored.) +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. (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.) For example, given a pair of events: From e5a3df3706082c61e2fca5719a3895c4aa77815e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 1 Sep 2022 15:29:48 +0100 Subject: [PATCH 4/5] Update content/client-server-api/modules/event_replacements.md Co-authored-by: Travis Ralston --- content/client-server-api/modules/event_replacements.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/content/client-server-api/modules/event_replacements.md b/content/client-server-api/modules/event_replacements.md index cac34fe80..af85196c7 100644 --- a/content/client-server-api/modules/event_replacements.md +++ b/content/client-server-api/modules/event_replacements.md @@ -134,10 +134,14 @@ ratchet entry should **not** be re-used. 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. (Note that server implementations must not *actually* overwrite +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.) +below. +{{% /boxes/note %}} For example, given a pair of events: From 563a1832a8759ffbac04d5cfbf4614b1824a0711 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 1 Sep 2022 15:38:17 +0100 Subject: [PATCH 5/5] Apply suggestions from code review --- content/client-server-api/_index.md | 6 +++--- content/client-server-api/modules/event_replacements.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/content/client-server-api/_index.md b/content/client-server-api/_index.md index e0ca8a836..b1b55159f 100644 --- a/content/client-server-api/_index.md +++ b/content/client-server-api/_index.md @@ -1938,14 +1938,14 @@ parent event, for example. {{% /boxes/note %}} To allow the server to aggregate and find child events for a parent, the `m.relates_to` -key of an event MUST be included in the cleartext (unencrypted) part of the event. It cannot be +key of an event MUST be included in the plaintext copy of the event. It cannot be exclusively recorded in the encrypted payload as the server cannot decrypt the event for processing. {{% boxes/warning %}} If an encrypted event contains an `m.relates_to` in its payload, it should be -ignored and instead favour the cleartext `m.relates_to` copy (including when there -is no cleartext copy). This is to ensure the client's behaviour matches the server's +ignored and instead favour the plaintext `m.relates_to` copy (including when there +is no plaintext copy). This is to ensure the client's behaviour matches the server's capability to handle relationships. {{% /boxes/warning %}} diff --git a/content/client-server-api/modules/event_replacements.md b/content/client-server-api/modules/event_replacements.md index af85196c7..52597abd2 100644 --- a/content/client-server-api/modules/event_replacements.md +++ b/content/client-server-api/modules/event_replacements.md @@ -41,9 +41,9 @@ replacement events. an event's content property, such as `formatted_body` (see [`m.room.message` `msgtypes`](#mroommessage-msgtypes)). -#### Validity of message edit events +#### Validity of replacement events -There are a number of requirements on message edit events, which must be satisfied for the replacement to be considered valid: +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