Skip to content

Commit 2334c28

Browse files
richvdhclokep
authored andcommitted
1 parent 1c0101c commit 2334c28

File tree

3 files changed

+333
-10
lines changed

3 files changed

+333
-10
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `m.replace` relations (event edits), as per [MSC2676](https://github.com/matrix-org/matrix-spec-proposals/pull/2676).

content/client-server-api/_index.md

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1955,16 +1955,6 @@ rooms, or the relationship missing properties required by the schema below. Clie
19551955
handling such invalid relationships should show the events independently of each
19561956
other, optionally with an error message.
19571957

1958-
{{% boxes/note %}}
1959-
While this specification describes an `m.relates_to` object containing a `rel_type`, there
1960-
is not currently any relationship type which uses this structure. Replies, described below,
1961-
form their relationship outside of the `rel_type` as a legacy type of relationship. Future
1962-
versions of the specification might change replies to better match the relationship structures.
1963-
1964-
Custom `rel_type`s can, and should, still use the schema described above for relevant
1965-
behaviour.
1966-
{{% /boxes/note %}}
1967-
19681958
`m.relates_to` is defined as follows:
19691959

19701960
{{% definition path="api/client-server/definitions/m.relates_to" %}}
@@ -1974,6 +1964,7 @@ behaviour.
19741964
This specification describes the following relationship types:
19751965

19761966
* [Rich replies](#rich-replies) (**Note**: does not use `rel_type`).
1967+
* [Event replacements](#event-replacements).
19771968

19781969
#### Aggregations
19791970

@@ -2643,3 +2634,4 @@ systems.
26432634
{{< cs-module name="server_notices" >}}
26442635
{{< cs-module name="moderation_policies" >}}
26452636
{{< cs-module name="spaces" >}}
2637+
{{< cs-module name="event_replacements" >}}
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
---
2+
type: module
3+
---
4+
5+
### Event replacements
6+
7+
{{% added-in v="1.4" %}}
8+
9+
Event replacements, or "message edit events", are events that use an [event
10+
relationship](#forming-relationships-between-events)
11+
with a `rel_type` of `m.replace`, which indicates that the original event is
12+
intended to be replaced.
13+
14+
An example of a message edit event might look like this:
15+
16+
```json
17+
{
18+
"type": "m.room.message",
19+
"content": {
20+
"body": "* Hello! My name is bar",
21+
"msgtype": "m.text",
22+
"m.new_content": {
23+
"body": "Hello! My name is bar",
24+
"msgtype": "m.text"
25+
},
26+
"m.relates_to": {
27+
"rel_type": "m.replace",
28+
"event_id": "$some_event_id"
29+
}
30+
},
31+
// ... other fields required by events
32+
}
33+
```
34+
35+
The `content` of the replacement must contain a `m.new_content` property which
36+
defines the replacement `content`. The normal `content` properties (`body`,
37+
`msgtype` etc.) provide a fallback for clients which do not understand
38+
replacement events.
39+
40+
`m.new_content` can include any properties that would normally be found in
41+
an event's content property, such as `formatted_body` (see [`m.room.message`
42+
`msgtypes`](#mroommessage-msgtypes)).
43+
44+
#### Validity of replacement events
45+
46+
There are a number of requirements on replacement events, which must be satisfied for the replacement to be considered valid:
47+
48+
* As with all event relationships, the original event and replacement event
49+
must have the same `room_id` (i.e. you cannot send an event in
50+
one room and then an edited version in a different room).
51+
52+
* The original event and replacement event must have the same `sender`
53+
(i.e. you cannot edit someone else's messages).
54+
55+
* The replacement and original events must have the same `type` (i.e. you
56+
cannot change the original event's type).
57+
58+
* The replacement and original events must not have a `state_key` property
59+
(i.e. you cannot edit state events at all).
60+
61+
* The original event must not, itself, have a `rel_type` of `m.replace`
62+
(i.e. you cannot edit an edit — though you can send multiple edits for a
63+
single original event).
64+
65+
* The replacement event (once decrypted, if appropriate) must have an
66+
`m.new_content` property.
67+
68+
If any of these criteria are not satisfied, implementations should ignore the
69+
replacement event (the content of the original should not be replaced, and the
70+
edit should not be included in the server-side aggregation).
71+
72+
Note that the [`msgtype`](#mroommessage-msgtypes) property of replacement
73+
`m.room.message` events does *not* need to be the same as in the original event. For
74+
example, it is legitimate to replace an `m.text` event with an `m.emote`.
75+
76+
#### Editing encrypted events
77+
78+
If the original event was [encrypted](#end-to-end-encryption), the replacement
79+
should be too. In that case, `m.new_content` is placed in the content of the
80+
encrypted payload. As with all event relationships, the `m.relates_to` property
81+
must be sent in the unencrypted (cleartext) part of the event.
82+
83+
For example, a replacement for an encrypted event might look like this:
84+
85+
```json
86+
{
87+
"type": "m.room.encrypted",
88+
"content": {
89+
"m.relates_to": {
90+
"rel_type": "m.replace",
91+
"event_id": "$some_event_id"
92+
},
93+
"algorithm": "m.megolm.v1.aes-sha2",
94+
"sender_key": "<sender_curve25519_key>",
95+
"device_id": "<sender_device_id>",
96+
"session_id": "<outbound_group_session_id>",
97+
"ciphertext": "<encrypted_payload_base_64>"
98+
}
99+
// irrelevant fields not shown
100+
}
101+
```
102+
103+
... and, once decrypted, the payload might look like this:
104+
105+
```json
106+
{
107+
"type": "m.room.<event_type>",
108+
"room_id": "!some_room_id",
109+
"content": {
110+
"body": "* Hello! My name is bar",
111+
"msgtype": "m.text",
112+
"m.new_content": {
113+
"body": "Hello! My name is bar",
114+
"msgtype": "m.text"
115+
}
116+
}
117+
}
118+
```
119+
120+
Note that:
121+
122+
* There is no `m.relates_to` property in the encrypted payload. If there was, it would be ignored.
123+
* 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.
124+
125+
{{% boxes/note %}}
126+
The payload of an encrypted replacement event must be encrypted as normal, including
127+
ratcheting any [Megolm](#mmegolmv1aes-sha2) session as normal. The original Megolm
128+
ratchet entry should **not** be re-used.
129+
{{% /boxes/note %}}
130+
131+
132+
#### Applying `m.new_content`
133+
134+
When applying a replacement, the `content` of the original event is treated as
135+
being overwritten entirely by `m.new_content`, with the exception of `m.relates_to`,
136+
which is left *unchanged*. Any `m.relates_to` property within `m.new_content`
137+
is ignored.
138+
139+
{{% boxes/note %}}
140+
Note that server implementations must not *actually* overwrite
141+
the original event's `content`: instead the server presents it as being overwritten
142+
when it is served over the client-server API. See [Server-side replacement of content](#server-side-replacement-of-content)
143+
below.
144+
{{% /boxes/note %}}
145+
146+
For example, given a pair of events:
147+
148+
```json
149+
{
150+
"event_id": "$original_event",
151+
"type": "m.room.message",
152+
"content": {
153+
"body": "I really like cake",
154+
"msgtype": "m.text",
155+
"formatted_body": "I really like cake",
156+
}
157+
}
158+
```
159+
160+
```json
161+
{
162+
"event_id": "$edit_event",
163+
"type": "m.room.message",
164+
"content": {
165+
"body": "* I really like *chocolate* cake",
166+
"msgtype": "m.text",
167+
"m.new_content": {
168+
"body": "I really like *chocolate* cake",
169+
"msgtype": "m.text",
170+
"com.example.extension_property": "chocolate"
171+
},
172+
"m.relates_to": {
173+
"rel_type": "m.replace",
174+
"event_id": "$original_event_id"
175+
}
176+
}
177+
}
178+
```
179+
180+
... then the end result is an event as shown below:
181+
182+
```json
183+
{
184+
"event_id": "$original_event",
185+
"type": "m.room.message",
186+
"content": {
187+
"body": "I really like *chocolate* cake",
188+
"msgtype": "m.text",
189+
"com.example.extension_property": "chocolate"
190+
}
191+
}
192+
```
193+
194+
Note that `formatted_body` is now absent, because it was absent in the
195+
replacement event.
196+
197+
#### Server behaviour
198+
199+
##### Server-side aggregation of `m.replace` relationships
200+
201+
Note that there can be multiple events with an `m.replace` relationship to a
202+
given event (for example, if an event is edited multiple times). These should
203+
be [aggregated](#aggregations) by the homeserver.
204+
205+
The aggregation format of `m.replace` relationships gives the `event_id`,
206+
`origin_server_ts`, and `sender` of the **most recent** replacement event. The
207+
most recent event is determined by comparing `origin_server_ts`; if two or more
208+
replacement events have identical `origin_server_ts`, the event with the
209+
lexicographically largest `event_id` is treated as more recent.
210+
211+
This aggregation is bundled under the `unsigned` property as `m.relations` for any
212+
event that is the target of an `m.replace` relationship. For example:
213+
214+
```json
215+
{
216+
"event_id": "$original_event_id",
217+
// irrelevant fields not shown
218+
"unsigned": {
219+
"m.relations": {
220+
"m.replace": {
221+
"event_id": "$latest_edit_event_id",
222+
"origin_server_ts": 1649772304313,
223+
"sender": "@editing_user:localhost"
224+
}
225+
}
226+
}
227+
}
228+
```
229+
230+
If the original event is
231+
[redacted](#redactions), any
232+
`m.replace` relationship should **not** be bundled with it (whether or not any
233+
subsequent replacements are themselves redacted). Note that this behaviour is
234+
specific to the `m.replace` relationship. See also [redactions of edited
235+
events](#redactions-of-edited-events) below.
236+
237+
##### Server-side replacement of content
238+
239+
Whenever an `m.replace` is to be bundled with an event as above, the server
240+
should also modify the content of the original event according to the
241+
`m.new_content` of the most recent replacement event (determined as above).
242+
243+
An exception applies to [`GET /_matrix/client/v3/rooms/{roomId}/event/{eventId}`](#get_matrixclientv3roomsroomideventeventid),
244+
which should return the unmodified event (though the relationship should still
245+
be bundled, as described above).
246+
247+
#### Client behaviour
248+
249+
Clients can often ignore `m.replace` events, because any events returned
250+
by the server to the client will be updated by the server to account for
251+
subsequent edits.
252+
253+
However, clients should apply the replacement themselves when the server is
254+
unable to do so. This happens in the following situations:
255+
256+
* The client has already received and stored the original event before the
257+
message edit event arrives.
258+
259+
* The original event (and hence its replacement) are encrypted.
260+
261+
Client authors are reminded to take note of the requirements for [Validity of
262+
message edit events](#validity-of-message-edit-events), and to ignore any
263+
invalid edit events that are received.
264+
265+
##### Permalinks
266+
267+
When creating [links](/appendices/#uris) to events (also known as permalinks),
268+
clients build links which reference the event that the creator of the permalink
269+
is viewing at that point (which might be a message edit event).
270+
271+
The client viewing the permalink should resolve this reference to the original
272+
event, and then display the most recent version of that event.
273+
274+
#### Redactions of edited events
275+
276+
When an event using a `rel_type` of `m.replace` is [redacted](#redactions), it
277+
removes that edit revision. This has little effect if there were subsequent
278+
edits. However, if it was the most recent edit, the event is in effect
279+
reverted to its content before the redacted edit.
280+
281+
Redacting the *original* message in effect removes the message, including all
282+
subsequent edits, from the visible timeline. In this situation, homeservers
283+
will return an empty `content` for the original event as with any other
284+
redacted event, and as
285+
[above](#server-side-aggregation-of-mreplace-relationships) the replacement
286+
events will not be bundled with the original event. Note that the subsequent edits are
287+
not actually redacted themselves: they simply serve no purpose within the visible timeline.
288+
289+
#### Edits of replies
290+
291+
Some particular constraints apply to events which replace a
292+
[reply](#rich-replies). In particular:
293+
294+
* In contrast to the original reply, there should be no `m.in_reply_to`
295+
property in the the `m.relates_to` object, since it would be redundant (see
296+
[Applying `m.new_content`](#applying-mnew_content) above, which notes that
297+
the original event's `m.relates_to` is preserved), as well as being contrary
298+
to the spirit of the event relationships mechanism which expects only one
299+
"parent" per event.
300+
301+
* `m.new_content` should **not** contain any [reply
302+
fallback](#fallbacks-for-rich-replies),
303+
since it is assumed that any client which can handle edits can also display
304+
replies natively. However, the `content` of the replacement event should provide
305+
fallback content for clients which support neither rich replies nor edits.
306+
307+
An example of an edit to a reply is as follows:
308+
309+
```json
310+
{
311+
"type": "m.room.message",
312+
// irrelevant fields not shown
313+
"content": {
314+
"body": "> <@alice:example.org> question\n\n* reply",
315+
"msgtype": "m.text",
316+
"format": "org.matrix.custom.html",
317+
"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",
318+
"m.new_content": {
319+
"body": "reply",
320+
"msgtype": "m.text",
321+
"format": "org.matrix.custom.html",
322+
"formatted_body": "reply"
323+
},
324+
"m.relates_to": {
325+
"rel_type": "m.replace",
326+
"event_id": "$original_reply_event"
327+
}
328+
}
329+
}
330+
```

0 commit comments

Comments
 (0)