package event import ( "context" "fmt" "github.com/rs/zerolog" "github.com/vincentc-afk/gitea-notification-hub/internal/identity" "github.com/vincentc-afk/gitea-notification-hub/internal/slack" "github.com/vincentc-afk/gitea-notification-hub/internal/webhook" ) // Processor handles processing of webhook events type Processor struct { resolver identity.Resolver slack slack.Client logger zerolog.Logger } // NewProcessor creates a new event processor func NewProcessor(resolver identity.Resolver, slackClient slack.Client, logger zerolog.Logger) *Processor { return &Processor{ resolver: resolver, slack: slackClient, logger: logger.With().Str("component", "processor").Logger(), } } // ProcessPullRequest handles pull_request webhook events func (p *Processor) ProcessPullRequest(ctx context.Context, event *webhook.PullRequestEvent) error { logger := p.logger.With(). Int64("pr_number", event.Number). Str("action", event.Action). Str("repo", event.Repository.FullName). Logger() logger.Info().Msg("processing pull request event") switch event.Action { case "review_requested": return p.handleReviewRequested(ctx, event, logger) case "opened": return p.handleOpened(ctx, event, logger) case "closed": return p.handleClosed(ctx, event, logger) default: logger.Debug().Msg("ignoring pull_request action") return nil } } // handleReviewRequested processes review_requested events func (p *Processor) handleReviewRequested(ctx context.Context, event *webhook.PullRequestEvent, logger zerolog.Logger) error { logger.Info().Msg("processing review_requested event") var reviewer *webhook.GiteaUser // Approach 1: event.RequestedReviewers array (standard) if len(event.RequestedReviewers) > 0 { reviewer = &event.RequestedReviewers[0] logger.Info(). Int("count", len(event.RequestedReviewers)). Str("reviewer", reviewer.Login). Msg("found reviewer in RequestedReviewers array") } else if event.RequestedReviewer != nil { // Approach 2: singular field (edge case) reviewer = event.RequestedReviewer logger.Info(). Str("reviewer", reviewer.Login). Msg("found reviewer in RequestedReviewer singular field") } else if len(event.PullRequest.Assignees) > 0 { // Approach 3: fall back to PR assignees reviewer = &event.PullRequest.Assignees[0] logger.Info(). Str("reviewer", reviewer.Login). Msg("falling back to PR assignee as reviewer") } else { logger.Warn().Msg("no reviewer found in event payload") return fmt.Errorf("no reviewer found for review_requested event (PR #%d)", event.Number) } // Resolve the reviewer's Slack identity resolved, err := p.resolver.Resolve(ctx, User{ GiteaUsername: reviewer.Login, GiteaID: reviewer.ID, Email: reviewer.Email, FullName: reviewer.FullName, }) if err != nil { logger.Error(). Err(err). Str("reviewer", reviewer.Login). Msg("failed to resolve reviewer identity") return fmt.Errorf("resolving reviewer %s: %w", reviewer.Login, err) } if resolved.SlackID == "" { logger.Warn(). Str("reviewer", reviewer.Login). Msg("reviewer has no Slack ID, skipping DM") return nil } // Send Slack DM dmText := fmt.Sprintf( ":bell: *Review Requested*\n*Repository:* %s\n*Pull Request:* #%d - %s\n*Requested by:* %s\n\n<%s|View on Gitea>", event.Repository.FullName, event.Number, event.PullRequest.Title, event.Sender.Login, event.PullRequest.HTMLURL, ) if err := p.slack.SendDM(ctx, resolved.SlackID, dmText); err != nil { logger.Error(). Err(err). Str("slack_id", resolved.SlackID). Msg("failed to send Slack DM") return fmt.Errorf("sending Slack DM to %s: %w", resolved.SlackID, err) } logger.Info(). Str("reviewer", reviewer.Login). Str("slack_id", resolved.SlackID). Msg("successfully sent review_requested notification") return nil } // handleOpened processes pull_request opened events func (p *Processor) handleOpened(ctx context.Context, event *webhook.PullRequestEvent, logger zerolog.Logger) error { logger.Info().Msg("processing PR opened event") for _, assignee := range event.PullRequest.Assignees { resolved, err := p.resolver.Resolve(ctx, User{ GiteaUsername: assignee.Login, GiteaID: assignee.ID, Email: assignee.Email, FullName: assignee.FullName, }) if err != nil { logger.Error().Err(err).Str("assignee", assignee.Login).Msg("failed to resolve assignee") continue } if resolved.SlackID == "" { continue } text := fmt.Sprintf( ":new: *PR Opened*\n*Repository:* %s\n*PR:* #%d - %s\n*By:* %s\n\n<%s|View on Gitea>", event.Repository.FullName, event.Number, event.PullRequest.Title, event.Sender.Login, event.PullRequest.HTMLURL, ) if err := p.slack.SendDM(ctx, resolved.SlackID, text); err != nil { logger.Error().Err(err).Str("slack_id", resolved.SlackID).Msg("failed to send DM") } } return nil } // handleClosed processes pull_request closed/merged events func (p *Processor) handleClosed(ctx context.Context, event *webhook.PullRequestEvent, logger zerolog.Logger) error { if event.PullRequest.Merged { logger.Info().Msg("processing PR merged event") } else { logger.Info().Msg("processing PR closed (without merge) event") } verb := "Closed" if event.PullRequest.Merged { verb = "Merged" } for _, assignee := range event.PullRequest.Assignees { resolved, err := p.resolver.Resolve(ctx, User{ GiteaUsername: assignee.Login, GiteaID: assignee.ID, Email: assignee.Email, FullName: assignee.FullName, }) if err != nil { logger.Error().Err(err).Str("assignee", assignee.Login).Msg("failed to resolve assignee") continue } if resolved.SlackID == "" { continue } var emoji string if event.PullRequest.Merged { emoji = ":white_check_mark:" } else { emoji = ":x:" } text := fmt.Sprintf( "%s *PR %s*\n*Repository:* %s\n*PR:* #%d - %s\n\n<%s|View on Gitea>", emoji, verb, event.Repository.FullName, event.Number, event.PullRequest.Title, event.PullRequest.HTMLURL, ) if err := p.slack.SendDM(ctx, resolved.SlackID, text); err != nil { logger.Error().Err(err).Str("slack_id", resolved.SlackID).Msg("failed to send DM") } } return nil }