im

package
v1.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 2, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ImChatCreate = common.Shortcut{
	Service:     "im",
	Command:     "+chat-create",
	Description: "Create a group chat with bot identity; bot-only; creates private/public chats, invites users/bots, optionally sets bot manager",
	Risk:        "write",
	Scopes:      []string{"im:chat:create"},
	AuthTypes:   []string{"bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "name", Desc: "group name (required for public groups, max 60 chars)"},
		{Name: "description", Desc: "group description (max 100 chars)"},
		{Name: "users", Desc: "comma-separated user open_ids (ou_xxx) to invite, max 50"},
		{Name: "bots", Desc: "comma-separated bot app IDs (cli_xxx) to invite, max 5"},
		{Name: "owner", Desc: "owner open_id (ou_xxx); defaults to the bot if not specified"},
		{Name: "type", Default: "private", Desc: "chat type", Enum: []string{"private", "public"}},
		{Name: "set-bot-manager", Type: "bool", Desc: "set the bot that creates this chat as manager"},
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		body := buildCreateChatBody(runtime)
		params := map[string]interface{}{"user_id_type": "open_id"}
		if runtime.Bool("set-bot-manager") {
			params["set_bot_manager"] = true
		}
		return common.NewDryRunAPI().
			POST("/open-apis/im/v1/chats").
			Params(params).
			Body(body)
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		name := runtime.Str("name")
		chatType := runtime.Str("type")

		if chatType == "public" && len([]rune(name)) < 2 {
			return output.ErrValidation("--name is required for public groups and must be at least 2 characters")
		}

		if len([]rune(name)) > 60 {
			return output.ErrValidation("--name exceeds the maximum of 60 characters (got %d)", len([]rune(name)))
		}

		if desc := runtime.Str("description"); len([]rune(desc)) > 100 {
			return output.ErrValidation("--description exceeds the maximum of 100 characters (got %d)", len([]rune(desc)))
		}

		if users := runtime.Str("users"); users != "" {
			ids := common.SplitCSV(users)
			if len(ids) > 50 {
				return output.ErrValidation("--users exceeds the maximum of 50 (got %d)", len(ids))
			}
			for _, id := range ids {
				if _, err := common.ValidateUserID(id); err != nil {
					return err
				}
			}
		}

		if bots := runtime.Str("bots"); bots != "" {
			ids := common.SplitCSV(bots)
			if len(ids) > 5 {
				return output.ErrValidation("--bots exceeds the maximum of 5 (got %d)", len(ids))
			}
			for _, id := range ids {
				if !strings.HasPrefix(id, "cli_") {
					return output.ErrValidation("invalid bot id %q: expected app ID (cli_xxx)", id)
				}
			}
		}

		if owner := runtime.Str("owner"); owner != "" {
			if _, err := common.ValidateUserID(owner); err != nil {
				return err
			}
		}
		return nil
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		body := buildCreateChatBody(runtime)

		qp := larkcore.QueryParams{"user_id_type": []string{"open_id"}}
		if runtime.Bool("set-bot-manager") {
			qp["set_bot_manager"] = []string{"true"}
		}
		resData, err := runtime.DoAPIJSON(http.MethodPost, "/open-apis/im/v1/chats", qp, body)
		if err != nil {
			return err
		}

		outData := map[string]interface{}{
			"chat_id":   resData["chat_id"],
			"name":      resData["name"],
			"chat_type": resData["chat_type"],
			"owner_id":  resData["owner_id"],
			"external":  resData["external"],
		}

		if chatID, ok := resData["chat_id"].(string); ok && chatID != "" {
			linkData, err := runtime.DoAPIJSON(http.MethodPost,
				fmt.Sprintf("/open-apis/im/v1/chats/%s/link", validate.EncodePathSegment(chatID)),
				nil, nil)
			if err == nil {
				outData["share_link"] = linkData["share_link"]
			}
		}

		runtime.OutFormat(outData, nil, func(w io.Writer) {
			fmt.Fprintf(w, "Group created successfully\n\n")
			output.PrintTable(w, []map[string]interface{}{outData})
			if link, ok := outData["share_link"].(string); ok && link != "" {
				fmt.Fprintf(w, "\nShare link: %s\n", link)
			}
		})
		return nil
	},
}
View Source
var ImChatMessageList = common.Shortcut{
	Service:     "im",
	Command:     "+chat-messages-list",
	Description: "List messages in a chat or P2P conversation; user/bot; accepts --chat-id or --user-id, resolves P2P chat_id, supports time range/sort/pagination",
	Risk:        "read",
	Scopes:      []string{"im:message:readonly"},
	UserScopes:  []string{"im:message.group_msg:get_as_user", "im:message.p2p_msg:get_as_user", "contact:user.base:readonly"},
	BotScopes:   []string{"im:message.group_msg", "im:message.p2p_msg:readonly"},
	AuthTypes:   []string{"user", "bot"},
	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: "start", Desc: "start time (ISO 8601)"},
		{Name: "end", Desc: "end time (ISO 8601)"},
		{Name: "sort", Default: "desc", Desc: "sort order", Enum: []string{"asc", "desc"}},
		{Name: "page-size", Default: "50", Desc: "page size (1-50)"},
		{Name: "page-token", Desc: "pagination token for next page"},
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		d := common.NewDryRunAPI()
		chatId, err := resolveChatIDForMessagesList(runtime, true)
		if err != nil {
			return d.Desc(err.Error())
		}
		if runtime.Str("user-id") != "" {
			d.Desc("(--user-id provided) Will resolve P2P chat_id via POST /open-apis/im/v1/chat_p2p/batch_query at execution time")
		}
		params, err := buildChatMessageListRequest(runtime, chatId)
		if err != nil {
			return d.Desc(err.Error())
		}
		dryParams := make(map[string]interface{}, len(params))
		for k, vs := range params {
			if len(vs) > 0 {
				dryParams[k] = vs[0]
			}
		}
		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")
			}
			return err
		}

		if chatFlag := runtime.Str("chat-id"); chatFlag != "" {
			if _, err := common.ValidateChatID(chatFlag); err != nil {
				return err
			}
		}
		if userFlag := runtime.Str("user-id"); userFlag != "" {
			if _, err := common.ValidateUserID(userFlag); err != nil {
				return err
			}
		}

		chatId := runtime.Str("chat-id")
		if chatId == "" {
			chatId = "<resolved_chat_id>"
		}
		_, err := buildChatMessageListRequest(runtime, chatId)
		return err
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		chatId, err := resolveChatIDForMessagesList(runtime, false)
		if err != nil {
			return err
		}
		params, err := buildChatMessageListRequest(runtime, chatId)
		if err != nil {
			return err
		}

		data, err := runtime.DoAPIJSON(http.MethodGet, "/open-apis/im/v1/messages", params, nil)
		if err != nil {
			return err
		}
		rawItems, _ := data["items"].([]interface{})
		hasMore, nextPageToken := common.PaginationMeta(data)

		nameCache := make(map[string]string)
		messages := make([]map[string]interface{}, 0, len(rawItems))
		for _, item := range rawItems {
			m, _ := item.(map[string]interface{})
			messages = append(messages, convertlib.FormatMessageItem(m, runtime, nameCache))
		}

		convertlib.ResolveSenderNames(runtime, messages, nameCache)
		convertlib.AttachSenderNames(messages, nameCache)
		convertlib.ExpandThreadReplies(runtime, messages, nameCache, convertlib.ThreadRepliesPerThread, convertlib.ThreadRepliesTotalLimit)

		outData := map[string]interface{}{
			"messages":   messages,
			"total":      len(messages),
			"has_more":   hasMore,
			"page_token": nextPageToken,
		}
		runtime.OutFormat(outData, nil, func(w io.Writer) {
			if len(messages) == 0 {
				fmt.Fprintln(w, "No messages in this time range.")
				return
			}
			var rows []map[string]interface{}
			for _, msg := range messages {
				row := map[string]interface{}{
					"time": msg["create_time"],
					"type": msg["msg_type"],
				}
				if sender, ok := msg["sender"].(map[string]interface{}); ok {
					if name, _ := sender["name"].(string); name != "" {
						row["sender"] = name
					}
				}
				if content, _ := msg["content"].(string); content != "" {
					row["content"] = convertlib.TruncateContent(content, 40)
				}
				rows = append(rows, row)
			}
			output.PrintTable(w, rows)
			moreHint := ""
			if hasMore {
				moreHint = fmt.Sprintf(" (more available, page_token: %s)", nextPageToken)
			}
			fmt.Fprintf(w, "\n%d message(s)%s\ntip: use --format json to view full message content\n", len(messages), moreHint)
		})
		return nil
	},
}
View Source
var ImChatSearch = common.Shortcut{
	Service:     "im",
	Command:     "+chat-search",
	Description: "Search visible group chats by keyword and/or member open_ids (e.g. look up chat_id by group name); user/bot; supports member/type filters, sorting, and pagination",
	Risk:        "read",
	Scopes:      []string{"im:chat:read"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "query", Desc: "search keyword (max 64 chars)"},
		{Name: "search-types", Desc: "chat types, comma-separated (private, external, public_joined, public_not_joined)"},
		{Name: "member-ids", Desc: "filter by member open_ids, comma-separated"},
		{Name: "is-manager", Type: "bool", Desc: "only show chats you created or manage"},
		{Name: "disable-search-by-user", Type: "bool", Desc: "disable search-by-member-name (default: search by member name first, then group name)"},
		{Name: "sort-by", Desc: "sort field (descending)", Enum: []string{"create_time_desc", "update_time_desc", "member_count_desc"}},
		{Name: "page-size", Type: "int", Default: "20", Desc: "page size (1-100)"},
		{Name: "page-token", Desc: "pagination token for next page"},
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		body := buildSearchChatBody(runtime)
		params := buildSearchChatParams(runtime)
		return common.NewDryRunAPI().
			POST("/open-apis/im/v2/chats/search").
			Params(params).
			Body(body)
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		query := runtime.Str("query")
		memberIDs := runtime.Str("member-ids")
		if query == "" && memberIDs == "" {
			return output.ErrValidation("--query and --member-ids cannot both be empty; provide at least one (e.g. --query \"team-name\" or --member-ids \"ou_xxx\")")
		}
		if query != "" && len([]rune(query)) > 64 {
			return output.ErrValidation("--query exceeds the maximum of 64 characters (got %d)", len([]rune(query)))
		}
		if st := runtime.Str("search-types"); st != "" {
			allowed := map[string]struct{}{
				"private":           {},
				"external":          {},
				"public_joined":     {},
				"public_not_joined": {},
			}
			for _, item := range common.SplitCSV(st) {
				if _, ok := allowed[item]; !ok {
					return output.ErrValidation("invalid --search-types value %q: expected one of private, external, public_joined, public_not_joined", item)
				}
			}
		}
		if mi := runtime.Str("member-ids"); mi != "" {
			ids := common.SplitCSV(mi)
			if len(ids) > 50 {
				return output.ErrValidation("--member-ids exceeds the maximum of 50 (got %d)", len(ids))
			}
			for _, id := range ids {
				if _, err := common.ValidateUserID(id); err != nil {
					return err
				}
			}
		}
		if n := runtime.Int("page-size"); n < 1 || n > 100 {
			return output.ErrValidation("--page-size must be an integer between 1 and 100")
		}
		return nil
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		body := buildSearchChatBody(runtime)
		params := buildSearchChatParams(runtime)
		resData, err := runtime.CallAPI("POST", "/open-apis/im/v2/chats/search", params, body)
		if err != nil {
			return err
		}

		rawItems, _ := resData["items"].([]interface{})
		totalF, _ := util.ToFloat64(resData["total"])
		total := totalF
		hasMore, pageToken := common.PaginationMeta(resData)

		// Extract MetaData from each item
		var items []map[string]interface{}
		for _, raw := range rawItems {
			item, _ := raw.(map[string]interface{})
			if item == nil {
				continue
			}
			meta, _ := item["meta_data"].(map[string]interface{})
			if meta == nil {
				continue
			}
			items = append(items, meta)
		}

		outData := map[string]interface{}{
			"chats":      items,
			"total":      int(total),
			"has_more":   hasMore,
			"page_token": pageToken,
		}

		runtime.OutFormat(outData, nil, func(w io.Writer) {
			if len(items) == 0 {
				fmt.Fprintln(w, "No matching group chats found.")
				return
			}
			var rows []map[string]interface{}
			for _, m := range items {
				row := map[string]interface{}{
					"chat_id": m["chat_id"],
					"name":    m["name"],
				}
				if desc, _ := m["description"].(string); desc != "" {
					row["description"] = desc
				}
				if ownerID, _ := m["owner_id"].(string); ownerID != "" {
					row["owner_id"] = ownerID
				}
				if chatMode, _ := m["chat_mode"].(string); chatMode != "" {
					row["chat_mode"] = chatMode
				}
				if external, ok := m["external"].(bool); ok {
					row["external"] = external
				}
				if status, _ := m["chat_status"].(string); status != "" {
					row["chat_status"] = status
				}
				if createTime, _ := m["create_time"].(string); createTime != "" {
					row["create_time"] = createTime
				}
				rows = append(rows, row)
			}
			output.PrintTable(w, rows)
			moreHint := ""
			if hasMore {
				moreHint = " (more available, use --page-token to fetch next page"
				if pageToken != "" {
					moreHint += fmt.Sprintf(", page_token: %s", pageToken)
				}
				moreHint += ")"
			}
			fmt.Fprintf(w, "\n%d chat(s) found%s\n", int(total), moreHint)
		})
		return nil
	},
}
View Source
var ImChatUpdate = common.Shortcut{
	Service:     "im",
	Command:     "+chat-update",
	Description: "Update group chat name or description; user/bot; updates a chat's name or description",
	Risk:        "write",
	Scopes:      []string{"im:chat:update"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "chat-id", Desc: "chat ID (oc_xxx)", Required: true},
		{Name: "name", Desc: "group name (max 60 chars)"},
		{Name: "description", Desc: "group description (max 100 chars)"},
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		chatID := runtime.Str("chat-id")
		body := buildUpdateChatBody(runtime)
		return common.NewDryRunAPI().
			PUT(fmt.Sprintf("/open-apis/im/v1/chats/%s", validate.EncodePathSegment(chatID))).
			Params(map[string]interface{}{"user_id_type": "open_id"}).
			Body(body)
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		chat := runtime.Str("chat-id")
		if _, err := common.ValidateChatID(chat); err != nil {
			return err
		}

		name := runtime.Str("name")
		if name != "" && len([]rune(name)) > 60 {
			return output.ErrValidation("--name exceeds the maximum of 60 characters (got %d)", len([]rune(name)))
		}

		if desc := runtime.Str("description"); desc != "" && len([]rune(desc)) > 100 {
			return output.ErrValidation("--description exceeds the maximum of 100 characters (got %d)", len([]rune(desc)))
		}

		body := buildUpdateChatBody(runtime)
		if len(body) == 0 {
			return output.ErrValidation("at least one field must be specified to update")
		}

		return nil
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		chatID := runtime.Str("chat-id")
		body := buildUpdateChatBody(runtime)

		_, err := runtime.DoAPIJSON(http.MethodPut,
			fmt.Sprintf("/open-apis/im/v1/chats/%s", validate.EncodePathSegment(chatID)),
			larkcore.QueryParams{"user_id_type": []string{"open_id"}},
			body,
		)
		if err != nil {
			return err
		}

		runtime.OutFormat(map[string]interface{}{"chat_id": chatID}, nil, func(w io.Writer) {
			fmt.Fprintf(w, "Group updated successfully (chat_id: %s)\n", chatID)
		})
		return nil
	},
}
View Source
var ImMessagesMGet = common.Shortcut{
	Service:     "im",
	Command:     "+messages-mget",
	Description: "Batch get messages by IDs; user/bot; fetches up to 50 om_ message IDs, formats sender names, expands thread replies",
	Risk:        "read",
	Scopes:      []string{"im:message:readonly"},
	UserScopes:  []string{"im:message.group_msg:get_as_user", "im:message.p2p_msg:get_as_user", "contact:user.basic_profile:readonly"},
	BotScopes:   []string{"im:message.group_msg", "im:message.p2p_msg:readonly", "contact:user.base:readonly"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "message-ids", Desc: "message IDs, comma-separated (om_xxx,om_yyy)", Required: true},
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		ids := common.SplitCSV(runtime.Str("message-ids"))
		return common.NewDryRunAPI().GET(buildMGetURL(ids))
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		ids := common.SplitCSV(runtime.Str("message-ids"))
		if len(ids) == 0 {
			return output.ErrValidation("--message-ids is required (comma-separated om_xxx)")
		}
		if len(ids) > maxMGetMessageIDs {
			return output.ErrValidation("--message-ids supports at most %d IDs per request (got %d)", maxMGetMessageIDs, len(ids))
		}
		for _, id := range ids {
			if _, err := validateMessageID(id); err != nil {
				return err
			}
		}
		return nil
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		ids := common.SplitCSV(runtime.Str("message-ids"))
		mgetURL := buildMGetURL(ids)

		data, err := runtime.DoAPIJSON(http.MethodGet, mgetURL, nil, nil)
		if err != nil {
			return err
		}

		rawItems, _ := data["items"].([]interface{})

		nameCache := make(map[string]string)
		messages := make([]map[string]interface{}, 0, len(rawItems))
		for _, item := range rawItems {
			m, _ := item.(map[string]interface{})
			messages = append(messages, convertlib.FormatMessageItem(m, runtime, nameCache))
		}

		convertlib.ResolveSenderNames(runtime, messages, nameCache)
		convertlib.AttachSenderNames(messages, nameCache)
		convertlib.ExpandThreadReplies(runtime, messages, nameCache, convertlib.ThreadRepliesPerThread, convertlib.ThreadRepliesTotalLimit)

		outData := map[string]interface{}{
			"messages": messages,
			"total":    len(messages),
		}
		runtime.OutFormat(outData, nil, func(w io.Writer) {
			if len(messages) == 0 {
				fmt.Fprintln(w, "No messages found.")
				return
			}
			var rows []map[string]interface{}
			for _, msg := range messages {
				row := map[string]interface{}{
					"message_id": msg["message_id"],
					"time":       msg["create_time"],
					"type":       msg["msg_type"],
				}
				if sender, ok := msg["sender"].(map[string]interface{}); ok {
					if name, _ := sender["name"].(string); name != "" {
						row["sender"] = name
					}
				}
				if content, _ := msg["content"].(string); content != "" {
					row["content"] = convertlib.TruncateContent(content, 40)
				}
				rows = append(rows, row)
			}
			output.PrintTable(w, rows)
			fmt.Fprintf(w, "\n%d message(s)\ntip: use --format json to view full message content\n", len(messages))
		})
		return nil
	},
}
View Source
var ImMessagesReply = common.Shortcut{
	Service:     "im",
	Command:     "+messages-reply",
	Description: "Reply to a message (supports thread replies); user/bot; supports text/markdown/post/media replies, reply-in-thread, idempotency key",
	Risk:        "write",
	Scopes:      []string{"im:message:send_as_bot"},
	UserScopes:  []string{"im:message.send_as_user", "im:message"},
	BotScopes:   []string{"im:message:send_as_bot"},
	AuthTypes:   []string{"bot", "user"},
	Flags: []common.Flag{
		{Name: "message-id", Desc: "message ID (om_xxx)", Required: true},
		{Name: "msg-type", Default: "text", Desc: "message type for --content JSON; when using --text/--markdown/--image/--file/--video/--audio, the effective type is inferred automatically", Enum: []string{"text", "post", "image", "file", "audio", "media", "interactive", "share_chat", "share_user"}},
		{Name: "content", Desc: "(one of --content/--text/--markdown/--image/--file/--video/--audio required) message content JSON"},
		{Name: "text", Desc: "plain text message (auto-wrapped as JSON)"},
		{Name: "markdown", Desc: "markdown text (auto-wrapped as post format with style optimization; image URLs auto-resolved)"},
		{Name: "image", Desc: "image_key, local file path"},
		{Name: "file", Desc: "file_key, local file path"},
		{Name: "video", Desc: "video file_key, local file path; must be used together with --video-cover"},
		{Name: "video-cover", Desc: "video cover image_key, local file path; required when using --video"},
		{Name: "audio", Desc: "audio file_key, local file path"},
		{Name: "reply-in-thread", Type: "bool", Desc: "reply in thread (message appears in thread stream instead of main chat)"},
		{Name: "idempotency-key", Desc: "idempotency key (prevents duplicate sends)"},
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		messageId := runtime.Str("message-id")
		msgType := runtime.Str("msg-type")
		content := runtime.Str("content")
		desc := ""
		text := runtime.Str("text")
		markdown := runtime.Str("markdown")
		imageKey := runtime.Str("image")
		fileKey := runtime.Str("file")
		videoKey := runtime.Str("video")
		videoCoverKey := runtime.Str("video-cover")
		audioKey := runtime.Str("audio")
		replyInThread := runtime.Bool("reply-in-thread")
		idempotencyKey := runtime.Str("idempotency-key")

		if markdown != "" {
			msgType = "post"
			content, desc = wrapMarkdownAsPostForDryRun(markdown)
		} else if mt, c, d := buildMediaContentFromKey(text, imageKey, fileKey, videoKey, videoCoverKey, audioKey); mt != "" {
			msgType, content, desc = mt, c, d
		}
		if msgType == "text" || msgType == "post" {
			content = normalizeAtMentions(content)
		}

		body := map[string]interface{}{"msg_type": msgType, "content": content}
		if replyInThread {
			body["reply_in_thread"] = true
		}
		if idempotencyKey != "" {
			body["uuid"] = idempotencyKey
		}

		d := common.NewDryRunAPI()
		if desc != "" {
			d.Desc(desc)
		}
		return d.
			POST("/open-apis/im/v1/messages/:message_id/reply").
			Body(body).
			Set("message_id", messageId)
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		messageId := runtime.Str("message-id")
		msgType := runtime.Str("msg-type")
		content := runtime.Str("content")
		text := runtime.Str("text")
		markdown := runtime.Str("markdown")
		imageKey := runtime.Str("image")
		fileKey := runtime.Str("file")
		videoKey := runtime.Str("video")
		videoCoverKey := runtime.Str("video-cover")
		audioKey := runtime.Str("audio")

		if !isMediaKey(imageKey) {
			if _, err := validate.SafeLocalFlagPath("--image", imageKey); err != nil {
				return output.ErrValidation("%v", err)
			}
		}
		if !isMediaKey(fileKey) {
			if _, err := validate.SafeLocalFlagPath("--file", fileKey); err != nil {
				return output.ErrValidation("%v", err)
			}
		}
		if !isMediaKey(videoKey) {
			if _, err := validate.SafeLocalFlagPath("--video", videoKey); err != nil {
				return output.ErrValidation("%v", err)
			}
		}
		if !isMediaKey(videoCoverKey) {
			if _, err := validate.SafeLocalFlagPath("--video-cover", videoCoverKey); err != nil {
				return output.ErrValidation("%v", err)
			}
		}
		if !isMediaKey(audioKey) {
			if _, err := validate.SafeLocalFlagPath("--audio", audioKey); err != nil {
				return output.ErrValidation("%v", err)
			}
		}

		if messageId == "" {
			return output.ErrValidation("--message-id is required (om_xxx)")
		}
		if _, err := validateMessageID(messageId); err != nil {
			return err
		}

		if msg := validateContentFlags(text, markdown, content, imageKey, fileKey, videoKey, videoCoverKey, audioKey); msg != "" {
			return output.ErrValidation(msg)
		}
		if content != "" && !json.Valid([]byte(content)) {
			return output.ErrValidation("--content is not valid JSON: %s\nexample: --content '{\"text\":\"hello\"}' or --text 'hello'", content)
		}
		if msg := validateExplicitMsgType(runtime.Cmd, msgType, text, markdown, imageKey, fileKey, videoKey, audioKey); msg != "" {
			return output.ErrValidation(msg)
		}

		return nil
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		messageId := runtime.Str("message-id")
		msgType := runtime.Str("msg-type")
		content := runtime.Str("content")
		text := runtime.Str("text")
		markdown := runtime.Str("markdown")
		imageVal := runtime.Str("image")
		fileVal := runtime.Str("file")
		videoVal := runtime.Str("video")
		videoCoverVal := runtime.Str("video-cover")
		audioVal := runtime.Str("audio")
		replyInThread := runtime.Bool("reply-in-thread")
		idempotencyKey := runtime.Str("idempotency-key")
		if !isMediaKey(imageVal) {
			if _, err := validate.SafeLocalFlagPath("--image", imageVal); err != nil {
				return output.ErrValidation("%v", err)
			}
		}
		if !isMediaKey(fileVal) {
			if _, err := validate.SafeLocalFlagPath("--file", fileVal); err != nil {
				return output.ErrValidation("%v", err)
			}
		}
		if !isMediaKey(videoVal) {
			if _, err := validate.SafeLocalFlagPath("--video", videoVal); err != nil {
				return output.ErrValidation("%v", err)
			}
		}
		if !isMediaKey(videoCoverVal) {
			if _, err := validate.SafeLocalFlagPath("--video-cover", videoCoverVal); err != nil {
				return output.ErrValidation("%v", err)
			}
		}
		if !isMediaKey(audioVal) {
			if _, err := validate.SafeLocalFlagPath("--audio", audioVal); err != nil {
				return output.ErrValidation("%v", err)
			}
		}

		if markdown != "" {
			msgType, content = "post", resolveMarkdownAsPost(ctx, runtime, markdown)
		} else if mt, c, err := resolveMediaContent(ctx, runtime, text, imageVal, fileVal, videoVal, videoCoverVal, audioVal); err != nil {
			return err
		} else if mt != "" {
			msgType, content = mt, c
		}

		normalizedContent := content
		if msgType == "text" || msgType == "post" {
			normalizedContent = normalizeAtMentions(content)
		}

		data := map[string]interface{}{
			"msg_type": msgType,
			"content":  normalizedContent,
		}
		if replyInThread {
			data["reply_in_thread"] = true
		}
		if idempotencyKey != "" {
			data["uuid"] = idempotencyKey
		}

		resData, err := runtime.DoAPIJSON(http.MethodPost,
			fmt.Sprintf("/open-apis/im/v1/messages/%s/reply", validate.EncodePathSegment(messageId)),
			nil, data)
		if err != nil {
			return err
		}

		runtime.Out(map[string]interface{}{
			"message_id":  resData["message_id"],
			"chat_id":     resData["chat_id"],
			"create_time": common.FormatTimeWithSeconds(resData["create_time"]),
		}, nil)
		return nil
	},
}
View Source
var ImMessagesResourcesDownload = common.Shortcut{
	Service:     "im",
	Command:     "+messages-resources-download",
	Description: "Download images/files from a message; user/bot; downloads image/file resources by message-id and file-key to a safe relative output path",
	Risk:        "write",
	Scopes:      []string{"im:message:readonly"},
	AuthTypes:   []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "message-id", Desc: "message ID (om_xxx)", Required: true},
		{Name: "file-key", Desc: "resource key (img_xxx or file_xxx)", Required: true},
		{Name: "type", Desc: "resource type (image or file)", Required: true, Enum: []string{"image", "file"}},
		{Name: "output", Desc: "local save path (relative only, no .. traversal; defaults to file_key)"},
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		fileKey := runtime.Str("file-key")
		outputPath := runtime.Str("output")
		if outputPath == "" {
			outputPath = fileKey
		}
		return common.NewDryRunAPI().
			GET("/open-apis/im/v1/messages/:message_id/resources/:file_key").
			Params(map[string]interface{}{"type": runtime.Str("type")}).
			Set("message_id", runtime.Str("message-id")).Set("file_key", fileKey).
			Set("type", runtime.Str("type")).Set("output", outputPath)
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if messageId := runtime.Str("message-id"); messageId == "" {
			return output.ErrValidation("--message-id is required (om_xxx)")
		} else if _, err := validateMessageID(messageId); err != nil {
			return err
		}
		relPath, err := normalizeDownloadOutputPath(runtime.Str("file-key"), runtime.Str("output"))
		if err != nil {
			return output.ErrValidation("%s", err)
		}
		if _, err := validate.SafeOutputPath(relPath); err != nil {
			return output.ErrValidation("unsafe output path: %s", err)
		}
		return nil
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		messageId := runtime.Str("message-id")
		fileKey := runtime.Str("file-key")
		fileType := runtime.Str("type")
		relPath, err := normalizeDownloadOutputPath(fileKey, runtime.Str("output"))
		if err != nil {
			return output.ErrValidation("invalid output path: %s", err)
		}
		safePath, err := validate.SafeOutputPath(relPath)
		if err != nil {
			return output.ErrValidation("unsafe output path: %s", err)
		}

		finalPath, sizeBytes, err := downloadIMResourceToPath(ctx, runtime, messageId, fileKey, fileType, safePath)
		if err != nil {
			return err
		}

		runtime.Out(map[string]interface{}{"saved_path": finalPath, "size_bytes": sizeBytes}, nil)
		return nil
	},
}
View Source
var ImMessagesSearch = common.Shortcut{
	Service:     "im",
	Command:     "+messages-search",
	Description: "Search messages across chats (supports keyword, sender, time range filters) with user identity; user-only; filters by chat/sender/attachment/time, enriches results via mget and chats batch_query",
	Risk:        "read",
	Scopes:      []string{"search:message", "contact:user.basic_profile:readonly"},
	AuthTypes:   []string{"user"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "query", Desc: "search keyword"},
		{Name: "chat-id", Desc: "limit to chat IDs, comma-separated"},
		{Name: "sender", Desc: "sender open_ids, comma-separated"},
		{Name: "include-attachment-type", Desc: "include attachment type filter", Enum: []string{"file", "image", "video", "link"}},
		{Name: "chat-type", Desc: "chat type", Enum: []string{"group", "p2p"}},
		{Name: "sender-type", Desc: "sender type", Enum: []string{"user", "bot"}},
		{Name: "exclude-sender-type", Desc: "exclude sender type", Enum: []string{"user", "bot"}},
		{Name: "is-at-me", Type: "bool", Desc: "only messages that @me"},
		{Name: "start", Desc: "start time(ISO 8601) with local timezone offset (e.g. 2026-03-24T00:00:00+08:00)"},
		{Name: "end", Desc: "end time(ISO 8601) with local timezone offset (e.g. 2026-03-25T23:59:59+08:00)"},
		{Name: "page-size", Default: "20", Desc: "page size (1-50)"},
		{Name: "page-token", Desc: "page token"},
		{Name: "page-all", Type: "bool", Desc: "automatically paginate search results"},
		{Name: "page-limit", Type: "int", Default: "20", Desc: "max search pages when auto-pagination is enabled (default 20, max 40)"},
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		req, err := buildMessagesSearchRequest(runtime)
		if err != nil {
			return common.NewDryRunAPI().Desc(err.Error())
		}
		dryParams := make(map[string]interface{}, len(req.params))
		for k, vs := range req.params {
			if len(vs) > 0 {
				dryParams[k] = vs[0]
			}
		}
		autoPaginate, pageLimit := messagesSearchPaginationConfig(runtime)
		d := common.NewDryRunAPI()
		if autoPaginate {
			d = d.Desc(fmt.Sprintf("Step 1: search messages (auto-paginates up to %d page(s))", pageLimit))
		} else {
			d = d.Desc("Step 1: search messages")
		}
		return d.
			POST("/open-apis/im/v1/messages/search").
			Params(dryParams).
			Body(req.body).
			Desc("Step 2 (if results): GET /open-apis/im/v1/messages/mget?message_ids=...  — batch fetch message details (max 50)").
			Desc("Step 3 (if results): POST /open-apis/im/v1/chats/batch_query  — fetch chat names for context")
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		_, err := buildMessagesSearchRequest(runtime)
		return err
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		req, err := buildMessagesSearchRequest(runtime)
		if err != nil {
			return err
		}

		rawItems, hasMore, nextPageToken, truncatedByLimit, pageLimit, err := searchMessages(runtime, req)
		if err != nil {
			return err
		}

		if len(rawItems) == 0 {
			outData := map[string]interface{}{
				"messages":   []interface{}{},
				"total":      0,
				"has_more":   hasMore,
				"page_token": nextPageToken,
			}
			runtime.OutFormat(outData, nil, func(w io.Writer) {
				fmt.Fprintln(w, "No matching messages found.")
			})
			return nil
		}

		messageIds := make([]string, 0, len(rawItems))
		for _, item := range rawItems {
			if itemMap, ok := item.(map[string]interface{}); ok {
				if metaData, ok := itemMap["meta_data"].(map[string]interface{}); ok {
					if id, ok := metaData["message_id"].(string); ok && id != "" {
						messageIds = append(messageIds, id)
					}
				}
			}
		}

		msgItems, err := batchMGetMessages(runtime, messageIds)
		if err != nil {

			outData := map[string]interface{}{
				"message_ids": messageIds,
				"total":       len(messageIds),
				"has_more":    hasMore,
				"page_token":  nextPageToken,
				"note":        "failed to fetch message details, returning ID list only",
			}
			runtime.OutFormat(outData, nil, func(w io.Writer) {
				fmt.Fprintf(w, "Found %d messages (failed to fetch details):\n", len(messageIds))
				for _, id := range messageIds {
					fmt.Fprintln(w, " ", id)
				}
			})
			return nil
		}

		chatIds := make([]string, 0, len(msgItems))
		chatSeen := make(map[string]bool)
		for _, item := range msgItems {
			m, _ := item.(map[string]interface{})
			if chatId, _ := m["chat_id"].(string); chatId != "" {
				if !chatSeen[chatId] {
					chatSeen[chatId] = true
					chatIds = append(chatIds, chatId)
				}
			}
		}
		chatContexts := map[string]map[string]interface{}{}
		if len(chatIds) > 0 {
			chatContexts = batchQueryChatContexts(runtime, chatIds)
		}

		nameCache := make(map[string]string)
		enriched := make([]map[string]interface{}, 0, len(msgItems))
		for _, item := range msgItems {
			m, _ := item.(map[string]interface{})
			chatId, _ := m["chat_id"].(string)

			msg := convertlib.FormatMessageItem(m, runtime, nameCache)
			if chatId != "" {
				msg["chat_id"] = chatId
			}
			if chatCtx, ok := chatContexts[chatId]; ok {
				chatMode, _ := chatCtx["chat_mode"].(string)
				chatName, _ := chatCtx["name"].(string)
				if chatMode == "p2p" {
					msg["chat_type"] = "p2p"
					if p2pId, _ := chatCtx["p2p_target_id"].(string); p2pId != "" {
						msg["chat_partner"] = map[string]interface{}{"open_id": p2pId}
					}
				} else {
					msg["chat_type"] = chatMode
					if chatName != "" {
						msg["chat_name"] = chatName
					}
				}
			}
			enriched = append(enriched, msg)
		}

		convertlib.ResolveSenderNames(runtime, enriched, nameCache)
		convertlib.AttachSenderNames(enriched, nameCache)

		outData := map[string]interface{}{
			"messages":   enriched,
			"total":      len(enriched),
			"has_more":   hasMore,
			"page_token": nextPageToken,
		}
		runtime.OutFormat(outData, nil, func(w io.Writer) {
			if len(enriched) == 0 {
				fmt.Fprintln(w, "No matching messages found.")
				return
			}
			var rows []map[string]interface{}
			for _, msg := range enriched {
				row := map[string]interface{}{
					"time": msg["create_time"],
					"type": msg["msg_type"],
				}
				if sender, ok := msg["sender"].(map[string]interface{}); ok {
					if name, _ := sender["name"].(string); name != "" {
						row["sender"] = name
					}
				}
				if chatName, ok := msg["chat_name"].(string); ok && chatName != "" {
					row["chat"] = chatName
				} else if chatType, ok := msg["chat_type"].(string); ok && chatType == "p2p" {
					row["chat"] = "p2p"
				} else if cid, ok := msg["chat_id"].(string); ok {
					row["chat"] = cid
				}
				if content, _ := msg["content"].(string); content != "" {
					row["content"] = convertlib.TruncateContent(content, 30)
				}
				rows = append(rows, row)
			}
			output.PrintTable(w, rows)
			moreHint := ""
			if hasMore {
				moreHint = " (more available, use --page-token to fetch next page)"
			}
			fmt.Fprintf(w, "\n%d search result(s)%s\n", len(enriched), moreHint)
			if truncatedByLimit {
				fmt.Fprintf(w, "warning: stopped after fetching %d page(s); use --page-limit, --page-all, or --page-token to continue\n", pageLimit)
			}
		})
		return nil
	},
}
View Source
var ImMessagesSend = common.Shortcut{
	Service:     "im",
	Command:     "+messages-send",
	Description: "Send a message to a chat or direct message; user/bot; sends to chat-id or user-id with text/markdown/post/media, supports idempotency key",
	Risk:        "write",
	Scopes:      []string{"im:message:send_as_bot"},
	UserScopes:  []string{"im:message.send_as_user", "im:message"},
	BotScopes:   []string{"im:message:send_as_bot"},
	AuthTypes:   []string{"bot", "user"},
	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: "msg-type", Default: "text", Desc: "message type for --content JSON; when using --text/--markdown/--image/--file/--video/--audio, the effective type is inferred automatically", Enum: []string{"text", "post", "image", "file", "audio", "media", "interactive", "share_chat", "share_user"}},
		{Name: "content", Desc: "(one of --content/--text/--markdown/--image/--file/--video/--audio required) message content JSON"},
		{Name: "text", Desc: "plain text message (auto-wrapped as JSON)"},
		{Name: "markdown", Desc: "markdown text (auto-wrapped as post format with style optimization; image URLs auto-resolved)"},
		{Name: "idempotency-key", Desc: "idempotency key (prevents duplicate sends)"},
		{Name: "image", Desc: "image_key, local file path"},
		{Name: "file", Desc: "file_key, local file path"},
		{Name: "video", Desc: "video file_key, local file path; must be used together with --video-cover"},
		{Name: "video-cover", Desc: "video cover image_key, local file path; required when using --video"},
		{Name: "audio", Desc: "audio file_key, local file path"},
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		chatFlag := runtime.Str("chat-id")
		userFlag := runtime.Str("user-id")
		msgType := runtime.Str("msg-type")
		content := runtime.Str("content")
		desc := ""
		text := runtime.Str("text")
		markdown := runtime.Str("markdown")
		idempotencyKey := runtime.Str("idempotency-key")
		imageKey := runtime.Str("image")
		fileKey := runtime.Str("file")
		videoKey := runtime.Str("video")
		videoCoverKey := runtime.Str("video-cover")
		audioKey := runtime.Str("audio")

		if markdown != "" {
			msgType = "post"
			content, desc = wrapMarkdownAsPostForDryRun(markdown)
		} else if mt, c, d := buildMediaContentFromKey(text, imageKey, fileKey, videoKey, videoCoverKey, audioKey); mt != "" {
			msgType, content, desc = mt, c, d
		}

		receiveIdType := "chat_id"
		receiveId := chatFlag
		if userFlag != "" {
			receiveIdType = "open_id"
			receiveId = userFlag
		}

		if msgType == "text" || msgType == "post" {
			content = normalizeAtMentions(content)
		}

		body := map[string]interface{}{"receive_id": receiveId, "msg_type": msgType, "content": content}
		if idempotencyKey != "" {
			body["uuid"] = idempotencyKey
		}

		d := common.NewDryRunAPI()
		if desc != "" {
			d.Desc(desc)
		}
		return d.
			POST("/open-apis/im/v1/messages").
			Params(map[string]interface{}{"receive_id_type": receiveIdType}).
			Body(body)
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		chatFlag := runtime.Str("chat-id")
		userFlag := runtime.Str("user-id")
		msgType := runtime.Str("msg-type")
		content := runtime.Str("content")
		text := runtime.Str("text")
		markdown := runtime.Str("markdown")
		imageKey := runtime.Str("image")
		fileKey := runtime.Str("file")
		videoKey := runtime.Str("video")
		videoCoverKey := runtime.Str("video-cover")
		audioKey := runtime.Str("audio")

		if !isMediaKey(imageKey) {
			if _, err := validate.SafeLocalFlagPath("--image", imageKey); err != nil {
				return output.ErrValidation("%v", err)
			}
		}
		if !isMediaKey(fileKey) {
			if _, err := validate.SafeLocalFlagPath("--file", fileKey); err != nil {
				return output.ErrValidation("%v", err)
			}
		}
		if !isMediaKey(videoKey) {
			if _, err := validate.SafeLocalFlagPath("--video", videoKey); err != nil {
				return output.ErrValidation("%v", err)
			}
		}
		if !isMediaKey(videoCoverKey) {
			if _, err := validate.SafeLocalFlagPath("--video-cover", videoCoverKey); err != nil {
				return output.ErrValidation("%v", err)
			}
		}
		if !isMediaKey(audioKey) {
			if _, err := validate.SafeLocalFlagPath("--audio", audioKey); err != nil {
				return output.ErrValidation("%v", err)
			}
		}

		if err := common.ExactlyOne(runtime, "chat-id", "user-id"); err != nil {
			return err
		}

		if chatFlag != "" {
			if _, err := common.ValidateChatID(chatFlag); err != nil {
				return err
			}
		}
		if userFlag != "" {
			if _, err := common.ValidateUserID(userFlag); err != nil {
				return err
			}
		}

		if msg := validateContentFlags(text, markdown, content, imageKey, fileKey, videoKey, videoCoverKey, audioKey); msg != "" {
			return common.FlagErrorf(msg)
		}
		if content != "" && !json.Valid([]byte(content)) {
			return common.FlagErrorf("--content is not valid JSON: %s\nexample: --content '{\"text\":\"hello\"}' or --text 'hello'", content)
		}
		if msg := validateExplicitMsgType(runtime.Cmd, msgType, text, markdown, imageKey, fileKey, videoKey, audioKey); msg != "" {
			return common.FlagErrorf(msg)
		}

		return nil
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		chatFlag := runtime.Str("chat-id")
		userFlag := runtime.Str("user-id")
		msgType := runtime.Str("msg-type")
		content := runtime.Str("content")
		text := runtime.Str("text")
		markdown := runtime.Str("markdown")
		idempotencyKey := runtime.Str("idempotency-key")
		imageVal := runtime.Str("image")
		fileVal := runtime.Str("file")
		videoVal := runtime.Str("video")
		videoCoverVal := runtime.Str("video-cover")
		audioVal := runtime.Str("audio")
		if !isMediaKey(imageVal) {
			if _, err := validate.SafeLocalFlagPath("--image", imageVal); err != nil {
				return output.ErrValidation("%v", err)
			}
		}
		if !isMediaKey(fileVal) {
			if _, err := validate.SafeLocalFlagPath("--file", fileVal); err != nil {
				return output.ErrValidation("%v", err)
			}
		}
		if !isMediaKey(videoVal) {
			if _, err := validate.SafeLocalFlagPath("--video", videoVal); err != nil {
				return output.ErrValidation("%v", err)
			}
		}
		if !isMediaKey(videoCoverVal) {
			if _, err := validate.SafeLocalFlagPath("--video-cover", videoCoverVal); err != nil {
				return output.ErrValidation("%v", err)
			}
		}
		if !isMediaKey(audioVal) {
			if _, err := validate.SafeLocalFlagPath("--audio", audioVal); err != nil {
				return output.ErrValidation("%v", err)
			}
		}

		if markdown != "" {
			msgType, content = "post", resolveMarkdownAsPost(ctx, runtime, markdown)
		} else if mt, c, err := resolveMediaContent(ctx, runtime, text, imageVal, fileVal, videoVal, videoCoverVal, audioVal); err != nil {
			return err
		} else if mt != "" {
			msgType, content = mt, c
		}

		receiveIdType := "chat_id"
		receiveId := chatFlag
		if userFlag != "" {
			receiveIdType = "open_id"
			receiveId = userFlag
		}

		normalizedContent := content
		if msgType == "text" || msgType == "post" {
			normalizedContent = normalizeAtMentions(content)
		}

		data := map[string]interface{}{
			"receive_id": receiveId,
			"msg_type":   msgType,
			"content":    normalizedContent,
		}
		if idempotencyKey != "" {
			data["uuid"] = idempotencyKey
		}

		resData, err := runtime.DoAPIJSON(http.MethodPost, "/open-apis/im/v1/messages",
			larkcore.QueryParams{"receive_id_type": []string{receiveIdType}}, data)
		if err != nil {
			return err
		}

		runtime.Out(map[string]interface{}{
			"message_id":  resData["message_id"],
			"chat_id":     resData["chat_id"],
			"create_time": common.FormatTimeWithSeconds(resData["create_time"]),
		}, nil)
		return nil
	},
}
View Source
var ImThreadsMessagesList = common.Shortcut{
	Service:     "im",
	Command:     "+threads-messages-list",
	Description: "List messages in a thread; user/bot; accepts om_/omt_ input, resolves message IDs to thread_id, supports sort/pagination",
	Risk:        "read",
	Scopes:      []string{"im:message:readonly"},
	UserScopes:  []string{"im:message.group_msg:get_as_user", "im:message.p2p_msg:get_as_user", "contact:user.basic_profile:readonly"},
	BotScopes:   []string{"im:message.group_msg", "im:message.p2p_msg:readonly", "contact:user.base:readonly"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "thread", Desc: "thread ID (om_xxx or omt_xxx)", Required: true},
		{Name: "sort", Default: "asc", Desc: "sort order", Enum: []string{"asc", "desc"}},
		{Name: "page-size", Default: "50", Desc: "page size (1-500)"},
		{Name: "page-token", Desc: "page token"},
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		threadFlag := runtime.Str("thread")
		sortFlag := runtime.Str("sort")
		pageSizeStr := runtime.Str("page-size")
		pageToken := runtime.Str("page-token")

		sortType := "ByCreateTimeAsc"
		if sortFlag == "desc" {
			sortType = "ByCreateTimeDesc"
		}

		pageSize, _ := common.ValidatePageSize(runtime, "page-size", threadsMessagesMaxPageSize, 1, threadsMessagesMaxPageSize)

		d := common.NewDryRunAPI()
		containerID := threadFlag
		if messageIDRe.MatchString(threadFlag) {
			d.Desc("(--thread provided as message ID) Will resolve thread_id via GET /open-apis/im/v1/messages/:message_id at execution time")
			containerID = "<resolved_thread_id>"
		}

		params := map[string]interface{}{
			"container_id_type":     "thread",
			"container_id":          containerID,
			"sort_type":             sortType,
			"page_size":             pageSize,
			"card_msg_content_type": "raw_card_content",
		}
		if pageToken != "" {
			params["page_token"] = pageToken
		}

		return d.
			GET("/open-apis/im/v1/messages").
			Params(params).
			Set("thread", threadFlag).Set("sort", sortFlag).Set("page_size", pageSizeStr)
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		threadId := runtime.Str("thread")
		if threadId == "" {
			return output.ErrValidation("--thread is required (om_xxx or omt_xxx)")
		}
		if !strings.HasPrefix(threadId, "om_") && !strings.HasPrefix(threadId, "omt_") {
			return output.ErrValidation("invalid --thread %q: must start with om_ or omt_", threadId)
		}
		_, err := common.ValidatePageSize(runtime, "page-size", threadsMessagesMaxPageSize, 1, threadsMessagesMaxPageSize)
		return err
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		threadId, err := resolveThreadID(runtime, runtime.Str("thread"))
		if err != nil {
			return err
		}
		sortFlag := runtime.Str("sort")
		pageToken := runtime.Str("page-token")

		sortType := "ByCreateTimeAsc"
		if sortFlag == "desc" {
			sortType = "ByCreateTimeDesc"
		}

		pageSize, _ := common.ValidatePageSize(runtime, "page-size", threadsMessagesMaxPageSize, 1, threadsMessagesMaxPageSize)

		params := map[string][]string{
			"container_id_type":     []string{"thread"},
			"container_id":          []string{threadId},
			"sort_type":             []string{sortType},
			"page_size":             []string{strconv.Itoa(pageSize)},
			"card_msg_content_type": []string{"raw_card_content"},
		}
		if pageToken != "" {
			params["page_token"] = []string{pageToken}
		}

		data, err := runtime.DoAPIJSON(http.MethodGet, "/open-apis/im/v1/messages", params, nil)
		if err != nil {
			return err
		}
		rawItems, _ := data["items"].([]interface{})
		hasMore, nextPageToken := common.PaginationMeta(data)

		nameCache := make(map[string]string)
		messages := make([]map[string]interface{}, 0, len(rawItems))
		for _, item := range rawItems {
			m, _ := item.(map[string]interface{})
			messages = append(messages, convertlib.FormatMessageItem(m, runtime, nameCache))
		}

		convertlib.ResolveSenderNames(runtime, messages, nameCache)
		convertlib.AttachSenderNames(messages, nameCache)

		outData := map[string]interface{}{
			"thread_id":  threadId,
			"messages":   messages,
			"total":      len(messages),
			"has_more":   hasMore,
			"page_token": nextPageToken,
		}
		runtime.OutFormat(outData, nil, func(w io.Writer) {
			if len(messages) == 0 {
				fmt.Fprintln(w, "No messages in this thread.")
				return
			}
			var rows []map[string]interface{}
			for _, msg := range messages {
				row := map[string]interface{}{
					"time": msg["create_time"],
					"type": msg["msg_type"],
				}
				if sender, ok := msg["sender"].(map[string]interface{}); ok {
					if name, _ := sender["name"].(string); name != "" {
						row["sender"] = name
					}
				}
				if content, _ := msg["content"].(string); content != "" {
					row["content"] = convertlib.TruncateContent(content, 40)
				}
				rows = append(rows, row)
			}
			output.PrintTable(w, rows)
			moreHint := ""
			if hasMore {
				moreHint = fmt.Sprintf(" (more available, page_token: %s)", nextPageToken)
			}
			fmt.Fprintf(w, "\n%d thread message(s)%s\ntip: use --format json to view full message content\n", len(messages), moreHint)
		})
		return nil
	},
}

Functions

func Shortcuts

func Shortcuts() []common.Shortcut

Shortcuts returns all im shortcuts.

Types

This section is empty.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL