package bot import ( "context" "fmt" "log" "github.com/mymmrac/telego" "github.com/mymmrac/telego/telegoutil" "github.com/salvacybersec/keyhunter/pkg/engine" "github.com/salvacybersec/keyhunter/pkg/scheduler" ) // NotifyNewFindings sends a notification to all subscribers about scan results. // It returns the number of messages successfully sent and any per-subscriber errors. // If FindingCount is 0 and Error is nil, no notification is sent (silent success). // If Error is non-nil, an error notification is sent instead. func (b *Bot) NotifyNewFindings(result scheduler.JobResult) (int, []error) { // No notification for zero-finding success. if result.FindingCount == 0 && result.Error == nil { return 0, nil } subs, err := b.cfg.DB.ListSubscribers() if err != nil { log.Printf("notify: listing subscribers: %v", err) return 0, []error{fmt.Errorf("listing subscribers: %w", err)} } if len(subs) == 0 { return 0, nil } var msg string if result.Error != nil { msg = formatErrorNotification(result) } else { msg = formatNotification(result) } var sent int var errs []error for _, sub := range subs { if b.bot == nil { // No telego bot (test mode) -- count as would-send. continue } params := telegoutil.Message(telego.ChatID{ID: sub.ChatID}, msg) if _, sendErr := b.bot.SendMessage(context.Background(), params); sendErr != nil { log.Printf("notify: sending to chat %d: %v", sub.ChatID, sendErr) errs = append(errs, fmt.Errorf("chat %d: %w", sub.ChatID, sendErr)) continue } sent++ } return sent, errs } // NotifyFinding sends a real-time notification about an individual finding // to all subscribers. The key is always masked. func (b *Bot) NotifyFinding(finding engine.Finding) (int, []error) { subs, err := b.cfg.DB.ListSubscribers() if err != nil { log.Printf("notify: listing subscribers: %v", err) return 0, []error{fmt.Errorf("listing subscribers: %w", err)} } if len(subs) == 0 { return 0, nil } msg := formatFindingNotification(finding) var sent int var errs []error for _, sub := range subs { if b.bot == nil { continue } params := telegoutil.Message(telego.ChatID{ID: sub.ChatID}, msg) if _, sendErr := b.bot.SendMessage(context.Background(), params); sendErr != nil { log.Printf("notify: sending finding to chat %d: %v", sub.ChatID, sendErr) errs = append(errs, fmt.Errorf("chat %d: %w", sub.ChatID, sendErr)) continue } sent++ } return sent, errs } // formatNotification builds the notification message for a successful scan // with findings. func formatNotification(result scheduler.JobResult) string { return fmt.Sprintf( "New findings from scheduled scan!\n\nJob: %s\nNew keys found: %d\nDuration: %s\n\nUse /stats for details.", result.JobName, result.FindingCount, result.Duration, ) } // formatErrorNotification builds the notification message for a scan that // encountered an error. func formatErrorNotification(result scheduler.JobResult) string { return fmt.Sprintf( "Scheduled scan error\n\nJob: %s\nDuration: %s\nError: %v", result.JobName, result.Duration, result.Error, ) } // formatFindingNotification builds the notification message for an individual // finding. Always uses the masked key. func formatFindingNotification(finding engine.Finding) string { return fmt.Sprintf( "New key detected!\nProvider: %s\nKey: %s\nSource: %s:%d\nConfidence: %s", finding.ProviderName, finding.KeyMasked, finding.Source, finding.LineNumber, finding.Confidence, ) }