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)"}, }, 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") } } } } 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"}, 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"}, {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)"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { for _, flag := range []string{"summary", "description", "rrule", "calendar-id"} { if val := runtime.Str(flag); val != "" { if err := common.RejectDangerousChars("--"+flag, val); err != nil { return output.ErrValidation(err.Error()) } } } 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 output.ErrValidation("invalid attendee id format %q: should start with 'ou_', 'oc_', or 'omm_'", id) } } } if runtime.Str("start") == "" { return common.FlagErrorf("specify --start (e.g. '2026-03-12T14:00+08:00')") } if runtime.Str("end") == "" { return common.FlagErrorf("specify --end (e.g. '2026-03-12T15:00+08:00')") } startTs, err := common.ParseTime(runtime.Str("start")) if err != nil { return common.FlagErrorf("--start: %v", err) } endTs, err := common.ParseTime(runtime.Str("end"), "end") if err != nil { return common.FlagErrorf("--end: %v", err) } s, err := strconv.ParseInt(startTs, 10, 64) if err != nil { return common.FlagErrorf("invalid start time: %v", err) } e, err := strconv.ParseInt(endTs, 10, 64) if err != nil { return common.FlagErrorf("invalid end time: %v", err) } if e <= s { return common.FlagErrorf("end time must be after start time") } 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 output.ErrValidation("--start: %v", err) } endTs, err := common.ParseTime(runtime.Str("end"), "end") if err != nil { return output.ErrValidation("--end: %v", err) } eventData := buildEventData(runtime, startTs, endTs) data, err := runtime.CallAPI("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 output.Errorf(output.ExitAPI, "api_error", "failed to create event: no event_id returned") } if attendeesStr := runtime.Str("attendee-ids"); attendeesStr != "" { currentUserId := "" if !runtime.IsBot() { currentUserId = runtime.UserOpenId() } attendees, err := parseAttendees(attendeesStr, currentUserId) if err != nil { return output.ErrValidation("invalid attendee id: %v", err) } _, err = runtime.CallAPI("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 { _, rollbackErr := runtime.RawAPI("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 output.Errorf(output.ExitAPI, "api_error", "failed to add attendees: %v; rollback also failed, orphan event_id=%s needs manual cleanup", rollbackErr, eventId) } return output.Errorf(output.ExitAPI, "api_error", "failed to add attendees: %v; event rolled back successfully", err) } } 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) } } runtime.Out(map[string]interface{}{ "event_id": eventId, "summary": event["summary"], "start": startStr, "end": endStr, }, nil) return nil }, }
View Source
var CalendarFreebusy = common.Shortcut{ Service: "calendar", Command: "+freebusy", Description: "Query user free/busy and RSVP status", 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: "user-id", Desc: "target user open_id (ou_ prefix, default: current user)"}, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { userId := runtime.Str("user-id") if userId == "" { userId = runtime.UserOpenId() } timeMin, timeMax, err := parseFreebusyTimeRange(runtime) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } return common.NewDryRunAPI(). POST("/open-apis/calendar/v4/freebusy/list"). Body(map[string]interface{}{"time_min": timeMin, "time_max": timeMax, "user_id": userId, "need_rsvp_status": true}) }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { userId := runtime.Str("user-id") if userId == "" && runtime.IsBot() { return common.FlagErrorf("--user-id is required for bot identity") } if userId == "" && runtime.UserOpenId() == "" { return common.FlagErrorf("cannot determine user ID, specify --user-id or ensure you are logged in") } if userId != "" { if _, err := common.ValidateUserID(userId); err != nil { return err } } return nil }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { userId := runtime.Str("user-id") if userId == "" { userId = runtime.UserOpenId() } timeMin, timeMax, err := parseFreebusyTimeRange(runtime) if err != nil { return output.ErrValidation("--start/--end: %v", err) } data, err := runtime.CallAPI("POST", "/open-apis/calendar/v4/freebusy/list", nil, map[string]interface{}{ "time_min": timeMin, "time_max": timeMax, "user_id": userId, "need_rsvp_status": true, }) if err != nil { return err } items, _ := data["freebusy_list"].([]interface{}) runtime.OutFormat(items, &output.Meta{Count: len(items)}, func(w io.Writer) { if len(items) == 0 { fmt.Fprintln(w, "No busy periods in this time range.") return } var rows []map[string]interface{} for _, item := range items { m, ok := item.(map[string]interface{}) if !ok { continue } rows = append(rows, map[string]interface{}{ "start": m["start_time"], "end": m["end_time"], }) } output.PrintTable(w, rows) fmt.Fprintf(w, "\n%d busy period(s) total\n", len(items)) }) 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 { for _, flag := range []string{"calendar-id", "event-id", "rsvp-status"} { if val := strings.TrimSpace(runtime.Str(flag)); val != "" { if err := common.RejectDangerousChars("--"+flag, val); err != nil { return output.ErrValidation(err.Error()) } } } eventId := strings.TrimSpace(runtime.Str("event-id")) if eventId == "" { return output.ErrValidation("event-id cannot be empty") } 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.DoAPIJSON("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 CalendarSuggestion = common.Shortcut{ Service: "calendar", Command: "+suggestion", Description: "Intelligently suggest available meeting times to simplify scheduling", 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 { durationMinutes := runtime.Int(flagDurationMinutes) if durationMinutes != 0 && (durationMinutes < 1 || durationMinutes > 1440) { return output.ErrValidation("--duration-minutes must be between 1 and 1440") } for _, flag := range []string{flagEventRrule, flagTimezone} { if val := runtime.Str(flag); val != "" { if err := common.RejectDangerousChars("--"+flag, val); err != nil { return output.ErrValidation(err.Error()) } } } 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 output.ErrValidation("invalid attendee id format %q: should start with 'ou_' or 'oc_'", id) } } } startInput := runtime.Str(flagStart) if startInput != "" { if _, err := common.ParseTime(startInput); err != nil { return output.ErrValidation("invalid start time: %v", err) } } endInput := runtime.Str(flagEnd) if endInput != "" { if _, err := common.ParseTime(endInput, "end"); err != nil { return output.ErrValidation("invalid end time: %v", err) } } 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 output.ErrValidation("invalid range format in --exclude: %q, expect start~end", r) } if _, err := common.ParseTime(parts[0]); err != nil { return output.ErrValidation("invalid start time in --exclude: %q (%v)", parts[0], err) } if _, err := common.ParseTime(parts[1], "end"); err != nil { return output.ErrValidation("invalid end time in --exclude: %q (%v)", parts[1], err) } } } 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 { return output.ErrWithHint(output.ExitInternal, "request_fail", "api request fail", err.Error()) } if apiResp.StatusCode < http.StatusOK || apiResp.StatusCode >= http.StatusMultipleChoices { return output.ErrAPI(apiResp.StatusCode, "", string(apiResp.RawBody)) } var resp = &OpenAPIResponse[*SuggestionResponse]{} if err := json.Unmarshal(apiResp.RawBody, &resp); err != nil { return output.ErrWithHint(output.ExitInternal, "validation", "unmarshal response fail", err.Error()) } if resp.Code != 0 { return output.ErrAPI(resp.Code, resp.Msg, resp.Data) } 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 }, }
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 ¶
Click to show internal directories.
Click to hide internal directories.