docs-matrix-spec/locales/zh-Hans/client-server-api/modules/event_replacements.md
2025-04-20 16:13:37 +08:00

317 lines
No EOL
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

### 事件替换
{{% 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"
}
}
}
```