Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions errs/subtypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,17 @@ const (

// CategoryAPI subtypes
const (
SubtypeRateLimit Subtype = "rate_limit" // request rate limit exceeded
SubtypeConflict Subtype = "conflict" // resource state conflict (e.g. concurrent modification)
SubtypeCrossTenant Subtype = "cross_tenant" // operation crosses tenant boundary (not supported)
SubtypeCrossBrand Subtype = "cross_brand" // operation crosses brand boundary (feishu vs lark, not supported)
SubtypeInvalidParameters Subtype = "invalid_parameters" // API-side parameter validation rejected the request
SubtypeOwnershipMismatch Subtype = "ownership_mismatch" // caller is not the resource owner
SubtypeNotFound Subtype = "not_found" // referenced resource does not exist (HTTP 404 alignment)
SubtypeServerError Subtype = "server_error" // upstream server-side transient error (HTTP 5xx alignment, retryable)
SubtypeQuotaExceeded Subtype = "quota_exceeded" // resource quota / collection size limit reached (assignees, followers, members, etc.)
SubtypeAlreadyExists Subtype = "already_exists" // idempotency violation: resource already exists in target state
SubtypeRateLimit Subtype = "rate_limit" // request rate limit exceeded
SubtypeConflict Subtype = "conflict" // resource state conflict (e.g. concurrent modification)
SubtypeCrossTenant Subtype = "cross_tenant" // operation crosses tenant boundary (not supported)
SubtypeCrossBrand Subtype = "cross_brand" // operation crosses brand boundary (feishu vs lark, not supported)
SubtypeInvalidParameters Subtype = "invalid_parameters" // API-side parameter validation rejected the request
SubtypeOwnershipMismatch Subtype = "ownership_mismatch" // caller is not the resource owner
SubtypeNotFound Subtype = "not_found" // referenced resource does not exist (HTTP 404 alignment)
SubtypeServerError Subtype = "server_error" // upstream server-side transient error (HTTP 5xx alignment, retryable)
SubtypeQuotaExceeded Subtype = "quota_exceeded" // resource quota / collection size limit reached (assignees, followers, members, etc.)
SubtypeAlreadyExists Subtype = "already_exists" // idempotency violation: resource already exists in target state
SubtypeFeatureNotAvailable Subtype = "feature_not_available" // the resource exists, but this integration cannot manage the requested feature
)

// CategoryPolicy subtypes (security-policy envelope shape)
Expand Down
278 changes: 278 additions & 0 deletions shortcuts/apps/apps_member.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package apps

import (
"context"
"fmt"
"io"
"text/tabwriter"

"github.com/larksuite/cli/shortcuts/common"
)

const (
memberReadHint = "verify --app-id identifies a Miaoda app you can access; list apps with `lark-cli apps +list`"
memberWriteHint = "verify the app, external member ID, and current collaborator policy; read the latest state before retrying"
)

var AppsMemberList = common.Shortcut{
Service: appsService, Command: "+member-list", Description: "List application collaborators",
Risk: "read", Scopes: []string{"spark:app:read"}, AuthTypes: []string{"user"},
Tips: []string{"Example: lark-cli apps +member-list --app-id <app_id>"},
HasFormat: true,
Flags: []common.Flag{
{Name: "app-id", Desc: "Miaoda app ID (app_...)"},
{Name: "role", Desc: "filter permission", Enum: memberRoles},
{Name: "member-type", Desc: "filter collaborator type", Enum: memberListTypes},
},
Validate: func(ctx context.Context, rctx *common.RuntimeContext) error {
if err := validateMemberAppID(rctx); err != nil {
return err
}
_, err := buildMemberListParams(rctx)
return err
},
DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI {
params, _ := buildMemberListParams(rctx)
return common.NewDryRunAPI().
GET(memberListURL(rctx)).
Desc("List application collaborators").
Params(params)
},
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
params, err := buildMemberListParams(rctx)
if err != nil {
return err

Check warning on line 47 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L47

Added line #L47 was not covered by tests
}
data, err := rctx.CallAPITyped("GET", memberListURL(rctx), params, nil)
if err != nil {
return withAppsHint(normalizeMemberAPIError(err), memberReadHint)

Check warning on line 51 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L51

Added line #L51 was not covered by tests
}
out, err := projectMemberListData(data)
if err != nil {
return err

Check warning on line 55 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L55

Added line #L55 was not covered by tests
}
rctx.OutFormat(out, nil, func(w io.Writer) { renderMemberListPretty(w, out) })
return nil
},
}

