loading up the forgejo repo on tangled to test page performance
at forgejo 7.0 kB view raw
1// Copyright 2019 The Gitea Authors. All rights reserved. 2// SPDX-License-Identifier: MIT 3 4package webhook 5 6import ( 7 "context" 8 "errors" 9 "fmt" 10 "html/template" 11 "net/http" 12 "strings" 13 14 "forgejo.org/models/db" 15 repo_model "forgejo.org/models/repo" 16 user_model "forgejo.org/models/user" 17 webhook_model "forgejo.org/models/webhook" 18 "forgejo.org/modules/git" 19 "forgejo.org/modules/graceful" 20 "forgejo.org/modules/log" 21 "forgejo.org/modules/optional" 22 "forgejo.org/modules/queue" 23 "forgejo.org/modules/setting" 24 api "forgejo.org/modules/structs" 25 "forgejo.org/modules/util" 26 webhook_module "forgejo.org/modules/webhook" 27 "forgejo.org/services/forms" 28 "forgejo.org/services/webhook/sourcehut" 29 30 "github.com/gobwas/glob" 31) 32 33type Handler interface { 34 Type() webhook_module.HookType 35 Metadata(*webhook_model.Webhook) any 36 // UnmarshalForm provides a function to bind the request to the form. 37 // If form implements the [binding.Validator] interface, the Validate method will be called 38 UnmarshalForm(bind func(form any)) forms.WebhookForm 39 NewRequest(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error) 40 Icon(size int) template.HTML 41} 42 43var webhookHandlers = []Handler{ 44 defaultHandler{true}, 45 defaultHandler{false}, 46 gogsHandler{}, 47 48 slackHandler{}, 49 discordHandler{}, 50 dingtalkHandler{}, 51 telegramHandler{}, 52 msteamsHandler{}, 53 feishuHandler{}, 54 matrixHandler{}, 55 wechatworkHandler{}, 56 packagistHandler{}, 57 sourcehut.BuildsHandler{}, 58} 59 60// GetWebhookHandler return the handler for a given webhook type (nil if not found) 61func GetWebhookHandler(name webhook_module.HookType) Handler { 62 for _, h := range webhookHandlers { 63 if h.Type() == name { 64 return h 65 } 66 } 67 return nil 68} 69 70// List provides a list of the supported webhooks 71func List() []Handler { 72 return webhookHandlers 73} 74 75// IsValidHookTaskType returns true if a webhook registered 76func IsValidHookTaskType(name string) bool { 77 return GetWebhookHandler(name) != nil 78} 79 80// hookQueue is a global queue of web hooks 81var hookQueue *queue.WorkerPoolQueue[int64] 82 83// getPayloadBranch returns branch for hook event, if applicable. 84func getPayloadBranch(p api.Payloader) string { 85 var ref string 86 switch pp := p.(type) { 87 case *api.CreatePayload: 88 ref = pp.Ref 89 case *api.DeletePayload: 90 ref = pp.Ref 91 case *api.PushPayload: 92 ref = pp.Ref 93 } 94 if strings.HasPrefix(ref, git.BranchPrefix) { 95 return ref[len(git.BranchPrefix):] 96 } 97 return "" 98} 99 100// EventSource represents the source of a webhook action. Repository and/or Owner must be set. 101type EventSource struct { 102 Repository *repo_model.Repository 103 Owner *user_model.User 104} 105 106// handle delivers hook tasks 107func handler(items ...int64) []int64 { 108 ctx := graceful.GetManager().HammerContext() 109 110 for _, taskID := range items { 111 task, err := webhook_model.GetHookTaskByID(ctx, taskID) 112 if err != nil { 113 if errors.Is(err, util.ErrNotExist) { 114 log.Warn("GetHookTaskByID[%d] warn: %v", taskID, err) 115 } else { 116 log.Error("GetHookTaskByID[%d] failed: %v", taskID, err) 117 } 118 continue 119 } 120 121 if task.IsDelivered { 122 // Already delivered in the meantime 123 log.Trace("Task[%d] has already been delivered", task.ID) 124 continue 125 } 126 127 if err := Deliver(ctx, task); err != nil { 128 log.Error("Unable to deliver webhook task[%d]: %v", task.ID, err) 129 } 130 } 131 132 return nil 133} 134 135func enqueueHookTask(taskID int64) error { 136 err := hookQueue.Push(taskID) 137 if err != nil && err != queue.ErrAlreadyInQueue { 138 return err 139 } 140 return nil 141} 142 143func checkBranch(w *webhook_model.Webhook, branch string) bool { 144 if w.BranchFilter == "" || w.BranchFilter == "*" { 145 return true 146 } 147 148 g, err := glob.Compile(w.BranchFilter) 149 if err != nil { 150 // should not really happen as BranchFilter is validated 151 log.Error("CheckBranch failed: %s", err) 152 return false 153 } 154 155 return g.Match(branch) 156} 157 158// PrepareWebhook creates a hook task and enqueues it for processing. 159// The payload is saved as-is. The adjustments depending on the webhook type happen 160// right before delivery, in the [Deliver] method. 161func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook_module.HookEventType, p api.Payloader) error { 162 // Skip sending if webhooks are disabled. 163 if setting.DisableWebhooks { 164 return nil 165 } 166 167 for _, e := range w.EventCheckers() { 168 if event == e.Type { 169 if !e.Has() { 170 return nil 171 } 172 173 break 174 } 175 } 176 177 // Avoid sending "0 new commits" to non-integration relevant webhooks (e.g. slack, discord, etc.). 178 // Integration webhooks (e.g. drone) still receive the required data. 179 if pushEvent, ok := p.(*api.PushPayload); ok && 180 w.Type != webhook_module.FORGEJO && w.Type != webhook_module.GITEA && w.Type != webhook_module.GOGS && 181 len(pushEvent.Commits) == 0 { 182 return nil 183 } 184 185 // If payload has no associated branch (e.g. it's a new tag, issue, etc.), 186 // branch filter has no effect. 187 if branch := getPayloadBranch(p); branch != "" { 188 if !checkBranch(w, branch) { 189 log.Info("Branch %q doesn't match branch filter %q, skipping", branch, w.BranchFilter) 190 return nil 191 } 192 } 193 194 payload, err := p.JSONPayload() 195 if err != nil { 196 return fmt.Errorf("JSONPayload for %s: %w", event, err) 197 } 198 199 task, err := webhook_model.CreateHookTask(ctx, &webhook_model.HookTask{ 200 HookID: w.ID, 201 PayloadContent: string(payload), 202 EventType: event, 203 PayloadVersion: 2, 204 }) 205 if err != nil { 206 return fmt.Errorf("CreateHookTask for %s: %w", event, err) 207 } 208 209 return enqueueHookTask(task.ID) 210} 211 212// PrepareWebhooks adds new webhooks to task queue for given payload. 213func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_module.HookEventType, p api.Payloader) error { 214 owner := source.Owner 215 216 var ws []*webhook_model.Webhook 217 218 if source.Repository != nil { 219 repoHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{ 220 RepoID: source.Repository.ID, 221 IsActive: optional.Some(true), 222 }) 223 if err != nil { 224 return fmt.Errorf("ListWebhooksByOpts: %w", err) 225 } 226 ws = append(ws, repoHooks...) 227 228 owner = source.Repository.MustOwner(ctx) 229 } 230 231 // append additional webhooks of a user or organization 232 if owner != nil { 233 ownerHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{ 234 OwnerID: owner.ID, 235 IsActive: optional.Some(true), 236 }) 237 if err != nil { 238 return fmt.Errorf("ListWebhooksByOpts: %w", err) 239 } 240 ws = append(ws, ownerHooks...) 241 } 242 243 // Add any admin-defined system webhooks 244 systemHooks, err := webhook_model.GetSystemWebhooks(ctx, true) 245 if err != nil { 246 return fmt.Errorf("GetSystemWebhooks: %w", err) 247 } 248 ws = append(ws, systemHooks...) 249 250 if len(ws) == 0 { 251 return nil 252 } 253 254 for _, w := range ws { 255 if err := PrepareWebhook(ctx, w, event, p); err != nil { 256 return err 257 } 258 } 259 return nil 260} 261 262// ReplayHookTask replays a webhook task 263func ReplayHookTask(ctx context.Context, w *webhook_model.Webhook, uuid string) error { 264 task, err := webhook_model.ReplayHookTask(ctx, w.ID, uuid) 265 if err != nil { 266 return err 267 } 268 269 return enqueueHookTask(task.ID) 270}