diff --git a/shortcuts/vc/vc_meeting_events.go b/shortcuts/vc/vc_meeting_events.go index 813010753..1b9257e7f 100644 --- a/shortcuts/vc/vc_meeting_events.go +++ b/shortcuts/vc/vc_meeting_events.go @@ -23,8 +23,8 @@ const ( minVCMeetingEventsPageSize = 20 maxVCMeetingEventsPageSize = 100 maxVCMeetingEventSummarySize = 80 - defaultVCMeetingPageLimit = 20 - maxVCMeetingPageLimit = 40 + defaultVCMeetingPageLimit = 50 + maxVCMeetingPageLimit = 200 ) // toUnixSeconds converts a supported CLI time input into a Unix seconds string. @@ -54,8 +54,8 @@ var VCMeetingEvents = common.Shortcut{ {Name: "end", Desc: "time upper bound (ISO 8601, YYYY-MM-DD, or Unix seconds)"}, {Name: "page-token", Desc: "page token for the next page"}, {Name: "page-size", Default: "20", Desc: "page size, 20-100 (default 20)"}, - {Name: "page-all", Type: "bool", Desc: "automatically paginate through all pages (max 40)"}, - {Name: "page-limit", Type: "int", Default: "20", Desc: "max page limit when --page-all is set (default 20, max 40)"}, + {Name: "page-all", Type: "bool", Desc: "automatically paginate through all pages (uses max page limit 200 unless --page-limit is set)"}, + {Name: "page-limit", Type: "int", Default: "50", Desc: "max pages to fetch during auto-pagination; setting this flag also enables auto-pagination (default 50, max 200)"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if err := validateMeetingEventsMeetingID(runtime.Str("meeting-id")); err != nil { @@ -64,9 +64,6 @@ var VCMeetingEvents = common.Shortcut{ if _, _, err := parseMeetingEventsTimeRange(runtime); err != nil { return err } - if _, err := common.ValidatePageSize(runtime, "page-size", defaultVCMeetingEventsSize, minVCMeetingEventsPageSize, maxVCMeetingEventsPageSize); err != nil { - return err - } if _, err := meetingEventsPageLimit(runtime); err != nil { return err } @@ -81,7 +78,15 @@ var VCMeetingEvents = common.Shortcut{ if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } - dryRun := common.NewDryRunAPI().GET(vcMeetingEventsAPIPath) + autoPaginate, pageLimit, err := meetingEventsPaginationConfig(runtime) + if err != nil { + return common.NewDryRunAPI().Set("error", err.Error()) + } + dryRun := common.NewDryRunAPI() + if autoPaginate { + dryRun = dryRun.Desc(fmt.Sprintf("Auto-paginates up to %d page(s)", pageLimit)) + } + dryRun = dryRun.GET(vcMeetingEventsAPIPath) if flat := flattenQueryParams(params); len(flat) > 0 { dryRun.Params(flat) } @@ -96,6 +101,7 @@ var VCMeetingEvents = common.Shortcut{ if err != nil { return err } + events = compactMeetingEvents(events) rows := buildMeetingEventRows(events) outData := map[string]interface{}{ "events": events, @@ -118,6 +124,27 @@ var VCMeetingEvents = common.Shortcut{ }, } +func meetingEventsPageSize(runtime *common.RuntimeContext) (int, error) { + if runtime.Bool("page-all") { + return maxVCMeetingEventsPageSize, nil + } + pageSizeStr := strings.TrimSpace(runtime.Str("page-size")) + if pageSizeStr == "" { + return defaultVCMeetingEventsSize, nil + } + pageSize, err := strconv.Atoi(pageSizeStr) + if err != nil { + return 0, common.FlagErrorf("invalid --page-size %q: must be an integer", pageSizeStr) + } + if pageSize < minVCMeetingEventsPageSize { + return minVCMeetingEventsPageSize, nil + } + if pageSize > maxVCMeetingEventsPageSize { + return maxVCMeetingEventsPageSize, nil + } + return pageSize, nil +} + func meetingEventsPageLimit(runtime *common.RuntimeContext) (int, error) { limit := runtime.Int("page-limit") if limit == 0 && !runtime.Cmd.Flags().Changed("page-limit") { @@ -129,6 +156,21 @@ func meetingEventsPageLimit(runtime *common.RuntimeContext) (int, error) { return limit, nil } +func meetingEventsPaginationConfig(runtime *common.RuntimeContext) (autoPaginate bool, pageLimit int, err error) { + pageLimit, err = meetingEventsPageLimit(runtime) + if err != nil { + return false, 0, err + } + autoPaginate = runtime.Bool("page-all") + if runtime.Cmd != nil && runtime.Cmd.Flags().Changed("page-limit") { + autoPaginate = true + } + if runtime.Bool("page-all") && (runtime.Cmd == nil || !runtime.Cmd.Flags().Changed("page-limit")) { + pageLimit = maxVCMeetingPageLimit + } + return autoPaginate, pageLimit, nil +} + func validateMeetingEventsMeetingID(meetingID string) error { meetingID = strings.TrimSpace(meetingID) if meetingID == "" { @@ -175,7 +217,7 @@ func parseMeetingEventsTimeRange(runtime *common.RuntimeContext) (string, string } func buildMeetingEventsParams(runtime *common.RuntimeContext, startTime, endTime string) (larkcore.QueryParams, error) { - pageSize, err := common.ValidatePageSize(runtime, "page-size", defaultVCMeetingEventsSize, minVCMeetingEventsPageSize, maxVCMeetingEventsPageSize) + pageSize, err := meetingEventsPageSize(runtime) if err != nil { return nil, err } @@ -200,7 +242,11 @@ func fetchMeetingEvents(ctx context.Context, runtime *common.RuntimeContext, sta if err != nil { return nil, nil, false, "", err } - if !runtime.Bool("page-all") { + autoPaginate, pageLimit, err := meetingEventsPaginationConfig(runtime) + if err != nil { + return nil, nil, false, "", err + } + if !autoPaginate { data, err := runtime.DoAPIJSON(http.MethodGet, vcMeetingEventsAPIPath, params, nil) if err != nil { return nil, nil, false, "", err @@ -214,10 +260,6 @@ func fetchMeetingEvents(ctx context.Context, runtime *common.RuntimeContext, sta return data, events, hasMore, pageToken, nil } - pageLimit, err := meetingEventsPageLimit(runtime) - if err != nil { - return nil, nil, false, "", err - } var ( allEvents []interface{} lastData map[string]interface{} @@ -272,6 +314,35 @@ func flattenQueryParams(params larkcore.QueryParams) map[string]interface{} { return flat } +func compactMeetingEvents(events []interface{}) []interface{} { + compacted := make([]interface{}, 0, len(events)) + for _, raw := range events { + event, _ := raw.(map[string]interface{}) + if event == nil { + continue + } + if payload := common.GetMap(event, "payload"); payload != nil { + event["payload"] = compactMeetingPayload(payload) + } + compacted = append(compacted, event) + } + return compacted +} + +func compactMeetingPayload(payload map[string]interface{}) map[string]interface{} { + if payload == nil { + return nil + } + compacted := make(map[string]interface{}, len(payload)) + for key, value := range payload { + if items, ok := value.([]interface{}); ok && len(items) == 0 { + continue + } + compacted[key] = value + } + return compacted +} + func buildMeetingEventRows(events []interface{}) []map[string]interface{} { rows := make([]map[string]interface{}, 0, len(events)) for _, raw := range events { diff --git a/shortcuts/vc/vc_meeting_events_test.go b/shortcuts/vc/vc_meeting_events_test.go index 8c89f11fb..b21a72c93 100644 --- a/shortcuts/vc/vc_meeting_events_test.go +++ b/shortcuts/vc/vc_meeting_events_test.go @@ -23,7 +23,7 @@ func newMeetingEventsRuntime() *common.RuntimeContext { cmd.Flags().String("page-token", "", "") cmd.Flags().String("page-size", "", "") cmd.Flags().Bool("page-all", false, "") - cmd.Flags().Int("page-limit", 20, "") + cmd.Flags().Int("page-limit", 50, "") return common.TestNewRuntimeContext(cmd, defaultConfig()) } @@ -64,6 +64,32 @@ func participantJoinedEvent() map[string]interface{} { } } +func chatReceivedEvent() map[string]interface{} { + return map[string]interface{}{ + "event_id": "event-2", + "event_type": "chat_received", + "event_time": "2026-04-17T08:05:00Z", + "payload": map[string]interface{}{ + "activity_event_type": "chat_received", + "participant_joined_items": []interface{}{}, + "participant_left_items": []interface{}{}, + "transcript_received_items": []interface{}{}, + "magic_share_started_items": []interface{}{}, + "magic_share_ended_items": []interface{}{}, + "chat_received_items": []interface{}{ + map[string]interface{}{ + "content": "hello", + "message_type": 3, + "operator": map[string]interface{}{ + "id": "u1", + "user_name": "Alice", + }, + }, + }, + }, + } +} + func TestChatReceivedSummary_MultipleItems(t *testing.T) { payload := map[string]interface{}{ "chat_received_items": []interface{}{ @@ -149,30 +175,39 @@ func TestMeetingEvents_Validation_InvalidTimeRange(t *testing.T) { } } -func TestMeetingEvents_Validation_InvalidPageSize(t *testing.T) { +func TestMeetingEvents_Validation_PageSizeBelowMinDoesNotError(t *testing.T) { runtime := newMeetingEventsRuntime() _ = runtime.Cmd.Flags().Set("meeting-id", "7628568141510692381") _ = runtime.Cmd.Flags().Set("page-size", "10") err := VCMeetingEvents.Validate(context.Background(), runtime) - if err == nil { - t.Fatal("expected validation error for invalid page size") + if err != nil { + t.Fatalf("expected no validation error for page-size clamp, got: %v", err) } - if !strings.Contains(err.Error(), "must be between 20 and 100") { - t.Fatalf("unexpected error: %v", err) +} + +func TestMeetingEvents_Validation_PageAllIgnoresInvalidPageSize(t *testing.T) { + runtime := newMeetingEventsRuntime() + _ = runtime.Cmd.Flags().Set("meeting-id", "7628568141510692381") + _ = runtime.Cmd.Flags().Set("page-all", "true") + _ = runtime.Cmd.Flags().Set("page-size", "10") + + err := VCMeetingEvents.Validate(context.Background(), runtime) + if err != nil { + t.Fatalf("expected no validation error when page-all ignores page-size, got: %v", err) } } func TestMeetingEvents_Validation_InvalidPageLimit(t *testing.T) { runtime := newMeetingEventsRuntime() _ = runtime.Cmd.Flags().Set("meeting-id", "7628568141510692381") - _ = runtime.Cmd.Flags().Set("page-limit", "41") + _ = runtime.Cmd.Flags().Set("page-limit", "201") err := VCMeetingEvents.Validate(context.Background(), runtime) if err == nil { t.Fatal("expected validation error for invalid page limit") } - if !strings.Contains(err.Error(), "must be between 1 and 40") { + if !strings.Contains(err.Error(), "must be between 1 and 200") { t.Fatalf("unexpected error: %v", err) } } @@ -204,6 +239,49 @@ func TestBuildMeetingEventsParams(t *testing.T) { } } +func TestBuildMeetingEventsParams_PageSizeBelowMinClampsToMin(t *testing.T) { + runtime := newMeetingEventsRuntime() + _ = runtime.Cmd.Flags().Set("meeting-id", "7628568141510692381") + _ = runtime.Cmd.Flags().Set("page-size", "10") + + params, err := buildMeetingEventsParams(runtime, "", "") + if err != nil { + t.Fatalf("buildMeetingEventsParams() error = %v", err) + } + if got := params["page_size"][0]; got != "20" { + t.Fatalf("page_size = %q, want %q when below min", got, "20") + } +} + +func TestBuildMeetingEventsParams_PageSizeAboveMaxClampsToMax(t *testing.T) { + runtime := newMeetingEventsRuntime() + _ = runtime.Cmd.Flags().Set("meeting-id", "7628568141510692381") + _ = runtime.Cmd.Flags().Set("page-size", "999") + + params, err := buildMeetingEventsParams(runtime, "", "") + if err != nil { + t.Fatalf("buildMeetingEventsParams() error = %v", err) + } + if got := params["page_size"][0]; got != "100" { + t.Fatalf("page_size = %q, want %q when above max", got, "100") + } +} + +func TestBuildMeetingEventsParams_PageAllUsesMaxPageSize(t *testing.T) { + runtime := newMeetingEventsRuntime() + _ = runtime.Cmd.Flags().Set("meeting-id", "7628568141510692381") + _ = runtime.Cmd.Flags().Set("page-all", "true") + _ = runtime.Cmd.Flags().Set("page-size", "50") + + params, err := buildMeetingEventsParams(runtime, "", "") + if err != nil { + t.Fatalf("buildMeetingEventsParams() error = %v", err) + } + if got := params["page_size"][0]; got != "100" { + t.Fatalf("page_size = %q, want %q when page-all is set", got, "100") + } +} + func TestMeetingEvents_DryRun(t *testing.T) { f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig()) err := mountAndRun(t, VCMeetingEvents, []string{ @@ -235,6 +313,24 @@ func TestMeetingEvents_DryRun(t *testing.T) { } } +func TestMeetingEvents_DryRun_PageAllUsesMaxLimit(t *testing.T) { + f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, VCMeetingEvents, []string{ + "+meeting-events", + "--meeting-id", "7628568141510692381", + "--page-all", + "--dry-run", + "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !strings.Contains(stdout.String(), "Auto-paginates up to 200 page(s)") { + t.Fatalf("dry-run output missing auto-pagination description: %s", stdout.String()) + } +} + func TestMeetingEvents_ExecuteJSON_PageAll(t *testing.T) { f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, true, "pt_2")) @@ -263,6 +359,30 @@ func TestMeetingEvents_ExecuteJSON_PageAll(t *testing.T) { } } +func TestMeetingEvents_ExecuteJSON_PageLimitEnablesPagination(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) + reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, true, "pt_2")) + reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, false, "")) + + err := mountAndRun(t, VCMeetingEvents, []string{ + "+meeting-events", + "--meeting-id", "7628568141510692381", + "--format", "json", + "--page-limit", "2", + "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + reg.Verify(t) + + out := strings.ReplaceAll(stdout.String(), " ", "") + out = strings.ReplaceAll(out, "\n", "") + if count := strings.Count(out, `"event_type":"participant_joined"`); count != 2 { + t.Fatalf("expected 2 aggregated events, got %d: %s", count, stdout.String()) + } +} + func TestMeetingEvents_ExecuteJSON(t *testing.T) { f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, true, "1710000000000000000")) @@ -292,6 +412,38 @@ func TestMeetingEvents_ExecuteJSON(t *testing.T) { } } +func TestMeetingEvents_ExecuteJSON_PrunesEmptySlices(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) + reg.Register(meetingEventsStub([]interface{}{chatReceivedEvent()}, false, "")) + + err := mountAndRun(t, VCMeetingEvents, []string{ + "+meeting-events", + "--meeting-id", "7628568141510692381", + "--format", "json", + "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + reg.Verify(t) + + out := stdout.String() + for _, unwanted := range []string{ + `"participant_joined_items": []`, + `"participant_left_items": []`, + `"transcript_received_items": []`, + `"magic_share_started_items": []`, + `"magic_share_ended_items": []`, + } { + if strings.Contains(out, unwanted) { + t.Fatalf("json output should not contain %q: %s", unwanted, out) + } + } + if !strings.Contains(out, `"message_type": 3`) { + t.Fatalf("json output should keep numeric fields: %s", out) + } +} + func TestMeetingEvents_ExecutePretty(t *testing.T) { f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, true, "1710000000000000000"))