### 事件替换 {{% 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": "", "device_id": "", "session_id": "", "ciphertext": "" } // 未显示无关字段 } ``` 一旦解密,负载内容可能如下: ```json { "type": "m.room.", "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" } } } ```