Files
wehub-resource-sync bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

219 lines
7.6 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package im
import (
"context"
"fmt"
"strings"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/shortcuts/common"
)
// ImFlagCreate provides the +flag-create shortcut for creating a bookmark on a message.
var ImFlagCreate = common.Shortcut{
Service: "im",
Command: "+flag-create",
Description: "Create a bookmark on a message; user-only; defaults to message-layer flag; use --flag-type feed for feed-layer flag (item_type auto-detected from chat mode)",
Risk: "write",
UserScopes: flagWriteLookupScopes,
AuthTypes: []string{"user"},
HasFormat: true,
Flags: []common.Flag{
{Name: "message-id", Desc: "message ID (om_xxx)"},
{Name: "item-type", Desc: "item type override: default|thread|msg_thread (rarely needed)"},
{Name: "flag-type", Desc: "flag type: message (default) or feed"},
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
_, err := buildCreateItemForPreview(runtime)
return err
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
item, err := buildCreateItemForPreview(runtime)
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
d := common.NewDryRunAPI().
POST("/open-apis/im/v1/flags").
Body(map[string]any{"flag_items": []any{item}})
if m, ok := item.(map[string]string); ok && m["item_type"] == "<auto:thread|msg_thread>" {
d.Desc("feed-layer item_type is auto-detected at execution time by reading the message chat and chat_mode")
}
return d
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
item, err := buildCreateItem(runtime)
if err != nil {
return err
}
// Combo validation already done in Validate, but double-check as a safety net.
if !isValidCombo(parseItemTypeFromRaw(item.ItemType), parseFlagTypeFromRaw(item.FlagType)) {
return errs.NewValidationError(errs.SubtypeInvalidArgument,
"invalid (item_type=%s, flag_type=%s) combination; the server only accepts "+
"(default, message), (thread, feed), or (msg_thread, feed)",
item.ItemType, item.FlagType).WithParams(
errs.InvalidParam{Name: "--item-type", Reason: "unsupported with the given --flag-type"},
errs.InvalidParam{Name: "--flag-type", Reason: "unsupported with the given --item-type"})
}
data, err := runtime.DoAPIJSONTyped("POST", "/open-apis/im/v1/flags", nil,
map[string]any{"flag_items": []flagItem{item}})
if err != nil {
return err
}
runtime.Out(data, nil)
return nil
},
}
// buildCreateItemForPreview derives a preview payload without making network calls.
// Feed-layer execution auto-detects item_type from chat_mode, but dry-run must
// not query the message or chat APIs, so it uses an explicit placeholder.
func buildCreateItemForPreview(rt *common.RuntimeContext) (any, error) {
id, err := flagMessageID(rt)
if err != nil {
return nil, err
}
itOverride := strings.TrimSpace(rt.Str("item-type"))
ftOverride := strings.TrimSpace(rt.Str("flag-type"))
combo, err := parseExplicitFlagCombo(itOverride, ftOverride)
if err != nil {
return nil, err
}
flagType := FlagTypeMessage
if combo.FlagTypeSet {
flagType = combo.FlagType
}
if flagType == FlagTypeMessage {
return newFlagItem(id, ItemTypeDefault, FlagTypeMessage), nil
}
if combo.ItemTypeSet {
return newFlagItem(id, combo.ItemType, FlagTypeFeed), nil
}
return map[string]string{
"item_id": id,
"item_type": "<auto:thread|msg_thread>",
"flag_type": fmt.Sprintf("%d", int(FlagTypeFeed)),
}, nil
}
// buildCreateItem derives a flagItem for the create path.
//
// Resolution logic:
// 1. No --flag-type or --flag-type=message → (default, message)
// 2. --flag-type=feed (no --item-type) → query message to get chat_id,
// then query chat_mode to determine: topic-style → (thread, feed), regular → (msg_thread, feed)
// 3. Both --item-type and --flag-type provided → honor verbatim (for edge cases)
func buildCreateItem(rt *common.RuntimeContext) (flagItem, error) {
id, err := flagMessageID(rt)
if err != nil {
return flagItem{}, err
}
itOverride := strings.TrimSpace(rt.Str("item-type"))
ftOverride := strings.TrimSpace(rt.Str("flag-type"))
combo, err := parseExplicitFlagCombo(itOverride, ftOverride)
if err != nil {
return flagItem{}, err
}
flagType := FlagTypeMessage
if combo.FlagTypeSet {
flagType = combo.FlagType
}
// Message-layer flag: always (default, message)
if flagType == FlagTypeMessage {
return newFlagItem(id, ItemTypeDefault, FlagTypeMessage), nil
}
// Feed-layer flag: need to determine item_type from chat_mode
if combo.ItemTypeSet {
// User explicitly specified item-type, honor it
return newFlagItem(id, combo.ItemType, FlagTypeFeed), nil
}
chatID, err := getMessageChatID(rt, id)
if err != nil {
return flagItem{}, appendIMRecoveryHint(err, "specify --item-type explicitly")
}
if chatID == "" {
return flagItem{}, errs.NewValidationError(errs.SubtypeInvalidArgument,
"message does not belong to a chat; feed-layer flags are only for messages in chats")
}
feedIT, err := resolveThreadFeedItemType(rt, chatID)
if err != nil {
return flagItem{}, appendIMRecoveryHint(err, "specify --item-type explicitly")
}
return newFlagItem(id, feedIT, FlagTypeFeed), nil
}
type explicitFlagCombo struct {
ItemType ItemType
FlagType FlagType
ItemTypeSet bool
FlagTypeSet bool
}
func parseExplicitFlagCombo(itOverride, ftOverride string) (explicitFlagCombo, error) {
itOverride = strings.TrimSpace(itOverride)
ftOverride = strings.TrimSpace(ftOverride)
var combo explicitFlagCombo
if itOverride != "" {
it, err := parseItemType(itOverride)
if err != nil {
return explicitFlagCombo{}, err
}
combo.ItemType = it
combo.ItemTypeSet = true
}
if ftOverride != "" {
ft, err := parseFlagType(ftOverride)
if err != nil {
return explicitFlagCombo{}, err
}
combo.FlagType = ft
combo.FlagTypeSet = true
}
if combo.ItemTypeSet && !combo.FlagTypeSet {
switch combo.ItemType {
case ItemTypeThread, ItemTypeMsgThread:
return explicitFlagCombo{}, errs.NewValidationError(errs.SubtypeInvalidArgument,
"--item-type=%s requires --flag-type=feed; message-layer flags always use item-type=default", itOverride).WithParams(
errs.InvalidParam{Name: "--item-type", Reason: "requires --flag-type=feed"},
errs.InvalidParam{Name: "--flag-type", Reason: "must be feed for this --item-type"})
case ItemTypeDefault:
return explicitFlagCombo{}, errs.NewValidationError(errs.SubtypeInvalidArgument,
"--item-type=default requires --flag-type=message; or omit both to use default behavior").WithParams(
errs.InvalidParam{Name: "--item-type", Reason: "default requires --flag-type=message"},
errs.InvalidParam{Name: "--flag-type", Reason: "must be message for --item-type=default"})
}
}
if combo.ItemTypeSet && combo.FlagTypeSet && !isValidCombo(combo.ItemType, combo.FlagType) {
return explicitFlagCombo{}, errs.NewValidationError(errs.SubtypeInvalidArgument,
"invalid --item-type=%s --flag-type=%s combination; supported pairs are default+message, thread+feed, and msg_thread+feed",
itOverride, ftOverride).WithParams(
errs.InvalidParam{Name: "--item-type", Reason: "unsupported pairing"},
errs.InvalidParam{Name: "--flag-type", Reason: "unsupported pairing"})
}
return combo, nil
}
// validateExplicitCombo validates the (item_type, flag_type) combination when
// the user explicitly provides flags. It does not make API calls - it only
// validates the logic for what the user explicitly specified.
func validateExplicitCombo(itOverride, ftOverride string) error {
_, err := parseExplicitFlagCombo(itOverride, ftOverride)
return err
}