11 KiB
事件替换
{{% added-in v="1.4" %}}
事件替换,或称“消息编辑事件”,是指那些使用 事件关系,rel_type
为 m.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
属性(如 body
、msgtype
等)则为不支持替换事件的客户端提供兼容回退。
m.new_content
可以包含事件内容中通常存在的任意属性,例如 formatted_body
(参见 m.room.message
msgtypes
)。
替换事件的有效性
替换事件需满足一系列要求,才能被视为有效替换:
- 如同所有的事件关系一样,原始事件和替换事件必须具有相同的
room_id
(即不能在一个房间发送事件,在另一个房间发送其编辑版本)。 - 原始事件与替换事件必须拥有相同的
sender
(即不能编辑他人的消息)。 - 替换事件和原始事件的
type
必须相同(即不能更改原始事件的类型)。 - 替换事件和原始事件不得包含
state_key
属性(即完全不能编辑状态事件)。 - 原始事件本身不能具有
rel_type
为m.replace
(即不能编辑一条编辑事件——但可以为同一原始事件发送多次编辑)。 - 替换事件(若适用,解密后)必须包含
m.new_content
属性。
如果未满足上述任一条件,则实现应忽略该替换事件(不应替换原文内容,也不应将该编辑纳入服务端聚合)。
请注意,替换事件 m.room.message
的 msgtype
属性不必与原始事件相同。例如,将 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
关系的聚合包含在被目标事件的 unsigned
的 m.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_type
为 m.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"
}
}
}