295 lines
8.8 KiB
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 ×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)
|
||
|
}
|
||
|
|
||
|
}
|