package api import ( "bytes" giteaTypes "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/fatih/structs" "go.uber.org/zap" "gorm.io/gorm" "io" "net/http" "regexp" "strings" ) 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 *giteaTypes.PullRequestPayload, r *http.Request) { ghEvent := convertPullRequestEvent(request) githubAction := translatePrAction(request.Action) r.Header.Set("X-GitHub-Event", githubAction) whm.sendWebhooks(ghEvent, r) } 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) { rawMap := structs.New(request).Map() rawMap["installation"] = struct { ID uint `json:"id"` }{ID: app.ID} payloadBytes, err := json.Marshal(rawMap) 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 := toNormalizedJson(payloadBytes) signature := generatePayloadSignature(payload, app.WebhookSecret) req.Header.Set("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) } } /* * Ported from https://github.com/octokit/webhooks.js/blob/984f3f51e077dbc7637aa55406c3bb114dbb7f82/src/to-normalized-json-string.ts */ func toNormalizedJson(payload []byte) []byte { jsonString := string(payload) // Regex to find Unicode escape sequences re := regexp.MustCompile(`\\u([\da-fA-F]{4})`) // Function to convert found sequences to uppercase replaceFunc := func(match string) string { parts := strings.Split(match, "\\u") // Convert the Unicode sequence part to uppercase return parts[0] + "\\u" + strings.ToUpper(parts[1]) } // Replace the matches in the jsonString normalizedString := re.ReplaceAllStringFunc(jsonString, replaceFunc) return []byte(normalizedString) }