Files
larksuite-cli/shortcuts/im/im_chat_create.go
shifengjuan-dev 5e0ac02f08 feat(im): add --chat-mode topic to +chat-create (#790)
Adds --chat-mode group|topic to lark-cli im +chat-create so users and AI agents can create 话题群 (topic chats) directly via the CLI. Without this, requests to create a topic chat silently fall back to a normal conversation group. Default remains group; chat_mode is now always emitted in the POST /open-apis/im/v1/chats request body.

Change-Id: I79385e2e8606f84e3f27de240d1b41037bf51261
2026-05-13 18:03:58 +08:00

179 lines
6.3 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package im
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/shortcuts/common"
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
)
// ImChatCreate is the +chat-create shortcut: creates a group chat or topic
// chat via POST /open-apis/im/v1/chats. Supports user and bot identities;
// --chat-mode selects group (default) or topic; --type selects private
// (default) or public; --users/--bots invite members at creation.
var ImChatCreate = common.Shortcut{
Service: "im",
Command: "+chat-create",
Description: "Create a group chat or topic chat; user/bot; --chat-mode group|topic; private/public; invites users/bots; optionally sets bot manager",
Risk: "write",
UserScopes: []string{"im:chat:create_by_user"},
BotScopes: []string{"im:chat:create"},
AuthTypes: []string{"bot", "user"},
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 bot (--as bot) or authorized user (--as user)"},
{Name: "type", Default: "private", Desc: "chat type", Enum: []string{"private", "public"}},
{Name: "chat-mode", Default: "group", Desc: "group mode (\"topic\" creates a topic chat; differs from a normal group in topic-message mode)", Enum: []string{"group", "topic"}},
{Name: "set-bot-manager", Type: "bool", Desc: "set the bot that creates this chat as manager (bot identity only)"},
},
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") && runtime.IsBot() {
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 {
if runtime.Bool("set-bot-manager") && !runtime.IsBot() {
return output.ErrValidation("--set-bot-manager is only supported with bot identity (--as bot)")
}
name := runtime.Str("name")
chatType := runtime.Str("type")
// Public groups must have a name with at least 2 characters.
if chatType == "public" && len([]rune(name)) < 2 {
return output.ErrValidation("--name is required for public groups and must be at least 2 characters")
}
// Group name length must not exceed 60 characters.
if len([]rune(name)) > 60 {
return output.ErrValidation("--name exceeds the maximum of 60 characters (got %d)", len([]rune(name)))
}
// Description length must not exceed 100 characters.
if desc := runtime.Str("description"); len([]rune(desc)) > 100 {
return output.ErrValidation("--description exceeds the maximum of 100 characters (got %d)", len([]rune(desc)))
}
// Validate users.
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
}
}
}
// Validate bots.
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)
}
}
}
// Validate owner.
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"],
}
// Try to fetch the group share link without blocking on failure.
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
},
}
// buildCreateChatBody assembles the POST /open-apis/im/v1/chats request
// body. chat_mode is always emitted; an empty value (which can slip past
// validateEnumFlags, since that helper skips empty strings) is pinned to
// "group" so the wire never carries an unspecified chat_mode value.
func buildCreateChatBody(runtime *common.RuntimeContext) map[string]interface{} {
chatMode := runtime.Str("chat-mode")
if chatMode == "" {
chatMode = "group"
}
body := map[string]interface{}{
"chat_type": runtime.Str("type"),
"chat_mode": chatMode,
}
if name := runtime.Str("name"); name != "" {
body["name"] = name
}
if desc := runtime.Str("description"); desc != "" {
body["description"] = desc
}
if users := runtime.Str("users"); users != "" {
body["user_id_list"] = common.SplitCSV(users)
}
if bots := runtime.Str("bots"); bots != "" {
body["bot_id_list"] = common.SplitCSV(bots)
}
if owner := runtime.Str("owner"); owner != "" {
body["owner_id"] = owner
}
return body
}