317 lines
No EOL
11 KiB
Markdown
317 lines
No EOL
11 KiB
Markdown
### 事件替换
|
||
|
||
{{% added-in v="1.4" %}}
|
||
|
||
事件替换,或称“消息编辑事件”,是指那些使用 [事件关系](#forming-relationships-between-events),`rel_type` 为 `m.replace` 的事件,表示原始事件将被替换。
|
||
|
||
一条消息编辑事件的示例如下:
|
||
|
||
```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"
|
||
}
|
||
},
|
||
// ... 事件所需的其他字段
|
||
}
|
||
```
|
||
|
||
替换事件的 `content` 必须包含 `m.new_content` 属性,用于定义替换后的内容。正常的 `content` 属性(如 `body`、`msgtype` 等)则为不支持替换事件的客户端提供兼容回退。
|
||
|
||
`m.new_content` 可以包含事件内容中通常存在的任意属性,例如 `formatted_body`(参见 [`m.room.message` `msgtypes`](#mroommessage-msgtypes))。
|
||
|
||
#### 替换事件的有效性
|
||
|
||
替换事件需满足一系列要求,才能被视为有效替换:
|
||
|
||
* 如同所有的事件关系一样,原始事件和替换事件必须具有相同的 `room_id`(即不能在一个房间发送事件,在另一个房间发送其编辑版本)。
|
||
* 原始事件与替换事件必须拥有相同的 `sender`(即不能编辑他人的消息)。
|
||
* 替换事件和原始事件的 `type` 必须相同(即不能更改原始事件的类型)。
|
||
* 替换事件和原始事件不得包含 `state_key` 属性(即完全不能编辑状态事件)。
|
||
* 原始事件本身不能具有 `rel_type` 为 `m.replace`(即不能编辑一条编辑事件——但可以为同一原始事件发送多次编辑)。
|
||
* 替换事件(若适用,解密后)必须包含 `m.new_content` 属性。
|
||
|
||
如果未满足上述任一条件,则实现应忽略该替换事件(不应替换原文内容,也不应将该编辑纳入服务端聚合)。
|
||
|
||
请注意,替换事件 [`m.room.message`](#mroommessage-msgtypes) 的 `msgtype` 属性**不必**与原始事件相同。例如,将 `m.text` 事件替换为 `m.emote` 是合法的。
|
||
|
||
#### 编辑加密事件
|
||
|
||
若原始事件是 [加密](#end-to-end-encryption) 的,则替换事件也应加密。在这种情况下,`m.new_content` 被放置于加密负载的内容中。如同所有事件关系,`m.relates_to` 属性必须位于事件的未加密(明文)部分。
|
||
|
||
例如,一个加密事件的替换事件可能如下所示:
|
||
|
||
```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>"
|
||
}
|
||
// 未显示无关字段
|
||
}
|
||
```
|
||
|
||
一旦解密,负载内容可能如下:
|
||
|
||
```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"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
请注意:
|
||
|
||
* 加密负载中**没有** `m.relates_to` 属性。如果有,将会被忽略。
|
||
* `m.room.encrypted` 事件明文内容中**没有** `m.new_content` 属性。如果有,同样会被忽略。
|
||
|
||
{{% boxes/note %}}
|
||
加密替换事件的负载必须如常加密,包括像往常一样推进任何 [Megolm](#mmegolmv1aes-sha2) 会话。**不应**重复使用原有的 Megolm ratchet 条目。
|
||
{{% /boxes/note %}}
|
||
|
||
#### 应用 `m.new_content`
|
||
|
||
应用替换时,原始事件的 `content` 被视为被 `m.new_content` 全量覆盖,仅保留 `m.relates_to` 属性**不变**。`m.new_content` 内部的任何 `m.relates_to` 属性均被忽略。
|
||
|
||
例如,给定以下两条事件:
|
||
|
||
```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"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
……最终结果如下所示:
|
||
|
||
```json
|
||
{
|
||
"event_id": "$original_event",
|
||
"type": "m.room.message",
|
||
"content": {
|
||
"body": "I really like *chocolate* cake",
|
||
"msgtype": "m.text",
|
||
"com.example.extension_property": "chocolate"
|
||
}
|
||
}
|
||
```
|
||
|
||
注意此时 `formatted_body` 已不存在,因为替换事件中已省略该字段。
|
||
|
||
#### 服务器行为
|
||
|
||
##### 服务端对 `m.replace` 关系的聚合
|
||
|
||
{{% changed-in v="1.7" %}}
|
||
|
||
请注意,同一个原始事件可以有多个 `m.replace` 关系的事件(例如多次编辑)。这些应由主服务器进行 [聚合](#aggregations-of-child-events)。
|
||
|
||
`m.replace` 关系的聚合格式会提供**最新**的替换事件,格式 [同常规](#room-event-format)。
|
||
|
||
最新事件通过比较 `origin_server_ts` 决定;若有两个或以上替换事件 `origin_server_ts` 相同,则以字典序最大的 `event_id` 为最新。
|
||
|
||
同其他子事件聚合一样,对应于 `m.replace` 关系的聚合包含在被目标事件的 `unsigned` 的 `m.relations` 属性下。例如:
|
||
|
||
```json
|
||
{
|
||
"event_id": "$original_event_id",
|
||
"type": "m.room.message",
|
||
"content": {
|
||
"body": "I really like cake",
|
||
"msgtype": "m.text",
|
||
"formatted_body": "I really like cake"
|
||
},
|
||
"unsigned": {
|
||
"m.relations": {
|
||
"m.replace": {
|
||
"event_id": "$latest_edit_event_id",
|
||
"origin_server_ts": 1649772304313,
|
||
"sender": "@editing_user:localhost"
|
||
"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"
|
||
},
|
||
"m.relates_to": {
|
||
"rel_type": "m.replace",
|
||
"event_id": "$original_event_id"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 未显示无关字段
|
||
}
|
||
```
|
||
|
||
如果原始事件被 [抹除](#redactions),则任何
|
||
`m.replace` 关系**不应**与其打包(无论后续替换本身是否被抹除)。请注意,此行为特定于 `m.replace` 关系。另请参考下文 [已编辑事件的抹除](#redactions-of-edited-events)。
|
||
|
||
**注意:**原始事件的 `content` 保持不变。特别是服务器**不应**将内容用替换事件内容替换。
|
||
|
||
{{% boxes/rationale %}}
|
||
此前规范版本要求服务器在向客户端提供已编辑事件时替换其内容(除
|
||
[`GET /_matrix/client/v3/rooms/{roomId}/event/{eventId}`](#get_matrixclientv3roomsroomideventeventid)
|
||
接口外)。然而,这样会导致客户端实现难以保持一致,因此服务器不再进行此操作。
|
||
{{% /boxes/rationale %}}
|
||
|
||
#### 客户端行为
|
||
|
||
由于服务器不会替换任何已编辑事件内容,客户端应注意所有收到的替换事件,并尽可能和适当时应用替换。
|
||
|
||
客户端作者请注意 [替换事件的有效性](#validity-of-replacement-events) 要求,忽略所有无效的替换事件。
|
||
|
||
##### 永久链接
|
||
|
||
创建指向事件的[链接](/appendices/#uris)(即永久链接)时,客户端将构建指向其当前所见事件的链接(可能是消息编辑事件)。
|
||
|
||
查看该永久链接的客户端应定位到原始事件,并显示该事件的最新版本。
|
||
|
||
#### 已编辑事件的抹除
|
||
|
||
当使用 `rel_type` 为 `m.replace` 的事件被 [抹除](#redactions) 时,该编辑修订被移除。如果有后续编辑,影响较小;但如果这是最新编辑,则事件实际上回退为被抹除编辑前的内容。
|
||
|
||
抹除*原始*消息则实际上移除该消息及所有后续编辑,使其不再出现在可见时间线上。在这种情况下,homeserver 会如同处理其他抹除事件一样,为原始事件返回空的 `content`,且如
|
||
[前述](#server-side-aggregation-of-mreplace-relationships) 替换事件不会打包于原始事件对应的聚合中。注意后续编辑本身并没有被真正抹除:它们仅在可见时间线之外不发挥作用。
|
||
|
||
#### 带有提及的事件编辑
|
||
|
||
编辑包含 [用户和房间提及](#user-and-room-mentions) 的事件时,替换事件会含有两个 `m.mentions` 属性:
|
||
|
||
* 位于 `content` 顶层的,记录该修订中产生的新提及。
|
||
* 位于 `m.new_content` 属性内的,记录事件最新版本中所有已解析的提及。
|
||
|
||
以上差异可确保用户不会对事件的每次编辑都收到通知,但又允许提及新用户(或在编辑幅度足够大的情况下重新通知)。
|
||
|
||
例如,存在一条提及 Alice 的事件:
|
||
|
||
```json
|
||
{
|
||
"event_id": "$original_event",
|
||
"type": "m.room.message",
|
||
"content": {
|
||
"body": "Hello Alice!",
|
||
"m.mentions": {
|
||
"user_ids": ["@alice:example.org"]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
编辑后同时提及 Bob:
|
||
|
||
```json
|
||
{
|
||
"content": {
|
||
"body": "* Hello Alice & Bob!",
|
||
"m.mentions": {
|
||
"user_ids": [
|
||
// 仅包含新提及的用户
|
||
"@bob:example.org"
|
||
]
|
||
},
|
||
"m.new_content": {
|
||
"body": "Hello Alice & Bob!",
|
||
"m.mentions": {
|
||
"user_ids": [
|
||
// 包含所有已提及的用户
|
||
"@alice:example.org",
|
||
"@bob:example.org"
|
||
]
|
||
},
|
||
},
|
||
"m.relates_to": {
|
||
"rel_type": "m.replace",
|
||
"event_id": "$original_event"
|
||
}
|
||
},
|
||
// 事件需要的其他字段
|
||
}
|
||
```
|
||
|
||
若某一修订移除了某个用户的提及,则该用户的 Matrix ID 不应出现在任何 `m.mentions` 属性中。
|
||
|
||
客户端也可据此调整 [提及事件的客户端行为](#user-and-room-mentions),通过检查 `m.new_content` 下的 `m.mentions` 属性判定事件是否提及当前用户。
|
||
|
||
#### 回复消息的编辑
|
||
|
||
对替换 [回复](#rich-replies) 的事件存在特殊约束:与原始回复不同,`m.relates_to` 对象中**不得**出现 `m.in_reply_to` 属性,因为这将显得多余(见上文[应用 `m.new_content`](#applying-mnew_content) 章节已说明原始事件的 `m.relates_to` 会保留),且与事件关系机制“一事件只存在一个‘父级’”的理念相悖。
|
||
|
||
{{% boxes/note %}}
|
||
{{% changed-in v="1.13" %}}
|
||
规范早期版本允许替换 [回复](#rich-replies) 的事件在 `content` 中包含回退信息。此规则已废除。
|
||
{{% /boxes/note %}}
|
||
|
||
编辑回复的示例如下:
|
||
|
||
```json
|
||
{
|
||
"type": "m.room.message",
|
||
// 未显示无关字段
|
||
"content": {
|
||
"body": "* reply",
|
||
"msgtype": "m.text",
|
||
"m.new_content": {
|
||
"body": "reply",
|
||
"msgtype": "m.text",
|
||
},
|
||
"m.relates_to": {
|
||
"rel_type": "m.replace",
|
||
"event_id": "$original_reply_event"
|
||
}
|
||
}
|
||
}
|
||
``` |