var AppsMemberAdd = common.Shortcut{
Service: appsService, Command: "+member-add", Description: "Add an application collaborator",
Risk: "high-risk-write", Scopes: []string{"spark:app:write"}, AuthTypes: []string{"user"},
Tips: []string{"Example: lark-cli apps +member-add --app-id <app_id> --member-type openid --member-id <open_id> --perm view --yes"},
HasFormat: true,
Flags: []common.Flag{
{Name: "app-id", Desc: "Miaoda app ID (app_...)"},
{Name: "member-type", Desc: "collaborator ID type", Required: true, Enum: memberWriteTypes},
{Name: "member-id", Desc: "one external user, chat, or department ID", Required: true},
{Name: "perm", Desc: "collaborator permission", Required: true, Enum: memberRoles},
{Name: "need-notification", Type: "bool", Desc: "notify the collaborator"},
},
Validate: func(ctx context.Context, rctx *common.RuntimeContext) error {
return validateMemberMutation(rctx, true)
},
DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI {
body, _ := buildMemberAddRequest(rctx)
return common.NewDryRunAPI().
POST(memberListURL(rctx)).
Desc("Add an application collaborator").
Body(body)
},
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
body, err := buildMemberAddRequest(rctx)
if err != nil {
return err

Check warning on line 87 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L87

Added line #L87 was not covered by tests
}
data, err := rctx.CallAPITyped("POST", memberListURL(rctx), nil, body)
if err != nil {
return withAppsHint(normalizeMemberAPIError(err), memberWriteHint)

Check warning on line 91 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L91

Added line #L91 was not covered by tests
}
out, err := projectMemberAddData(data)
if err != nil {
return err
}
rctx.OutFormat(out, nil, func(w io.Writer) { renderMemberMutationPretty(w, "added", out.Member, out.Changed) })
return nil
},
}

var AppsMemberUpdate = common.Shortcut{
Service: appsService, Command: "+member-update", Description: "Update an application collaborator",
Risk: "high-risk-write", Scopes: []string{"spark:app:write"}, AuthTypes: []string{"user"},
Tips: []string{"Example: lark-cli apps +member-update --app-id <app_id> --member-type openid --member-id <open_id> --perm edit --yes"},
HasFormat: true,
Flags: []common.Flag{
{Name: "app-id", Desc: "Miaoda app ID (app_...)"},
{Name: "member-type", Desc: "collaborator ID type", Required: true, Enum: memberWriteTypes},
{Name: "member-id", Desc: "one external user, chat, or department ID", Required: true},
{Name: "perm", Desc: "collaborator permission", Required: true, Enum: memberRoles},
},
Validate: func(ctx context.Context, rctx *common.RuntimeContext) error {
return validateMemberMutation(rctx, true)
},
DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI {
body, _ := buildMemberUpdateRequest(rctx)
return common.NewDryRunAPI().
PATCH(memberListURL(rctx)).
Desc("Update an application collaborator").
Body(body)
},
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
body, err := buildMemberUpdateRequest(rctx)
if err != nil {
return err

Check warning on line 126 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L126

Added line #L126 was not covered by tests
}
data, err := rctx.CallAPITyped("PATCH", memberListURL(rctx), nil, body)
if err != nil {
return withAppsHint(normalizeMemberAPIError(err), memberWriteHint)

Check warning on line 130 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L130

Added line #L130 was not covered by tests
}
out, err := projectMemberUpdateData(data)
if err != nil {
return err

Check warning on line 134 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L134

Added line #L134 was not covered by tests
}
rctx.OutFormat(out, nil, func(w io.Writer) {
renderMemberMutationPretty(w, "updated", out.Member, out.Changed)
fmt.Fprintf(w, "permission: %s -> %s\n", memberDisplayValue(out.BeforeRole), memberDisplayValue(out.AfterRole))
})

Check warning on line 139 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L137-L139

Added lines #L137 - L139 were not covered by tests
return nil
},
}

var AppsMemberRemove = common.Shortcut{
Service: appsService, Command: "+member-remove", Description: "Remove an application collaborator",
Risk: "high-risk-write", Scopes: []string{"spark:app:write"}, AuthTypes: []string{"user"},
Tips: []string{"Example: lark-cli apps +member-remove --app-id <app_id> --member-type openid --member-id <open_id> --yes"},
HasFormat: true,
Flags: []common.Flag{
{Name: "app-id", Desc: "Miaoda app ID (app_...)"},
{Name: "member-type", Desc: "collaborator ID type", Required: true, Enum: memberWriteTypes},
{Name: "member-id", Desc: "one external user, chat, or department ID", Required: true},
},
Validate: func(ctx context.Context, rctx *common.RuntimeContext) error {
return validateMemberMutation(rctx, false)
},
DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI {
body, _ := buildMemberRemoveRequest(rctx)
return common.NewDryRunAPI().
POST(memberRemoveURL(rctx)).
Desc("Remove an application collaborator").
Body(body)
},
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
body, err := buildMemberRemoveRequest(rctx)
if err != nil {
return err

Check warning on line 167 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L167

Added line #L167 was not covered by tests
}
data, err := rctx.CallAPITyped("POST", memberRemoveURL(rctx), nil, body)
if err != nil {
return withAppsHint(normalizeMemberAPIError(err), memberWriteHint)

Check warning on line 171 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L171

Added line #L171 was not covered by tests
}
out, err := projectMemberRemoveData(data)
if err != nil {
return err

Check warning on line 175 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L175

Added line #L175 was not covered by tests
}
rctx.OutFormat(out, nil, func(w io.Writer) { renderMemberMutationPretty(w, "removed", out.Member, out.Changed) })
return nil
},
}

