diff --git a/shortcuts/im/builders_test.go b/shortcuts/im/builders_test.go index 81acb001..d44570d8 100644 --- a/shortcuts/im/builders_test.go +++ b/shortcuts/im/builders_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "github.com/larksuite/cli/internal/core" "github.com/larksuite/cli/shortcuts/common" "github.com/spf13/cobra" ) @@ -395,6 +396,28 @@ func TestShortcutValidateBranches(t *testing.T) { } }) + t.Run("ImChatMessageList rejects both targets", func(t *testing.T) { + runtime := newTestRuntimeContext(t, map[string]string{ + "chat-id": "oc_abc", + "user-id": "ou_123", + }, nil) + err := ImChatMessageList.Validate(context.Background(), runtime) + if err == nil || !strings.Contains(err.Error(), "mutually exclusive") { + t.Fatalf("ImChatMessageList.Validate() error = %v, want mutually exclusive", err) + } + }) + + t.Run("ImChatMessageList rejects user target for bot identity", func(t *testing.T) { + runtime := newTestRuntimeContext(t, map[string]string{ + "user-id": "ou_123", + }, nil) + setRuntimeField(t, runtime, "resolvedAs", core.AsBot) + err := ImChatMessageList.Validate(context.Background(), runtime) + if err == nil || !strings.Contains(err.Error(), "requires user identity") { + t.Fatalf("ImChatMessageList.Validate() error = %v, want requires user identity", err) + } + }) + t.Run("ImMessagesMGet empty ids", func(t *testing.T) { runtime := newTestRuntimeContext(t, map[string]string{ "message-ids": " , ", diff --git a/shortcuts/im/coverage_additional_test.go b/shortcuts/im/coverage_additional_test.go index 3500021a..5359432a 100644 --- a/shortcuts/im/coverage_additional_test.go +++ b/shortcuts/im/coverage_additional_test.go @@ -270,7 +270,7 @@ func TestResolveChatIDForMessagesList(t *testing.T) { }) t.Run("user resolved through p2p lookup", func(t *testing.T) { - runtime := newBotShortcutRuntime(t, shortcutRoundTripFunc(func(req *http.Request) (*http.Response, error) { + runtime := newUserShortcutRuntime(t, shortcutRoundTripFunc(func(req *http.Request) (*http.Response, error) { switch { case strings.Contains(req.URL.Path, "/open-apis/im/v1/chat_p2p/batch_query"): return shortcutJSONResponse(200, map[string]interface{}{ @@ -300,6 +300,23 @@ func TestResolveChatIDForMessagesList(t *testing.T) { t.Fatalf("resolveChatIDForMessagesList() = %q, want %q", got, "oc_resolved") } }) + + t.Run("user target rejected for bot identity", func(t *testing.T) { + runtime := newBotShortcutRuntime(t, shortcutRoundTripFunc(func(req *http.Request) (*http.Response, error) { + return nil, fmt.Errorf("unexpected request: %s", req.URL.String()) + })) + cmd := &cobra.Command{Use: "test"} + cmd.Flags().String("user-id", "", "") + if err := cmd.Flags().Set("user-id", "ou_123"); err != nil { + t.Fatalf("Flags().Set() error = %v", err) + } + runtime.Cmd = cmd + + _, err := resolveChatIDForMessagesList(runtime, false) + if err == nil || !strings.Contains(err.Error(), "requires user identity") { + t.Fatalf("resolveChatIDForMessagesList() error = %v, want requires user identity", err) + } + }) } func TestBuildMessagesSearchRequest(t *testing.T) { diff --git a/shortcuts/im/helpers.go b/shortcuts/im/helpers.go index 50fed673..ff8d5a9f 100644 --- a/shortcuts/im/helpers.go +++ b/shortcuts/im/helpers.go @@ -377,6 +377,9 @@ func mediaFallbackOrError(originalValue, mediaType string, uploadErr error) (str // resolveP2PChatID resolves user open_id to P2P chat_id. func resolveP2PChatID(runtime *common.RuntimeContext, openID string) (string, error) { + if runtime.IsBot() { + return "", output.Errorf(output.ExitValidation, "validation", "--user-id requires user identity (--as user); use --chat-id when calling with bot identity") + } apiResp, err := runtime.DoAPI(&larkcore.ApiReq{ HttpMethod: http.MethodPost, ApiPath: "/open-apis/im/v1/chat_p2p/batch_query", diff --git a/shortcuts/im/helpers_network_test.go b/shortcuts/im/helpers_network_test.go index d7b6c374..f10e8507 100644 --- a/shortcuts/im/helpers_network_test.go +++ b/shortcuts/im/helpers_network_test.go @@ -107,12 +107,17 @@ func newBotShortcutRuntime(t *testing.T, rt http.RoundTripper) *common.RuntimeCo return runtime } +func newUserShortcutRuntime(t *testing.T, rt http.RoundTripper) *common.RuntimeContext { + t.Helper() + runtime := newBotShortcutRuntime(t, rt) + setRuntimeField(t, runtime, "resolvedAs", core.AsUser) + return runtime +} + func TestResolveP2PChatID(t *testing.T) { - var gotAuth string - runtime := newBotShortcutRuntime(t, shortcutRoundTripFunc(func(req *http.Request) (*http.Response, error) { + runtime := newUserShortcutRuntime(t, shortcutRoundTripFunc(func(req *http.Request) (*http.Response, error) { switch { case strings.Contains(req.URL.Path, "/open-apis/im/v1/chat_p2p/batch_query"): - gotAuth = req.Header.Get("Authorization") return shortcutJSONResponse(200, map[string]interface{}{ "code": 0, "data": map[string]interface{}{ @@ -133,13 +138,10 @@ func TestResolveP2PChatID(t *testing.T) { if got != "oc_123" { t.Fatalf("resolveP2PChatID() = %q, want %q", got, "oc_123") } - if gotAuth != "Bearer tenant-token" { - t.Fatalf("Authorization header = %q, want %q", gotAuth, "Bearer tenant-token") - } } func TestResolveP2PChatIDNotFound(t *testing.T) { - runtime := newBotShortcutRuntime(t, shortcutRoundTripFunc(func(req *http.Request) (*http.Response, error) { + runtime := newUserShortcutRuntime(t, shortcutRoundTripFunc(func(req *http.Request) (*http.Response, error) { switch { case strings.Contains(req.URL.Path, "/open-apis/im/v1/chat_p2p/batch_query"): return shortcutJSONResponse(200, map[string]interface{}{ @@ -159,6 +161,17 @@ func TestResolveP2PChatIDNotFound(t *testing.T) { } } +func TestResolveP2PChatIDRejectsBot(t *testing.T) { + runtime := newBotShortcutRuntime(t, shortcutRoundTripFunc(func(req *http.Request) (*http.Response, error) { + return nil, fmt.Errorf("unexpected request: %s", req.URL.String()) + })) + + _, err := resolveP2PChatID(runtime, "ou_123") + if err == nil || !strings.Contains(err.Error(), "requires user identity") { + t.Fatalf("resolveP2PChatID() error = %v, want requires user identity", err) + } +} + func TestResolveThreadID(t *testing.T) { t.Run("thread id passthrough", func(t *testing.T) { got, err := resolveThreadID(newBotShortcutRuntime(t, shortcutRoundTripFunc(func(req *http.Request) (*http.Response, error) { diff --git a/shortcuts/im/im_chat_messages_list.go b/shortcuts/im/im_chat_messages_list.go index e4d906fd..9307e6b9 100644 --- a/shortcuts/im/im_chat_messages_list.go +++ b/shortcuts/im/im_chat_messages_list.go @@ -28,7 +28,7 @@ var ImChatMessageList = common.Shortcut{ HasFormat: true, Flags: []common.Flag{ {Name: "chat-id", Desc: "(required, mutually exclusive with --user-id) chat ID (oc_xxx)"}, - {Name: "user-id", Desc: "(required, mutually exclusive with --chat-id) user open_id (ou_xxx)"}, + {Name: "user-id", Desc: "(required, mutually exclusive with --chat-id; user identity only) user open_id (ou_xxx)"}, {Name: "start", Desc: "start time (ISO 8601)"}, {Name: "end", Desc: "end time (ISO 8601)"}, {Name: "sort", Default: "desc", Desc: "sort order", Enum: []string{"asc", "desc"}}, @@ -57,11 +57,21 @@ var ImChatMessageList = common.Shortcut{ return d.GET("/open-apis/im/v1/messages").Params(dryParams) }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { - if err := common.ExactlyOne(runtime, "chat-id", "user-id"); err != nil { - if runtime.Str("chat-id") == "" && runtime.Str("user-id") == "" { - return common.FlagErrorf("specify at least one of --chat-id or --user-id") + // Under bot identity, --user-id is not supported; require --chat-id only. + if runtime.IsBot() { + if runtime.Str("user-id") != "" { + return common.FlagErrorf("--user-id requires user identity (--as user); use --chat-id when calling with bot identity") + } + if runtime.Str("chat-id") == "" { + return common.FlagErrorf("specify --chat-id (bot identity does not support --user-id)") + } + } else { + if err := common.ExactlyOne(runtime, "chat-id", "user-id"); err != nil { + if runtime.Str("chat-id") == "" && runtime.Str("user-id") == "" { + return common.FlagErrorf("specify at least one of --chat-id or --user-id") + } + return err } - return err } // Validate ID formats diff --git a/skills/lark-im/references/lark-im-chat-messages-list.md b/skills/lark-im/references/lark-im-chat-messages-list.md index 4d760247..a3f61f0f 100644 --- a/skills/lark-im/references/lark-im-chat-messages-list.md +++ b/skills/lark-im/references/lark-im-chat-messages-list.md @@ -36,7 +36,7 @@ lark-cli im +chat-messages-list --chat-id oc_xxx --format json | Parameter | Required | Description | |------|------|------| | `--chat-id ` | One of two | Specify the conversation by its chat_id directly (e.g., group chat `oc_xxx`) | -| `--user-id ` | One of two | Specify a DM conversation by the other user's open_id (`ou_xxx`); p2p chat_id is resolved automatically | +| `--user-id ` | One of two | Specify a DM conversation by the other user's open_id (`ou_xxx`); p2p chat_id is resolved automatically. Requires user identity (`--as user`); not supported with bot identity | | `--start