mirror of
https://github.com/larksuite/cli.git
synced 2026-07-06 08:12:36 +08:00
- +member-add: wrap POST /spaces/{id}/members; --member-type / --member-role
enums, optional --need-notification query (omitted entirely when the flag
is unset, instead of forcing need_notification=false), my_library
resolution under --as user, flattened single-member output
- +member-remove: wrap DELETE /spaces/{id}/members/{member_id}; surfaces the
required member_type + member_role body the API expects, my_library
resolution, fallback to echoing the caller's inputs when the API omits
the member echo
- +member-list: wrap GET /spaces/{id}/members; reuses the +space-list /
+node-list pagination contract (single page by default, --page-all walks
every page capped by --page-limit, --page-token resumes a cursor)
- All three reject bot identity + my_library upfront with a clear hint and
declare the narrowest scope the API accepts (wiki:member:create /
wiki:member:update / wiki:member:retrieve) so tokens carrying only the
narrow scope are not false-rejected by the exact-string preflight
- skill docs: reference pages for the three new shortcuts + SKILL.md
shortcuts table; switch the membership flow guidance from raw
`wiki members create` to the new +member-add path
Change-Id: I158a86aa7f00bb7cecc7a4e99346f3fb151b3c09
154 lines
5.7 KiB
Go
154 lines
5.7 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package wiki
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/larksuite/cli/internal/output"
|
|
"github.com/larksuite/cli/internal/validate"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
// WikiMemberRemove wraps DELETE /open-apis/wiki/v2/spaces/{space_id}/members/{member_id}.
|
|
// Unlike most DELETEs, this API requires a body specifying member_type and
|
|
// member_role, since the path :member_id is ambiguous without both. The
|
|
// shortcut surfaces both as flags and flattens the returned member object.
|
|
var WikiMemberRemove = common.Shortcut{
|
|
Service: "wiki",
|
|
Command: "+member-remove",
|
|
Description: "Remove a member from a wiki space",
|
|
Risk: "write",
|
|
// The API also accepts wiki:wiki; we declare the narrowest valid scope so
|
|
// tokens carrying only wiki:member:update aren't false-rejected by the
|
|
// exact-string scope preflight (see +space-list for the full reasoning).
|
|
Scopes: []string{"wiki:member:update"},
|
|
AuthTypes: []string{"user", "bot"},
|
|
Flags: []common.Flag{
|
|
{Name: "space-id", Desc: "wiki space ID; use my_library for the personal document library (user only)", Required: true},
|
|
{Name: "member-id", Desc: "member ID; interpretation is decided by --member-type", Required: true},
|
|
{Name: "member-type", Desc: "ID type for --member-id (must match the original grant)", Required: true, Enum: wikiMemberTypes},
|
|
{Name: "member-role", Desc: "role being revoked (must match the original grant)", Required: true, Enum: wikiMemberRoles},
|
|
},
|
|
Tips: []string{
|
|
"--member-type and --member-role must match the original grant; revoking a non-existent (member_id, type, role) tuple is a no-op error from the API.",
|
|
"To switch a member from admin to member or vice versa, remove the old role first, then call +member-add with the new one.",
|
|
"--space-id my_library is a per-user alias and is only valid with --as user.",
|
|
},
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
_, err := readWikiMemberRemoveSpec(runtime)
|
|
return err
|
|
},
|
|
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
|
spec, err := readWikiMemberRemoveSpec(runtime)
|
|
if err != nil {
|
|
return common.NewDryRunAPI().Set("error", err.Error())
|
|
}
|
|
return buildWikiMemberRemoveDryRun(spec)
|
|
},
|
|
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
spec, err := readWikiMemberRemoveSpec(runtime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
spaceID, err := resolveWikiMemberSpaceID(runtime, spec.SpaceID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Fprintf(runtime.IO().ErrOut, "Removing wiki space member %s (type=%s, role=%s) from space %s...\n",
|
|
common.MaskToken(spec.MemberID), spec.MemberType, spec.MemberRole, common.MaskToken(spaceID))
|
|
|
|
path := fmt.Sprintf(
|
|
"/open-apis/wiki/v2/spaces/%s/members/%s",
|
|
validate.EncodePathSegment(spaceID),
|
|
validate.EncodePathSegment(spec.MemberID),
|
|
)
|
|
data, err := runtime.CallAPI("DELETE", path, nil, spec.RequestBody())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
out := map[string]interface{}{"space_id": spaceID}
|
|
for k, v := range wikiMemberRecord(common.GetMap(data, "member")) {
|
|
out[k] = v
|
|
}
|
|
// Defensive default: if the API omits the member echo, or echoes empty
|
|
// strings for any of the three identifying fields, fall back to the
|
|
// caller's inputs per-field so scripts still see what was removed.
|
|
if common.GetString(out, "member_id") == "" {
|
|
out["member_id"] = spec.MemberID
|
|
}
|
|
if common.GetString(out, "member_type") == "" {
|
|
out["member_type"] = spec.MemberType
|
|
}
|
|
if common.GetString(out, "member_role") == "" {
|
|
out["member_role"] = spec.MemberRole
|
|
}
|
|
fmt.Fprintf(runtime.IO().ErrOut, "Removed wiki space member %s\n", common.MaskToken(common.GetString(out, "member_id")))
|
|
runtime.Out(out, nil)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
// wikiMemberRemoveSpec is the normalized CLI input.
|
|
type wikiMemberRemoveSpec struct {
|
|
SpaceID string
|
|
MemberID string
|
|
MemberType string
|
|
MemberRole string
|
|
}
|
|
|
|
// RequestBody builds the JSON body the DELETE endpoint requires.
|
|
func (spec wikiMemberRemoveSpec) RequestBody() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"member_type": spec.MemberType,
|
|
"member_role": spec.MemberRole,
|
|
}
|
|
}
|
|
|
|
func readWikiMemberRemoveSpec(runtime *common.RuntimeContext) (wikiMemberRemoveSpec, error) {
|
|
spec := wikiMemberRemoveSpec{
|
|
SpaceID: strings.TrimSpace(runtime.Str("space-id")),
|
|
MemberID: strings.TrimSpace(runtime.Str("member-id")),
|
|
MemberType: strings.ToLower(strings.TrimSpace(runtime.Str("member-type"))),
|
|
MemberRole: strings.ToLower(strings.TrimSpace(runtime.Str("member-role"))),
|
|
}
|
|
if err := validateWikiMemberSpaceID(runtime, spec.SpaceID); err != nil {
|
|
return wikiMemberRemoveSpec{}, err
|
|
}
|
|
if spec.MemberID == "" {
|
|
return wikiMemberRemoveSpec{}, output.ErrValidation("--member-id is required and cannot be blank")
|
|
}
|
|
// Enum membership for --member-type / --member-role is enforced by the
|
|
// framework's validateEnumFlags (runner.go) before Validate runs.
|
|
return spec, nil
|
|
}
|
|
|
|
func buildWikiMemberRemoveDryRun(spec wikiMemberRemoveSpec) *common.DryRunAPI {
|
|
dry := common.NewDryRunAPI()
|
|
if spec.SpaceID == wikiMyLibrarySpaceID {
|
|
dry.Desc("2-step orchestration: resolve my_library -> remove wiki space member").
|
|
GET("/open-apis/wiki/v2/spaces/my_library").
|
|
Desc("[1] Resolve my_library space ID")
|
|
dry.DELETE(fmt.Sprintf(
|
|
"/open-apis/wiki/v2/spaces/%s/members/%s",
|
|
"<resolved_space_id>",
|
|
validate.EncodePathSegment(spec.MemberID),
|
|
)).
|
|
Desc("[2] Remove wiki space member").
|
|
Body(spec.RequestBody())
|
|
return dry
|
|
}
|
|
return dry.DELETE(fmt.Sprintf(
|
|
"/open-apis/wiki/v2/spaces/%s/members/%s",
|
|
validate.EncodePathSegment(spec.SpaceID),
|
|
validate.EncodePathSegment(spec.MemberID),
|
|
)).
|
|
Body(spec.RequestBody())
|
|
}
|