Documentation
¶
Index ¶
Constants ¶
View Source
const (
PrimaryCalendarIDStr = "primary"
)
Variables ¶
View Source
var CalendarAgenda = common.Shortcut{ Service: "calendar", Command: "+agenda", Description: "View calendar agenda (defaults to today)", Risk: "read", Scopes: []string{"calendar:calendar.event:read"}, AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ {Name: "start", Desc: "start time (ISO 8601, default: start of today)"}, {Name: "end", Desc: "end time (ISO 8601, default: end of start day)"}, {Name: "calendar-id", Desc: "calendar ID (default: primary)"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if err := rejectCalendarAutoBotFallback(runtime); err != nil { return err } warnCalendarTimezoneMismatch(runtime, calendarTimeInputRange{Flag: "start", Value: runtime.Str("start")}, calendarTimeInputRange{Flag: "end", Value: runtime.Str("end")}, ) return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { startInt, endInt, err := parseTimeRange(runtime) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } calendarId := runtime.Str("calendar-id") d := common.NewDryRunAPI() switch calendarId { case "": d.Desc("(calendar-id omitted) Will use primary calendar") calendarId = "<primary>" case "primary": calendarId = "<primary>" } return d. GET("/open-apis/calendar/v4/calendars/:calendar_id/events/instance_view"). Params(map[string]interface{}{"start_time": fmt.Sprintf("%d", startInt), "end_time": fmt.Sprintf("%d", endInt)}). Set("calendar_id", calendarId) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { startInt, endInt, err := parseTimeRange(runtime) if err != nil { return err } calendarId := strings.TrimSpace(runtime.Str("calendar-id")) if calendarId == "" { calendarId = PrimaryCalendarIDStr } items, err := fetchInstanceViewRange(ctx, runtime, calendarId, startInt, endInt, 0) if err != nil { return err } visible := dedupeAndSortItems(items) filtered := make([]map[string]interface{}, 0) for _, e := range visible { status, _ := e["status"].(string) if status != "cancelled" { delete(e, "status") delete(e, "attendees") if startMap, ok := e["start_time"].(map[string]interface{}); ok { if tsStr, ok := startMap["timestamp"].(string); ok && tsStr != "" { if ts, err := strconv.ParseInt(tsStr, 10, 64); err == nil { startMap["datetime"] = time.Unix(ts, 0).Local().Format(time.RFC3339) delete(startMap, "timestamp") } } } if endMap, ok := e["end_time"].(map[string]interface{}); ok { if tsStr, ok := endMap["timestamp"].(string); ok && tsStr != "" { if ts, err := strconv.ParseInt(tsStr, 10, 64); err == nil { endMap["datetime"] = time.Unix(ts, 0).Local().Format(time.RFC3339) delete(endMap, "timestamp") } } if dt, _ := endMap["datetime"].(string); dt == "" { if dateStr, ok := endMap["date"].(string); ok && dateStr != "" { if t, err := time.ParseInLocation("2006-01-02", dateStr, time.UTC); err == nil { endMap["date"] = t.Add(-1 * time.Second).Format("2006-01-02") } } } } collapseDescription(e) filtered = append(filtered, e) } } runtime.OutFormat(filtered, &output.Meta{Count: len(filtered)}, func(w io.Writer) { if len(filtered) == 0 { fmt.Fprintln(w, "No events in this time range.") return } var rows []map[string]interface{} for _, e := range filtered { summary, _ := e["summary"].(string) if summary == "" { summary = "(untitled)" } summary = common.TruncateStr(summary, 40) startMap, _ := e["start_time"].(map[string]interface{}) endMap, _ := e["end_time"].(map[string]interface{}) startStr, _ := startMap["datetime"].(string) if startStr == "" { startStr, _ = startMap["date"].(string) } endStr, _ := endMap["datetime"].(string) if endStr == "" { endStr, _ = endMap["date"].(string) } freeBusyStatus, _ := e["free_busy_status"].(string) selfRsvpStatus, _ := e["self_rsvp_status"].(string) eventId, _ := e["event_id"].(string) rows = append(rows, map[string]interface{}{ "event_id": eventId, "summary": summary, "start": startStr, "end": endStr, "free_busy_status": freeBusyStatus, "self_rsvp_status": selfRsvpStatus, }) } output.PrintTable(w, rows) fmt.Fprintf(w, "\n%d event(s) total\n", len(filtered)) }) return nil }, }
View Source
var CalendarCreate = common.Shortcut{ Service: "calendar", Command: "+create", Description: "Create a calendar event and optionally invite attendees", Risk: "write", Scopes: []string{"calendar:calendar.event:create", "calendar:calendar.event:update"}, AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ {Name: "summary", Desc: "event title"}, {Name: "start", Desc: "start time (ISO 8601)", Required: true}, {Name: "end", Desc: "end time (ISO 8601)", Required: true}, {Name: "description", Desc: "event description as Markdown (@file or - for stdin); the unified description field. Supports bold/italic/underline/strikethrough, links, headings (`#`..`###`), blockquotes (`>`), ordered/unordered lists, horizontal rules (`---`), GFM tables, and images (``; a remote URL is used as-is, and a local image path relative to and inside the current working directory is auto-uploaded to Lark drive and rendered inline — absolute/out-of-cwd paths are rejected). A Lark doc URL (bare or as a Markdown link) is auto-resolved to an inline doc-mention chip showing its title. Inside a GFM table cell, stack multiple lines with `<br>`; each line may itself be an ordered/unordered list item, image or styled text (e.g. `1. a<br>2. b`, `- x<br>- y`, `<br>**bold**`).", Input: []string{common.File, common.Stdin}}, {Name: "attendee-ids", Desc: "attendee IDs, comma-separated (supports user ou_, chat oc_, room omm_)"}, {Name: "calendar-id", Desc: "calendar ID (default: primary)"}, {Name: "rrule", Desc: "recurrence rule (rfc5545)"}, {Name: "meeting-owner-id", Desc: "VC meeting owner open_id (ou_). Only effective as a bot on the app calendar; owner must be an in-tenant user"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if err := rejectCalendarAutoBotFallback(runtime); err != nil { return err } for _, flag := range []string{"summary", "description", "rrule", "calendar-id"} { if val := runtime.Str(flag); val != "" { if err := common.RejectDangerousCharsTyped("--"+flag, val); err != nil { return err } } } if attendeesStr := runtime.Str("attendee-ids"); attendeesStr != "" { for _, id := range strings.Split(attendeesStr, ",") { id = strings.TrimSpace(id) if id == "" { continue } if !strings.HasPrefix(id, "ou_") && !strings.HasPrefix(id, "oc_") && !strings.HasPrefix(id, "omm_") { return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid attendee id format %q: should start with 'ou_', 'oc_', or 'omm_'", id).WithParam("--attendee-ids") } } } if ownerId := strings.TrimSpace(runtime.Str("meeting-owner-id")); ownerId != "" { if err := common.RejectDangerousCharsTyped("--meeting-owner-id", ownerId); err != nil { return err } if !strings.HasPrefix(ownerId, "ou_") || ownerId == "ou_" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid --meeting-owner-id %q: meeting owner must be a user open_id starting with 'ou_'", ownerId).WithParam("--meeting-owner-id") } if !runtime.IsBot() { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--meeting-owner-id only takes effect when running as a bot on the app calendar; re-run with --as bot").WithParam("--meeting-owner-id") } } if runtime.Str("start") == "" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "specify --start (e.g. '2026-03-12T14:00+08:00')").WithParam("--start") } if runtime.Str("end") == "" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "specify --end (e.g. '2026-03-12T15:00+08:00')").WithParam("--end") } startTs, err := common.ParseTime(runtime.Str("start")) if err != nil { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--start: %v", err).WithParam("--start") } endTs, err := common.ParseTime(runtime.Str("end"), "end") if err != nil { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--end: %v", err).WithParam("--end") } s, err := strconv.ParseInt(startTs, 10, 64) if err != nil { return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid start time: %v", err).WithParam("--start") } e, err := strconv.ParseInt(endTs, 10, 64) if err != nil { return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid end time: %v", err).WithParam("--end") } if e <= s { return errs.NewValidationError(errs.SubtypeInvalidArgument, "end time must be after start time") } warnCalendarTimezoneMismatch(runtime, calendarTimeInputRange{Flag: "start", Value: runtime.Str("start")}, calendarTimeInputRange{Flag: "end", Value: runtime.Str("end")}, ) return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { calendarId := runtime.Str("calendar-id") d := common.NewDryRunAPI() switch calendarId { case "": d.Desc("(calendar-id omitted) Will use primary calendar") calendarId = "<primary>" case "primary": calendarId = "<primary>" } startTs, err := common.ParseTime(runtime.Str("start")) if err != nil { return common.NewDryRunAPI().Set("error", fmt.Sprintf("--start: %v", err)) } endTs, err := common.ParseTime(runtime.Str("end"), "end") if err != nil { return common.NewDryRunAPI().Set("error", fmt.Sprintf("--end: %v", err)) } eventData := buildEventData(runtime, startTs, endTs) attendeesStr := runtime.Str("attendee-ids") if attendeesStr != "" { attendees, err := parseAttendees(attendeesStr, "") if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } d.Desc("2-step: create event → add attendees (auto-rollback on failure)"). POST("/open-apis/calendar/v4/calendars/:calendar_id/events"). Desc("[1/2] Create event"). Body(eventData). POST("/open-apis/calendar/v4/calendars/:calendar_id/events/<event_id>/attendees"). Desc("[2/2] Add attendees (on failure: auto-delete event)"). Params(map[string]interface{}{"user_id_type": "open_id"}). Body(map[string]interface{}{"attendees": attendees, "need_notification": true}) } else { d.POST("/open-apis/calendar/v4/calendars/:calendar_id/events"). Body(eventData) } return d.Set("calendar_id", calendarId) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { calendarId := strings.TrimSpace(runtime.Str("calendar-id")) if calendarId == "" { calendarId = PrimaryCalendarIDStr } startTs, err := common.ParseTime(runtime.Str("start")) if err != nil { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--start: %v", err).WithParam("--start") } endTs, err := common.ParseTime(runtime.Str("end"), "end") if err != nil { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--end: %v", err).WithParam("--end") } if err := resolveDescriptionImages(runtime, calendarId); err != nil { return err } eventData := buildEventData(runtime, startTs, endTs) data, err := runtime.CallAPITyped("POST", fmt.Sprintf("/open-apis/calendar/v4/calendars/%s/events", validate.EncodePathSegment(calendarId)), nil, eventData) if err != nil { return err } event, _ := data["event"].(map[string]interface{}) eventId, _ := event["event_id"].(string) if eventId == "" { return errs.NewInternalError(errs.SubtypeInvalidResponse, "failed to create event: no event_id returned") } if attendeesStr := runtime.Str("attendee-ids"); attendeesStr != "" { selfId := selfAttendeeId(runtime) attendees, err := parseAttendees(attendeesStr, selfId) if err != nil { return withParam(err, "--attendee-ids") } _, err = runtime.CallAPITyped("POST", fmt.Sprintf("/open-apis/calendar/v4/calendars/%s/events/%s/attendees", validate.EncodePathSegment(calendarId), validate.EncodePathSegment(eventId)), map[string]interface{}{"user_id_type": "open_id"}, map[string]interface{}{ "attendees": attendees, "need_notification": true, }) if err != nil { err = guideApprovalRoomReasonError(err, attendees) _, rollbackErr := runtime.CallAPITyped("DELETE", fmt.Sprintf("/open-apis/calendar/v4/calendars/%s/events/%s", validate.EncodePathSegment(calendarId), validate.EncodePathSegment(eventId)), map[string]interface{}{"need_notification": false}, nil) if rollbackErr != nil { return withStepContext(err, "rollback also failed (%v); orphan event_id=%s needs manual cleanup", rollbackErr, eventId) } return withStepContext(err, "event rolled back successfully") } } startMap, _ := event["start_time"].(map[string]interface{}) endMap, _ := event["end_time"].(map[string]interface{}) if startMap != nil { if tsStr, ok := startMap["timestamp"].(string); ok && tsStr != "" { if ts, err := strconv.ParseInt(tsStr, 10, 64); err == nil { startMap["datetime"] = time.Unix(ts, 0).Local().Format(time.RFC3339) delete(startMap, "timestamp") } } } if endMap != nil { if tsStr, ok := endMap["timestamp"].(string); ok && tsStr != "" { if ts, err := strconv.ParseInt(tsStr, 10, 64); err == nil { endMap["datetime"] = time.Unix(ts, 0).Local().Format(time.RFC3339) delete(endMap, "timestamp") } } if dt, _ := endMap["datetime"].(string); dt == "" { if dateStr, ok := endMap["date"].(string); ok && dateStr != "" { if t, err := time.ParseInLocation("2006-01-02", dateStr, time.UTC); err == nil { endMap["date"] = t.Add(-1 * time.Second).Format("2006-01-02") } } } } var startStr, endStr string if startMap != nil { startStr, _ = startMap["datetime"].(string) if startStr == "" { startStr, _ = startMap["date"].(string) } } if endMap != nil { endStr, _ = endMap["datetime"].(string) if endStr == "" { endStr, _ = endMap["date"].(string) } } resultData := map[string]interface{}{ "event_id": eventId, "summary": event["summary"], "start": startStr, "end": endStr, } if recurrence, _ := event["recurrence"].(string); recurrence != "" { resultData["recurrence"] = recurrence } runtime.OutFormat(resultData, nil, func(w io.Writer) { var rows []map[string]interface{} rows = append(rows, resultData) output.PrintTable(w, rows) fmt.Fprintln(w, "\nEvent created successfully") }) return nil }, }
View Source
var CalendarDelete = common.Shortcut{ Service: "calendar", Command: "+delete", Description: "Delete a calendar event; requires --apply-to for recurring events and exceptions", Risk: "high-risk-write", Scopes: []string{"calendar:calendar.event:read", "calendar:calendar.event:delete"}, AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ {Name: "event-id", Desc: "event ID to delete", Required: true}, {Name: "calendar-id", Desc: "calendar ID (default: primary)"}, { Name: flagApplyTo, Enum: applyToValues, Desc: "recurring scope: single (this occurrence / exception only) | all (whole series and every exception) | this-and-following (truncate the series at this instance and drop every exception on/after it). Required on recurring events; ignored on non-recurring events.", }, {Name: "notify", Type: "bool", Default: "true", Desc: "send delete notification to attendees for the master event; exception cleanup silently uses need_notification=false so participants are not spammed"}, }, Tips: []string{ "Deleting an entire recurring series also removes every exception; pass --apply-to=all to confirm intent.", "Deleting `this-and-following` truncates the master (UNTIL = midnight of the pivot day in the event's timezone) and removes exceptions on/after that instance.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateCalendarDelete(runtime) }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { return dryRunCalendarDelete(runtime) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { return executeCalendarDelete(ctx, runtime) }, }
View Source
var CalendarFreebusy = common.Shortcut{ Service: "calendar", Command: "+freebusy", Description: "Query free/busy for one or more users. Merges overlapping busy intervals and supports per-user free / common-free views for multi-user scheduling.", Risk: "read", Scopes: []string{"calendar:calendar.free_busy:read"}, AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ {Name: "start", Desc: "start time (ISO 8601, default: today)"}, {Name: "end", Desc: "end time (ISO 8601, default: end of start day)"}, {Name: flagFreebusyUserID, Type: "string_slice", Desc: "target user open_id(s); repeatable or comma-separated; default: current user; bot identity must provide at least one"}, {Name: flagFreebusyType, Type: "string", Default: freebusyTypeBusy, Enum: []string{freebusyTypeBusy, freebusyTypeRawBusy, freebusyTypeFree, freebusyTypeCommonFree}, Desc: "output view: busy (default, per-user merged busy) | raw_busy (per-user upstream events with rsvp_status) | free (per-user free windows) | common_free (all-users common free)"}, {Name: flagFreebusyMinDuration, Type: "string", Desc: "minimum length for free/common_free candidates (Go duration, e.g. 30m, 1h); ignored for busy / raw_busy"}, }, Tips: []string{ "`--type busy` merges adjacent busy intervals, so item count != event count. For events/rsvp use `--type raw_busy` (own or others' calendars); for own attendees/details use `+get` / `+list-attendees`.", "Multi-user scheduling raw view: use `--type common_free --min-duration <dur>` and let the CLI compute the intersection instead of merging by hand.", }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { timeMin, timeMax, _, _, err := parseFreebusyTimeRange(runtime) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } userIDs, err := collectFreebusyUserIDs(runtime) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } typ, err := parseFreebusyType(runtime) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } body := map[string]interface{}{ "time_min": timeMin, "time_max": timeMax, "user_ids": userIDs, "need_rsvp_status": true, } d := common.NewDryRunAPI().POST(freebusyBatchPath).Body(body).Set("type", typ) if raw := strings.TrimSpace(runtime.Str(flagFreebusyMinDuration)); raw != "" { d.Set("min_duration", raw) } return d }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if err := rejectCalendarAutoBotFallback(runtime); err != nil { return err } if _, _, _, _, err := parseFreebusyTimeRange(runtime); err != nil { return err } if _, err := collectFreebusyUserIDs(runtime); err != nil { return err } if _, err := parseFreebusyType(runtime); err != nil { return err } if _, err := parseFreebusyMinDuration(runtime); err != nil { return err } warnCalendarTimezoneMismatch(runtime, calendarTimeInputRange{Flag: "start", Value: runtime.Str("start")}, calendarTimeInputRange{Flag: "end", Value: runtime.Str("end")}, ) return nil }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { timeMin, timeMax, startSec, endSec, err := parseFreebusyTimeRange(runtime) if err != nil { return err } userIDs, err := collectFreebusyUserIDs(runtime) if err != nil { return err } typ, err := parseFreebusyType(runtime) if err != nil { return err } minDur, err := parseFreebusyMinDuration(runtime) if err != nil { return err } winStart := time.Unix(startSec, 0) winEnd := time.Unix(endSec, 0) writeFreebusyMinDurationHints(runtime, typ, minDur, winStart, winEnd) data, err := runtime.CallAPITyped("POST", freebusyBatchPath, nil, map[string]interface{}{ "time_min": timeMin, "time_max": timeMax, "user_ids": userIDs, "need_rsvp_status": true, }) if err != nil { return err } perUser := parseFreebusyBatchResponse(data, userIDs) mergedByUser := make(map[string][]*freebusyInterval, len(userIDs)) for _, u := range userIDs { mergedByUser[u] = mergeBusyIntervals(rawToBusyIntervals(perUser[u])) } switch typ { case freebusyTypeBusy: users := make([]*freebusyUserBusy, 0, len(userIDs)) total := 0 for _, u := range userIDs { items := mergedByUser[u] if items == nil { items = []*freebusyInterval{} } users = append(users, &freebusyUserBusy{UserID: u, Busy: items}) total += len(items) } out := map[string]interface{}{"users": users} runtime.OutFormat(out, &output.Meta{Count: total}, func(w io.Writer) { if total == 0 { fmt.Fprintln(w, "No busy periods in this time range.") return } for _, u := range users { fmt.Fprintf(w, "user %s\n", u.UserID) if len(u.Busy) == 0 { fmt.Fprintln(w, " (no busy periods)") continue } rows := make([]map[string]interface{}, 0, len(u.Busy)) for _, it := range u.Busy { rows = append(rows, map[string]interface{}{ "start": it.StartTime, "end": it.EndTime, }) } output.PrintTable(w, rows) } fmt.Fprintf(w, "\n%d busy period(s) across %d user(s)\n", total, len(users)) }) return nil case freebusyTypeRawBusy: users := make([]*freebusyUserRawBusy, 0, len(userIDs)) total := 0 for _, u := range userIDs { items := sortRawItemsByStart(perUser[u]) if items == nil { items = []*freebusyRawItem{} } users = append(users, &freebusyUserRawBusy{UserID: u, RawBusy: items}) total += len(items) } out := map[string]interface{}{"users": users} runtime.OutFormat(out, &output.Meta{Count: total}, func(w io.Writer) { if total == 0 { fmt.Fprintln(w, "No events in this time range.") return } for _, u := range users { fmt.Fprintf(w, "user %s\n", u.UserID) if len(u.RawBusy) == 0 { fmt.Fprintln(w, " (no events)") continue } rows := make([]map[string]interface{}, 0, len(u.RawBusy)) for _, it := range u.RawBusy { rows = append(rows, map[string]interface{}{ "start": it.StartTime, "end": it.EndTime, "rsvp_status": it.RSVPStatus, }) } output.PrintTable(w, rows) } fmt.Fprintf(w, "\n%d event(s) across %d user(s)\n", total, len(users)) }) return nil case freebusyTypeFree: users := make([]*freebusyUserFree, 0, len(userIDs)) total := 0 var allFreeForHint []*freebusyFreeSlot for _, u := range userIDs { free := perUserFree(mergedByUser[u], winStart, winEnd, minDur) if free == nil { free = []*freebusyFreeSlot{} } users = append(users, &freebusyUserFree{UserID: u, Free: free}) total += len(free) allFreeForHint = append(allFreeForHint, free...) } writeFreebusyOffHoursHint(runtime, freebusyTypeFree, allFreeForHint) out := map[string]interface{}{"users": users} runtime.OutFormat(out, &output.Meta{Count: total}, func(w io.Writer) { if total == 0 { fmt.Fprintln(w, "No free slots in this time range.") return } for _, u := range users { fmt.Fprintf(w, "user %s\n", u.UserID) if len(u.Free) == 0 { fmt.Fprintln(w, " (no free slots)") continue } rows := make([]map[string]interface{}, 0, len(u.Free)) for _, it := range u.Free { rows = append(rows, map[string]interface{}{ "start": it.StartTime, "end": it.EndTime, "duration": it.Duration, }) } output.PrintTable(w, rows) } fmt.Fprintf(w, "\n%d free slot(s) across %d user(s)\n", total, len(users)) }) return nil case freebusyTypeCommonFree: usersBusy := make([][]*freebusyInterval, 0, len(userIDs)) for _, u := range userIDs { usersBusy = append(usersBusy, mergedByUser[u]) } free := commonFree(usersBusy, winStart, winEnd, minDur) if free == nil { free = []*freebusyFreeSlot{} } writeFreebusyOffHoursHint(runtime, freebusyTypeCommonFree, free) out := map[string]interface{}{"common_free": free} runtime.OutFormat(out, &output.Meta{Count: len(free)}, func(w io.Writer) { if len(free) == 0 { fmt.Fprintln(w, "No common free slots in this time range.") return } rows := make([]map[string]interface{}, 0, len(free)) for _, it := range free { rows = append(rows, map[string]interface{}{ "start": it.StartTime, "end": it.EndTime, "duration": it.Duration, }) } output.PrintTable(w, rows) fmt.Fprintf(w, "\n%d common free slot(s) across %d user(s)\n", len(free), len(userIDs)) }) return nil } return nil }, }
View Source
var CalendarGet = common.Shortcut{ Service: "calendar", Command: "+get", Description: "Get a single calendar event detail by calendar-id and event-id", Risk: "read", Scopes: []string{"calendar:calendar.event:read"}, AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ {Name: "calendar-id", Desc: "calendar ID (default: primary)"}, {Name: "event-id", Desc: "event ID", Required: true}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if err := rejectCalendarAutoBotFallback(runtime); err != nil { return err } for _, flag := range []string{"calendar-id", "event-id"} { if val := strings.TrimSpace(runtime.Str(flag)); val != "" { if err := common.RejectDangerousCharsTyped("--"+flag, val); err != nil { return err } } } eventId := strings.TrimSpace(runtime.Str("event-id")) if eventId == "" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "event-id cannot be empty").WithParam("--event-id") } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { calendarId := strings.TrimSpace(runtime.Str("calendar-id")) d := common.NewDryRunAPI() switch calendarId { case "": d.Desc("(calendar-id omitted) Will use primary calendar") calendarId = "<primary>" case "primary": calendarId = "<primary>" } eventId := strings.TrimSpace(runtime.Str("event-id")) return d. GET("/open-apis/calendar/v4/calendars/:calendar_id/events/:event_id"). Set("calendar_id", calendarId). Set("event_id", eventId) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { calendarId := strings.TrimSpace(runtime.Str("calendar-id")) if calendarId == "" { calendarId = PrimaryCalendarIDStr } eventId := strings.TrimSpace(runtime.Str("event-id")) event, err := resolveCalendarEventOrMaster(runtime, calendarId, eventId) if err != nil { return err } out, err := buildCalendarEventOutput(event) if err != nil { return err } runtime.OutFormat(out, nil, func(w io.Writer) { summary, _ := out["summary"].(string) if summary == "" { summary = "(untitled)" } startMap, _ := out["start_time"].(map[string]interface{}) endMap, _ := out["end_time"].(map[string]interface{}) startStr, _ := startMap["datetime"].(string) if startStr == "" { startStr, _ = startMap["date"].(string) } endStr, _ := endMap["datetime"].(string) if endStr == "" { endStr, _ = endMap["date"].(string) } eventIdOut, _ := out["event_id"].(string) freeBusyStatus, _ := out["free_busy_status"].(string) selfRsvpStatus, _ := out["self_rsvp_status"].(string) row := map[string]interface{}{ "event_id": eventIdOut, "summary": summary, "start": startStr, "end": endStr, "free_busy_status": freeBusyStatus, "self_rsvp_status": selfRsvpStatus, } output.PrintTable(w, []map[string]interface{}{row}) fmt.Fprintln(w) }) return nil }, }
CalendarGet gets a single calendar event detail.
View Source
var CalendarJoinEvent = common.Shortcut{ Service: "calendar", Command: "+join-event", Description: "Join a calendar event via a share token (from a share link/QR code or a im share/RSVP card)", Risk: "write", Scopes: []string{"calendar:calendar.event:join"}, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ { Name: "token", Aliases: []string{"share-token"}, Desc: "share token from a share link/QR code or an IM share/RSVP card", Required: true, }, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { token := strings.TrimSpace(runtime.Str("token")) return common.NewDryRunAPI(). POST(joinEventPath). Body(map[string]any{"share_token": token}) }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if err := rejectCalendarAutoBotFallback(runtime); err != nil { return err } token := strings.TrimSpace(runtime.Str("token")) if token == "" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "share token cannot be empty").WithParam("--token") } if err := common.RejectDangerousCharsTyped("--token", token); err != nil { return err } return nil }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { token := strings.TrimSpace(runtime.Str("token")) _, err := runtime.CallAPITyped("POST", joinEventPath, nil, map[string]any{"share_token": token}) if err != nil { return err } runtime.Out(map[string]any{ "joined": true, }, nil) return nil }, }
View Source
var CalendarListAttendees = common.Shortcut{ Service: "calendar", Command: "+list-attendees", Description: "List attendees of a calendar event; supports --type filter and page-token pagination", Risk: "read", Scopes: []string{"calendar:calendar.event:read"}, AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ {Name: "calendar-id", Desc: "calendar ID (default: primary)"}, {Name: "event-id", Desc: "event ID", Required: true}, {Name: "type", Type: "string_slice", Desc: "filter by attendee type; repeatable or comma-separated (user|resource|chat|third_party); empty means all"}, {Name: "page-size", Type: "int", Desc: fmt.Sprintf("upstream page size; range [%d, %d] (values outside are clamped), default %d", listAttendeesMinPageSize, listAttendeesMaxPageSize, listAttendeesDefaultPageSize)}, {Name: "page-token", Desc: "upstream page token for the next page"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if err := rejectCalendarAutoBotFallback(runtime); err != nil { return err } for _, flag := range []string{"calendar-id", "event-id", "page-token"} { if val := strings.TrimSpace(runtime.Str(flag)); val != "" { if err := common.RejectDangerousCharsTyped("--"+flag, val); err != nil { return err } } } eventId := strings.TrimSpace(runtime.Str("event-id")) if eventId == "" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "event-id cannot be empty").WithParam("--event-id") } if _, err := normalizeTypeFilter(runtime.StrSlice("type")); err != nil { return err } if pageSize := runtime.Int("page-size"); pageSize < 0 { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--page-size must be a non-negative integer").WithParam("--page-size") } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { calendarId := strings.TrimSpace(runtime.Str("calendar-id")) d := common.NewDryRunAPI() switch calendarId { case "": d.Desc("(calendar-id omitted) Will use primary calendar") calendarId = "<primary>" case "primary": calendarId = "<primary>" } eventId := strings.TrimSpace(runtime.Str("event-id")) pageSize, _ := resolveListAttendeesPageSize(runtime) params := map[string]interface{}{ "page_size": pageSize, } if pageToken := strings.TrimSpace(runtime.Str("page-token")); pageToken != "" { params["page_token"] = pageToken } return d. GET("/open-apis/calendar/v4/calendars/:calendar_id/events/:event_id/attendees"). Params(params). Set("calendar_id", calendarId). Set("event_id", eventId) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { calendarId := strings.TrimSpace(runtime.Str("calendar-id")) if calendarId == "" { calendarId = PrimaryCalendarIDStr } eventId := strings.TrimSpace(runtime.Str("event-id")) allowed, err := normalizeTypeFilter(runtime.StrSlice("type")) if err != nil { return err } pageSize, hint := resolveListAttendeesPageSize(runtime) if hint != "" { fmt.Fprintln(runtime.IO().ErrOut, hint) } params := map[string]interface{}{ "page_size": pageSize, } if pageToken := strings.TrimSpace(runtime.Str("page-token")); pageToken != "" { params["page_token"] = pageToken } data, err := runtime.CallAPITyped("GET", fmt.Sprintf("/open-apis/calendar/v4/calendars/%s/events/%s/attendees", validate.EncodePathSegment(calendarId), validate.EncodePathSegment(eventId)), params, nil) if err != nil { return err } if data == nil { data = map[string]interface{}{} } items := common.GetSlice(data, "items") hasMore, _ := data["has_more"].(bool) pageToken, _ := data["page_token"].(string) attendees := make([]map[string]interface{}, 0, len(items)) for _, raw := range items { item, ok := raw.(map[string]interface{}) if !ok { continue } if len(allowed) > 0 { t, _ := item["type"].(string) if _, keep := allowed[t]; !keep { continue } } attendees = append(attendees, projectAttendee(item)) } out := listAttendeesOutput{ Attendees: attendees, HasMore: hasMore, PageToken: pageToken, } runtime.OutFormat(out, &output.Meta{Count: len(attendees)}, func(w io.Writer) { if len(attendees) == 0 { fmt.Fprintln(w, "No attendees found.") return } groups := map[string][]map[string]interface{}{} order := []string{string(attendeeTypeResource), string(attendeeTypeUser), string(attendeeTypeChat), string(attendeeTypeThirdParty)} for _, a := range attendees { t, _ := a["type"].(string) groups[t] = append(groups[t], a) } titles := map[string]string{ string(attendeeTypeResource): "rooms", string(attendeeTypeUser): "users", string(attendeeTypeChat): "chats", string(attendeeTypeThirdParty): "third_parties", } for _, t := range order { group := groups[t] if len(group) == 0 { continue } fmt.Fprintf(w, "%s (%d)\n", titles[t], len(group)) var rows []map[string]interface{} for _, a := range group { row := map[string]interface{}{ "display_name": a["display_name"], } if attendeeType(t) != attendeeTypeChat { row["rsvp_status"] = a["rsvp_status"] } switch attendeeType(t) { case attendeeTypeUser: row["user_id"] = a["user_id"] case attendeeTypeResource: row["room_id"] = a["room_id"] case attendeeTypeChat: row["chat_id"] = a["chat_id"] case attendeeTypeThirdParty: row["third_party_email"] = a["third_party_email"] } rows = append(rows, row) } output.PrintTable(w, rows) fmt.Fprintln(w) } fmt.Fprintf(w, "%d attendee(s) total", len(attendees)) if hasMore { fmt.Fprintf(w, "; more available, page_token: %s", pageToken) } fmt.Fprintln(w) }) return nil }, }
CalendarListAttendees lists attendees of a calendar event.
View Source
var CalendarMeeting = common.Shortcut{ Service: "calendar", Command: "+meeting", Description: "Get meeting info for calendar events (meeting_id, meeting_note)", Risk: "read", Scopes: []string{"calendar:calendar.event:read"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "event-ids", Desc: "calendar event instance IDs, comma-separated for batch", Required: true}, {Name: "calendar-id", Desc: "calendar ID (default: primary)"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if err := rejectCalendarAutoBotFallback(runtime); err != nil { return err } ids := common.SplitCSV(runtime.Str("event-ids")) const maxBatchSize = 50 if len(ids) > maxBatchSize { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--event-ids: too many IDs (%d), maximum is %d", len(ids), maxBatchSize).WithParam("--event-ids") } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { calendarID := runtime.Str("calendar-id") if calendarID == "" { calendarID = "<primary>" } return common.NewDryRunAPI(). POST(fmt.Sprintf("/open-apis/calendar/v4/calendars/%s/events/mget_instance_relation_info", calendarID)). Set("event_ids", common.SplitCSV(runtime.Str("event-ids"))). Set("calendar_id", calendarID) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { errOut := runtime.IO().ErrOut instanceIDs := common.SplitCSV(runtime.Str("event-ids")) calendarID := strings.TrimSpace(runtime.Str("calendar-id")) if calendarID == "" { calendarID = PrimaryCalendarIDStr } results := make([]*meetingInfoItem, 0, len(instanceIDs)) fmt.Fprintf(errOut, "%s querying %d event_id(s)\n", meetingLogPrefix, len(instanceIDs)) for _, id := range instanceIDs { if err := ctx.Err(); err != nil { return err } fmt.Fprintf(errOut, "%s querying event_id=%s ...\n", meetingLogPrefix, id) results = append(results, fetchEventMeetingInfo(ctx, runtime, id, calendarID)) } successCount := 0 for _, r := range results { if r.Error == "" { successCount++ } } fmt.Fprintf(errOut, "%s done: %d total, %d succeeded, %d failed\n", meetingLogPrefix, len(results), successCount, len(results)-successCount) if successCount == 0 && len(results) > 0 { return runtime.OutPartialFailure(map[string]any{"meetings": results}, &output.Meta{Count: len(results)}) } outData := map[string]any{"meetings": results} runtime.OutFormat(outData, &output.Meta{Count: len(results)}, func(w io.Writer) { if len(results) == 0 { fmt.Fprintln(w, "No events.") return } var rows []map[string]interface{} for _, r := range results { row := map[string]interface{}{"event_id": r.EventID} if r.Error != "" { row["status"] = "FAIL" row["error"] = r.Error } else { row["status"] = "OK" if r.MeetingID != "" { row["meeting_id"] = r.MeetingID } if r.MeetingNote != "" { row["meeting_note"] = r.MeetingNote } if r.Hint != "" { row["hint"] = r.Hint } } rows = append(rows, row) } output.PrintTable(w, rows) fmt.Fprintf(w, "\n%d event(s), %d succeeded, %d failed\n", len(results), successCount, len(results)-successCount) }) return nil }, }
CalendarMeeting gets meeting info for calendar events.
View Source
var CalendarRoomFind = common.Shortcut{ Service: "calendar", Command: "+room-find", Description: "Find available meeting room candidates for one or more event time slots", Risk: "read", Scopes: []string{"calendar:calendar.free_busy:read"}, AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ {Name: flagSlot, Type: "string_array", Desc: "event time slot in start~end format; repeatable"}, {Name: flagCity, Type: "string", Desc: "meeting room city constraint"}, {Name: flagBuilding, Type: "string", Desc: "meeting room building constraint"}, {Name: flagFloor, Type: "string", Desc: "meeting room floor constraint (e.g., F2)"}, {Name: flagRoomName, Type: "string", Desc: "meeting room name constraint; comma-separated for multiple names (e.g., 01,02,03)"}, {Name: flagMinCapacity, Type: "int", Desc: "minimum meeting room capacity"}, {Name: flagMaxCapacity, Type: "int", Desc: "maximum meeting room capacity"}, {Name: flagAttendees, Type: "string", Desc: "attendee IDs, comma-separated (supports user ou_, chat oc_)"}, {Name: flagEventRrule, Type: "string", Desc: "event recurrence rule"}, {Name: flagTimezone, Type: "string", Desc: "current time zone"}, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { baseReq, err := buildRoomFindBaseRequest(runtime) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } slots, err := parseRoomFindSlots(runtime) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } d := common.NewDryRunAPI() for _, slot := range slots { req := *baseReq req.EventStartTime = slot.Start req.EventEndTime = slot.End d.POST(roomFindPath). Desc(fmt.Sprintf("Lookup meeting room suggestions for %s - %s", slot.Start, slot.End)). Body(req) } return d }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if err := rejectCalendarAutoBotFallback(runtime); err != nil { return err } for _, flag := range []string{flagCity, flagBuilding, flagFloor, flagEventRrule, flagTimezone} { if val := strings.TrimSpace(runtime.Str(flag)); val != "" { if err := common.RejectDangerousCharsTyped("--"+flag, val); err != nil { return err } } } for _, name := range strings.Split(runtime.Str(flagRoomName), ",") { name = strings.TrimSpace(name) if name == "" { continue } if err := common.RejectDangerousCharsTyped("--"+flagRoomName, name); err != nil { return err } } if _, err := parseRoomFindSlots(runtime); err != nil { return err } if _, _, err := parseRoomFindAttendees(runtime.Str(flagAttendees), ""); err != nil { return err } if minCapacity := runtime.Int(flagMinCapacity); minCapacity < 0 { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--min-capacity must be >= 0").WithParam("--min-capacity") } if maxCapacity := runtime.Int(flagMaxCapacity); maxCapacity < 0 { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--max-capacity must be >= 0").WithParam("--max-capacity") } if minCapacity, maxCapacity := runtime.Int(flagMinCapacity), runtime.Int(flagMaxCapacity); minCapacity > 0 && maxCapacity > 0 && minCapacity > maxCapacity { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--min-capacity must be <= --max-capacity").WithParam("--min-capacity") } var tzInputs []calendarTimeInputRange for _, raw := range runtime.StrArray(flagSlot) { tzInputs = append(tzInputs, collectCalendarRangeInputs(flagSlot, raw)...) } warnCalendarTimezoneMismatch(runtime, tzInputs...) return nil }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { baseReq, err := buildRoomFindBaseRequest(runtime) if err != nil { return err } slots, err := parseRoomFindSlots(runtime) if err != nil { return err } out, err := collectRoomFindResults(slots, roomFindWorkers, func(slot roomFindSlot) ([]*roomFindSuggestion, error) { req := *baseReq req.EventStartTime = slot.Start req.EventEndTime = slot.End return callRoomFind(runtime, &req) }) if err != nil { return err } runtime.OutFormat(out, &output.Meta{Count: len(out.TimeSlots)}, func(w io.Writer) { if len(out.TimeSlots) == 0 { fmt.Fprintln(w, "No meeting room suggestions available.") return } for _, slot := range out.TimeSlots { fmt.Fprintf(w, "%s - %s\n", slot.Start, slot.End) if len(slot.MeetingRooms) == 0 { fmt.Fprintf(w, "0 meeting room(s) found: %s\n", slot.Hint) continue } var rows []map[string]interface{} for _, room := range slot.MeetingRooms { rows = append(rows, map[string]interface{}{ "room_id": room.RoomID, "room_name": room.RoomName, "capacity": room.Capacity, "reserve_until_time": room.ReserveUntilTime, }) } output.PrintTable(w, rows) fmt.Fprintf(w, "%d meeting room(s) found\n", len(slot.MeetingRooms)) fmt.Fprintln(w) } }) return nil }, }
View Source
var CalendarRsvp = common.Shortcut{ Service: "calendar", Command: "+rsvp", Description: "Reply to a calendar event (accept/decline/tentative)", Risk: "write", Scopes: []string{"calendar:calendar.event:reply"}, AuthTypes: []string{"user", "bot"}, HasFormat: false, Flags: []common.Flag{ {Name: "calendar-id", Desc: "calendar ID (default: primary)"}, {Name: "event-id", Desc: "event ID", Required: true}, {Name: "rsvp-status", Desc: "reply status", Required: true, Enum: []string{"accept", "decline", "tentative"}}, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { calendarId := strings.TrimSpace(runtime.Str("calendar-id")) d := common.NewDryRunAPI() switch calendarId { case "": d.Desc("(calendar-id omitted) Will use primary calendar") calendarId = "<primary>" case "primary": calendarId = "<primary>" } eventId := strings.TrimSpace(runtime.Str("event-id")) status := strings.TrimSpace(runtime.Str("rsvp-status")) return d. POST("/open-apis/calendar/v4/calendars/:calendar_id/events/:event_id/reply"). Body(map[string]interface{}{"rsvp_status": status}). Set("calendar_id", calendarId). Set("event_id", eventId) }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if err := rejectCalendarAutoBotFallback(runtime); err != nil { return err } for _, flag := range []string{"calendar-id", "event-id", "rsvp-status"} { if val := strings.TrimSpace(runtime.Str(flag)); val != "" { if err := common.RejectDangerousCharsTyped("--"+flag, val); err != nil { return err } } } eventId := strings.TrimSpace(runtime.Str("event-id")) if eventId == "" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "event-id cannot be empty").WithParam("--event-id") } return nil }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { calendarId := strings.TrimSpace(runtime.Str("calendar-id")) if calendarId == "" { calendarId = PrimaryCalendarIDStr } eventId := strings.TrimSpace(runtime.Str("event-id")) status := strings.TrimSpace(runtime.Str("rsvp-status")) _, err := runtime.CallAPITyped("POST", fmt.Sprintf("/open-apis/calendar/v4/calendars/%s/events/%s/reply", validate.EncodePathSegment(calendarId), validate.EncodePathSegment(eventId)), nil, map[string]interface{}{ "rsvp_status": status, }) if err != nil { return err } runtime.Out(map[string]interface{}{ "calendar_id": calendarId, "event_id": eventId, "rsvp_status": status, }, nil) return nil }, }
View Source
var CalendarSearchEvent = common.Shortcut{ Service: "calendar", Command: "+search-event", Description: "Search calendar events by keyword, time range, and attendees", Risk: "read", Scopes: []string{"calendar:calendar.event:read"}, AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ {Name: "calendar-id", Desc: "calendar ID (default: primary)"}, {Name: "query", Desc: "search keyword"}, {Name: "attendee-ids", Desc: "attendee IDs, comma-separated (supports user ou_, chat oc_, room omm_)"}, {Name: "start", Desc: "search time range start (ISO 8601 or YYYY-MM-DD)"}, {Name: "end", Desc: "search time range end (ISO 8601 or YYYY-MM-DD)"}, {Name: "page-token", Desc: "page token for next page"}, {Name: "page-size", Default: "20", Desc: "page size, 1-30 (default 20)"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if err := rejectCalendarAutoBotFallback(runtime); err != nil { return err } if _, _, err := parseSearchEventTimeRange(runtime); err != nil { return err } if _, err := common.ValidatePageSizeTyped(runtime, "page-size", defaultSearchEventPageSize, 1, maxSearchEventPageSize); err != nil { return err } warnCalendarTimezoneMismatch(runtime, calendarTimeInputRange{Flag: "start", Value: runtime.Str("start")}, calendarTimeInputRange{Flag: "end", Value: runtime.Str("end")}, ) return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { calendarID := runtime.Str("calendar-id") if calendarID == "" { calendarID = "<primary>" } return common.NewDryRunAPI(). POST(fmt.Sprintf("/open-apis/calendar/v4/calendars/%s/events/search_event", calendarID)). Set("calendar_id", calendarID) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { calendarID := strings.TrimSpace(runtime.Str("calendar-id")) if calendarID == "" { calendarID = PrimaryCalendarIDStr } startTime, endTime, err := parseSearchEventTimeRange(runtime) if err != nil { return err } body := &searchEventRequestBody{ Query: strings.TrimSpace(runtime.Str("query")), } if filter := buildSearchEventFilter(runtime, startTime, endTime); filter != nil { body.Filter = filter } params := map[string]any{} pageSize, _ := strconv.Atoi(strings.TrimSpace(runtime.Str("page-size"))) if pageSize <= 0 { pageSize = defaultSearchEventPageSize } params["page_size"] = strconv.Itoa(pageSize) if pt := strings.TrimSpace(runtime.Str("page-token")); pt != "" { params["page_token"] = pt } data, err := runtime.CallAPITyped("POST", fmt.Sprintf("/open-apis/calendar/v4/calendars/%s/events/search_event", validate.EncodePathSegment(calendarID)), params, body) if err != nil { return err } if data == nil { data = map[string]any{} } items := common.GetSlice(data, "items") hasMore, _ := data["has_more"].(bool) pageToken, _ := data["page_token"].(string) outItems := make([]searchEventItem, 0, len(items)) for _, raw := range items { item, _ := raw.(map[string]any) if item == nil { continue } meta, _ := item["meta_data"].(map[string]any) out := searchEventItem{} if meta != nil { if v, ok := meta["event_id"].(string); ok { out.EventID = v } if v, ok := meta["summary"].(string); ok { out.Summary = v } if v, ok := meta["is_all_day"].(bool); ok { out.IsAllDay = v } if v, ok := meta["app_link"].(string); ok { out.AppLink = v } if start, ok := meta["start"].(map[string]any); ok { out.Start = extractTimeInfo(start) } if end, ok := meta["end"].(map[string]any); ok { out.End = extractTimeInfo(end) } } outItems = append(outItems, out) } outData := searchEventOutput{ CalendarID: calendarID, Items: outItems, HasMore: hasMore, PageToken: pageToken, } runtime.OutFormat(outData, &output.Meta{Count: len(outItems)}, func(w io.Writer) { if len(outItems) == 0 { fmt.Fprintln(w, "No events found.") return } var rows []map[string]interface{} for _, item := range outItems { row := map[string]interface{}{ "event_id": item.EventID, "summary": common.TruncateStr(item.Summary, 40), } if item.Start != nil { if item.Start.DateTime != "" { row["start"] = item.Start.DateTime } else if item.Start.Date != "" { row["start"] = item.Start.Date } } if item.End != nil { if item.End.DateTime != "" { row["end"] = item.End.DateTime } else if item.End.Date != "" { row["end"] = item.End.Date } } if item.IsAllDay { row["is_all_day"] = true } rows = append(rows, row) } output.PrintTable(w, rows) fmt.Fprintf(w, "\n%d event(s) found\n", len(outItems)) }) if hasMore && runtime.Format != "json" && runtime.Format != "" { fmt.Fprintf(runtime.IO().Out, "\n(more available, page_token: %s)\n", pageToken) } return nil }, }
CalendarSearchEvent searches calendar events by keyword, time range, and attendees.
View Source
var CalendarSuggestion = common.Shortcut{ Service: "calendar", Command: "+suggestion", Description: "Intelligently suggest available time blocks based on unclear time ranges", Risk: "read", Scopes: []string{"calendar:calendar.free_busy:read"}, AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ {Name: flagStart, Type: "string", Desc: "search start time (ISO 8601, default: current time)"}, {Name: flagEnd, Type: "string", Desc: "search end time (ISO 8601, default: end of start day)"}, {Name: flagAttendees, Type: "string", Desc: "attendee IDs, comma-separated (supports user (open_id) ou_xxx, or chat oc_xxx) ids"}, {Name: flagEventRrule, Type: "string", Desc: "event recurrence rules"}, {Name: flagDurationMinutes, Type: "int", Desc: "duration (minutes)"}, {Name: flagTimezone, Type: "string", Desc: "current time zone"}, {Name: flagExclude, Type: "string", Desc: "excluded event times (ISO 8601, e.g. '2026-03-19T10:00:00+08:00~2026-03-19T11:00:00+08:00'), comma-separated"}, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { req, err := buildSuggestionRequest(runtime) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } return common.NewDryRunAPI(). POST(suggestionPath). Body(req) }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if err := rejectCalendarAutoBotFallback(runtime); err != nil { return err } durationMinutes := runtime.Int(flagDurationMinutes) if durationMinutes != 0 && (durationMinutes < 1 || durationMinutes > 1440) { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--duration-minutes must be between 1 and 1440").WithParam("--duration-minutes") } for _, flag := range []string{flagEventRrule, flagTimezone} { if val := runtime.Str(flag); val != "" { if err := common.RejectDangerousCharsTyped("--"+flag, val); err != nil { return err } } } if attendeesStr := runtime.Str(flagAttendees); attendeesStr != "" { for _, id := range strings.Split(attendeesStr, ",") { id = strings.TrimSpace(id) if id == "" { continue } if !strings.HasPrefix(id, "ou_") && !strings.HasPrefix(id, "oc_") { return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid attendee id format %q: should start with 'ou_' or 'oc_'", id).WithParam("--" + flagAttendees) } } } startInput := runtime.Str(flagStart) if startInput != "" { if _, err := common.ParseTime(startInput); err != nil { return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid start time: %v", err).WithParam("--start") } } endInput := runtime.Str(flagEnd) if endInput != "" { if _, err := common.ParseTime(endInput, "end"); err != nil { return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid end time: %v", err).WithParam("--end") } } excludeStr := runtime.Str(flagExclude) if excludeStr != "" { excludeStr = strings.TrimSpace(excludeStr) ranges := strings.Split(excludeStr, ",") for _, r := range ranges { r = strings.TrimSpace(r) if r == "" { continue } parts := strings.Split(r, "~") if len(parts) != 2 { return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid range format in --exclude: %q, expect start~end", r).WithParam("--exclude") } if _, err := common.ParseTime(parts[0]); err != nil { return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid start time in --exclude: %q (%v)", parts[0], err).WithParam("--exclude") } if _, err := common.ParseTime(parts[1], "end"); err != nil { return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid end time in --exclude: %q (%v)", parts[1], err).WithParam("--exclude") } } } tzInputs := []calendarTimeInputRange{ {Flag: flagStart, Value: runtime.Str(flagStart)}, {Flag: flagEnd, Value: runtime.Str(flagEnd)}, } warnCalendarTimezoneMismatch(runtime, tzInputs...) return nil }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { req, err := buildSuggestionRequest(runtime) if err != nil { return err } apiResp, err := runtime.DoAPI(&larkcore.ApiReq{ HttpMethod: "POST", ApiPath: suggestionPath, Body: req, }) if err != nil { if _, ok := errs.ProblemOf(err); ok { return err } return errs.WrapInternal(err) } if _, err := runtime.ClassifyAPIResponse(apiResp); err != nil { return err } var resp = &OpenAPIResponse[*SuggestionResponse]{} if err := json.Unmarshal(apiResp.RawBody, &resp); err != nil { return errs.NewInternalError(errs.SubtypeInvalidResponse, "unmarshal response fail").WithCause(err) } data := resp.Data var suggestions []*EventTime var aiGuidance string if data != nil { suggestions = data.Suggestions aiGuidance = data.AiActionGuidance } runtime.OutFormat(data, &output.Meta{Count: len(suggestions)}, func(w io.Writer) { if len(suggestions) == 0 { fmt.Fprintln(w, "No suggestions available.") } else { var rows []map[string]interface{} for _, item := range suggestions { rows = append(rows, map[string]interface{}{ "start": item.EventStartTime, "end": item.EventEndTime, "reason": item.RecommendReason, }) } output.PrintTable(w, rows) fmt.Fprintf(w, "\n%d suggestion(s) found\n", len(suggestions)) } if aiGuidance != "" { fmt.Fprintf(w, "\nAction Guidance: %s\n", aiGuidance) } }) return nil }, }
View Source
var CalendarTransfer = common.Shortcut{ Service: "calendar", Command: "+transfer", Description: "Transfer the organizer role of a calendar event to another user or bot", Risk: "high-risk-write", Scopes: []string{"calendar:calendar.event:transfer"}, ConditionalScopes: []string{"calendar:calendar.event:read"}, AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ {Name: "event-id", Desc: "event ID to transfer (uid_originalTime)", Required: true}, {Name: "to-user-id", Desc: "receiver open_id (ou_...); becomes the new organizer. May be a user or a bot", Required: true}, {Name: "calendar-id", Desc: "calendar ID the event lives on (default: primary)"}, {Name: flagRemoveOriginalOrganizer, Type: "bool", Default: "false", Desc: "remove the original organizer instead of keeping them as an attendee; the server forces this on a shared calendar, where original_organizer_removed is omitted from the result"}, {Name: flagTransferSeries, Type: "bool", Default: "false", Desc: "confirm transferring the entire recurring series; required for recurring events because a single occurrence cannot be transferred"}, }, Tips: []string{ "Transferring is irreversible and also moves meeting minutes, notes and attachments to the new organizer; pass --yes to confirm.", "--as must be the event's current organizer (user or bot); --to-user-id may be a user or a bot, so all four user/bot directions are expressed by those two flags.", `Example: lark-cli calendar +transfer --event-id <uid_originalTime> --to-user-id ou_xxx --yes`, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateCalendarTransfer(runtime) }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { return dryRunCalendarTransfer(runtime) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { return executeCalendarTransfer(ctx, runtime) }, }
View Source
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 as Markdown (@file or - for stdin); the unified description field. Supports bold/italic/underline/strikethrough, links, headings (`#`..`###`), blockquotes (`>`), ordered/unordered lists, horizontal rules (`---`), GFM tables, and images (``; a remote URL is used as-is, and a local image path relative to and inside the current working directory is auto-uploaded to Lark drive and rendered inline — absolute/out-of-cwd paths are rejected). A Lark doc URL (bare or as a Markdown link) is auto-resolved to an inline doc-mention chip showing its title. Inside a GFM table cell, stack multiple lines with `<br>`; each line may itself be an ordered/unordered list item, image or styled text (e.g. `1. a<br>2. b`, `- x<br>- y`, `<br>**bold**`). Passing an empty string clears the description.", Input: []string{common.File, common.Stdin}}, {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: flagApplyTo, Enum: applyToValues, Desc: "recurring scope: single (this occurrence / exception only) | all (whole series and every exception) | this-and-following (truncate the series at this instance and create a new series carrying the requested edits). Required on recurring events; ignored on non-recurring events.", }, {Name: "notify", Type: "bool", Default: "true", Desc: "send update notification to attendees"}, {Name: flagSkipRoomCheck, Type: "bool", Default: "false", Hidden: true, Desc: "skip meeting-room availability precheck (default checks rooms whenever a new room is added or the time/rrule of a room-attached event changes)"}, }, 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) }, }
Functions ¶
Types ¶
type OpenAPIResponse ¶
type SuggestionRequest ¶
type SuggestionRequest struct {
SearchStartTime string `json:"search_start_time,omitempty"`
SearchEndTime string `json:"search_end_time,omitempty"`
Timezone string `json:"timezone,omitempty"`
EventRrule string `json:"event_rrule,omitempty"`
DurationMinutes int `json:"duration_minutes,omitempty"`
AttendeeUserIds []string `json:"attendee_user_ids,omitempty"`
AttendeeChatIds []string `json:"attendee_chat_ids,omitempty"`
ExcludedEventTimes []*EventTime `json:"excluded_event_times,omitempty"`
}
type SuggestionResponse ¶
Source Files
¶
- calendar_agenda.go
- calendar_create.go
- calendar_delete.go
- calendar_freebusy.go
- calendar_get.go
- calendar_join_event.go
- calendar_list_attendees.go
- calendar_meeting.go
- calendar_recurring.go
- calendar_room_check.go
- calendar_room_find.go
- calendar_rsvp.go
- calendar_search_event.go
- calendar_suggestion.go
- calendar_transfer.go
- calendar_update.go
- calendar_update_recurring.go
- description_rich_images.go
- errors.go
- helpers.go
- shortcuts.go
Click to show internal directories.
Click to hide internal directories.