项目文件夹

文件
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

383 行
13 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package calendar
import (
"context"
"fmt"
"io"
"strconv"
"strings"
"time"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/shortcuts/common"
)
var CalendarUpdate = common.Shortcut{
Service: "calendar",
Command: "+update",
Description: "Update a calendar event and incrementally add or remove attendees",
Risk: "write",
Scopes: []string{"calendar:calendar.event:update"},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
Flags: []common.Flag{
{Name: "event-id", Desc: "event ID to update", Required: true},
{Name: "calendar-id", Desc: "calendar ID (default: primary)"},
{Name: "summary", Desc: "event title"},
{Name: "description", Desc: "event description"},
{Name: "start", Desc: "new start time (ISO 8601); requires --end"},
{Name: "end", Desc: "new end time (ISO 8601); requires --start"},
{Name: "rrule", Desc: "recurrence rule (rfc5545)"},
{Name: "add-attendee-ids", Desc: "attendee IDs to add, comma-separated (supports user ou_, chat oc_, room omm_)"},
{Name: "remove-attendee-ids", Desc: "attendee IDs to remove, comma-separated (supports user ou_, chat oc_, room omm_)"},
{Name: "notify", Type: "bool", Default: "true", Desc: "send update notification to attendees"},
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
return validateCalendarUpdate(runtime)
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
return dryRunCalendarUpdate(runtime)
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
return executeCalendarUpdate(ctx, runtime)
},
}
func validateCalendarUpdate(runtime *common.RuntimeContext) error {
if err := rejectCalendarAutoBotFallback(runtime); err != nil {
return err
}
for _, flag := range []string{"event-id", "summary", "description", "rrule", "calendar-id", "start", "end", "add-attendee-ids", "remove-attendee-ids"} {
if val := runtime.Str(flag); val != "" {
if err := common.RejectDangerousCharsTyped("--"+flag, val); err != nil {
return err
}
}
}
if strings.TrimSpace(runtime.Str("event-id")) == "" {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "specify --event-id").WithParam("--event-id")
}
if _, _, err := buildCalendarUpdateEventData(runtime); err != nil {
return err
}
if err := validateCalendarUpdateAttendees(runtime); err != nil {
return err
}
if !hasCalendarUpdateOperation(runtime) {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "nothing to update: specify at least one of --summary, --description, --start/--end, --rrule, --add-attendee-ids, or --remove-attendee-ids")
}
return nil
}
func validateCalendarUpdateAttendees(runtime *common.RuntimeContext) error {
addIDs, err := parseCalendarAttendeeIDs(runtime.Str("add-attendee-ids"))
if err != nil {
return withParam(err, "--add-attendee-ids")
}
removeIDs, err := parseCalendarAttendeeIDs(runtime.Str("remove-attendee-ids"))
if err != nil {
return withParam(err, "--remove-attendee-ids")
}
removeSet := make(map[string]struct{}, len(removeIDs))
for _, id := range removeIDs {
removeSet[id] = struct{}{}
}
for _, id := range addIDs {
if _, ok := removeSet[id]; ok {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "attendee id %q appears in both --add-attendee-ids and --remove-attendee-ids", id)
}
}
return nil
}
func hasCalendarUpdateOperation(runtime *common.RuntimeContext) bool {
if len(runtime.Str("add-attendee-ids")) > 0 || len(runtime.Str("remove-attendee-ids")) > 0 {
return true
}
body, hasEventFields, err := buildCalendarUpdateEventData(runtime)
return err == nil && hasEventFields && len(body) > 0
}
func buildCalendarUpdateEventData(runtime *common.RuntimeContext) (map[string]interface{}, bool, error) {
body := map[string]interface{}{}
hasFields := false
for _, field := range []string{"summary", "description"} {
if runtime.Cmd.Flags().Changed(field) {
body[field] = runtime.Str(field)
hasFields = true
}
}
if runtime.Cmd.Flags().Changed("rrule") {
rrule := strings.TrimSpace(runtime.Str("rrule"))
if rrule != "" {
body["recurrence"] = rrule
hasFields = true
}
}
startChanged := runtime.Cmd.Flags().Changed("start")
endChanged := runtime.Cmd.Flags().Changed("end")
if startChanged != endChanged {
return nil, false, errs.NewValidationError(errs.SubtypeInvalidArgument, "--start and --end must be specified together when updating event time")
}
if startChanged {
startTs, err := common.ParseTime(runtime.Str("start"))
if err != nil {
return nil, false, errs.NewValidationError(errs.SubtypeInvalidArgument, "--start: %v", err).WithParam("--start")
}
endTs, err := common.ParseTime(runtime.Str("end"), "end")
if err != nil {
return nil, false, errs.NewValidationError(errs.SubtypeInvalidArgument, "--end: %v", err).WithParam("--end")
}
s, err := strconv.ParseInt(startTs, 10, 64)
if err != nil {
return nil, false, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid start time: %v", err).WithParam("--start")
}
e, err := strconv.ParseInt(endTs, 10, 64)
if err != nil {
return nil, false, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid end time: %v", err).WithParam("--end")
}
if e <= s {
return nil, false, errs.NewValidationError(errs.SubtypeInvalidArgument, "end time must be after start time")
}
body["start_time"] = map[string]string{"timestamp": startTs}
body["end_time"] = map[string]string{"timestamp": endTs}
hasFields = true
}
if hasFields {
body["need_notification"] = runtime.Bool("notify")
}
return body, hasFields, nil
}
func parseCalendarAttendeeIDs(attendeesStr string) ([]string, error) {
if strings.TrimSpace(attendeesStr) == "" {
return nil, nil
}
seen := map[string]struct{}{}
var ids []string
for _, raw := range strings.Split(attendeesStr, ",") {
id := strings.TrimSpace(raw)
if id == "" {
continue
}
if !strings.HasPrefix(id, "ou_") && !strings.HasPrefix(id, "oc_") && !strings.HasPrefix(id, "omm_") {
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid attendee id format %q: should start with 'ou_', 'oc_', or 'omm_'", id)
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
ids = append(ids, id)
}
return ids, nil
}
func attendeeDeleteIDs(attendeesStr string) ([]map[string]string, error) {
ids, err := parseCalendarAttendeeIDs(attendeesStr)
if err != nil {
return nil, err
}
deleteIDs := make([]map[string]string, 0, len(ids))
for _, id := range ids {
switch {
case strings.HasPrefix(id, "oc_"):
deleteIDs = append(deleteIDs, map[string]string{"type": "chat", "chat_id": id})
case strings.HasPrefix(id, "omm_"):
deleteIDs = append(deleteIDs, map[string]string{"type": "resource", "room_id": id})
case strings.HasPrefix(id, "ou_"):
deleteIDs = append(deleteIDs, map[string]string{"type": "user", "user_id": id})
default:
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid attendee id format %q: should start with 'ou_', 'oc_', or 'omm_'", id).WithParam("--remove-attendee-ids")
}
}
return deleteIDs, nil
}
func calendarUpdateIDs(runtime *common.RuntimeContext) (calendarID string, eventID string) {
calendarID = strings.TrimSpace(runtime.Str("calendar-id"))
if calendarID == "" {
calendarID = PrimaryCalendarIDStr
}
eventID = strings.TrimSpace(runtime.Str("event-id"))
return calendarID, eventID
}
func calendarUpdateEventPath(calendarID, eventID string) string {
return fmt.Sprintf("/open-apis/calendar/v4/calendars/%s/events/%s", validate.EncodePathSegment(calendarID), validate.EncodePathSegment(eventID))
}
func calendarUpdateAttendeesPath(calendarID, eventID string) string {
return calendarUpdateEventPath(calendarID, eventID) + "/attendees"
}
func dryRunCalendarUpdate(runtime *common.RuntimeContext) *common.DryRunAPI {
calendarID, eventID := calendarUpdateIDs(runtime)
displayCalendarID := calendarID
if displayCalendarID == "" || displayCalendarID == "primary" {
displayCalendarID = "<primary>"
}
body, hasEventFields, err := buildCalendarUpdateEventData(runtime)
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
d := common.NewDryRunAPI().Set("calendar_id", displayCalendarID).Set("event_id", eventID)
opCount := 0
if hasEventFields {
opCount++
}
if strings.TrimSpace(runtime.Str("remove-attendee-ids")) != "" {
opCount++
}
if strings.TrimSpace(runtime.Str("add-attendee-ids")) != "" {
opCount++
}
if opCount > 1 {
d.Desc("multi-step update: event fields, attendee removal, and attendee addition run in order when requested")
}
steps := 0
if hasEventFields {
steps++
d.PATCH("/open-apis/calendar/v4/calendars/:calendar_id/events/:event_id").
Desc(fmt.Sprintf("[%d] Update event fields", steps)).
Params(map[string]interface{}{"user_id_type": "open_id"}).
Body(body)
}
if removeStr := runtime.Str("remove-attendee-ids"); strings.TrimSpace(removeStr) != "" {
deleteIDs, err := attendeeDeleteIDs(removeStr)
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
steps++
d.POST("/open-apis/calendar/v4/calendars/:calendar_id/events/:event_id/attendees/batch_delete").
Desc(fmt.Sprintf("[%d] Remove attendees", steps)).
Params(map[string]interface{}{"user_id_type": "open_id"}).
Body(map[string]interface{}{"delete_ids": deleteIDs, "need_notification": runtime.Bool("notify")})
}
if addStr := runtime.Str("add-attendee-ids"); strings.TrimSpace(addStr) != "" {
attendees, err := parseAttendees(addStr, "")
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
steps++
d.POST("/open-apis/calendar/v4/calendars/:calendar_id/events/:event_id/attendees").
Desc(fmt.Sprintf("[%d] Add attendees", steps)).
Params(map[string]interface{}{"user_id_type": "open_id"}).
Body(map[string]interface{}{"attendees": attendees, "need_notification": runtime.Bool("notify")})
}
return d
}
func executeCalendarUpdate(_ context.Context, runtime *common.RuntimeContext) error {
calendarID, eventID := calendarUpdateIDs(runtime)
if eventID == "" {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "specify --event-id").WithParam("--event-id")
}
body, hasEventFields, err := buildCalendarUpdateEventData(runtime)
if err != nil {
return err
}
completed := []string{}
event := map[string]interface{}{}
if hasEventFields {
data, err := runtime.CallAPITyped("PATCH", calendarUpdateEventPath(calendarID, eventID), map[string]interface{}{"user_id_type": "open_id"}, body)
if err != nil {
return withStepContext(err, "failed to update event %s after completed steps %v", eventID, completed)
}
if v, _ := data["event"].(map[string]interface{}); v != nil {
event = v
}
completed = append(completed, "event")
}
removedCount := 0
if removeStr := runtime.Str("remove-attendee-ids"); strings.TrimSpace(removeStr) != "" {
deleteIDs, err := attendeeDeleteIDs(removeStr)
if err != nil {
return err
}
_, err = runtime.CallAPITyped("POST", calendarUpdateAttendeesPath(calendarID, eventID)+"/batch_delete",
map[string]interface{}{"user_id_type": "open_id"},
map[string]interface{}{"delete_ids": deleteIDs, "need_notification": runtime.Bool("notify")})
if err != nil {
return withStepContext(err, "failed to remove attendees from event %s after completed steps %v", eventID, completed)
}
removedCount = len(deleteIDs)
completed = append(completed, "remove_attendees")
}
addedCount := 0
if addStr := runtime.Str("add-attendee-ids"); strings.TrimSpace(addStr) != "" {
attendees, err := parseAttendees(addStr, "")
if err != nil {
return withParam(err, "--add-attendee-ids")
}
_, err = runtime.CallAPITyped("POST", calendarUpdateAttendeesPath(calendarID, eventID),
map[string]interface{}{"user_id_type": "open_id"},
map[string]interface{}{"attendees": attendees, "need_notification": runtime.Bool("notify")})
if err != nil {
return withStepContext(err, "failed to add attendees to event %s after completed steps %v", eventID, completed)
}
addedCount = len(attendees)
}
result := calendarUpdateResult(eventID, event, addedCount, removedCount)
runtime.OutFormat(result, nil, func(w io.Writer) {
output.PrintTable(w, []map[string]interface{}{result})
fmt.Fprintln(w, "\nEvent updated successfully")
})
return nil
}
func calendarUpdateResult(eventID string, event map[string]interface{}, addedCount, removedCount int) map[string]interface{} {
result := map[string]interface{}{
"event_id": eventID,
"attendees_added_count": addedCount,
"attendees_removed_count": removedCount,
}
if summary, _ := event["summary"].(string); summary != "" {
result["summary"] = summary
}
if description, _ := event["description"].(string); description != "" {
result["description"] = description
}
if start := formatCalendarEventTime(event["start_time"]); start != "" {
result["start"] = start
}
if end := formatCalendarEventTime(event["end_time"]); end != "" {
result["end"] = end
}
return result
}
func formatCalendarEventTime(v interface{}) string {
m, _ := v.(map[string]interface{})
if m == nil {
return ""
}
if tsStr, _ := m["timestamp"].(string); tsStr != "" {
if ts, err := strconv.ParseInt(tsStr, 10, 64); err == nil {
return time.Unix(ts, 0).Local().Format(time.RFC3339)
}
}
if dt, _ := m["datetime"].(string); dt != "" {
return dt
}
if date, _ := m["date"].(string); date != "" {
return date
}
return ""
}