diff --git a/shortcuts/im/im_chat_members_add.go b/shortcuts/im/im_chat_members_add.go index c0d3091d1..1d069ccc0 100644 --- a/shortcuts/im/im_chat_members_add.go +++ b/shortcuts/im/im_chat_members_add.go @@ -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 +} diff --git a/shortcuts/im/im_chat_members_add_test.go b/shortcuts/im/im_chat_members_add_test.go index fb2616946..a88c04b81 100644 --- a/shortcuts/im/im_chat_members_add_test.go +++ b/shortcuts/im/im_chat_members_add_test.go @@ -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) + } +}