var AppsMemberSettingsGet = common.Shortcut{
Service: appsService, Command: "+member-settings-get", Description: "Get application collaborator settings",
Risk: "read", Scopes: []string{"spark:app:read"}, AuthTypes: []string{"user"},
Tips: []string{"Example: lark-cli apps +member-settings-get --app-id <app_id>"},
HasFormat: true,
Flags: []common.Flag{{Name: "app-id", Desc: "Miaoda app ID (app_...)"}},
Validate: func(ctx context.Context, rctx *common.RuntimeContext) error {
return validateMemberAppID(rctx)
},
DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI {
return common.NewDryRunAPI().
GET(memberSettingsURL(rctx)).
Desc("Get application collaborator settings")
},
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
data, err := rctx.CallAPITyped("GET", memberSettingsURL(rctx), nil, nil)
if err != nil {
return withAppsHint(normalizeMemberAPIError(err), memberReadHint)

Check warning on line 199 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L199

Added line #L199 was not covered by tests
}
out, err := projectMemberSettingsGetData(data)
if err != nil {
return err

Check warning on line 203 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L203

Added line #L203 was not covered by tests
}
rctx.OutFormat(out, nil, func(w io.Writer) { renderMemberSettingsPretty(w, out.Settings) })
return nil
},
}

var AppsMemberSettingsSet = common.Shortcut{
Service: appsService, Command: "+member-settings-set", Description: "Update application collaborator settings",
Risk: "high-risk-write", Scopes: []string{"spark:app:write"}, AuthTypes: []string{"user"},
Tips: []string{"Example: lark-cli apps +member-settings-set --app-id <app_id> --external-access enabled --yes"},
HasFormat: true,
Flags: memberSettingsSetFlags(),
Validate: func(ctx context.Context, rctx *common.RuntimeContext) error {
return validateMemberSettingsSet(rctx)
},
DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI {
body, _ := buildMemberSettingsUpdateRequest(rctx)
return common.NewDryRunAPI().
PATCH(memberSettingsURL(rctx)).
Desc("Update application collaborator settings").
Body(body)
},
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
body, err := buildMemberSettingsUpdateRequest(rctx)
if err != nil {
return err

Check warning on line 229 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L229

Added line #L229 was not covered by tests
}
data, err := rctx.CallAPITyped("PATCH", memberSettingsURL(rctx), nil, body)
if err != nil {
return withAppsHint(normalizeMemberAPIError(err), memberWriteHint)

Check warning on line 233 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L233

Added line #L233 was not covered by tests
}
out, err := projectMemberSettingsSetData(data)
if err != nil {
return err

Check warning on line 237 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L237

Added line #L237 was not covered by tests
}
rctx.OutFormat(out, nil, func(w io.Writer) {
fmt.Fprintf(w, "changed: %t\n", out.Changed)
renderMemberSettingsPretty(w, out.Settings)
})

Check warning on line 242 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L240-L242

Added lines #L240 - L242 were not covered by tests
return nil
},
}

func renderMemberListPretty(w io.Writer, out memberListOutput) {
fmt.Fprintf(w, "%d collaborator(s)\n", len(out.Items))
tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
fmt.Fprintln(tw, "MEMBER_TYPE\tMEMBER_ID\tNAME\tROLE")
for _, item := range out.Items {
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n",
memberDisplayValue(item.MemberType),
memberDisplayValue(item.MemberID),
memberDisplayValue(item.Name),
memberDisplayValue(item.Role),
)
}
_ = tw.Flush()
}

func renderMemberMutationPretty(w io.Writer, action string, member memberOutput, changed bool) {
fmt.Fprintf(w, "%s: %s %s (%s); changed: %t\n",
action,
memberDisplayValue(member.MemberType),
memberDisplayValue(member.MemberID),
memberDisplayValue(member.Role),
changed,
)

Check warning on line 269 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L262-L269

Added lines #L262 - L269 were not covered by tests
}

func renderMemberSettingsPretty(w io.Writer, settings memberSettingsResponse) {
for _, spec := range memberSettingSpecs {
if value := spec.responseValue(settings); value != nil {
fmt.Fprintf(w, "%s: %s\n", spec.field, memberDisplayValue(*value))

Check warning on line 275 in shortcuts/apps/apps_member.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_member.go#L272-L275

Added lines #L272 - L275 were not covered by tests
}
}
}
Loading
Loading