|
| 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