Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spec event edits #1211

Merged
merged 6 commits into from
Sep 21, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelogs/client_server/newsfragments/1211.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `m.replace` relations (event edits), as per [MSC2676](https://github.com/matrix-org/matrix-spec-proposals/pull/2676).
18 changes: 5 additions & 13 deletions content/client-server-api/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
richvdh marked this conversation as resolved.
Show resolved Hide resolved
richvdh marked this conversation as resolved.
Show resolved Hide resolved
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
richvdh marked this conversation as resolved.
Show resolved Hide resolved
capability to handle relationships.
{{% /boxes/warning %}}

Expand All @@ -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" %}}
Expand All @@ -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

Expand Down Expand Up @@ -2643,3 +2634,4 @@ systems.
{{< cs-module name="server_notices" >}}
{{< cs-module name="moderation_policies" >}}
{{< cs-module name="spaces" >}}
{{< cs-module name="event_replacements" >}}
319 changes: 319 additions & 0 deletions content/client-server-api/modules/event_replacements.md
Original file line number Diff line number Diff line change
@@ -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)
richvdh marked this conversation as resolved.
Show resolved Hide resolved
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
turt2live marked this conversation as resolved.
Show resolved Hide resolved
{
"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
turt2live marked this conversation as resolved.
Show resolved Hide resolved

There are a number of requirements on message edit events, which must be satisfied for the replacement to be considered valid:
richvdh marked this conversation as resolved.
Show resolved Hide resolved

* 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).
richvdh marked this conversation as resolved.
Show resolved Hide resolved

* 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).
richvdh marked this conversation as resolved.
Show resolved Hide resolved

* Neither the replacement nor original events may have a `state_key` property
richvdh marked this conversation as resolved.
Show resolved Hide resolved
(i.e. it is not possible to edit a state event).
richvdh marked this conversation as resolved.
Show resolved Hide resolved

* 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
richvdh marked this conversation as resolved.
Show resolved Hide resolved
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:
richvdh marked this conversation as resolved.
Show resolved Hide resolved

```json
turt2live marked this conversation as resolved.
Show resolved Hide resolved
{
"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": "<sender_curve25519_key>",
"device_id": "<sender_device_id>",
"session_id": "<outbound_group_session_id>",
"ciphertext": "<encrypted_payload_base_64>"
}
// ...
richvdh marked this conversation as resolved.
Show resolved Hide resolved
}
```

... and, once decrypted, the payload might look like this:

```json
{
"type": "m.room.<event_type>",
"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.)
richvdh marked this conversation as resolved.
Show resolved Hide resolved
* There is no `m.new_content` property in the cleartext content of the `m.room.encrypted` event. (Again, any such property would be ignored.)
richvdh marked this conversation as resolved.
Show resolved Hide resolved

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.
richvdh marked this conversation as resolved.
Show resolved Hide resolved


#### 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.)
richvdh marked this conversation as resolved.
Show resolved Hide resolved

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:
turt2live marked this conversation as resolved.
Show resolved Hide resolved

```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
richvdh marked this conversation as resolved.
Show resolved Hide resolved
event that is the target of an `m.replace` relationship. For example:

```json
turt2live marked this conversation as resolved.
Show resolved Hide resolved
{
"event_id": "$original_event_id",
// ...
richvdh marked this conversation as resolved.
Show resolved Hide resolved
"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
richvdh marked this conversation as resolved.
Show resolved Hide resolved
`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
richvdh marked this conversation as resolved.
Show resolved Hide resolved

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.
richvdh marked this conversation as resolved.
Show resolved Hide resolved

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.
richvdh marked this conversation as resolved.
Show resolved Hide resolved

##### 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.
richvdh marked this conversation as resolved.
Show resolved Hide resolved

#### 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
richvdh marked this conversation as resolved.
Show resolved Hide resolved
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),
richvdh marked this conversation as resolved.
Show resolved Hide resolved
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
turt2live marked this conversation as resolved.
Show resolved Hide resolved
{
"type": "m.room.message",
richvdh marked this conversation as resolved.
Show resolved Hide resolved
"content": {
"body": "> <@alice:example.org> question\n\n* reply",
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!somewhere:example.org/$event:example.org\">In reply to</a> <a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a><br />question</blockquote></mx-reply>* 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"
}
}
}
```