Add sender summaries to forward bundles
This commit is contained in:
@@ -1354,6 +1354,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
|
||||
**状态:新增。**
|
||||
|
||||
- 调用 4.19 接口,`mode:"bundle"`;服务端生成一条 `forward_bundle` 目标消息,内容含 `bundle_id,title,item_count,preview[],items[],source_refs[]`。首版 Bundle 明细以内联快照随消息下发,后续如消息量增大再扩展 `GET /v1/im/forward-bundles/{bundle_id}?cursor=` 分页详情。
|
||||
- `items[]` 和 `preview[]` 保留 `from` 原发送人 ID,并新增 `from_user` 用户摘要:`{"user_id":"usr...","display_name":"张三","avatar_file_id":"file_avatar"}`。头像为空时可缺省;若资料查询失败,服务端至少返回 `user_id/display_name` 兜底。
|
||||
- 快照保留显示名、时间和内容摘要,对撤回/敏感字段按策略脱敏;不得允许接收端利用 Bundle 获取原会话额外权限。
|
||||
- 多目标共享不可变 Bundle 内容但分别生成目标消息与授权记录。
|
||||
|
||||
|
||||
+77
-9
@@ -9380,10 +9380,10 @@ func buildForwardBundle(principal *imPrincipal, sources []struct {
|
||||
Topic string `json:"topic"`
|
||||
Seq imForwardSeq `json:"seq"`
|
||||
}, target string, sourceReadable map[string]error, comment string) (map[string]any, []map[string]any) {
|
||||
items := make([]map[string]any, 0, len(sources))
|
||||
preview := make([]map[string]any, 0, min(len(sources), 3))
|
||||
sourceRefs := make([]map[string]any, 0, len(sources))
|
||||
failures := make([]map[string]any, 0)
|
||||
sourceMessages := make([]*types.Message, 0, len(sources))
|
||||
sourceTopics := make([]string, 0, len(sources))
|
||||
sourceSeqs := make([]int64, 0, len(sources))
|
||||
for _, source := range sources {
|
||||
sourceTopic, sourceOK := normalizeIMForwardTopic(principal.Uid, source.Topic)
|
||||
if sourceTopic == "" || int64(source.Seq) <= 0 || !sourceOK {
|
||||
@@ -9412,22 +9412,38 @@ func buildForwardBundle(principal *imPrincipal, sources []struct {
|
||||
failures = append(failures, forwardFailurePayload(sourceTopic, int64(source.Seq), target, "MESSAGE_UNAVAILABLE"))
|
||||
continue
|
||||
}
|
||||
sourceMessages = append(sourceMessages, msg)
|
||||
sourceTopics = append(sourceTopics, sourceTopic)
|
||||
sourceSeqs = append(sourceSeqs, int64(source.Seq))
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return nil, failures
|
||||
}
|
||||
userSummaries := forwardMessageUserSummaries(principal.TenantID, sourceMessages)
|
||||
items := make([]map[string]any, 0, len(sourceMessages))
|
||||
preview := make([]map[string]any, 0, min(len(sourceMessages), 3))
|
||||
sourceRefs := make([]map[string]any, 0, len(sourceMessages))
|
||||
for i, msg := range sourceMessages {
|
||||
snapshot := favoriteSnapshot(msg)
|
||||
if fromUser := forwardMessageUserSummary(msg.From, userSummaries); fromUser != nil {
|
||||
snapshot["from_user"] = fromUser
|
||||
}
|
||||
items = append(items, snapshot)
|
||||
ref := map[string]any{"topic": sourceTopic, "seq": int64(source.Seq)}
|
||||
ref := map[string]any{"topic": sourceTopics[i], "seq": sourceSeqs[i]}
|
||||
sourceRefs = append(sourceRefs, ref)
|
||||
if len(preview) < 3 {
|
||||
preview = append(preview, map[string]any{
|
||||
previewItem := map[string]any{
|
||||
"from": msg.From,
|
||||
"type": imMessageType(msg),
|
||||
"summary": textSnippet(messageText(*msg), ""),
|
||||
"ts": msg.CreatedAt.UTC().Format(time.RFC3339Nano),
|
||||
})
|
||||
}
|
||||
if fromUser := forwardMessageUserSummary(msg.From, userSummaries); fromUser != nil {
|
||||
previewItem["from_user"] = fromUser
|
||||
}
|
||||
preview = append(preview, previewItem)
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return nil, failures
|
||||
}
|
||||
bundleID := "fbundle_" + store.Store.GetUidString()
|
||||
return map[string]any{
|
||||
"bundle_id": bundleID,
|
||||
@@ -9441,6 +9457,58 @@ func buildForwardBundle(principal *imPrincipal, sources []struct {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func forwardMessageUserSummaries(tenantID types.TenantID, messages []*types.Message) map[types.Uid]map[string]any {
|
||||
if store.Users == nil || len(messages) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[types.Uid]bool)
|
||||
uids := make([]types.Uid, 0, len(messages))
|
||||
for _, msg := range messages {
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
uid := types.ParseUid(msg.From)
|
||||
if uid.IsZero() || seen[uid] {
|
||||
continue
|
||||
}
|
||||
seen[uid] = true
|
||||
uids = append(uids, uid)
|
||||
}
|
||||
if len(uids) == 0 {
|
||||
return nil
|
||||
}
|
||||
users, err := store.Users.GetAll(tenantID, uids...)
|
||||
if err != nil {
|
||||
logs.Warn.Printf("im http: failed to load forward user summaries: %v", err)
|
||||
return nil
|
||||
}
|
||||
summaries := make(map[types.Uid]map[string]any, len(users))
|
||||
for i := range users {
|
||||
uid := users[i].Uid()
|
||||
if uid.IsZero() {
|
||||
uid = types.ParseUid(users[i].Id)
|
||||
}
|
||||
if !uid.IsZero() {
|
||||
summaries[uid] = userSummaryPayload(&users[i], "")
|
||||
}
|
||||
}
|
||||
return summaries
|
||||
}
|
||||
|
||||
func forwardMessageUserSummary(from string, summaries map[types.Uid]map[string]any) map[string]any {
|
||||
uid := types.ParseUid(from)
|
||||
if uid.IsZero() {
|
||||
return nil
|
||||
}
|
||||
if summary, ok := summaries[uid]; ok && summary != nil {
|
||||
return summary
|
||||
}
|
||||
return map[string]any{
|
||||
"user_id": uid.UserId(),
|
||||
"display_name": uid.UserId(),
|
||||
}
|
||||
}
|
||||
|
||||
func prepareHTTPForwardMessage(tenantID types.TenantID, msg *types.Message) error {
|
||||
if msg == nil || msg.TenantID != tenantID || strings.TrimSpace(msg.Topic) == "" {
|
||||
return types.ErrMalformed
|
||||
|
||||
@@ -5362,13 +5362,17 @@ func TestServeIMMessageForwardBundleSavesForwardBundle(t *testing.T) {
|
||||
|
||||
oldSubs := store.Subs
|
||||
oldMessages := store.Messages
|
||||
oldUsers := store.Users
|
||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||
messages := mock_store.NewMockMessagesPersistenceInterface(ctrl)
|
||||
users := mock_store.NewMockUsersPersistenceInterface(ctrl)
|
||||
store.Subs = subs
|
||||
store.Messages = messages
|
||||
store.Users = users
|
||||
t.Cleanup(func() {
|
||||
store.Subs = oldSubs
|
||||
store.Messages = oldMessages
|
||||
store.Users = oldUsers
|
||||
})
|
||||
|
||||
subs.EXPECT().Get(types.TenantID(7), "grpTarget", types.Uid(42), false).Return(&types.Subscription{
|
||||
@@ -5396,6 +5400,11 @@ func TestServeIMMessageForwardBundleSavesForwardBundle(t *testing.T) {
|
||||
CreatedAt: time.Date(2026, 8, 13, 9, 0, 0, 0, time.UTC),
|
||||
},
|
||||
}, nil)
|
||||
users.EXPECT().GetAll(types.TenantID(7), types.Uid(99)).Return([]types.User{{
|
||||
ObjHeader: types.ObjHeader{Id: types.Uid(99).String()},
|
||||
TenantID: 7,
|
||||
Public: types.KVMap{"display_name": "后端-potter", "avatar_file_id": "avatar-99"},
|
||||
}}, nil)
|
||||
messages.EXPECT().Save(types.TenantID(7), gomock.Any(), gomock.Nil(), true).DoAndReturn(
|
||||
func(_ types.TenantID, msg *types.Message, _ []string, _ bool) (*types.MessageSaveResult, error) {
|
||||
if msg.Topic != "grpTarget" || msg.Head["x-im-type"] != "forward_bundle" {
|
||||
@@ -5405,6 +5414,16 @@ func TestServeIMMessageForwardBundleSavesForwardBundle(t *testing.T) {
|
||||
if content["bundle_id"] != "fbundle_bundle-1" || content["item_count"] != 1 {
|
||||
t.Fatalf("unexpected bundle content: %#v", content)
|
||||
}
|
||||
bundleItems := content["items"].([]map[string]any)
|
||||
itemUser := bundleItems[0]["from_user"].(map[string]any)
|
||||
if itemUser["user_id"] != types.Uid(99).UserId() || itemUser["display_name"] != "后端-potter" || itemUser["avatar_file_id"] != "avatar-99" {
|
||||
t.Fatalf("unexpected item sender summary: %#v", itemUser)
|
||||
}
|
||||
previewItems := content["preview"].([]map[string]any)
|
||||
previewUser := previewItems[0]["from_user"].(map[string]any)
|
||||
if previewUser["user_id"] != types.Uid(99).UserId() || previewUser["display_name"] != "后端-potter" || previewUser["avatar_file_id"] != "avatar-99" {
|
||||
t.Fatalf("unexpected preview sender summary: %#v", previewUser)
|
||||
}
|
||||
return &types.MessageSaveResult{SeqId: 23, MarkedReadBySender: true}, nil
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user