@@ -8,8 +8,11 @@ import (
88 "os"
99 "time"
1010
11+ "github.com/codeGROOVE-dev/slacker/pkg/dailyreport"
1112 "github.com/codeGROOVE-dev/slacker/pkg/github"
13+ "github.com/codeGROOVE-dev/slacker/pkg/home"
1214 "github.com/codeGROOVE-dev/turnclient/pkg/turn"
15+ gogithub "github.com/google/go-github/v50/github"
1316)
1417
1518// makePollEventKey creates an event key for poll-based PR processing.
@@ -192,6 +195,9 @@ func (c *Coordinator) pollAndReconcileWithSearcher(ctx context.Context, searcher
192195 "processed" , successCount ,
193196 "errors" , errorCount ,
194197 "next_poll" , "5m" )
198+
199+ // Check daily reports for users involved in these PRs
200+ c .checkDailyReports (ctx , org , prs )
195201}
196202
197203// reconcilePR checks a single PR and sends notifications if needed.
@@ -439,3 +445,155 @@ func (c *Coordinator) StartupReconciliation(ctx context.Context) {
439445 "skipped" , skippedCount ,
440446 "errors" , errorCount )
441447}
448+
449+ // extractUniqueGitHubUsers extracts all unique GitHub usernames involved in PRs.
450+ // Currently extracts PR authors. The FetchDashboard call will determine which users
451+ // have incoming PRs to review (as reviewers/assignees).
452+ func extractUniqueGitHubUsers (prs []github.PRSnapshot ) map [string ]bool {
453+ users := make (map [string ]bool )
454+
455+ for i := range prs {
456+ pr := & prs [i ]
457+
458+ // Add author
459+ if pr .Author != "" {
460+ users [pr .Author ] = true
461+ }
462+ }
463+
464+ return users
465+ }
466+
467+ // checkDailyReports checks if users involved in PRs should receive daily reports.
468+ // Efficiently extracts unique GitHub users from polled PRs instead of iterating all workspace users.
469+ // Reports are sent between 6am-11:30am user local time, with 23+ hour intervals.
470+ func (c * Coordinator ) checkDailyReports (ctx context.Context , org string , prs []github.PRSnapshot ) {
471+ // Check if daily reports are enabled for this org
472+ cfg , exists := c .configManager .Config (org )
473+ if ! exists {
474+ slog .Debug ("skipping daily reports - no config found" , "org" , org )
475+ return
476+ }
477+
478+ if cfg .Global .DisableDailyReport {
479+ slog .Debug ("daily reports disabled for org" , "org" , org )
480+ return
481+ }
482+
483+ // Get domain for user mapping
484+ domain := cfg .Global .EmailDomain
485+
486+ // Extract unique GitHub users from PRs (authors, reviewers, assignees)
487+ githubUsers := extractUniqueGitHubUsers (prs )
488+ if len (githubUsers ) == 0 {
489+ slog .Debug ("no users involved in PRs, skipping daily reports" , "org" , org )
490+ return
491+ }
492+
493+ slog .Debug ("checking daily reports for PR-involved users" ,
494+ "org" , org ,
495+ "unique_github_users" , len (githubUsers ),
496+ "window" , "6am-11:30am local time" ,
497+ "min_interval" , "23 hours" )
498+
499+ // Get GitHub client for dashboard fetching
500+ token := c .github .InstallationToken (ctx )
501+ if token == "" {
502+ slog .Warn ("skipping daily reports - no GitHub token" , "org" , org )
503+ return
504+ }
505+
506+ ghClient , ok := c .github .Client ().(* gogithub.Client )
507+ if ! ok {
508+ slog .Error ("skipping daily reports - failed to get GitHub client" )
509+ return
510+ }
511+
512+ // Create daily report sender and dashboard fetcher
513+ sender := dailyreport .NewSender (c .stateStore , c .slack )
514+ fetcher := home .NewFetcher (ghClient , c .stateStore , token , "ready-to-review[bot]" )
515+
516+ sentCount := 0
517+ skippedCount := 0
518+ errorCount := 0
519+
520+ for githubUsername := range githubUsers {
521+ // Map GitHub user to Slack user ID
522+ slackUserID , err := c .userMapper .SlackHandle (ctx , githubUsername , org , domain )
523+ if err != nil || slackUserID == "" {
524+ slog .Debug ("skipping daily report - no Slack mapping" ,
525+ "github_user" , githubUsername ,
526+ "error" , err )
527+ skippedCount ++
528+ continue
529+ }
530+
531+ // Fetch user's dashboard (incoming/outgoing PRs)
532+ dashboard , err := fetcher .FetchDashboard (ctx , githubUsername , []string {org })
533+ if err != nil {
534+ slog .Debug ("skipping daily report - dashboard fetch failed" ,
535+ "github_user" , githubUsername ,
536+ "slack_user" , slackUserID ,
537+ "error" , err )
538+ errorCount ++
539+ continue
540+ }
541+
542+ // Build user blocking info
543+ userInfo := dailyreport.UserBlockingInfo {
544+ GitHubUsername : githubUsername ,
545+ SlackUserID : slackUserID ,
546+ IncomingPRs : dashboard .IncomingPRs ,
547+ OutgoingPRs : dashboard .OutgoingPRs ,
548+ }
549+
550+ // Check if should send report
551+ if ! sender .ShouldSendReport (ctx , userInfo ) {
552+ // Record timestamp even if no report sent (no PRs or outside window)
553+ // This prevents checking this user again for 23 hours
554+ if len (dashboard .IncomingPRs ) == 0 && len (dashboard .OutgoingPRs ) == 0 {
555+ if err := c .stateStore .RecordReportSent (ctx , slackUserID , time .Now ()); err != nil {
556+ slog .Debug ("failed to record empty report timestamp" ,
557+ "github_user" , githubUsername ,
558+ "slack_user" , slackUserID ,
559+ "error" , err )
560+ }
561+ }
562+ skippedCount ++
563+ continue
564+ }
565+
566+ // Send the report
567+ if err := sender .SendReport (ctx , userInfo ); err != nil {
568+ slog .Warn ("failed to send daily report" ,
569+ "github_user" , githubUsername ,
570+ "slack_user" , slackUserID ,
571+ "error" , err )
572+ errorCount ++
573+ continue
574+ }
575+
576+ slog .Info ("sent daily report" ,
577+ "github_user" , githubUsername ,
578+ "slack_user" , slackUserID ,
579+ "incoming_prs" , len (dashboard .IncomingPRs ),
580+ "outgoing_prs" , len (dashboard .OutgoingPRs ))
581+ sentCount ++
582+
583+ // Rate limit to avoid overwhelming Slack/GitHub APIs
584+ select {
585+ case <- ctx .Done ():
586+ slog .Info ("daily report check canceled" , "org" , org )
587+ return
588+ case <- time .After (500 * time .Millisecond ):
589+ }
590+ }
591+
592+ if sentCount > 0 || errorCount > 0 {
593+ slog .Info ("daily report check complete" ,
594+ "org" , org ,
595+ "sent" , sentCount ,
596+ "skipped" , skippedCount ,
597+ "errors" , errorCount )
598+ }
599+ }
0 commit comments