Skip to content

Commit 9adf562

Browse files
committed
Improve daily reports, implement proper presence detection
1 parent 0b7a263 commit 9adf562

28 files changed

Lines changed: 1810 additions & 510 deletions

cmd/server/main.go

Lines changed: 192 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919

2020
"github.com/codeGROOVE-dev/discordian/internal/bot"
2121
"github.com/codeGROOVE-dev/discordian/internal/config"
22+
"github.com/codeGROOVE-dev/discordian/internal/dailyreport"
2223
"github.com/codeGROOVE-dev/discordian/internal/discord"
2324
"github.com/codeGROOVE-dev/discordian/internal/format"
2425
"github.com/codeGROOVE-dev/discordian/internal/github"
@@ -185,23 +186,23 @@ func (ca *configAdapter) Config(org string) (usermapping.OrgConfig, bool) {
185186

186187
// coordinatorManager manages bot coordinators for multiple GitHub orgs.
187188
type coordinatorManager struct {
188-
cfg config.ServerConfig
189-
githubManager *github.Manager
190-
configManager *config.Manager
191-
guildManager *discord.GuildManager
189+
startTime time.Time
192190
store state.Store
191+
slashHandlers map[string]*discord.SlashCommandHandler
192+
discordClients map[string]*discord.Client
193+
lastEventTime map[string]time.Time
193194
notifyMgr *notify.Manager
194-
reverseMapper *usermapping.ReverseMapper // Discord ID -> GitHub username
195-
active map[string]context.CancelFunc // org -> cancel func
196-
failed map[string]time.Time // org -> last failure time
197-
discordClients map[string]*discord.Client // guildID -> client
198-
slashHandlers map[string]*discord.SlashCommandHandler // guildID -> handler
199-
coordinators map[string]*bot.Coordinator // org -> coordinator
200-
startTime time.Time
201-
lastEventTime map[string]time.Time // org -> last event time
202-
dmsSent int64 // Total DMs sent since start
203-
dailyReports int64 // Total daily reports sent since start
204-
channelMsgs int64 // Total channel messages sent since start
195+
reverseMapper *usermapping.ReverseMapper
196+
active map[string]context.CancelFunc
197+
guildManager *discord.GuildManager
198+
failed map[string]time.Time
199+
coordinators map[string]*bot.Coordinator
200+
configManager *config.Manager
201+
githubManager *github.Manager
202+
cfg config.ServerConfig
203+
dmsSent int64
204+
dailyReports int64
205+
channelMsgs int64
205206
mu sync.Mutex
206207
}
207208

@@ -390,7 +391,7 @@ func (cm *coordinatorManager) startSingleCoordinator(ctx context.Context, org st
390391
"sprinkler_url", sprinklerURL)
391392

392393
// Create sprinkler client with token provider (will fetch fresh tokens automatically)
393-
sprinklerClient, err := bot.NewSprinklerClient(bot.SprinklerConfig{
394+
sprinklerClient, err := bot.NewSprinklerClient(orgCtx, bot.SprinklerConfig{
394395
ServerURL: sprinklerURL,
395396
TokenProvider: tokenProvider,
396397
Organization: org,
@@ -480,6 +481,7 @@ func (cm *coordinatorManager) discordClientForGuild(_ context.Context, guildID s
480481
slashHandler.SetReportGetter(cm)
481482
slashHandler.SetUserMapGetter(cm)
482483
slashHandler.SetChannelMapGetter(cm)
484+
slashHandler.SetDailyReportGetter(cm)
483485

484486
// Register slash commands with Discord
485487
if err := slashHandler.RegisterCommands(guildID); err != nil {
@@ -772,6 +774,180 @@ func (cm *coordinatorManager) Report(ctx context.Context, guildID, userID string
772774
}, nil
773775
}
774776

777+
// DailyReport implements discord.DailyReportGetter interface.
778+
func (cm *coordinatorManager) DailyReport(ctx context.Context, guildID, userID string, force bool) (*discord.DailyReportDebug, error) {
779+
slog.Info("daily report requested",
780+
"guild_id", guildID,
781+
"user_id", userID,
782+
"force", force)
783+
784+
cm.mu.Lock()
785+
defer cm.mu.Unlock()
786+
787+
// Find orgs for this guild
788+
var orgsForGuild []string
789+
for org := range cm.active {
790+
cfg, exists := cm.configManager.Config(org)
791+
if exists && cfg.Global.GuildID == guildID {
792+
orgsForGuild = append(orgsForGuild, org)
793+
}
794+
}
795+
796+
if len(orgsForGuild) == 0 {
797+
return nil, fmt.Errorf("no org found for guild %s", guildID)
798+
}
799+
800+
// Find GitHub username
801+
var githubUsername string
802+
for _, org := range orgsForGuild {
803+
coord, exists := cm.coordinators[org]
804+
if !exists {
805+
continue
806+
}
807+
forwardCache := coord.ExportUserMapperCache()
808+
for ghUser, discordID := range forwardCache {
809+
if discordID == userID {
810+
githubUsername = ghUser
811+
break
812+
}
813+
}
814+
if githubUsername != "" {
815+
break
816+
}
817+
}
818+
819+
// Try reverse mapper if not found
820+
if githubUsername == "" {
821+
githubUsername = cm.reverseMapper.GitHubUsername(ctx, userID, &configAdapter{cm.configManager}, orgsForGuild)
822+
}
823+
824+
if githubUsername == "" {
825+
return nil, errors.New("no GitHub username mapping found for Discord user")
826+
}
827+
828+
// Get Discord client for this guild
829+
discordClient, exists := cm.discordClients[guildID]
830+
if !exists {
831+
return nil, fmt.Errorf("no Discord client for guild %s", guildID)
832+
}
833+
834+
// Check if user is currently online
835+
isOnline := discordClient.IsUserActive(ctx, userID)
836+
837+
// Get daily report state
838+
reportInfo, _ := cm.store.DailyReportInfo(ctx, userID)
839+
now := time.Now()
840+
841+
// Calculate timing info
842+
hoursSinceLastSent := float64(0)
843+
if !reportInfo.LastSentAt.IsZero() {
844+
hoursSinceLastSent = now.Sub(reportInfo.LastSentAt).Hours()
845+
}
846+
847+
nextEligibleAt := reportInfo.LastSentAt.Add(20 * time.Hour)
848+
849+
// Search for PRs (same as Report method)
850+
var incomingPRs []discord.PRSummary
851+
var outgoingPRs []discord.PRSummary
852+
853+
searcher := github.NewSearcher(cm.githubManager.AppClient(), slog.Default())
854+
855+
for _, org := range orgsForGuild {
856+
client, exists := cm.githubManager.ClientForOrg(org)
857+
if !exists {
858+
continue
859+
}
860+
861+
turn := bot.NewTurnClient(cm.cfg.TurnURL, client)
862+
863+
// Search authored PRs
864+
authored, err := searcher.ListAuthoredPRs(ctx, org, githubUsername)
865+
if err == nil {
866+
for _, pr := range authored {
867+
summary := analyzePRForReport(ctx, pr, githubUsername, turn)
868+
if summary != nil {
869+
outgoingPRs = append(outgoingPRs, *summary)
870+
}
871+
}
872+
}
873+
874+
// Search review-requested PRs
875+
review, err := searcher.ListReviewRequestedPRs(ctx, org, githubUsername)
876+
if err == nil {
877+
for _, pr := range review {
878+
summary := analyzePRForReport(ctx, pr, githubUsername, turn)
879+
if summary != nil {
880+
incomingPRs = append(incomingPRs, *summary)
881+
}
882+
}
883+
}
884+
}
885+
886+
// Determine eligibility
887+
eligible := true
888+
reason := "All conditions met"
889+
reportSent := false
890+
891+
// Check conditions
892+
switch {
893+
case len(incomingPRs) == 0 && len(outgoingPRs) == 0:
894+
eligible = false
895+
reason = "No PRs found for user"
896+
case !force && hoursSinceLastSent < 20:
897+
eligible = false
898+
reason = fmt.Sprintf("Rate limited: only %.1f hours since last report (need 20)", hoursSinceLastSent)
899+
case !force && !isOnline:
900+
eligible = false
901+
reason = "User is offline"
902+
default:
903+
// eligible and reason already set
904+
}
905+
906+
// Send report if forced or eligible
907+
if force || eligible {
908+
// Create daily report sender
909+
sender := dailyreport.NewSender(cm.store, slog.Default())
910+
911+
// Register guild DM sender
912+
sender.RegisterGuild(guildID, discordClient)
913+
914+
userInfo := dailyreport.UserBlockingInfo{
915+
GitHubUsername: githubUsername,
916+
DiscordUserID: userID,
917+
GuildID: guildID,
918+
IncomingPRs: incomingPRs,
919+
OutgoingPRs: outgoingPRs,
920+
}
921+
922+
// Send the report
923+
if err := sender.SendReport(ctx, userInfo); err != nil {
924+
slog.Error("failed to send daily report",
925+
"error", err,
926+
"github_user", githubUsername,
927+
"discord_id", userID)
928+
reason = fmt.Sprintf("Failed to send: %s", err)
929+
} else {
930+
reportSent = true
931+
reason = "Report sent successfully"
932+
cm.dailyReports++
933+
}
934+
}
935+
936+
return &discord.DailyReportDebug{
937+
UserOnline: isOnline,
938+
LastSentAt: reportInfo.LastSentAt,
939+
NextEligibleAt: nextEligibleAt,
940+
LastSeenActiveAt: time.Time{}, // Not tracked yet
941+
HoursSinceLastSent: hoursSinceLastSent,
942+
MinutesActive: 0, // Not tracked yet
943+
Eligible: eligible,
944+
Reason: reason,
945+
IncomingPRCount: len(incomingPRs),
946+
OutgoingPRCount: len(outgoingPRs),
947+
ReportSent: reportSent,
948+
}, nil
949+
}
950+
775951
// UserMappings implements discord.UserMapGetter interface.
776952
func (cm *coordinatorManager) UserMappings(ctx context.Context, guildID string) (*discord.UserMappings, error) {
777953
slog.Info("user mappings requested",

0 commit comments

Comments
 (0)