feat(im): merge chat-members-add batch results into a ledger

This commit is contained in:
zhangheng.023
2026-07-21 17:01:08 +08:00
parent e02039185e
commit a022d52238
2 changed files with 152 additions and 0 deletions

View File

@@ -4,10 +4,14 @@
package im
import (
"fmt"
"net/http"
"strings"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/shortcuts/common"
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
)
const imChatMembersAddPathFmt = "/open-apis/im/v1/chats/%s/members"
@@ -70,3 +74,85 @@ func validateChatMembersAdd(runtime *common.RuntimeContext) error {
}
return nil
}
// chatMembersAddResult is the merged ledger across the users-call and the
// bots-call.
type chatMembersAddResult struct {
succeeded []string
invalid []string
notExisted []string
pendingApproval []string
callErrors []map[string]interface{}
}
func newChatMembersAddResult() *chatMembersAddResult {
return &chatMembersAddResult{
succeeded: []string{},
invalid: []string{},
notExisted: []string{},
pendingApproval: []string{},
callErrors: []map[string]interface{}{},
}
}
// addChatMembersBatch issues one chat.members.create call for a single
// member_id_type and folds the outcome into res. A full-call error (e.g.
// missing scope, chat-wide bot cap exceeded) is recorded as a call_errors
// entry carrying the affected id_list, rather than aborting the other call.
func addChatMembersBatch(runtime *common.RuntimeContext, chatID, memberType, memberIDType string, ids []string, res *chatMembersAddResult) {
path := fmt.Sprintf(imChatMembersAddPathFmt, validate.EncodePathSegment(chatID))
data, err := runtime.DoAPIJSONTyped(http.MethodPost, path,
larkcore.QueryParams{
"member_id_type": []string{memberIDType},
"succeed_type": []string{"1"},
},
map[string]interface{}{"id_list": ids},
)
if err != nil {
res.callErrors = append(res.callErrors, map[string]interface{}{
"member_type": memberType,
"id_list": ids,
"error": err.Error(),
})
return
}
invalid := stringsFromAny(data["invalid_id_list"])
notExisted := stringsFromAny(data["not_existed_id_list"])
pending := stringsFromAny(data["pending_approval_id_list"])
res.invalid = append(res.invalid, invalid...)
res.notExisted = append(res.notExisted, notExisted...)
res.pendingApproval = append(res.pendingApproval, pending...)
failed := make(map[string]struct{}, len(invalid)+len(notExisted)+len(pending))
for _, id := range invalid {
failed[id] = struct{}{}
}
for _, id := range notExisted {
failed[id] = struct{}{}
}
for _, id := range pending {
failed[id] = struct{}{}
}
for _, id := range ids {
if _, isFailed := failed[id]; !isFailed {
res.succeeded = append(res.succeeded, id)
}
}
}
// stringsFromAny converts a JSON-decoded []interface{} of strings to []string,
// skipping any non-string entries defensively.
func stringsFromAny(v interface{}) []string {
arr, ok := v.([]interface{})
if !ok {
return nil
}
out := make([]string, 0, len(arr))
for _, item := range arr {
if s, ok := item.(string); ok {
out = append(out, s)
}
}
return out
}

View File

@@ -152,3 +152,69 @@ func newChatMembersAddTestRuntime(t *testing.T, rtRoundTripper http.RoundTripper
runtime.Cmd = cmd
return runtime
}
func TestAddChatMembersBatch_AllSucceed(t *testing.T) {
rt := newUserShortcutRuntime(t, shortcutRoundTripFunc(func(req *http.Request) (*http.Response, error) {
return shortcutJSONResponse(200, map[string]interface{}{"code": 0, "data": map[string]interface{}{}}), nil
}))
res := newChatMembersAddResult()
addChatMembersBatch(rt, "oc_x", "user", "open_id", []string{"ou_a", "ou_b"}, res)
if !equalStringSlices(res.succeeded, []string{"ou_a", "ou_b"}) {
t.Errorf("succeeded = %v, want [ou_a ou_b]", res.succeeded)
}
if len(res.invalid) != 0 || len(res.notExisted) != 0 || len(res.pendingApproval) != 0 || len(res.callErrors) != 0 {
t.Errorf("expected no failures, got %+v", res)
}
}
func TestAddChatMembersBatch_PartialFailure(t *testing.T) {
rt := newUserShortcutRuntime(t, shortcutRoundTripFunc(func(req *http.Request) (*http.Response, error) {
return shortcutJSONResponse(200, map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"invalid_id_list": []interface{}{"ou_c"},
"not_existed_id_list": []interface{}{"ou_d"},
"pending_approval_id_list": []interface{}{"ou_e"},
},
}), nil
}))
res := newChatMembersAddResult()
addChatMembersBatch(rt, "oc_x", "user", "open_id", []string{"ou_a", "ou_b", "ou_c", "ou_d", "ou_e"}, res)
if !equalStringSlices(res.succeeded, []string{"ou_a", "ou_b"}) {
t.Errorf("succeeded = %v, want [ou_a ou_b]", res.succeeded)
}
if !equalStringSlices(res.invalid, []string{"ou_c"}) {
t.Errorf("invalid = %v, want [ou_c]", res.invalid)
}
if !equalStringSlices(res.notExisted, []string{"ou_d"}) {
t.Errorf("notExisted = %v, want [ou_d]", res.notExisted)
}
if !equalStringSlices(res.pendingApproval, []string{"ou_e"}) {
t.Errorf("pendingApproval = %v, want [ou_e]", res.pendingApproval)
}
}
func TestAddChatMembersBatch_CallLevelFailure(t *testing.T) {
rt := newUserShortcutRuntime(t, shortcutRoundTripFunc(func(req *http.Request) (*http.Response, error) {
return shortcutJSONResponse(400, map[string]interface{}{"code": 123, "msg": "bot count exceeds chat limit"}), nil
}))
res := newChatMembersAddResult()
addChatMembersBatch(rt, "oc_x", "bot", "app_id", []string{"cli_y"}, res)
if len(res.succeeded) != 0 {
t.Errorf("succeeded = %v, want empty (call failed)", res.succeeded)
}
if len(res.callErrors) != 1 {
t.Fatalf("callErrors = %v, want 1 entry", res.callErrors)
}
ce := res.callErrors[0]
if ce["member_type"] != "bot" {
t.Errorf("call_errors[0].member_type = %v, want bot", ce["member_type"])
}
ids, _ := ce["id_list"].([]string)
if !equalStringSlices(ids, []string{"cli_y"}) {
t.Errorf("call_errors[0].id_list = %v, want [cli_y]", ids)
}
}