fix: notify p2p message mutation on user channel
This commit is contained in:
+27
-1
@@ -455,6 +455,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
|
|||||||
| 修改好友备注 | 操作人本人 | `{"info":{"topic":"usr_actor","from":"usr_actor","what":"im","event":"friend.remark.updated","payload":{"user_id":"usr_actor"}}}` | 刷新好友列表和会话头显示名 |
|
| 修改好友备注 | 操作人本人 | `{"info":{"topic":"usr_actor","from":"usr_actor","what":"im","event":"friend.remark.updated","payload":{"user_id":"usr_actor"}}}` | 刷新好友列表和会话头显示名 |
|
||||||
| 消息撤回 | Topic 在线成员 | `{"info":{"topic":"p2p_or_grp","from":"usr_actor","what":"im","event":"message.recalled","payload":{"topic":"p2p_or_grp","seq":12,"version":2}}}` | 按 `topic+seq+version` 重新拉取或本地标记已撤回 |
|
| 消息撤回 | Topic 在线成员 | `{"info":{"topic":"p2p_or_grp","from":"usr_actor","what":"im","event":"message.recalled","payload":{"topic":"p2p_or_grp","seq":12,"version":2}}}` | 按 `topic+seq+version` 重新拉取或本地标记已撤回 |
|
||||||
| 消息编辑 | Topic 在线成员 | `{"info":{"topic":"p2p_or_grp","from":"usr_actor","what":"im","event":"message.edited","payload":{"topic":"p2p_or_grp","seq":12,"version":2}}}` | 按 `topic+seq+version` 重新拉取消息详情或修订记录 |
|
| 消息编辑 | Topic 在线成员 | `{"info":{"topic":"p2p_or_grp","from":"usr_actor","what":"im","event":"message.edited","payload":{"topic":"p2p_or_grp","seq":12,"version":2}}}` | 按 `topic+seq+version` 重新拉取消息详情或修订记录 |
|
||||||
|
| 单聊消息变更脏标记 | P2P 双方在线会话 | `{"info":{"topic":"usr_target","from":"usr_actor","what":"im","event":"message.mutation","payload":{"topic":"p2p...","seq":12,"version":2,"event":"message.recalled"}}}` | 当前未订阅该 P2P 也要标记会话脏,并调用 `message-mutations` 做补偿同步 |
|
||||||
| 群业务变化 | 群 Topic 在线成员 | `{"info":{"topic":"grp...","from":"usr_actor","what":"group","event":"group.member.joined","payload":{"topic":"grp...","user_id":"usr_xxx"}}}` | 按事件刷新群详情、成员列表、入群申请、公告、禁言或角色权限;除操作人外当前会写系统通知 |
|
| 群业务变化 | 群 Topic 在线成员 | `{"info":{"topic":"grp...","from":"usr_actor","what":"group","event":"group.member.joined","payload":{"topic":"grp...","user_id":"usr_xxx"}}}` | 按事件刷新群详情、成员列表、入群申请、公告、禁言或角色权限;除操作人外当前会写系统通知 |
|
||||||
| 会议变化 | 会议参会人 | `{"info":{"topic":"usr_target","from":"usr_actor","what":"im","event":"meeting.updated","payload":{"meeting_id":"mtg_1","version":2,"state":"scheduled"}}}` | 刷新会议列表/详情;除操作者外当前会写系统通知 |
|
| 会议变化 | 会议参会人 | `{"info":{"topic":"usr_target","from":"usr_actor","what":"im","event":"meeting.updated","payload":{"meeting_id":"mtg_1","version":2,"state":"scheduled"}}}` | 刷新会议列表/详情;除操作者外当前会写系统通知 |
|
||||||
| 安全风险确认 | 操作人本人 | `{"info":{"topic":"usr_actor","from":"usr_actor","what":"im","event":"security.risk.continue","payload":{"risk_challenge_id":"risk_1","decision":"continue"}}}` | 刷新安全验证页面;当前会写系统通知 |
|
| 安全风险确认 | 操作人本人 | `{"info":{"topic":"usr_actor","from":"usr_actor","what":"im","event":"security.risk.continue","payload":{"risk_challenge_id":"risk_1","decision":"continue"}}}` | 刷新安全验证页面;当前会写系统通知 |
|
||||||
@@ -1590,7 +1591,32 @@ Body:
|
|||||||
|
|
||||||
#### 4.21.3 消息变更同步水位
|
#### 4.21.3 消息变更同步水位
|
||||||
|
|
||||||
撤回、编辑属于 `seq` 不变的原地变更。在线且已订阅 Topic 时,客户端会收到 `message.recalled` / `message.edited` 实时事件;离线或未订阅时,需要靠会话级水位补偿。
|
撤回、编辑属于 `seq` 不变的原地变更。在线且已订阅 Topic 时,客户端会收到 `message.recalled` / `message.edited` 实时事件;单聊中如果客户端在线但当前没有订阅该 P2P,服务端会额外向双方用户通道下发 `message.mutation` 脏标记事件;离线或错过实时事件时,需要靠会话级水位补偿。
|
||||||
|
|
||||||
|
单聊未订阅脏标记示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"topic": "usr_target",
|
||||||
|
"from": "usr_actor",
|
||||||
|
"what": "im",
|
||||||
|
"event": "message.mutation",
|
||||||
|
"payload": {
|
||||||
|
"topic": "p2pJaOWxMvALjB4i1K_h_NZgQ",
|
||||||
|
"seq": 12,
|
||||||
|
"version": 2,
|
||||||
|
"event": "message.recalled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
收到 `message.mutation` 后不要直接改本地消息内容,先把 `payload.topic` 标记为脏,再调用:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion}&limit=50
|
||||||
|
```
|
||||||
|
|
||||||
会话列表同步时,`me.meta.sub[].private` 会带:
|
会话列表同步时,`me.meta.sub[].private` 会带:
|
||||||
|
|
||||||
|
|||||||
@@ -4234,6 +4234,32 @@ func serveIMMessageMutations(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func enqueueIMP2PMessageMutationEvents(tenantID types.TenantID, topic string, actor types.Uid, seq int64, version int, mutationEvent string) error {
|
||||||
|
if tenantID.IsZero() || topic == "" || actor.IsZero() || seq <= 0 || version <= 0 || mutationEvent == "" {
|
||||||
|
return types.ErrMalformed
|
||||||
|
}
|
||||||
|
uid1, uid2, err := types.ParseP2P(topic)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
payload := map[string]any{
|
||||||
|
"topic": topic,
|
||||||
|
"seq": seq,
|
||||||
|
"version": version,
|
||||||
|
"event": mutationEvent,
|
||||||
|
}
|
||||||
|
for _, uid := range []types.Uid{uid1, uid2} {
|
||||||
|
if uid.IsZero() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
eventID := stableIMEventID("message-mutation", topic, seq, version, mutationEvent, uid.UserId())
|
||||||
|
if err := enqueueIMBusinessEvent(tenantID, eventID, "user", uid.UserId(), actor, "message.mutation", payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func serveIMMessageContext(wrt http.ResponseWriter, req *http.Request) {
|
func serveIMMessageContext(wrt http.ResponseWriter, req *http.Request) {
|
||||||
if req.Method != http.MethodGet {
|
if req.Method != http.MethodGet {
|
||||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||||
@@ -4411,6 +4437,10 @@ func serveIMMessageRecall(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err = enqueueIMP2PMessageMutationEvents(principal.TenantID, topic, principal.Uid, seq, updated.Version, "message.recalled"); err != nil {
|
||||||
|
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
writeIMSuccess(wrt, req, messageMutationPayload(updated, "recalled"))
|
writeIMSuccess(wrt, req, messageMutationPayload(updated, "recalled"))
|
||||||
}
|
}
|
||||||
@@ -4473,6 +4503,8 @@ func serveIMMessageBatchRecall(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
if err = enqueueIMBusinessEvent(principal.TenantID, eventID, "topic", topic, principal.Uid,
|
if err = enqueueIMBusinessEvent(principal.TenantID, eventID, "topic", topic, principal.Uid,
|
||||||
"message.recalled", map[string]any{"topic": topic, "seq": item.Seq, "version": updated.Version}); err != nil {
|
"message.recalled", map[string]any{"topic": topic, "seq": item.Seq, "version": updated.Version}); err != nil {
|
||||||
result["event_status"] = "enqueue_failed"
|
result["event_status"] = "enqueue_failed"
|
||||||
|
} else if err = enqueueIMP2PMessageMutationEvents(principal.TenantID, topic, principal.Uid, item.Seq, updated.Version, "message.recalled"); err != nil {
|
||||||
|
result["event_status"] = "enqueue_failed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items = append(items, result)
|
items = append(items, result)
|
||||||
@@ -4541,6 +4573,10 @@ func serveIMMessageRevision(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err = enqueueIMP2PMessageMutationEvents(principal.TenantID, topic, principal.Uid, seq, updated.Version, "message.edited"); err != nil {
|
||||||
|
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
writeIMSuccess(wrt, req, messageMutationPayload(updated, "edited"))
|
writeIMSuccess(wrt, req, messageMutationPayload(updated, "edited"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7966,6 +7966,72 @@ func TestServeIMMessageRecallNormalizesP2PUserTopic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnqueueIMP2PMessageMutationEventsDeliversUserDirtyMarker(t *testing.T) {
|
||||||
|
oldStore := globals.sessionStore
|
||||||
|
defer func() { globals.sessionStore = oldStore }()
|
||||||
|
|
||||||
|
tenantID := types.TenantID(7)
|
||||||
|
actor := types.Uid(42)
|
||||||
|
peer := types.Uid(43)
|
||||||
|
topic := actor.P2PName(peer)
|
||||||
|
globals.sessionStore = realtimeTestSessionStore(
|
||||||
|
realtimeTestSession("actor-session", tenantID, actor, "me"),
|
||||||
|
realtimeTestSession("peer-session", tenantID, peer, "me"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := enqueueIMP2PMessageMutationEvents(tenantID, topic, actor, 9, 2, "message.recalled"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
sid string
|
||||||
|
uid types.Uid
|
||||||
|
}{
|
||||||
|
{"actor-session", actor},
|
||||||
|
{"peer-session", peer},
|
||||||
|
} {
|
||||||
|
sess := globals.sessionStore.Get(tc.sid)
|
||||||
|
select {
|
||||||
|
case raw := <-sess.send:
|
||||||
|
msg, ok := raw.(*ServerComMessage)
|
||||||
|
if !ok || msg.Info == nil {
|
||||||
|
t.Fatalf("unexpected outbound for %s: %#v", tc.sid, raw)
|
||||||
|
}
|
||||||
|
if msg.Info.Topic != tc.uid.UserId() || msg.Info.Event != "message.mutation" || msg.Info.What != "im" {
|
||||||
|
t.Fatalf("unexpected mutation marker for %s: %#v", tc.sid, msg.Info)
|
||||||
|
}
|
||||||
|
var payload map[string]any
|
||||||
|
if err := json.Unmarshal(msg.Info.Payload, &payload); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if payload["topic"] != topic || payload["event"] != "message.recalled" ||
|
||||||
|
payload["seq"].(float64) != 9 || payload["version"].(float64) != 2 {
|
||||||
|
t.Fatalf("unexpected mutation payload for %s: %#v", tc.sid, payload)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("expected mutation marker for %s", tc.sid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnqueueIMP2PMessageMutationEventsIgnoresGroupTopic(t *testing.T) {
|
||||||
|
oldStore := globals.sessionStore
|
||||||
|
defer func() { globals.sessionStore = oldStore }()
|
||||||
|
|
||||||
|
tenantID := types.TenantID(7)
|
||||||
|
actor := types.Uid(42)
|
||||||
|
globals.sessionStore = realtimeTestSessionStore(
|
||||||
|
realtimeTestSession("actor-session", tenantID, actor, "me"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := enqueueIMP2PMessageMutationEvents(tenantID, "grpRecall", actor, 9, 2, "message.recalled"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(globals.sessionStore.Get("actor-session").send) != 0 {
|
||||||
|
t.Fatal("group topic should not emit per-user mutation markers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestServeIMMessageRevision(t *testing.T) {
|
func TestServeIMMessageRevision(t *testing.T) {
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|||||||
Reference in New Issue
Block a user