package webhook import ( "encoding/json" "net/http" "github.com/rs/zerolog" ) // EventHandler processes webhook events type EventHandler interface { HandlePullRequest(event *PullRequestEvent) HandlePullRequestReview(event *PullRequestReviewEvent) HandlePullRequestComment(event *PullRequestCommentEvent) HandleIssue(event *IssueEvent) HandleIssueComment(event *IssueCommentEvent) } // Handler handles incoming Gitea webhooks type Handler struct { validator *Validator eventHandler EventHandler logger zerolog.Logger } // NewHandler creates a new webhook handler func NewHandler(secret string, eventHandler EventHandler, logger zerolog.Logger) *Handler { return &Handler{ validator: NewValidator(secret), eventHandler: eventHandler, logger: logger.With().Str("component", "webhook").Logger(), } } // ServeHTTP handles the webhook HTTP request func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Only accept POST requests if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } // Get event metadata eventType := GetEventType(r) deliveryID := GetDeliveryID(r) logger := h.logger.With(). Str("delivery_id", deliveryID). Str("event_type", string(eventType)). Logger() // Validate signature and read body body, err := h.validator.ValidateRequest(r) if err != nil { logger.Warn().Err(err).Msg("webhook validation failed") http.Error(w, "unauthorized", http.StatusUnauthorized) return } // Respond immediately with 200 OK // Process the event asynchronously w.WriteHeader(http.StatusOK) w.Write([]byte(`{"status":"accepted"}`)) // Process the event in a goroutine go h.processEvent(logger, eventType, body) } // processEvent parses and routes the event to the appropriate handler func (h *Handler) processEvent(logger zerolog.Logger, eventType GiteaEventType, body []byte) { var err error switch eventType { case EventPullRequest: var event PullRequestEvent if err = json.Unmarshal(body, &event); err == nil { logger.Info(). Str("action", event.Action). Int64("pr_number", event.Number). Str("repo", event.Repository.FullName). Msg("processing pull request event") h.eventHandler.HandlePullRequest(&event) } case EventPullRequestReview: var event PullRequestReviewEvent if err = json.Unmarshal(body, &event); err == nil { logger.Info(). Str("action", event.Action). Str("review_state", event.Review.State). Str("repo", event.Repository.FullName). Msg("processing pull request review event") h.eventHandler.HandlePullRequestReview(&event) } case EventPullRequestComment: var event PullRequestCommentEvent if err = json.Unmarshal(body, &event); err == nil { logger.Info(). Str("action", event.Action). Int64("pr_number", event.PullRequest.Number). Str("repo", event.Repository.FullName). Msg("processing pull request comment event") h.eventHandler.HandlePullRequestComment(&event) } case EventIssues: var event IssueEvent if err = json.Unmarshal(body, &event); err == nil { logger.Info(). Str("action", event.Action). Int64("issue_number", event.Issue.Number). Str("repo", event.Repository.FullName). Msg("processing issue event") h.eventHandler.HandleIssue(&event) } case EventIssueComment: var event IssueCommentEvent if err = json.Unmarshal(body, &event); err == nil { logger.Info(). Str("action", event.Action). Int64("issue_number", event.Issue.Number). Str("repo", event.Repository.FullName). Msg("processing issue comment event") h.eventHandler.HandleIssueComment(&event) } default: logger.Debug().Msg("ignoring unknown event type") return } if err != nil { logger.Error().Err(err).Msg("failed to parse event payload") } }