forgejo/services/webhook/webhook.go
Earl Warren 4b4aeff24b
[BRANDING] define the forgejo webhook type
templates/swagger/v1_json.tmpl updated with `make generate-swagger`

(cherry picked from commit 88899c492e)
(cherry picked from commit 7171bd9617)
(cherry picked from commit 1a742446c1)
(cherry picked from commit 132342ae8d)
(cherry picked from commit b47b815fbe)
(cherry picked from commit 2dbb52c8ec)
(cherry picked from commit c846de4322)
(cherry picked from commit c3d1ad7bbc)
(cherry picked from commit 972c772c0e)
2023-05-10 11:05:22 +02:00

272 lines
7 KiB
Go

// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package webhook
import (
"context"
"fmt"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/gobwas/glob"
)
type webhook struct {
name webhook_module.HookType
payloadCreator func(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error)
}
var webhooks = map[webhook_module.HookType]*webhook{
webhook_module.SLACK: {
name: webhook_module.SLACK,
payloadCreator: GetSlackPayload,
},
webhook_module.DISCORD: {
name: webhook_module.DISCORD,
payloadCreator: GetDiscordPayload,
},
webhook_module.DINGTALK: {
name: webhook_module.DINGTALK,
payloadCreator: GetDingtalkPayload,
},
webhook_module.TELEGRAM: {
name: webhook_module.TELEGRAM,
payloadCreator: GetTelegramPayload,
},
webhook_module.MSTEAMS: {
name: webhook_module.MSTEAMS,
payloadCreator: GetMSTeamsPayload,
},
webhook_module.FEISHU: {
name: webhook_module.FEISHU,
payloadCreator: GetFeishuPayload,
},
webhook_module.MATRIX: {
name: webhook_module.MATRIX,
payloadCreator: GetMatrixPayload,
},
webhook_module.WECHATWORK: {
name: webhook_module.WECHATWORK,
payloadCreator: GetWechatworkPayload,
},
webhook_module.PACKAGIST: {
name: webhook_module.PACKAGIST,
payloadCreator: GetPackagistPayload,
},
}
// IsValidHookTaskType returns true if a webhook registered
func IsValidHookTaskType(name string) bool {
if name == webhook_module.FORGEJO || name == webhook_module.GITEA || name == webhook_module.GOGS {
return true
}
_, ok := webhooks[name]
return ok
}
// hookQueue is a global queue of web hooks
var hookQueue queue.UniqueQueue
// getPayloadBranch returns branch for hook event, if applicable.
func getPayloadBranch(p api.Payloader) string {
switch pp := p.(type) {
case *api.CreatePayload:
if pp.RefType == "branch" {
return pp.Ref
}
case *api.DeletePayload:
if pp.RefType == "branch" {
return pp.Ref
}
case *api.PushPayload:
if strings.HasPrefix(pp.Ref, git.BranchPrefix) {
return pp.Ref[len(git.BranchPrefix):]
}
}
return ""
}
// EventSource represents the source of a webhook action. Repository and/or Owner must be set.
type EventSource struct {
Repository *repo_model.Repository
Owner *user_model.User
}
// handle delivers hook tasks
func handle(data ...queue.Data) []queue.Data {
ctx := graceful.GetManager().HammerContext()
for _, taskID := range data {
task, err := webhook_model.GetHookTaskByID(ctx, taskID.(int64))
if err != nil {
log.Error("GetHookTaskByID[%d] failed: %v", taskID.(int64), err)
continue
}
if task.IsDelivered {
// Already delivered in the meantime
log.Trace("Task[%d] has already been delivered", task.ID)
continue
}
if err := Deliver(ctx, task); err != nil {
log.Error("Unable to deliver webhook task[%d]: %v", task.ID, err)
}
}
return nil
}
func enqueueHookTask(taskID int64) error {
err := hookQueue.Push(taskID)
if err != nil && err != queue.ErrAlreadyInQueue {
return err
}
return nil
}
func checkBranch(w *webhook_model.Webhook, branch string) bool {
if w.BranchFilter == "" || w.BranchFilter == "*" {
return true
}
g, err := glob.Compile(w.BranchFilter)
if err != nil {
// should not really happen as BranchFilter is validated
log.Error("CheckBranch failed: %s", err)
return false
}
return g.Match(branch)
}
// PrepareWebhook creates a hook task and enqueues it for processing
func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook_module.HookEventType, p api.Payloader) error {
// Skip sending if webhooks are disabled.
if setting.DisableWebhooks {
return nil
}
for _, e := range w.EventCheckers() {
if event == e.Type {
if !e.Has() {
return nil
}
break
}
}
// Avoid sending "0 new commits" to non-integration relevant webhooks (e.g. slack, discord, etc.).
// Integration webhooks (e.g. drone) still receive the required data.
if pushEvent, ok := p.(*api.PushPayload); ok &&
w.Type != webhook_module.FORGEJO && w.Type != webhook_module.GITEA && w.Type != webhook_module.GOGS &&
len(pushEvent.Commits) == 0 {
return nil
}
// If payload has no associated branch (e.g. it's a new tag, issue, etc.),
// branch filter has no effect.
if branch := getPayloadBranch(p); branch != "" {
if !checkBranch(w, branch) {
log.Info("Branch %q doesn't match branch filter %q, skipping", branch, w.BranchFilter)
return nil
}
}
var payloader api.Payloader
var err error
webhook, ok := webhooks[w.Type]
if ok {
payloader, err = webhook.payloadCreator(p, event, w.Meta)
if err != nil {
return fmt.Errorf("create payload for %s[%s]: %w", w.Type, event, err)
}
} else {
payloader = p
}
task, err := webhook_model.CreateHookTask(ctx, &webhook_model.HookTask{
HookID: w.ID,
Payloader: payloader,
EventType: event,
})
if err != nil {
return fmt.Errorf("CreateHookTask: %w", err)
}
return enqueueHookTask(task.ID)
}
// PrepareWebhooks adds new webhooks to task queue for given payload.
func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_module.HookEventType, p api.Payloader) error {
owner := source.Owner
var ws []*webhook_model.Webhook
if source.Repository != nil {
repoHooks, err := webhook_model.ListWebhooksByOpts(ctx, &webhook_model.ListWebhookOptions{
RepoID: source.Repository.ID,
IsActive: util.OptionalBoolTrue,
})
if err != nil {
return fmt.Errorf("ListWebhooksByOpts: %w", err)
}
ws = append(ws, repoHooks...)
owner = source.Repository.MustOwner(ctx)
}
// check if owner is an org and append additional webhooks
if owner != nil && owner.IsOrganization() {
orgHooks, err := webhook_model.ListWebhooksByOpts(ctx, &webhook_model.ListWebhookOptions{
OrgID: owner.ID,
IsActive: util.OptionalBoolTrue,
})
if err != nil {
return fmt.Errorf("ListWebhooksByOpts: %w", err)
}
ws = append(ws, orgHooks...)
}
// Add any admin-defined system webhooks
systemHooks, err := webhook_model.GetSystemWebhooks(ctx, util.OptionalBoolTrue)
if err != nil {
return fmt.Errorf("GetSystemWebhooks: %w", err)
}
ws = append(ws, systemHooks...)
if len(ws) == 0 {
return nil
}
for _, w := range ws {
if err := PrepareWebhook(ctx, w, event, p); err != nil {
return err
}
}
return nil
}
// ReplayHookTask replays a webhook task
func ReplayHookTask(ctx context.Context, w *webhook_model.Webhook, uuid string) error {
task, err := webhook_model.ReplayHookTask(ctx, w.ID, uuid)
if err != nil {
return err
}
return enqueueHookTask(task.ID)
}