Files
gitea-notification-hub/internal/event/processor.go
T

211 lines
6.1 KiB
Go

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
}