MSC2676: Message editing (#2676)

* initial version of message editing proposal

* fix MSC numbers

* Fix JSON in example

Co-authored-by: Alexandre Morignot <erdnaxeli@gmail.com>

* clarifications

* remove obsolete "XXX:", and fix a typo

* Initial cleanup and restructuring

* Clarify algorithm for replacing content

* background

* More clarifications on applying edits

* Clarify behaviour of redactions

* Minor grammar fixes

* Move the section on `msgtype` down

It clutters up the initial description - let's move it down into a more
detailed section.

* Clarify how edits are ordered

- we use event_id as a tiebreaker.

We also have a section in "Future considerations" about this, so I don't think
we need the braindump in "Edge cases".

* Spec the behaviour for encrypted events

* Requirements for an edit event to be considered valid

* Collect "client behaviour" and "sever behaviour" together

... and clarify these sections.

* Clarify permalinks section

* Notes on edits of replies

* Clarify that `m.relates_to` within `m.new_content` is ignored

* Clarifications from review

* event ids are sorted lexicographically

* Clarify aggregation section

* minor clarifications

* Clarify which endpoints support edits

* move definition of latest edit

* Apply suggestions from code review

Co-authored-by: Hubert Chathi <hubert@uhoreg.ca>

* fix typo

* Attempt to clarify encrypted events

Co-authored-by: Alexandre Morignot <erdnaxeli@gmail.com>
Co-authored-by: Richard van der Hoff <richard@matrix.org>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
pull/3850/head
Hubert Chathi 2 years ago committed by Travis Ralston
parent 17e3cc17bf
commit 1bfa09cc2a

@ -0,0 +1,407 @@
# MSC2676: Message editing
Users may wish to edit previously sent messages, for example to correct typos.
This can be done by sending a new message with an indication that it replaces
the previously sent message.
This proposal is one in a series of proposals that defines a mechanism for
events to relate to each other. Together, these proposals replace
[MSC1849](https://github.com/matrix-org/matrix-doc/pull/1849).
* [MSC2674](https://github.com/matrix-org/matrix-doc/pull/2674) defines a
standard shape for indicating events which relate to other events.
* [MSC2675](https://github.com/matrix-org/matrix-doc/pull/2675) defines APIs to
let the server calculate the aggregations on behalf of the client, and so
bundle the related events with the original event where appropriate.
* This proposal defines how users can edit messages using this mechanism.
* [MSC2677](https://github.com/matrix-org/matrix-doc/pull/2677) defines how
users can annotate events, such as reacting to events with emoji, using this
mechanism.
## Background
Element-Web (then Riot-Web) and Synapse both implemented initial support for
message editing, following the proposals of MSC1849, in May 2019
([matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/pull/2952),
[synapse](https://github.com/matrix-org/synapse/pull/5209)). Element-Android
and Element-iOS also added implementations around that time. Unfortunately,
those implementations presented the feature as "production-ready", despite it
not yet having been adopted into the Matrix specification.
The current situation is therefore that client or server implementations hoping
to interact with Element users must simply follow the examples of that
implementation. In other words, message edits form part of the *de-facto* spec
despite not being formalised in the written spec. This is clearly a regrettable
situation. Hopefully, processes have improved over the last three years so that
this situation will not arise again. Nevertheless there is little we can do
now other than formalise the status quo.
This MSC, along with the others mentioned above, therefore seeks primarily to
do that. Although there is plenty of scope for improvement, we consider that
better done in *future* MSCs, based on a shared understanding of the *current*
implementation.
In short, this MSC prefers fidelity to the current implementations over
elegance of design.
## Proposal
### `m.replace` event relationship type
A new `rel_type` of `m.replace` is defined for use with the `m.relates_to`
field as defined in
[MSC2674](https://github.com/matrix-org/matrix-doc/pull/2674). This is
intended primarily for handling edits, and lets you define an event which
replaces an existing event.
Such an event, with `rel_type: m.replace`, is referred to as a "message edit event".
### `m.new_content` property
The `content` of a message edit event must contain a `m.new_content` property
which defines the replacement content. (This allows the normal `body` fields to
be used for a fallback for clients who do not understand replacement events.)
For instance, an `m.room.message` which replaces an existing event might look like:
```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"
}
}
}
```
The `m.new_content` can include any properties that would normally be found in
an event's `content` property, such as `formatted_body`.
#### Encrypted events
If the original event was encrypted, the replacement should be too. In that
case, `m.new_content` is placed in the `content` of the encrypted payload. The
`m.relates_to` property remains unencrypted, as required by the
[relationships](https://spec.matrix.org/v1.3/client-server-api/#forming-relationships-between-events)
section of the Client-Server API specification.
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": "<sender_curve25519_key>",
"device_id": "<sender_device_id>",
"session_id": "<outbound_group_session_id>",
"ciphertext": "<encrypted_payload_base_64>"
}
}
```
... 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.)
* There is no `m.new_content` property in the cleartext `content` of the
`m.room.encrypted` event. (Again, any such property would be ignored.)
For clarity: the payload must be encrypted as normal, ratcheting the Megolm session
as normal. The original Megolm ratchet entry should **not** be re-used.
#### Applying `m.new_content`
When applying a replacement, the `content` property of the original event is
replaced entirely by the `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.
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 <em>really</em> 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. Note that `formatted_body`
is now absent, because it was absent in the replacement event, but
`m.relates_to` remains unchanged (ie, absent).
```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 the `msgtype` property of `m.room.message` events need not be the
same as in the original event. For example, if a user intended to send a
message beginning with "/me", but their client sends an `m.emote` event
instead, they could edit the message to send be an `m.text` event as they had
originally intended.
### Validity of message edit events
Some message edit events are defined to be invalid. To be considered valid, all
of the following criteria must be satisfied:
* The replacement and original events must have the same `type`.
* Neither the replacement nor original events can be state events (ie, neither
may have a `state_key`).
* The original event must not, itself, have a `rel_type` of `m.replace`.
* The original event and replacement event must have the same `sender`.
* The replacement event (once decrypted, if appropriate) must have an
`m.new_content` property.
The original event and replacement event must also have the same `room_id`, as
required by the
[relationships](https://spec.matrix.org/v1.3/client-server-api/#forming-relationships-between-events)
section of the Client-Server API specification.
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).
### 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](https://spec.matrix.org/v1.3/client-server-api/#aggregations)
by the homeserver.
The format of the aggregation for `m.replace` simply gives gives the
`event_id`, `origin_server_ts`, and `sender` of the most recent replacement
event (as determined by `origin_server_ts`, falling back to a lexicographic
ordering of `event_id`).
This aggregation is bundled into the `unsigned/m.relations` property of any
event that is the target of an `m.replace` relationship. For example:
```json5
{
"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, any `m.replace` relationship should **not**
be bundled with it (whether or not any subsequent edits are themselves
redacted). Note that this behaviour is specific to the `m.replace`
relationship.
#### 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 edit (determined as above).
An exception applies to [`GET
/_matrix/client/v3/rooms/{roomId}/event/{eventId}`](https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomideventeventid),
which should return the *unmodified* event (though the relationship should
still be bundled, as described above).
The endpoints where this behaviour takes place is the same as those where
aggregations are bundled, with the exception of
`/room/{roomId}/event/{eventId}`. This includes:
* `GET /rooms/{roomId}/messages`
* `GET /rooms/{roomId}/context/{eventId}`
* `GET /rooms/{roomId}/relations/{eventId}`
* `GET /rooms/{roomId}/relations/{eventId}/{relType}`
* `GET /rooms/{roomId}/relations/{eventId}/{relType}/{eventType}`
* `GET /sync` when the relevant section has a `limited` value of `true`
* `POST /search` for any matching events under `room_events`.
### Client behaviour
Clients can often ignore message edit 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:
1. The client has already received and stored the original event before the message
edit event arrives.
2. 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
Permalinks to edited events should capture the event ID 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 ID to the original event
ID, and then display the most recent version of that event.
### Redactions
When a message using a `rel_type` of `m.replace` is redacted, 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. It must be noted that, although they are not immediately
visible in Element, subsequent edits remain unredacted and can be seen via API
calls. See [Future considerations](#future-considerations).
### Edits of replies
Some particular constraints apply to events which replace a
[reply](https://spec.matrix.org/v1.3/client-server-api/#rich-replies). In
particular:
* 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
[MSC2674](https://github.com/matrix-org/matrix-spec-proposals/pull/2674)
which expects only one relationship 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.
An example of an edit to a reply is as follows:
```json
{
"type": "m.room.message",
"content": {
"body": "> <@richvdh:sw1v.org> ab\n\n * ef",
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!qOZKfwKPirAoSosXrf:matrix.org/$1652807718765kOVDf:sw1v.org?via=matrix.org&amp;via=sw1v.org\">In reply to</a> <a href=\"https://matrix.to/#/@richvdh:sw1v.org\">@richvdh:sw1v.org</a><br>ab</blockquote></mx-reply> * ef",
"m.new_content": {
"body": "ef",
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"formatted_body": "ef"
},
"m.relates_to": {
"rel_type": "m.replace",
"event_id": "$original_reply_event"
}
}
}
```
## Future considerations
### Ordering of edits
In future we may wish to consider ordering replacements (or relations in
general) via a DAG rather than using `origin_server_ts` to determine ordering -
particularly to mitigate potential abuse of edits applied by moderators.
Whatever, care must be taken by the server to ensure that if there are multiple
replacement events, the server must consistently choose the same one as all
other servers.
### Redaction of edits
It is highly unintuitive that redacting the original event leaves subsequent
edits visible to curious eyes even though they are hidden from the
timeline. This is considered a bug which this MSC makes no attempt to
resolve. See also
[element-web#11978](https://github.com/vector-im/element-web/issues/11978) and
[synapse#5594](https://github.com/matrix-org/synapse/issues/5594).
### Edits to state events
There are various issues which would need to be resolved before edits to state
events could be supported. In particular, we would need to consider how the
semantically-meaningful fields of the content of a state event relate to
`m.new_content`. Variation between implementations could easily lead to
security problems (See
[element-web#21851](https://github.com/vector-im/element-web/issues/21851) for
example.)
### Editing other users' events
There is a usecase for users with sufficient power-level to edit other peoples'
events. For now, no attempt is made to support this. If it is supported in the
future, we would need to find a way to make it clear in the timeline.
Loading…
Cancel
Save