code.gitea.io/gitea@v1.21.7/services/webhook/webhook.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package webhook 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "strings" 11 12 repo_model "code.gitea.io/gitea/models/repo" 13 user_model "code.gitea.io/gitea/models/user" 14 webhook_model "code.gitea.io/gitea/models/webhook" 15 "code.gitea.io/gitea/modules/git" 16 "code.gitea.io/gitea/modules/graceful" 17 "code.gitea.io/gitea/modules/log" 18 "code.gitea.io/gitea/modules/queue" 19 "code.gitea.io/gitea/modules/setting" 20 api "code.gitea.io/gitea/modules/structs" 21 "code.gitea.io/gitea/modules/util" 22 webhook_module "code.gitea.io/gitea/modules/webhook" 23 24 "github.com/gobwas/glob" 25 ) 26 27 type webhook struct { 28 name webhook_module.HookType 29 payloadCreator func(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) 30 } 31 32 var webhooks = map[webhook_module.HookType]*webhook{ 33 webhook_module.SLACK: { 34 name: webhook_module.SLACK, 35 payloadCreator: GetSlackPayload, 36 }, 37 webhook_module.DISCORD: { 38 name: webhook_module.DISCORD, 39 payloadCreator: GetDiscordPayload, 40 }, 41 webhook_module.DINGTALK: { 42 name: webhook_module.DINGTALK, 43 payloadCreator: GetDingtalkPayload, 44 }, 45 webhook_module.TELEGRAM: { 46 name: webhook_module.TELEGRAM, 47 payloadCreator: GetTelegramPayload, 48 }, 49 webhook_module.MSTEAMS: { 50 name: webhook_module.MSTEAMS, 51 payloadCreator: GetMSTeamsPayload, 52 }, 53 webhook_module.FEISHU: { 54 name: webhook_module.FEISHU, 55 payloadCreator: GetFeishuPayload, 56 }, 57 webhook_module.MATRIX: { 58 name: webhook_module.MATRIX, 59 payloadCreator: GetMatrixPayload, 60 }, 61 webhook_module.WECHATWORK: { 62 name: webhook_module.WECHATWORK, 63 payloadCreator: GetWechatworkPayload, 64 }, 65 webhook_module.PACKAGIST: { 66 name: webhook_module.PACKAGIST, 67 payloadCreator: GetPackagistPayload, 68 }, 69 } 70 71 // IsValidHookTaskType returns true if a webhook registered 72 func IsValidHookTaskType(name string) bool { 73 if name == webhook_module.GITEA || name == webhook_module.GOGS { 74 return true 75 } 76 _, ok := webhooks[name] 77 return ok 78 } 79 80 // hookQueue is a global queue of web hooks 81 var hookQueue *queue.WorkerPoolQueue[int64] 82 83 // getPayloadBranch returns branch for hook event, if applicable. 84 func getPayloadBranch(p api.Payloader) string { 85 switch pp := p.(type) { 86 case *api.CreatePayload: 87 if pp.RefType == "branch" { 88 return pp.Ref 89 } 90 case *api.DeletePayload: 91 if pp.RefType == "branch" { 92 return pp.Ref 93 } 94 case *api.PushPayload: 95 if strings.HasPrefix(pp.Ref, git.BranchPrefix) { 96 return pp.Ref[len(git.BranchPrefix):] 97 } 98 } 99 return "" 100 } 101 102 // EventSource represents the source of a webhook action. Repository and/or Owner must be set. 103 type EventSource struct { 104 Repository *repo_model.Repository 105 Owner *user_model.User 106 } 107 108 // handle delivers hook tasks 109 func handler(items ...int64) []int64 { 110 ctx := graceful.GetManager().HammerContext() 111 112 for _, taskID := range items { 113 task, err := webhook_model.GetHookTaskByID(ctx, taskID) 114 if err != nil { 115 if errors.Is(err, util.ErrNotExist) { 116 log.Warn("GetHookTaskByID[%d] warn: %v", taskID, err) 117 } else { 118 log.Error("GetHookTaskByID[%d] failed: %v", taskID, err) 119 } 120 continue 121 } 122 123 if task.IsDelivered { 124 // Already delivered in the meantime 125 log.Trace("Task[%d] has already been delivered", task.ID) 126 continue 127 } 128 129 if err := Deliver(ctx, task); err != nil { 130 log.Error("Unable to deliver webhook task[%d]: %v", task.ID, err) 131 } 132 } 133 134 return nil 135 } 136 137 func enqueueHookTask(taskID int64) error { 138 err := hookQueue.Push(taskID) 139 if err != nil && err != queue.ErrAlreadyInQueue { 140 return err 141 } 142 return nil 143 } 144 145 func checkBranch(w *webhook_model.Webhook, branch string) bool { 146 if w.BranchFilter == "" || w.BranchFilter == "*" { 147 return true 148 } 149 150 g, err := glob.Compile(w.BranchFilter) 151 if err != nil { 152 // should not really happen as BranchFilter is validated 153 log.Error("CheckBranch failed: %s", err) 154 return false 155 } 156 157 return g.Match(branch) 158 } 159 160 // PrepareWebhook creates a hook task and enqueues it for processing 161 func 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.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 var payloader api.Payloader 195 var err error 196 webhook, ok := webhooks[w.Type] 197 if ok { 198 payloader, err = webhook.payloadCreator(p, event, w.Meta) 199 if err != nil { 200 return fmt.Errorf("create payload for %s[%s]: %w", w.Type, event, err) 201 } 202 } else { 203 payloader = p 204 } 205 206 task, err := webhook_model.CreateHookTask(ctx, &webhook_model.HookTask{ 207 HookID: w.ID, 208 Payloader: payloader, 209 EventType: event, 210 }) 211 if err != nil { 212 return fmt.Errorf("CreateHookTask: %w", err) 213 } 214 215 return enqueueHookTask(task.ID) 216 } 217 218 // PrepareWebhooks adds new webhooks to task queue for given payload. 219 func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_module.HookEventType, p api.Payloader) error { 220 owner := source.Owner 221 222 var ws []*webhook_model.Webhook 223 224 if source.Repository != nil { 225 repoHooks, err := webhook_model.ListWebhooksByOpts(ctx, &webhook_model.ListWebhookOptions{ 226 RepoID: source.Repository.ID, 227 IsActive: util.OptionalBoolTrue, 228 }) 229 if err != nil { 230 return fmt.Errorf("ListWebhooksByOpts: %w", err) 231 } 232 ws = append(ws, repoHooks...) 233 234 owner = source.Repository.MustOwner(ctx) 235 } 236 237 // append additional webhooks of a user or organization 238 if owner != nil { 239 ownerHooks, err := webhook_model.ListWebhooksByOpts(ctx, &webhook_model.ListWebhookOptions{ 240 OwnerID: owner.ID, 241 IsActive: util.OptionalBoolTrue, 242 }) 243 if err != nil { 244 return fmt.Errorf("ListWebhooksByOpts: %w", err) 245 } 246 ws = append(ws, ownerHooks...) 247 } 248 249 // Add any admin-defined system webhooks 250 systemHooks, err := webhook_model.GetSystemWebhooks(ctx, util.OptionalBoolTrue) 251 if err != nil { 252 return fmt.Errorf("GetSystemWebhooks: %w", err) 253 } 254 ws = append(ws, systemHooks...) 255 256 if len(ws) == 0 { 257 return nil 258 } 259 260 for _, w := range ws { 261 if err := PrepareWebhook(ctx, w, event, p); err != nil { 262 return err 263 } 264 } 265 return nil 266 } 267 268 // ReplayHookTask replays a webhook task 269 func ReplayHookTask(ctx context.Context, w *webhook_model.Webhook, uuid string) error { 270 task, err := webhook_model.ReplayHookTask(ctx, w.ID, uuid) 271 if err != nil { 272 return err 273 } 274 275 return enqueueHookTask(task.ID) 276 }