gitea-github-proxy/api/webhook_manager.go

295 lines
8.8 KiB
Go

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 &timestamp
}
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)
}
}