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

11 KiB
Raw Blame History

事件替换

{{% added-in v="1.4" %}}

事件替换,或称“消息编辑事件”,是指那些使用 事件关系rel_typem.replace 的事件,表示原始事件将被替换。

一条消息编辑事件的示例如下:

{
    "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 属性(如 bodymsgtype 等)则为不支持替换事件的客户端提供兼容回退。

m.new_content 可以包含事件内容中通常存在的任意属性,例如 formatted_body(参见 m.room.message msgtypes)。

替换事件的有效性

替换事件需满足一系列要求,才能被视为有效替换:

  • 如同所有的事件关系一样,原始事件和替换事件必须具有相同的 room_id(即不能在一个房间发送事件,在另一个房间发送其编辑版本)。
  • 原始事件与替换事件必须拥有相同的 sender(即不能编辑他人的消息)。
  • 替换事件和原始事件的 type 必须相同(即不能更改原始事件的类型)。
  • 替换事件和原始事件不得包含 state_key 属性(即完全不能编辑状态事件)。
  • 原始事件本身不能具有 rel_typem.replace(即不能编辑一条编辑事件——但可以为同一原始事件发送多次编辑)。
  • 替换事件(若适用,解密后)必须包含 m.new_content 属性。

如果未满足上述任一条件,则实现应忽略该替换事件(不应替换原文内容,也不应将该编辑纳入服务端聚合)。

请注意,替换事件 m.room.messagemsgtype 属性不必与原始事件相同。例如,将 m.text 事件替换为 m.emote 是合法的。

编辑加密事件

若原始事件是 加密 的,则替换事件也应加密。在这种情况下,m.new_content 被放置于加密负载的内容中。如同所有事件关系,m.relates_to 属性必须位于事件的未加密(明文)部分。

例如,一个加密事件的替换事件可能如下所示:

{
    "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>"
    }
    // 未显示无关字段
}

一旦解密,负载内容可能如下:

{
    "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 会话。不应重复使用原有的 Megolm ratchet 条目。 {{% /boxes/note %}}

应用 m.new_content

应用替换时,原始事件的 content 被视为被 m.new_content 全量覆盖,仅保留 m.relates_to 属性不变m.new_content 内部的任何 m.relates_to 属性均被忽略。

例如,给定以下两条事件:

{
    "event_id": "$original_event",
    "type": "m.room.message",
    "content": {
        "body": "I really like cake",
        "msgtype": "m.text",
        "formatted_body": "I really like cake",
    }
}
{
    "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"
        }
    }
}

……最终结果如下所示:

{
    "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 关系的事件(例如多次编辑)。这些应由主服务器进行 聚合

m.replace 关系的聚合格式会提供最新的替换事件,格式 同常规

最新事件通过比较 origin_server_ts 决定;若有两个或以上替换事件 origin_server_ts 相同,则以字典序最大的 event_id 为最新。

同其他子事件聚合一样,对应于 m.replace 关系的聚合包含在被目标事件的 unsignedm.relations 属性下。例如:

{
  "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"
          }
        }
      }
    }
  }
  // 未显示无关字段
}

如果原始事件被 抹除,则任何 m.replace 关系不应与其打包(无论后续替换本身是否被抹除)。请注意,此行为特定于 m.replace 关系。另请参考下文 已编辑事件的抹除

注意:原始事件的 content 保持不变。特别是服务器不应将内容用替换事件内容替换。

{{% boxes/rationale %}} 此前规范版本要求服务器在向客户端提供已编辑事件时替换其内容(除 GET /_matrix/client/v3/rooms/{roomId}/event/{eventId} 接口外)。然而,这样会导致客户端实现难以保持一致,因此服务器不再进行此操作。 {{% /boxes/rationale %}}

客户端行为

由于服务器不会替换任何已编辑事件内容,客户端应注意所有收到的替换事件,并尽可能和适当时应用替换。

客户端作者请注意 替换事件的有效性 要求,忽略所有无效的替换事件。

永久链接

创建指向事件的链接(即永久链接)时,客户端将构建指向其当前所见事件的链接(可能是消息编辑事件)。

查看该永久链接的客户端应定位到原始事件,并显示该事件的最新版本。

已编辑事件的抹除

当使用 rel_typem.replace 的事件被 抹除 时,该编辑修订被移除。如果有后续编辑,影响较小;但如果这是最新编辑,则事件实际上回退为被抹除编辑前的内容。

抹除原始消息则实际上移除该消息及所有后续编辑使其不再出现在可见时间线上。在这种情况下homeserver 会如同处理其他抹除事件一样,为原始事件返回空的 content,且如 前述 替换事件不会打包于原始事件对应的聚合中。注意后续编辑本身并没有被真正抹除:它们仅在可见时间线之外不发挥作用。

带有提及的事件编辑

编辑包含 用户和房间提及 的事件时,替换事件会含有两个 m.mentions 属性:

  • 位于 content 顶层的,记录该修订中产生的新提及。
  • 位于 m.new_content 属性内的,记录事件最新版本中所有已解析的提及。

以上差异可确保用户不会对事件的每次编辑都收到通知,但又允许提及新用户(或在编辑幅度足够大的情况下重新通知)。

例如,存在一条提及 Alice 的事件:

{
    "event_id": "$original_event",
    "type": "m.room.message",
    "content": {
        "body": "Hello Alice!",
        "m.mentions": {
            "user_ids": ["@alice:example.org"]
        }
    }
}

编辑后同时提及 Bob

{
  "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 属性中。

客户端也可据此调整 提及事件的客户端行为,通过检查 m.new_content 下的 m.mentions 属性判定事件是否提及当前用户。

回复消息的编辑

对替换 回复 的事件存在特殊约束:与原始回复不同,m.relates_to 对象中不得出现 m.in_reply_to 属性,因为这将显得多余(见上文应用 m.new_content 章节已说明原始事件的 m.relates_to 会保留),且与事件关系机制“一事件只存在一个‘父级’”的理念相悖。

{{% boxes/note %}} {{% changed-in v="1.13" %}} 规范早期版本允许替换 回复 的事件在 content 中包含回退信息。此规则已废除。 {{% /boxes/note %}}

编辑回复的示例如下:

{
  "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"
    }
  }
}