package api import ( "bytes" "code.gitea.io/gitea/modules/structs" "encoding/json" "git.lumeweb.com/LumeWeb/gitea-github-proxy/config" "git.lumeweb.com/LumeWeb/gitea-github-proxy/db/model" "github.com/google/go-github/v59/github" "go.uber.org/zap" "gorm.io/gorm" "io" "net/http" "time" ) type WebhookManager struct { config *config.Config db *gorm.DB logger *zap.Logger } type IncomingWebhookData struct { Headers http.Header } func NewWebhookManager(cfg *config.Config, db *gorm.DB, logger *zap.Logger) *WebhookManager { return &WebhookManager{config: cfg, db: db, logger: logger} } func (whm *WebhookManager) HandlePullRequest(request *structs.PullRequestPayload, r *http.Request) { // Mapping the sender sender := &github.User{ Login: &request.Sender.UserName, ID: &request.Sender.ID, AvatarURL: &request.Sender.AvatarURL, Name: &request.Sender.FullName, Email: &request.Sender.Email, SiteAdmin: &request.Sender.IsAdmin, } repo := &github.Repository{ ID: int64Ptr(request.Repository.ID), Name: stringPtr(request.Repository.Name), FullName: stringPtr(request.Repository.FullName), Description: stringPtr(request.Repository.Description), Homepage: stringPtr(request.Repository.Website), HTMLURL: stringPtr(request.Repository.HTMLURL), CloneURL: stringPtr(request.Repository.CloneURL), GitURL: stringPtr(request.Repository.CloneURL), SSHURL: stringPtr(request.Repository.SSHURL), DefaultBranch: stringPtr(request.Repository.DefaultBranch), CreatedAt: timePtr(request.Repository.Created), UpdatedAt: timePtr(request.Repository.Updated), Private: boolPtr(request.Repository.Private), Fork: boolPtr(request.Repository.Fork), Size: intPtr(request.Repository.Size), StargazersCount: intPtr(request.Repository.Stars), SubscribersCount: intPtr(request.Repository.Watchers), ForksCount: intPtr(request.Repository.Forks), Watchers: intPtr(request.Repository.Watchers), WatchersCount: intPtr(request.Repository.Stars), OpenIssuesCount: intPtr(request.Repository.OpenIssues), Archived: boolPtr(request.Repository.Archived), } pr := &github.PullRequest{ ID: int64Ptr(request.PullRequest.ID), Number: intPtr(int(request.PullRequest.Index)), State: stringPtr(string(request.PullRequest.State)), Title: stringPtr(request.PullRequest.Title), Body: stringPtr(request.PullRequest.Body), CreatedAt: timePtr(*request.PullRequest.Created), UpdatedAt: timePtr(*request.PullRequest.Updated), ClosedAt: timePtrIfNotNil(request.PullRequest.Closed), MergedAt: timePtrIfNotNil(request.PullRequest.Merged), Merged: boolPtr(request.PullRequest.HasMerged), Mergeable: boolPtr(request.PullRequest.Mergeable), MergeCommitSHA: request.PullRequest.MergedCommitID, URL: stringPtr(request.PullRequest.URL), HTMLURL: stringPtr(request.PullRequest.HTMLURL), DiffURL: stringPtr(request.PullRequest.DiffURL), PatchURL: stringPtr(request.PullRequest.PatchURL), Comments: intPtr(request.PullRequest.Comments), Assignee: convertUser(request.PullRequest.Assignee), Assignees: convertUsers(request.PullRequest.Assignees), Milestone: convertMilestone(request.PullRequest.Milestone), Labels: convertLabels(request.PullRequest.Labels), } // Convert PR branch info if request.PullRequest.Head != nil { pr.Head = convertPRBranch(request.PullRequest.Head) } if request.PullRequest.Base != nil { pr.Base = convertPRBranch(request.PullRequest.Base) } changes := &github.EditChange{ Title: &github.EditTitle{ From: stringPtr(request.Changes.Title.From), }, Body: &github.EditBody{ From: stringPtr(request.Changes.Body.From), }, Base: &github.EditBase{ Ref: &github.EditRef{ From: stringPtr(request.Changes.Ref.From), }, }, } ghEvent := &github.PullRequestEvent{ Action: stringPtr(string(request.Action)), Number: numberPtr(int(request.Index)), PullRequest: pr, Changes: changes, Repo: repo, Sender: sender, } githubAction := "" switch request.Action { case structs.HookIssueOpened: githubAction = "opened" case structs.HookIssueClosed: githubAction = "closed" case structs.HookIssueReOpened: githubAction = "reopened" case structs.HookIssueEdited: githubAction = "edited" case structs.HookIssueAssigned: githubAction = "assigned" case structs.HookIssueUnassigned: githubAction = "unassigned" case structs.HookIssueLabelUpdated: // GitHub does not have a direct "label_updated" event; use "labeled" as the closest action githubAction = "labeled" // Assuming you handle the update as adding a label case structs.HookIssueLabelCleared: // GitHub does not have a direct "label_cleared" event; use "unlabeled" as the closest action githubAction = "unlabeled" // Assuming you handle the clearing as removing a label case structs.HookIssueSynchronized: githubAction = "synchronize" case structs.HookIssueMilestoned: githubAction = "milestoned" case structs.HookIssueDemilestoned: githubAction = "demilestoned" case structs.HookIssueReviewed: // GitHub does not have a direct "reviewed" event for PRs; this might be closest to a review submitted githubAction = "review_submitted" // This is not a direct GitHub event, consider how best to map this action case structs.HookIssueReviewRequested: githubAction = "review_requested" case structs.HookIssueReviewRequestRemoved: githubAction = "review_request_removed" default: // Fallback for any unhandled actions githubAction = "unknown_action" } r.Header.Set("X-GitHub-Event", githubAction) whm.sendWebhooks(ghEvent, r) } func stringPtr(s string) *string { return &s } func numberPtr(n int) *int { return &n } func intPtr(n int) *int { return &n } func int64Ptr(n int64) *int64 { return &n } func boolPtr(b bool) *bool { return &b } func timePtr(t time.Time) *github.Timestamp { timestamp := github.Timestamp{Time: t} return ×tamp } func timePtrIfNotNil(t *time.Time) *github.Timestamp { if t == nil { return nil } return timePtr(*t) } func convertUser(user *structs.User) *github.User { if user == nil { return nil } return &github.User{ Login: stringPtr(user.UserName), ID: int64Ptr(user.ID), } } func convertUsers(users []*structs.User) []*github.User { var ghUsers []*github.User for _, user := range users { ghUsers = append(ghUsers, convertUser(user)) } return ghUsers } func convertMilestone(milestone *structs.Milestone) *github.Milestone { if milestone == nil { return nil } return &github.Milestone{ Title: stringPtr(milestone.Title), } } func convertLabels(labels []*structs.Label) []*github.Label { var ghLabels []*github.Label for _, label := range labels { ghLabels = append(ghLabels, &github.Label{ Name: stringPtr(label.Name), }) } return ghLabels } func convertPRBranch(branch *structs.PRBranchInfo) *github.PullRequestBranch { return &github.PullRequestBranch{ Label: stringPtr(branch.Name), Ref: stringPtr(branch.Ref), SHA: stringPtr(branch.Sha), } } func (whm *WebhookManager) sendWebhooks(request interface{}, r *http.Request) { var apps []model.Apps result := whm.db.Find(&apps) if result.Error != nil { whm.logger.Error("Failed to query apps", zap.Error(result.Error)) return } for _, app := range apps { go func(app model.Apps) { payloadBytes, err := json.Marshal(request) if err != nil { whm.logger.Error("Failed to marshal payload", zap.Error(err)) return } req, err := http.NewRequest("POST", app.WebhookUrl, bytes.NewBuffer(payloadBytes)) if err != nil { whm.logger.Error("Failed to create request", zap.Error(err), zap.String("url", app.WebhookUrl)) return } req.Header.Set("Content-Type", "application/json") for key, values := range r.Header { for _, value := range values { req.Header.Add(key, value) } } payload := getWebhookData(r, whm.logger) signature := generatePayloadSignature(payload, app.WebhookSecret) req.Header.Add("X-Hub-Signature-256", signature) client := &http.Client{} resp, err := client.Do(req) if err != nil { whm.logger.Error("Failed to send webhook", zap.Error(err), zap.String("url", app.WebhookUrl)) return } defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { whm.logger.Error("Failed to close response body", zap.Error(err)) } }(resp.Body) if resp.StatusCode >= 200 && resp.StatusCode < 300 { whm.logger.Info("Webhook sent successfully", zap.String("url", app.WebhookUrl)) } else { whm.logger.Error("Webhook failed", zap.String("url", app.WebhookUrl), zap.Int("status", resp.StatusCode)) } }(app) } }