fix im forwarding and message list regressions
This commit is contained in:
+4
-4
@@ -1358,7 +1358,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
|
|||||||
**状态:新增。**
|
**状态:新增。**
|
||||||
|
|
||||||
- `POST /v1/im/messages/forward`,Body `{"mode":"separate","sources":[{"topic":"p2p...","seq":12}],"target_topics":["grp..."],"comment":"..."}`。`seq` 兼容数字和字符串;P2P 源会话也可以传对方 `usr...` 用户 ID,服务端会归一化为 P2P topic。
|
- `POST /v1/im/messages/forward`,Body `{"mode":"separate","sources":[{"topic":"p2p...","seq":12}],"target_topics":["grp..."],"comment":"..."}`。`seq` 兼容数字和字符串;P2P 源会话也可以传对方 `usr...` 用户 ID,服务端会归一化为 P2P topic。
|
||||||
- 返回每个目标和源消息的 `client_mid/seq/status`;服务端保持源顺序,重新生成目标消息,不能复用原 `seq`。
|
- 返回每个目标和源消息的 `client_mid/seq/status`;`status` 取值为 `sent`、`pending`、`failed`。`pending` 表示服务端已分配 `seq/client_mid`,但 Kafka 投递结果暂不确定,客户端应按“发送中”展示,保留同一个 `Idempotency-Key` 重试或等待后续消息同步,不要立即生成第二条本地消息。服务端保持源顺序,重新生成目标消息,不能复用原 `seq`。
|
||||||
- 附件必须建立新的授权引用;原消息已撤回、过期或不可见时该项失败。留言作为每个目标的一条独立文本消息发送。
|
- 附件必须建立新的授权引用;原消息已撤回、过期或不可见时该项失败。留言作为每个目标的一条独立文本消息发送。
|
||||||
|
|
||||||
### 4.20 转发—合并转发—多选状态.png
|
### 4.20 转发—合并转发—多选状态.png
|
||||||
@@ -2084,10 +2084,10 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
|
|||||||
| `GET /v1/im/favorites/{favorite_id}` | Path favorite ID | `{"favorite_id":"fav_1","type":"file","snapshot":{"file_id":"file_1","name":"a.pdf"},"source":{"topic":"grp...","seq":12},"availability":"available"}` |
|
| `GET /v1/im/favorites/{favorite_id}` | Path favorite ID | `{"favorite_id":"fav_1","type":"file","snapshot":{"file_id":"file_1","name":"a.pdf"},"source":{"topic":"grp...","seq":12},"availability":"available"}` |
|
||||||
| `DELETE /v1/im/favorites/{favorite_id}` | Path favorite ID | `{"favorite_id":"fav_1","deleted":true}` |
|
| `DELETE /v1/im/favorites/{favorite_id}` | Path favorite ID | `{"favorite_id":"fav_1","deleted":true}` |
|
||||||
| `POST /v1/im/favorites/batch-delete` | `{"favorite_ids":["fav_1","fav_2"]}` | `{"items":[{"favorite_id":"fav_1","deleted":true},{"favorite_id":"fav_2","deleted":true}],"failed":[]}` |
|
| `POST /v1/im/favorites/batch-delete` | `{"favorite_ids":["fav_1","fav_2"]}` | `{"items":[{"favorite_id":"fav_1","deleted":true},{"favorite_id":"fav_2","deleted":true}],"failed":[]}` |
|
||||||
| `POST /v1/im/favorites/forward` | `{"favorite_ids":["fav_1"],"target_topics":["grp..."],"comment":"转给你"}` | `{"items":[{"favorite_id":"fav_1","target_topic":"grp...","status":"sent","seq":20}],"failed":[]}` |
|
| `POST /v1/im/favorites/forward` | `{"favorite_ids":["fav_1"],"target_topics":["grp..."],"comment":"转给你"}` | `{"items":[{"favorite_id":"fav_1","target_topic":"grp...","status":"sent","seq":20}],"failed":[]}`;Kafka 投递结果不确定时返回 `status:"pending"`、`reason:"MESSAGE_PERSIST_PENDING"` |
|
||||||
| `GET /v1/im/forward-targets?query=&cursor=` | Query only | `{"items":[{"topic":"grp...","type":"group","display_name":"项目群","avatar_file_id":"","can_receive_forward":true},{"topic":"slf","type":"file_assistant","display_name":"文件助手","can_receive_forward":true}],"next_cursor":"","has_more":false}` |
|
| `GET /v1/im/forward-targets?query=&cursor=` | Query only | `{"items":[{"topic":"grp...","type":"group","display_name":"项目群","avatar_file_id":"","can_receive_forward":true},{"topic":"slf","type":"file_assistant","display_name":"文件助手","can_receive_forward":true}],"next_cursor":"","has_more":false}` |
|
||||||
| `POST /v1/im/messages/forward` 逐条转发 | `{"mode":"separate","sources":[{"topic":"grp...","seq":12}],"target_topics":["usr..."],"comment":""}` | `{"items":[{"source":{"topic":"grp...","seq":12},"target_topic":"p2p...","client_mid":"mid_1","seq":30,"status":"sent"}],"failed":[]}` |
|
| `POST /v1/im/messages/forward` 逐条转发 | `{"mode":"separate","sources":[{"topic":"grp...","seq":12}],"target_topics":["usr..."],"comment":""}` | `{"items":[{"source":{"topic":"grp...","seq":12},"target_topic":"p2p...","client_mid":"mid_1","seq":30,"status":"sent"}],"failed":[]}`;Kafka 投递结果不确定时返回 `status:"pending"`、`reason:"MESSAGE_PERSIST_PENDING"` |
|
||||||
| `POST /v1/im/messages/forward` 合并转发 | `{"mode":"bundle","sources":[{"topic":"grp...","seq":12},{"topic":"grp...","seq":13}],"target_topics":["usr..."],"comment":""}` | `{"items":[{"sources":[{"topic":"grp...","seq":12},{"topic":"grp...","seq":13}],"target_topic":"p2p...","client_mid":"mid_1","seq":31,"status":"sent","mode":"bundle","bundle_id":"fbundle_1"}],"failed":[]}` |
|
| `POST /v1/im/messages/forward` 合并转发 | `{"mode":"bundle","sources":[{"topic":"grp...","seq":12},{"topic":"grp...","seq":13}],"target_topics":["usr..."],"comment":""}` | `{"items":[{"sources":[{"topic":"grp...","seq":12},{"topic":"grp...","seq":13}],"target_topic":"p2p...","client_mid":"mid_1","seq":31,"status":"sent","mode":"bundle","bundle_id":"fbundle_1"}],"failed":[]}`;Kafka 投递结果不确定时返回 `status:"pending"`、`reason:"MESSAGE_PERSIST_PENDING"` |
|
||||||
| `GET /v1/im/forward-bundles/{bundle_id}?cursor=` 待实现 | Query only | `{"bundle_id":"fbundle_1","title":"聊天记录","item_count":2,"items":[{"from_display":"张三","ts":"2026-08-20T09:00:00Z","summary":"文本摘要"}],"next_cursor":""}` |
|
| `GET /v1/im/forward-bundles/{bundle_id}?cursor=` 待实现 | Query only | `{"bundle_id":"fbundle_1","title":"聊天记录","item_count":2,"items":[{"from_display":"张三","ts":"2026-08-20T09:00:00Z","summary":"文本摘要"}],"next_cursor":""}` |
|
||||||
| `POST /v1/im/security/urls/check` | `{"url":"https://example.com","source":"favorite"}` | `{"url":"https://example.com","decision":"allow","risk_level":"low","reason":"ok","source":"favorite"}` |
|
| `POST /v1/im/security/urls/check` | `{"url":"https://example.com","source":"favorite"}` | `{"url":"https://example.com","decision":"allow","risk_level":"low","reason":"ok","source":"favorite"}` |
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,47 @@ func TestMessageSearchDoesNotClearScannedHead(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSearchPaginationCursorUsesLastReturnedRow(t *testing.T) {
|
||||||
|
source, err := os.ReadFile("tenant_adapter.go")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
body := string(source)
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
function string
|
||||||
|
forbidden string
|
||||||
|
required string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "p2p message search",
|
||||||
|
function: "func (a *adapter) TenantMessageSearch",
|
||||||
|
forbidden: "nextCursor = strconv.FormatInt(msg.SeqId, 10)",
|
||||||
|
required: "nextCursor = strconv.FormatInt(lastSeq, 10)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "group message search",
|
||||||
|
function: "func (a *adapter) tenantGroupMessageSearch",
|
||||||
|
forbidden: "nextCursor = strconv.FormatInt(msg.SeqId, 10)",
|
||||||
|
required: "nextCursor = strconv.FormatInt(lastSeq, 10)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "favorite list",
|
||||||
|
function: "func (a *adapter) TenantFavoriteList",
|
||||||
|
forbidden: "nextCursor = strconv.FormatInt(item.ID, 10)",
|
||||||
|
required: "nextCursor = strconv.FormatInt(lastID, 10)",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
fn := functionSource(t, body, tc.function)
|
||||||
|
if strings.Contains(fn, tc.forbidden) {
|
||||||
|
t.Fatalf("%s must not use the extra fetched row as next cursor", tc.name)
|
||||||
|
}
|
||||||
|
if !strings.Contains(fn, tc.required) {
|
||||||
|
t.Fatalf("%s must use the last returned row as next cursor", tc.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTenantFileAccessIncludesAvatarAudienceRules(t *testing.T) {
|
func TestTenantFileAccessIncludesAvatarAudienceRules(t *testing.T) {
|
||||||
source, err := os.ReadFile("tenant_adapter.go")
|
source, err := os.ReadFile("tenant_adapter.go")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -72,3 +113,16 @@ func TestTenantFileAccessIncludesAvatarAudienceRules(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func functionSource(t *testing.T, source, marker string) string {
|
||||||
|
t.Helper()
|
||||||
|
start := strings.Index(source, marker)
|
||||||
|
if start < 0 {
|
||||||
|
t.Fatalf("function %q not found", marker)
|
||||||
|
}
|
||||||
|
next := strings.Index(source[start+len(marker):], "\nfunc ")
|
||||||
|
if next < 0 {
|
||||||
|
return source[start:]
|
||||||
|
}
|
||||||
|
return source[start : start+len(marker)+next]
|
||||||
|
}
|
||||||
|
|||||||
@@ -2486,6 +2486,7 @@ func (a *adapter) TenantMessageSearch(id t.TenantID, topic string, uid t.Uid, qu
|
|||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
msgs := make([]t.Message, 0, limit)
|
msgs := make([]t.Message, 0, limit)
|
||||||
nextCursor := ""
|
nextCursor := ""
|
||||||
|
lastSeq := int64(0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
row := struct {
|
row := struct {
|
||||||
t.Message
|
t.Message
|
||||||
@@ -2496,10 +2497,11 @@ func (a *adapter) TenantMessageSearch(id t.TenantID, topic string, uid t.Uid, qu
|
|||||||
}
|
}
|
||||||
msg := finalizeTenantMessage(id, row.Message, row.RecalledBy)
|
msg := finalizeTenantMessage(id, row.Message, row.RecalledBy)
|
||||||
if len(msgs) == limit {
|
if len(msgs) == limit {
|
||||||
nextCursor = strconv.FormatInt(msg.SeqId, 10)
|
nextCursor = strconv.FormatInt(lastSeq, 10)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
msgs = append(msgs, msg)
|
msgs = append(msgs, msg)
|
||||||
|
lastSeq = msg.SeqId
|
||||||
}
|
}
|
||||||
if err = rows.Err(); err != nil {
|
if err = rows.Err(); err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
@@ -2546,6 +2548,7 @@ func (a *adapter) tenantGroupMessageSearch(id t.TenantID, topic string, uid t.Ui
|
|||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
msgs := make([]t.Message, 0, limit)
|
msgs := make([]t.Message, 0, limit)
|
||||||
nextCursor := ""
|
nextCursor := ""
|
||||||
|
lastSeq := int64(0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
row := struct {
|
row := struct {
|
||||||
t.Message
|
t.Message
|
||||||
@@ -2556,10 +2559,11 @@ func (a *adapter) tenantGroupMessageSearch(id t.TenantID, topic string, uid t.Ui
|
|||||||
}
|
}
|
||||||
msg := normalizeGroupMessage(id, row.Message, row.RecalledBy)
|
msg := normalizeGroupMessage(id, row.Message, row.RecalledBy)
|
||||||
if len(msgs) == limit {
|
if len(msgs) == limit {
|
||||||
nextCursor = strconv.FormatInt(msg.SeqId, 10)
|
nextCursor = strconv.FormatInt(lastSeq, 10)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
msgs = append(msgs, msg)
|
msgs = append(msgs, msg)
|
||||||
|
lastSeq = msg.SeqId
|
||||||
}
|
}
|
||||||
if err = rows.Err(); err != nil {
|
if err = rows.Err(); err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
@@ -7230,6 +7234,7 @@ func (a *adapter) TenantFavoriteList(id t.TenantID, uid t.Uid, itemType, queryTe
|
|||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
items := make([]t.FavoriteItem, 0, limit)
|
items := make([]t.FavoriteItem, 0, limit)
|
||||||
nextCursor := ""
|
nextCursor := ""
|
||||||
|
lastID := int64(0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
item, err := scanFavorite(rows)
|
item, err := scanFavorite(rows)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -7237,10 +7242,11 @@ func (a *adapter) TenantFavoriteList(id t.TenantID, uid t.Uid, itemType, queryTe
|
|||||||
}
|
}
|
||||||
item.TenantID = id
|
item.TenantID = id
|
||||||
if len(items) == limit {
|
if len(items) == limit {
|
||||||
nextCursor = strconv.FormatInt(item.ID, 10)
|
nextCursor = strconv.FormatInt(lastID, 10)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
|
lastID = item.ID
|
||||||
}
|
}
|
||||||
if err = rows.Err(); err != nil {
|
if err = rows.Err(); err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
|
|||||||
+95
-9
@@ -3745,9 +3745,17 @@ func serveIMFavoriteForward(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
saved, err := saveMessageMaybeKafka(req.Context(), principal.TenantID, msg, nil, true)
|
saved, err := saveMessageMaybeKafka(req.Context(), principal.TenantID, msg, nil, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if saved != nil && saved.Pending {
|
||||||
|
results = append(results, map[string]any{"target_topic": target, "favorite_id": favoriteID, "status": "pending", "seq": saved.SeqId, "reason": "MESSAGE_PERSIST_PENDING"})
|
||||||
|
continue
|
||||||
|
}
|
||||||
results = append(results, map[string]any{"target_topic": target, "favorite_id": favoriteID, "status": "failed", "reason": relationErrorReason(err)})
|
results = append(results, map[string]any{"target_topic": target, "favorite_id": favoriteID, "status": "failed", "reason": relationErrorReason(err)})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if saved != nil && saved.Pending {
|
||||||
|
results = append(results, map[string]any{"target_topic": target, "favorite_id": favoriteID, "status": "pending", "seq": saved.SeqId, "reason": "MESSAGE_PERSIST_PENDING"})
|
||||||
|
continue
|
||||||
|
}
|
||||||
results = append(results, map[string]any{"target_topic": target, "favorite_id": favoriteID, "status": "sent", "seq": saved.SeqId})
|
results = append(results, map[string]any{"target_topic": target, "favorite_id": favoriteID, "status": "sent", "seq": saved.SeqId})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3814,7 +3822,7 @@ func serveIMMessageForward(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
failed = append(failed, sourceFailures...)
|
failed = append(failed, sourceFailures...)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
clientMid := "fwd_" + store.Store.GetUidString()
|
clientMid := forwardClientMid(req.Header.Get("Idempotency-Key"), "bundle", target, len(body.Sources), body.Comment)
|
||||||
forwarded := &types.Message{
|
forwarded := &types.Message{
|
||||||
TenantID: principal.TenantID,
|
TenantID: principal.TenantID,
|
||||||
Topic: target,
|
Topic: target,
|
||||||
@@ -3828,9 +3836,36 @@ func serveIMMessageForward(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
saved, err := saveMessageMaybeKafka(req.Context(), principal.TenantID, forwarded, nil, true)
|
saved, err := saveMessageMaybeKafka(req.Context(), principal.TenantID, forwarded, nil, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if saved != nil && saved.Pending {
|
||||||
|
forwarded.SeqId = saved.SeqId
|
||||||
|
items = append(items, map[string]any{
|
||||||
|
"sources": bundle["source_refs"],
|
||||||
|
"target_topic": target,
|
||||||
|
"client_mid": clientMid,
|
||||||
|
"seq": saved.SeqId,
|
||||||
|
"status": "pending",
|
||||||
|
"reason": "MESSAGE_PERSIST_PENDING",
|
||||||
|
"mode": "bundle",
|
||||||
|
"bundle_id": bundle["bundle_id"],
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
failed = append(failed, forwardFailurePayload("", 0, target, relationErrorReason(err)))
|
failed = append(failed, forwardFailurePayload("", 0, target, relationErrorReason(err)))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if saved != nil && saved.Pending {
|
||||||
|
items = append(items, map[string]any{
|
||||||
|
"sources": bundle["source_refs"],
|
||||||
|
"target_topic": target,
|
||||||
|
"client_mid": clientMid,
|
||||||
|
"seq": saved.SeqId,
|
||||||
|
"status": "pending",
|
||||||
|
"reason": "MESSAGE_PERSIST_PENDING",
|
||||||
|
"mode": "bundle",
|
||||||
|
"bundle_id": bundle["bundle_id"],
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
forwarded.SeqId = saved.SeqId
|
forwarded.SeqId = saved.SeqId
|
||||||
broadcastHTTPForwardMessage(principal.TenantID, forwarded)
|
broadcastHTTPForwardMessage(principal.TenantID, forwarded)
|
||||||
items = append(items, map[string]any{
|
items = append(items, map[string]any{
|
||||||
@@ -3872,7 +3907,7 @@ func serveIMMessageForward(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
failed = append(failed, forwardFailurePayload(sourceTopic, int64(source.Seq), target, "MESSAGE_UNAVAILABLE"))
|
failed = append(failed, forwardFailurePayload(sourceTopic, int64(source.Seq), target, "MESSAGE_UNAVAILABLE"))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
clientMid := "fwd_" + store.Store.GetUidString()
|
clientMid := forwardClientMid(req.Header.Get("Idempotency-Key"), "separate", target, sourceTopic, int64(source.Seq))
|
||||||
forwarded := &types.Message{
|
forwarded := &types.Message{
|
||||||
TenantID: principal.TenantID,
|
TenantID: principal.TenantID,
|
||||||
Topic: target,
|
Topic: target,
|
||||||
@@ -3886,9 +3921,32 @@ func serveIMMessageForward(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
saved, err := saveMessageMaybeKafka(req.Context(), principal.TenantID, forwarded, nil, true)
|
saved, err := saveMessageMaybeKafka(req.Context(), principal.TenantID, forwarded, nil, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if saved != nil && saved.Pending {
|
||||||
|
forwarded.SeqId = saved.SeqId
|
||||||
|
items = append(items, map[string]any{
|
||||||
|
"source": map[string]any{"topic": sourceTopic, "seq": int64(source.Seq)},
|
||||||
|
"target_topic": target,
|
||||||
|
"client_mid": clientMid,
|
||||||
|
"seq": saved.SeqId,
|
||||||
|
"status": "pending",
|
||||||
|
"reason": "MESSAGE_PERSIST_PENDING",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
failed = append(failed, forwardFailurePayload(sourceTopic, int64(source.Seq), target, relationErrorReason(err)))
|
failed = append(failed, forwardFailurePayload(sourceTopic, int64(source.Seq), target, relationErrorReason(err)))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if saved != nil && saved.Pending {
|
||||||
|
items = append(items, map[string]any{
|
||||||
|
"source": map[string]any{"topic": sourceTopic, "seq": int64(source.Seq)},
|
||||||
|
"target_topic": target,
|
||||||
|
"client_mid": clientMid,
|
||||||
|
"seq": saved.SeqId,
|
||||||
|
"status": "pending",
|
||||||
|
"reason": "MESSAGE_PERSIST_PENDING",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
forwarded.SeqId = saved.SeqId
|
forwarded.SeqId = saved.SeqId
|
||||||
broadcastHTTPForwardMessage(principal.TenantID, forwarded)
|
broadcastHTTPForwardMessage(principal.TenantID, forwarded)
|
||||||
items = append(items, map[string]any{
|
items = append(items, map[string]any{
|
||||||
@@ -8042,7 +8100,7 @@ func messageSearchPayload(msg types.Message, query string) map[string]any {
|
|||||||
snippet := textSnippet(text, query)
|
snippet := textSnippet(text, query)
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"seq": msg.SeqId,
|
"seq": msg.SeqId,
|
||||||
"from": msg.From,
|
"from": messageFromUserID(msg.From),
|
||||||
"ts": msg.CreatedAt.UTC().Format(time.RFC3339Nano),
|
"ts": msg.CreatedAt.UTC().Format(time.RFC3339Nano),
|
||||||
"type": imMessageType(&msg),
|
"type": imMessageType(&msg),
|
||||||
"version": msg.Version,
|
"version": msg.Version,
|
||||||
@@ -8056,7 +8114,7 @@ func messageSearchPayload(msg types.Message, query string) map[string]any {
|
|||||||
func messageContextPayload(msg types.Message) map[string]any {
|
func messageContextPayload(msg types.Message) map[string]any {
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"seq": msg.SeqId,
|
"seq": msg.SeqId,
|
||||||
"from": msg.From,
|
"from": messageFromUserID(msg.From),
|
||||||
"ts": msg.CreatedAt.UTC().Format(time.RFC3339Nano),
|
"ts": msg.CreatedAt.UTC().Format(time.RFC3339Nano),
|
||||||
"type": imMessageType(&msg),
|
"type": imMessageType(&msg),
|
||||||
"version": msg.Version,
|
"version": msg.Version,
|
||||||
@@ -8070,7 +8128,7 @@ func mediaMessagePayload(msg types.Message) map[string]any {
|
|||||||
snapshot := favoriteSnapshot(&msg)
|
snapshot := favoriteSnapshot(&msg)
|
||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
"seq": msg.SeqId,
|
"seq": msg.SeqId,
|
||||||
"from": msg.From,
|
"from": messageFromUserID(msg.From),
|
||||||
"ts": msg.CreatedAt.UTC().Format(time.RFC3339Nano),
|
"ts": msg.CreatedAt.UTC().Format(time.RFC3339Nano),
|
||||||
"availability": messageAvailability(msg),
|
"availability": messageAvailability(msg),
|
||||||
}
|
}
|
||||||
@@ -8082,6 +8140,23 @@ func mediaMessagePayload(msg types.Message) map[string]any {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func messageFromUserID(from string) string {
|
||||||
|
from = strings.TrimSpace(from)
|
||||||
|
if from == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(from, "usr") {
|
||||||
|
if uid := types.ParseUserId(from); !uid.IsZero() {
|
||||||
|
return uid.UserId()
|
||||||
|
}
|
||||||
|
return from
|
||||||
|
}
|
||||||
|
if uid := types.ParseUid(from); !uid.IsZero() {
|
||||||
|
return uid.UserId()
|
||||||
|
}
|
||||||
|
return from
|
||||||
|
}
|
||||||
|
|
||||||
func callRecordPayloads(records []types.CallRecord, viewer types.Uid) []map[string]any {
|
func callRecordPayloads(records []types.CallRecord, viewer types.Uid) []map[string]any {
|
||||||
items := make([]map[string]any, 0, len(records))
|
items := make([]map[string]any, 0, len(records))
|
||||||
for _, record := range records {
|
for _, record := range records {
|
||||||
@@ -9379,7 +9454,7 @@ func userProfilePayload(user *types.User, ctx *types.RelationshipContext) map[st
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyOptionalProfileFields(data map[string]any, public map[string]any) {
|
func copyOptionalProfileFields(data map[string]any, public map[string]any) {
|
||||||
for _, key := range []string{"gender", "region", "public_account", "signature"} {
|
for _, key := range []string{"nickname", "gender", "region", "public_account", "signature"} {
|
||||||
if value, ok := public[key]; ok {
|
if value, ok := public[key]; ok {
|
||||||
data[key] = value
|
data[key] = value
|
||||||
}
|
}
|
||||||
@@ -9436,7 +9511,7 @@ func displayInitial(value any) string {
|
|||||||
func writeIMRelationError(wrt http.ResponseWriter, req *http.Request, err error) {
|
func writeIMRelationError(wrt http.ResponseWriter, req *http.Request, err error) {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, types.ErrNotFound):
|
case errors.Is(err, types.ErrNotFound):
|
||||||
writeIMError(wrt, req, http.StatusNotFound, 20004, imReasonMalformedRequest, "resource not found", nil)
|
writeIMError(wrt, req, http.StatusNotFound, 20004, imReasonResourceNotFound, "resource not found", nil)
|
||||||
case errors.Is(err, types.ErrPermissionDenied):
|
case errors.Is(err, types.ErrPermissionDenied):
|
||||||
writeIMError(wrt, req, http.StatusForbidden, 20005, imReasonPermissionDenied, "permission denied", nil)
|
writeIMError(wrt, req, http.StatusForbidden, 20005, imReasonPermissionDenied, "permission denied", nil)
|
||||||
case errors.Is(err, types.ErrInvalidResponse):
|
case errors.Is(err, types.ErrInvalidResponse):
|
||||||
@@ -9625,7 +9700,7 @@ func favoriteSnapshot(msg *types.Message) map[string]any {
|
|||||||
return map[string]any{
|
return map[string]any{
|
||||||
"topic": msg.Topic,
|
"topic": msg.Topic,
|
||||||
"seq": msg.SeqId,
|
"seq": msg.SeqId,
|
||||||
"from": msg.From,
|
"from": messageFromUserID(msg.From),
|
||||||
"ts": msg.CreatedAt.UTC().Format(time.RFC3339Nano),
|
"ts": msg.CreatedAt.UTC().Format(time.RFC3339Nano),
|
||||||
"type": imMessageType(msg),
|
"type": imMessageType(msg),
|
||||||
"version": msg.Version,
|
"version": msg.Version,
|
||||||
@@ -9677,6 +9752,17 @@ func forwardFailurePayload(topic string, seq int64, target, reason string) map[s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func forwardClientMid(requestKey string, parts ...any) string {
|
||||||
|
requestKey = strings.TrimSpace(requestKey)
|
||||||
|
if requestKey == "" {
|
||||||
|
return "fwd_" + store.Store.GetUidString()
|
||||||
|
}
|
||||||
|
args := make([]any, 0, len(parts)+2)
|
||||||
|
args = append(args, "forward", requestKey)
|
||||||
|
args = append(args, parts...)
|
||||||
|
return "fwd_" + stableIMEventID(args...)
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeIMForwardTopic(viewer types.Uid, topic string) (string, bool) {
|
func normalizeIMForwardTopic(viewer types.Uid, topic string) (string, bool) {
|
||||||
return normalizeIMUserVisibleTopic(viewer, topic)
|
return normalizeIMUserVisibleTopic(viewer, topic)
|
||||||
}
|
}
|
||||||
@@ -10624,7 +10710,7 @@ func normalizeIMAbuseTarget(tenantID types.TenantID, reporter types.Uid, target
|
|||||||
}
|
}
|
||||||
snapshot["topic"] = topic
|
snapshot["topic"] = topic
|
||||||
snapshot["seq"] = seq
|
snapshot["seq"] = seq
|
||||||
snapshot["from"] = msg.From
|
snapshot["from"] = messageFromUserID(msg.From)
|
||||||
snapshot["ts"] = msg.CreatedAt.UTC().Format(time.RFC3339Nano)
|
snapshot["ts"] = msg.CreatedAt.UTC().Format(time.RFC3339Nano)
|
||||||
return targetType, 0, topic, seq, snapshot, nil
|
return targetType, 0, topic, seq, snapshot, nil
|
||||||
default:
|
default:
|
||||||
|
|||||||
+136
-1
@@ -1625,6 +1625,21 @@ func TestServeIMTenantResolveRequiresAPIKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWriteIMRelationErrorUsesResourceNotFoundReason(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/v1/im/test", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
writeIMRelationError(rec, req, types.ErrNotFound)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusNotFound {
|
||||||
|
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusNotFound, rec.Body.String())
|
||||||
|
}
|
||||||
|
resp := decodeIMHTTPResponse(t, rec)
|
||||||
|
if resp.Reason != imReasonResourceNotFound {
|
||||||
|
t.Fatalf("reason: got %q, want %q", resp.Reason, imReasonResourceNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestServeIMTenantResolveRejectsMalformedTenantCode(t *testing.T) {
|
func TestServeIMTenantResolveRejectsMalformedTenantCode(t *testing.T) {
|
||||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/tenants/resolve",
|
req := httptest.NewRequest(http.MethodPost, "/v1/im/tenants/resolve",
|
||||||
bytes.NewBufferString(`{"tenant_code":"`+strings.Repeat("a", 65)+`"}`))
|
bytes.NewBufferString(`{"tenant_code":"`+strings.Repeat("a", 65)+`"}`))
|
||||||
@@ -5288,6 +5303,10 @@ func TestServeIMFavoriteBatchCreateNormalizesP2PUserTopic(t *testing.T) {
|
|||||||
if favorites.created == nil || favorites.created.SourceTopic != normalizedTopic {
|
if favorites.created == nil || favorites.created.SourceTopic != normalizedTopic {
|
||||||
t.Fatalf("favorite source topic was not normalized: %#v", favorites.created)
|
t.Fatalf("favorite source topic was not normalized: %#v", favorites.created)
|
||||||
}
|
}
|
||||||
|
snapshot := favorites.created.Snapshot.(map[string]any)
|
||||||
|
if snapshot["from"] != peer.UserId() {
|
||||||
|
t.Fatalf("favorite snapshot from must use full user_id: %#v", favorites.created.Snapshot)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServeIMFavoriteForwardSavesMessages(t *testing.T) {
|
func TestServeIMFavoriteForwardSavesMessages(t *testing.T) {
|
||||||
@@ -5679,6 +5698,100 @@ func TestServeIMMessageForwardKafkaAssignsSeqAndBroadcasts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServeIMMessageForwardKafkaPublishUnknownReturnsPending(t *testing.T) {
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||||
|
|
||||||
|
oldKafkaBus := kafkaBus
|
||||||
|
oldPublisher := publishKafkaMessagePersist
|
||||||
|
oldHub := globals.hub
|
||||||
|
oldSubs := store.Subs
|
||||||
|
oldTopics := store.Topics
|
||||||
|
oldMessages := store.Messages
|
||||||
|
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||||
|
topics := mock_store.NewMockTopicsPersistenceInterface(ctrl)
|
||||||
|
messages := mock_store.NewMockMessagesPersistenceInterface(ctrl)
|
||||||
|
kafkaBus = &kafkaRuntime{}
|
||||||
|
globals.hub = &Hub{routeSrv: make(chan *ServerComMessage, 3)}
|
||||||
|
store.Subs = subs
|
||||||
|
store.Topics = topics
|
||||||
|
store.Messages = messages
|
||||||
|
t.Cleanup(func() {
|
||||||
|
kafkaBus = oldKafkaBus
|
||||||
|
publishKafkaMessagePersist = oldPublisher
|
||||||
|
globals.hub = oldHub
|
||||||
|
store.Subs = oldSubs
|
||||||
|
store.Topics = oldTopics
|
||||||
|
store.Messages = oldMessages
|
||||||
|
})
|
||||||
|
|
||||||
|
targetUser := types.Uid(99)
|
||||||
|
targetTopic := types.Uid(42).P2PName(targetUser)
|
||||||
|
subs.EXPECT().Get(types.TenantID(7), targetTopic, types.Uid(42), false).Return(&types.Subscription{
|
||||||
|
TenantID: 7,
|
||||||
|
User: types.Uid(42).String(),
|
||||||
|
Topic: targetTopic,
|
||||||
|
ModeWant: types.ModeCPublic,
|
||||||
|
ModeGiven: types.ModeCPublic,
|
||||||
|
}, nil)
|
||||||
|
subs.EXPECT().Get(types.TenantID(7), "grpSource", types.Uid(42), false).Return(&types.Subscription{
|
||||||
|
TenantID: 7,
|
||||||
|
User: types.Uid(42).String(),
|
||||||
|
Topic: "grpSource",
|
||||||
|
ModeWant: types.ModeCPublic,
|
||||||
|
ModeGiven: types.ModeCPublic,
|
||||||
|
}, nil)
|
||||||
|
topics.EXPECT().Get(types.TenantID(7), targetTopic).Return(&types.Topic{
|
||||||
|
TenantID: 7,
|
||||||
|
SeqId: 40,
|
||||||
|
}, nil)
|
||||||
|
messages.EXPECT().GetOne(types.TenantID(7), "grpSource", int64(14)).Return(&types.Message{
|
||||||
|
TenantID: 7,
|
||||||
|
Topic: "grpSource",
|
||||||
|
SeqId: 14,
|
||||||
|
From: targetUser.String(),
|
||||||
|
Head: types.KVMap{"x-im-type": "text"},
|
||||||
|
Content: map[string]any{"text": "hello"},
|
||||||
|
ObjHeader: types.ObjHeader{
|
||||||
|
CreatedAt: time.Date(2026, 8, 13, 9, 0, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
publishKafkaMessagePersist = func(_ context.Context, _, _ string, _ kafkaEnvelope) error {
|
||||||
|
return context.DeadlineExceeded
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/v1/im/messages/forward", bytes.NewBufferString(`{"mode":"separate","sources":[{"topic":"grpSource","seq":14}],"target_topics":["`+targetUser.UserId()+`"]}`))
|
||||||
|
req.Header.Set("Authorization", "Bearer token-1")
|
||||||
|
req.Header.Set("Idempotency-Key", "idem-forward-1")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
serveIMMessageForward(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("forward status: got %d body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||||
|
items := data["items"].([]any)
|
||||||
|
if len(items) != 1 {
|
||||||
|
t.Fatalf("unexpected forward response: %#v", data)
|
||||||
|
}
|
||||||
|
got := items[0].(map[string]any)
|
||||||
|
if got["status"] != "pending" || got["reason"] != "MESSAGE_PERSIST_PENDING" || got["seq"].(float64) != 41 {
|
||||||
|
t.Fatalf("unexpected pending item: %#v", got)
|
||||||
|
}
|
||||||
|
if got["client_mid"] != "fwd_"+stableIMEventID("forward", "idem-forward-1", "separate", targetTopic, "grpSource", int64(14)) {
|
||||||
|
t.Fatalf("unexpected stable client mid: %#v", got)
|
||||||
|
}
|
||||||
|
if failed := data["failed"].([]any); len(failed) != 0 {
|
||||||
|
t.Fatalf("unexpected failed items: %#v", failed)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case delivered := <-globals.hub.routeSrv:
|
||||||
|
t.Fatalf("pending forward should not broadcast immediately: %#v", delivered)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestServeIMMessageForwardBundleSavesForwardBundle(t *testing.T) {
|
func TestServeIMMessageForwardBundleSavesForwardBundle(t *testing.T) {
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
@@ -6024,6 +6137,15 @@ func TestServeIMMessageSearchContextAndMedia(t *testing.T) {
|
|||||||
if searchRec.Code != http.StatusOK {
|
if searchRec.Code != http.StatusOK {
|
||||||
t.Fatalf("search status: got %d body=%s", searchRec.Code, searchRec.Body.String())
|
t.Fatalf("search status: got %d body=%s", searchRec.Code, searchRec.Body.String())
|
||||||
}
|
}
|
||||||
|
searchData := decodeIMHTTPResponse(t, searchRec).Data.(map[string]any)
|
||||||
|
searchItems := searchData["items"].([]any)
|
||||||
|
if len(searchItems) != 1 || searchItems[0].(map[string]any)["from"] != types.Uid(42).UserId() {
|
||||||
|
t.Fatalf("search from must use full user_id: %#v", searchData)
|
||||||
|
}
|
||||||
|
searchSnapshot := searchItems[0].(map[string]any)["snapshot"].(map[string]any)
|
||||||
|
if searchSnapshot["from"] != types.Uid(42).UserId() {
|
||||||
|
t.Fatalf("search snapshot from must use full user_id: %#v", searchSnapshot)
|
||||||
|
}
|
||||||
|
|
||||||
contextReq := httptest.NewRequest(http.MethodGet, "/v1/im/topics/grpAlpha/messages/3/context", nil)
|
contextReq := httptest.NewRequest(http.MethodGet, "/v1/im/topics/grpAlpha/messages/3/context", nil)
|
||||||
contextReq.Header.Set("Authorization", "Bearer token-1")
|
contextReq.Header.Set("Authorization", "Bearer token-1")
|
||||||
@@ -6032,6 +6154,11 @@ func TestServeIMMessageSearchContextAndMedia(t *testing.T) {
|
|||||||
if contextRec.Code != http.StatusOK {
|
if contextRec.Code != http.StatusOK {
|
||||||
t.Fatalf("context status: got %d body=%s", contextRec.Code, contextRec.Body.String())
|
t.Fatalf("context status: got %d body=%s", contextRec.Code, contextRec.Body.String())
|
||||||
}
|
}
|
||||||
|
contextData := decodeIMHTTPResponse(t, contextRec).Data.(map[string]any)
|
||||||
|
contextItems := contextData["items"].([]any)
|
||||||
|
if len(contextItems) != 1 || contextItems[0].(map[string]any)["from"] != types.Uid(42).UserId() {
|
||||||
|
t.Fatalf("context from must use full user_id: %#v", contextData)
|
||||||
|
}
|
||||||
|
|
||||||
mediaReq := httptest.NewRequest(http.MethodGet, "/v1/im/topics/grpAlpha/media?type=image", nil)
|
mediaReq := httptest.NewRequest(http.MethodGet, "/v1/im/topics/grpAlpha/media?type=image", nil)
|
||||||
mediaReq.Header.Set("Authorization", "Bearer token-1")
|
mediaReq.Header.Set("Authorization", "Bearer token-1")
|
||||||
@@ -6040,6 +6167,11 @@ func TestServeIMMessageSearchContextAndMedia(t *testing.T) {
|
|||||||
if mediaRec.Code != http.StatusOK {
|
if mediaRec.Code != http.StatusOK {
|
||||||
t.Fatalf("media status: got %d body=%s", mediaRec.Code, mediaRec.Body.String())
|
t.Fatalf("media status: got %d body=%s", mediaRec.Code, mediaRec.Body.String())
|
||||||
}
|
}
|
||||||
|
mediaData := decodeIMHTTPResponse(t, mediaRec).Data.(map[string]any)
|
||||||
|
mediaItems := mediaData["items"].([]any)
|
||||||
|
if len(mediaItems) != 1 || mediaItems[0].(map[string]any)["from"] != types.Uid(42).UserId() {
|
||||||
|
t.Fatalf("media from must use full user_id: %#v", mediaData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServeIMTopicReadActionsNormalizeP2PUserTopic(t *testing.T) {
|
func TestServeIMTopicReadActionsNormalizeP2PUserTopic(t *testing.T) {
|
||||||
@@ -7318,7 +7450,7 @@ func TestServeIMUserProfile(t *testing.T) {
|
|||||||
users.EXPECT().Get(types.TenantID(7), peer).Return(&types.User{
|
users.EXPECT().Get(types.TenantID(7), peer).Return(&types.User{
|
||||||
ObjHeader: types.ObjHeader{Id: peer.String()},
|
ObjHeader: types.ObjHeader{Id: peer.String()},
|
||||||
TenantID: 7,
|
TenantID: 7,
|
||||||
Public: map[string]any{"display_name": "Alice", "avatar_file_id": "file-1", "signature": "hello"},
|
Public: map[string]any{"display_name": "Alice", "nickname": "Alice", "avatar_file_id": "file-1", "signature": "hello"},
|
||||||
}, nil)
|
}, nil)
|
||||||
store.Users = users
|
store.Users = users
|
||||||
store.Relations = &testRelationsStore{context: &types.RelationshipContext{
|
store.Relations = &testRelationsStore{context: &types.RelationshipContext{
|
||||||
@@ -7353,6 +7485,9 @@ func TestServeIMUserProfile(t *testing.T) {
|
|||||||
if data["display_name"] != "老同事" || data["remark"] != "老同事" {
|
if data["display_name"] != "老同事" || data["remark"] != "老同事" {
|
||||||
t.Fatalf("profile display name should use friend remark: %#v", data)
|
t.Fatalf("profile display name should use friend remark: %#v", data)
|
||||||
}
|
}
|
||||||
|
if data["nickname"] != "Alice" {
|
||||||
|
t.Fatalf("profile should include original nickname: %#v", data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServeIMUserQRCodes(t *testing.T) {
|
func TestServeIMUserQRCodes(t *testing.T) {
|
||||||
|
|||||||
@@ -48,7 +48,10 @@ func saveMessageMaybeKafka(ctx context.Context, tenantID types.TenantID, msg *ty
|
|||||||
if runtimeReady && !claimed {
|
if runtimeReady && !claimed {
|
||||||
if claim != nil && claim.Seq > 0 {
|
if claim != nil && claim.Seq > 0 {
|
||||||
msg.SeqId = claim.Seq
|
msg.SeqId = claim.Seq
|
||||||
return &types.MessageSaveResult{SeqId: claim.Seq, Duplicate: true}, nil
|
return &types.MessageSaveResult{SeqId: claim.Seq, Duplicate: claim.Status == "done", Pending: claim.Status == "pending"}, nil
|
||||||
|
}
|
||||||
|
if claim != nil && claim.Status == "pending" {
|
||||||
|
return &types.MessageSaveResult{Pending: true}, types.ErrDuplicate
|
||||||
}
|
}
|
||||||
return nil, types.ErrDuplicate
|
return nil, types.ErrDuplicate
|
||||||
}
|
}
|
||||||
@@ -64,9 +67,12 @@ func saveMessageMaybeKafka(ctx context.Context, tenantID types.TenantID, msg *ty
|
|||||||
msg.InitTimes()
|
msg.InitTimes()
|
||||||
if err = enqueueKafkaMessagePersist(ctx, tenantID, msg, attachmentURLs, readBySender); err != nil {
|
if err = enqueueKafkaMessagePersist(ctx, tenantID, msg, attachmentURLs, readBySender); err != nil {
|
||||||
if clientMsgID != "" {
|
if clientMsgID != "" {
|
||||||
_ = store.MarkMessageUniqueFailed(tenantID, msg.Topic, fromUid, clientMsgID)
|
if markErr := store.MarkMessageUniquePending(tenantID, msg.Topic, fromUid, clientMsgID, msg.SeqId); markErr != nil {
|
||||||
|
logs.Warn.Printf("kafka message persist: failed to mark message unique key pending topic=%s seq=%d: %v", msg.Topic, msg.SeqId, markErr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil, err
|
logs.Warn.Printf("kafka message persist: publish state unknown topic=%s seq=%d client_mid=%s err=%v", msg.Topic, msg.SeqId, clientMsgID, err)
|
||||||
|
return &types.MessageSaveResult{SeqId: msg.SeqId, Pending: true, MarkedReadBySender: readBySender && !fromUid.IsZero()}, err
|
||||||
}
|
}
|
||||||
if clientMsgID != "" {
|
if clientMsgID != "" {
|
||||||
if err = store.MarkMessageUniqueDone(tenantID, msg.Topic, fromUid, clientMsgID, msg.SeqId, ""); err != nil {
|
if err = store.MarkMessageUniqueDone(tenantID, msg.Topic, fromUid, clientMsgID, msg.SeqId, ""); err != nil {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ type groupMessageRuntime interface {
|
|||||||
Close() error
|
Close() error
|
||||||
Claim(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string) (*groupMessageUniqueRecord, bool, error)
|
Claim(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string) (*groupMessageUniqueRecord, bool, error)
|
||||||
NextSeq(tenantID types.TenantID, topic string, baseline int64) (int64, error)
|
NextSeq(tenantID types.TenantID, topic string, baseline int64) (int64, error)
|
||||||
|
MarkPending(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string, seq int64) error
|
||||||
MarkDone(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string, seq int64, msgID string) error
|
MarkDone(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string, seq int64, msgID string) error
|
||||||
MarkFailed(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string) error
|
MarkFailed(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string) error
|
||||||
CacheHotMessage(tenantID types.TenantID, msg *types.Message) error
|
CacheHotMessage(tenantID types.TenantID, msg *types.Message) error
|
||||||
@@ -111,6 +112,13 @@ func MarkMessageUniqueDone(tenantID types.TenantID, topic string, from types.Uid
|
|||||||
return groupMsgRuntime.MarkDone(tenantID, topic, from, clientMsgID, seq, msgID)
|
return groupMsgRuntime.MarkDone(tenantID, topic, from, clientMsgID, seq, msgID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MarkMessageUniquePending(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string, seq int64) error {
|
||||||
|
if groupMsgRuntime == nil || !tenantID.IsValid() || topic == "" || from.IsZero() || clientMsgID == "" || seq <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return groupMsgRuntime.MarkPending(tenantID, topic, from, clientMsgID, seq)
|
||||||
|
}
|
||||||
|
|
||||||
func MarkMessageUniqueFailed(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string) error {
|
func MarkMessageUniqueFailed(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string) error {
|
||||||
if groupMsgRuntime == nil || !tenantID.IsValid() || topic == "" || from.IsZero() || clientMsgID == "" {
|
if groupMsgRuntime == nil || !tenantID.IsValid() || topic == "" || from.IsZero() || clientMsgID == "" {
|
||||||
return nil
|
return nil
|
||||||
@@ -464,6 +472,15 @@ func (r *redisGroupMessageRuntime) MarkDone(tenantID types.TenantID, topic strin
|
|||||||
return r.client.Set(context.Background(), r.uniqueKey(tenantID, topic, from, clientMsgID), raw, time.Duration(r.cfg.DoneTTL)*time.Second).Err()
|
return r.client.Set(context.Background(), r.uniqueKey(tenantID, topic, from, clientMsgID), raw, time.Duration(r.cfg.DoneTTL)*time.Second).Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *redisGroupMessageRuntime) MarkPending(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string, seq int64) error {
|
||||||
|
if clientMsgID == "" || seq <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rec := &groupMessageUniqueRecord{Status: "pending", Seq: seq, CreatedAt: time.Now().UTC()}
|
||||||
|
raw, _ := json.Marshal(rec)
|
||||||
|
return r.client.Set(context.Background(), r.uniqueKey(tenantID, topic, from, clientMsgID), raw, time.Duration(r.cfg.PendingTTL)*time.Second).Err()
|
||||||
|
}
|
||||||
|
|
||||||
func (r *redisGroupMessageRuntime) MarkFailed(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string) error {
|
func (r *redisGroupMessageRuntime) MarkFailed(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string) error {
|
||||||
if clientMsgID == "" {
|
if clientMsgID == "" {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ func (r *testGroupMessageRuntime) MarkDone(_ types.TenantID, topic string, from
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *testGroupMessageRuntime) MarkPending(_ types.TenantID, topic string, from types.Uid, clientMsgID string, seq int64) error {
|
||||||
|
key := topic + ":" + from.UserId() + ":" + clientMsgID
|
||||||
|
r.claims[key] = &groupMessageUniqueRecord{Status: "pending", Seq: seq, CreatedAt: time.Now().UTC()}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *testGroupMessageRuntime) MarkFailed(_ types.TenantID, topic string, from types.Uid, clientMsgID string) error {
|
func (r *testGroupMessageRuntime) MarkFailed(_ types.TenantID, topic string, from types.Uid, clientMsgID string) error {
|
||||||
key := topic + ":" + from.UserId() + ":" + clientMsgID
|
key := topic + ":" + from.UserId() + ":" + clientMsgID
|
||||||
r.claims[key] = &groupMessageUniqueRecord{Status: "failed", CreatedAt: time.Now().UTC()}
|
r.claims[key] = &groupMessageUniqueRecord{Status: "failed", CreatedAt: time.Now().UTC()}
|
||||||
|
|||||||
@@ -1269,6 +1269,7 @@ type MessageSaveResult struct {
|
|||||||
SeqId int64
|
SeqId int64
|
||||||
MarkedReadBySender bool
|
MarkedReadBySender bool
|
||||||
Duplicate bool
|
Duplicate bool
|
||||||
|
Pending bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Range is a range of message SeqIDs. Low end is inclusive (closed), high end is exclusive (open): [Low, Hi).
|
// Range is a range of message SeqIDs. Low end is inclusive (closed), high end is exclusive (open): [Low, Hi).
|
||||||
|
|||||||
Reference in New Issue
Block a user