summaryrefslogtreecommitdiffstats
path: root/models/webhook
diff options
context:
space:
mode:
authorLunny Xiao <xiaolunwen@gmail.com>2021-11-10 06:13:16 +0100
committerGitHub <noreply@github.com>2021-11-10 06:13:16 +0100
commit33fca2b537d36cf998dd27425b2bb8ed5b0965f3 (patch)
tree817f392502e1c176a5cd7e80290520cb940a8416 /models/webhook
parentAdded GetUserByIDCtx. (#17602) (diff)
downloadforgejo-33fca2b537d36cf998dd27425b2bb8ed5b0965f3.tar.xz
forgejo-33fca2b537d36cf998dd27425b2bb8ed5b0965f3.zip
Move webhook into models/webhook/ (#17579)
Diffstat (limited to 'models/webhook')
-rw-r--r--models/webhook/hooktask.go280
-rw-r--r--models/webhook/main_test.go16
-rw-r--r--models/webhook/webhook.go582
-rw-r--r--models/webhook/webhook_test.go328
4 files changed, 1206 insertions, 0 deletions
diff --git a/models/webhook/hooktask.go b/models/webhook/hooktask.go
new file mode 100644
index 0000000000..1967ded298
--- /dev/null
+++ b/models/webhook/hooktask.go
@@ -0,0 +1,280 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package webhook
+
+import (
+ "context"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+
+ gouuid "github.com/google/uuid"
+)
+
+// ___ ___ __ ___________ __
+// / | \ ____ ____ | | _\__ ___/____ _____| | __
+// / ~ \/ _ \ / _ \| |/ / | | \__ \ / ___/ |/ /
+// \ Y ( <_> | <_> ) < | | / __ \_\___ \| <
+// \___|_ / \____/ \____/|__|_ \ |____| (____ /____ >__|_ \
+// \/ \/ \/ \/ \/
+
+// HookEventType is the type of an hook event
+type HookEventType string
+
+// Types of hook events
+const (
+ HookEventCreate HookEventType = "create"
+ HookEventDelete HookEventType = "delete"
+ HookEventFork HookEventType = "fork"
+ HookEventPush HookEventType = "push"
+ HookEventIssues HookEventType = "issues"
+ HookEventIssueAssign HookEventType = "issue_assign"
+ HookEventIssueLabel HookEventType = "issue_label"
+ HookEventIssueMilestone HookEventType = "issue_milestone"
+ HookEventIssueComment HookEventType = "issue_comment"
+ HookEventPullRequest HookEventType = "pull_request"
+ HookEventPullRequestAssign HookEventType = "pull_request_assign"
+ HookEventPullRequestLabel HookEventType = "pull_request_label"
+ HookEventPullRequestMilestone HookEventType = "pull_request_milestone"
+ HookEventPullRequestComment HookEventType = "pull_request_comment"
+ HookEventPullRequestReviewApproved HookEventType = "pull_request_review_approved"
+ HookEventPullRequestReviewRejected HookEventType = "pull_request_review_rejected"
+ HookEventPullRequestReviewComment HookEventType = "pull_request_review_comment"
+ HookEventPullRequestSync HookEventType = "pull_request_sync"
+ HookEventRepository HookEventType = "repository"
+ HookEventRelease HookEventType = "release"
+)
+
+// Event returns the HookEventType as an event string
+func (h HookEventType) Event() string {
+ switch h {
+ case HookEventCreate:
+ return "create"
+ case HookEventDelete:
+ return "delete"
+ case HookEventFork:
+ return "fork"
+ case HookEventPush:
+ return "push"
+ case HookEventIssues, HookEventIssueAssign, HookEventIssueLabel, HookEventIssueMilestone:
+ return "issues"
+ case HookEventPullRequest, HookEventPullRequestAssign, HookEventPullRequestLabel, HookEventPullRequestMilestone,
+ HookEventPullRequestSync:
+ return "pull_request"
+ case HookEventIssueComment, HookEventPullRequestComment:
+ return "issue_comment"
+ case HookEventPullRequestReviewApproved:
+ return "pull_request_approved"
+ case HookEventPullRequestReviewRejected:
+ return "pull_request_rejected"
+ case HookEventPullRequestReviewComment:
+ return "pull_request_comment"
+ case HookEventRepository:
+ return "repository"
+ case HookEventRelease:
+ return "release"
+ }
+ return ""
+}
+
+// HookRequest represents hook task request information.
+type HookRequest struct {
+ URL string `json:"url"`
+ HTTPMethod string `json:"http_method"`
+ Headers map[string]string `json:"headers"`
+}
+
+// HookResponse represents hook task response information.
+type HookResponse struct {
+ Status int `json:"status"`
+ Headers map[string]string `json:"headers"`
+ Body string `json:"body"`
+}
+
+// HookTask represents a hook task.
+type HookTask struct {
+ ID int64 `xorm:"pk autoincr"`
+ RepoID int64 `xorm:"INDEX"`
+ HookID int64
+ UUID string
+ api.Payloader `xorm:"-"`
+ PayloadContent string `xorm:"TEXT"`
+ EventType HookEventType
+ IsDelivered bool
+ Delivered int64
+ DeliveredString string `xorm:"-"`
+
+ // History info.
+ IsSucceed bool
+ RequestContent string `xorm:"TEXT"`
+ RequestInfo *HookRequest `xorm:"-"`
+ ResponseContent string `xorm:"TEXT"`
+ ResponseInfo *HookResponse `xorm:"-"`
+}
+
+func init() {
+ db.RegisterModel(new(HookTask))
+}
+
+// BeforeUpdate will be invoked by XORM before updating a record
+// representing this object
+func (t *HookTask) BeforeUpdate() {
+ if t.RequestInfo != nil {
+ t.RequestContent = t.simpleMarshalJSON(t.RequestInfo)
+ }
+ if t.ResponseInfo != nil {
+ t.ResponseContent = t.simpleMarshalJSON(t.ResponseInfo)
+ }
+}
+
+// AfterLoad updates the webhook object upon setting a column
+func (t *HookTask) AfterLoad() {
+ t.DeliveredString = time.Unix(0, t.Delivered).Format("2006-01-02 15:04:05 MST")
+
+ if len(t.RequestContent) == 0 {
+ return
+ }
+
+ t.RequestInfo = &HookRequest{}
+ if err := json.Unmarshal([]byte(t.RequestContent), t.RequestInfo); err != nil {
+ log.Error("Unmarshal RequestContent[%d]: %v", t.ID, err)
+ }
+
+ if len(t.ResponseContent) > 0 {
+ t.ResponseInfo = &HookResponse{}
+ if err := json.Unmarshal([]byte(t.ResponseContent), t.ResponseInfo); err != nil {
+ log.Error("Unmarshal ResponseContent[%d]: %v", t.ID, err)
+ }
+ }
+}
+
+func (t *HookTask) simpleMarshalJSON(v interface{}) string {
+ p, err := json.Marshal(v)
+ if err != nil {
+ log.Error("Marshal [%d]: %v", t.ID, err)
+ }
+ return string(p)
+}
+
+// HookTasks returns a list of hook tasks by given conditions.
+func HookTasks(hookID int64, page int) ([]*HookTask, error) {
+ tasks := make([]*HookTask, 0, setting.Webhook.PagingNum)
+ return tasks, db.GetEngine(db.DefaultContext).
+ Limit(setting.Webhook.PagingNum, (page-1)*setting.Webhook.PagingNum).
+ Where("hook_id=?", hookID).
+ Desc("id").
+ Find(&tasks)
+}
+
+// CreateHookTask creates a new hook task,
+// it handles conversion from Payload to PayloadContent.
+func CreateHookTask(t *HookTask) error {
+ return createHookTask(db.GetEngine(db.DefaultContext), t)
+}
+
+func createHookTask(e db.Engine, t *HookTask) error {
+ data, err := t.Payloader.JSONPayload()
+ if err != nil {
+ return err
+ }
+ t.UUID = gouuid.New().String()
+ t.PayloadContent = string(data)
+ _, err = e.Insert(t)
+ return err
+}
+
+// UpdateHookTask updates information of hook task.
+func UpdateHookTask(t *HookTask) error {
+ _, err := db.GetEngine(db.DefaultContext).ID(t.ID).AllCols().Update(t)
+ return err
+}
+
+// FindUndeliveredHookTasks represents find the undelivered hook tasks
+func FindUndeliveredHookTasks() ([]*HookTask, error) {
+ tasks := make([]*HookTask, 0, 10)
+ if err := db.GetEngine(db.DefaultContext).Where("is_delivered=?", false).Find(&tasks); err != nil {
+ return nil, err
+ }
+ return tasks, nil
+}
+
+// FindRepoUndeliveredHookTasks represents find the undelivered hook tasks of one repository
+func FindRepoUndeliveredHookTasks(repoID int64) ([]*HookTask, error) {
+ tasks := make([]*HookTask, 0, 5)
+ if err := db.GetEngine(db.DefaultContext).Where("repo_id=? AND is_delivered=?", repoID, false).Find(&tasks); err != nil {
+ return nil, err
+ }
+ return tasks, nil
+}
+
+// CleanupHookTaskTable deletes rows from hook_task as needed.
+func CleanupHookTaskTable(ctx context.Context, cleanupType HookTaskCleanupType, olderThan time.Duration, numberToKeep int) error {
+ log.Trace("Doing: CleanupHookTaskTable")
+
+ if cleanupType == OlderThan {
+ deleteOlderThan := time.Now().Add(-olderThan).UnixNano()
+ deletes, err := db.GetEngine(db.DefaultContext).
+ Where("is_delivered = ? and delivered < ?", true, deleteOlderThan).
+ Delete(new(HookTask))
+ if err != nil {
+ return err
+ }
+ log.Trace("Deleted %d rows from hook_task", deletes)
+ } else if cleanupType == PerWebhook {
+ hookIDs := make([]int64, 0, 10)
+ err := db.GetEngine(db.DefaultContext).Table("webhook").
+ Where("id > 0").
+ Cols("id").
+ Find(&hookIDs)
+ if err != nil {
+ return err
+ }
+ for _, hookID := range hookIDs {
+ select {
+ case <-ctx.Done():
+ return db.ErrCancelledf("Before deleting hook_task records for hook id %d", hookID)
+ default:
+ }
+ if err = deleteDeliveredHookTasksByWebhook(hookID, numberToKeep); err != nil {
+ return err
+ }
+ }
+ }
+ log.Trace("Finished: CleanupHookTaskTable")
+ return nil
+}
+
+func deleteDeliveredHookTasksByWebhook(hookID int64, numberDeliveriesToKeep int) error {
+ log.Trace("Deleting hook_task rows for webhook %d, keeping the most recent %d deliveries", hookID, numberDeliveriesToKeep)
+ deliveryDates := make([]int64, 0, 10)
+ err := db.GetEngine(db.DefaultContext).Table("hook_task").
+ Where("hook_task.hook_id = ? AND hook_task.is_delivered = ? AND hook_task.delivered is not null", hookID, true).
+ Cols("hook_task.delivered").
+ Join("INNER", "webhook", "hook_task.hook_id = webhook.id").
+ OrderBy("hook_task.delivered desc").
+ Limit(1, int(numberDeliveriesToKeep)).
+ Find(&deliveryDates)
+ if err != nil {
+ return err
+ }
+
+ if len(deliveryDates) > 0 {
+ deletes, err := db.GetEngine(db.DefaultContext).
+ Where("hook_id = ? and is_delivered = ? and delivered <= ?", hookID, true, deliveryDates[0]).
+ Delete(new(HookTask))
+ if err != nil {
+ return err
+ }
+ log.Trace("Deleted %d hook_task rows for webhook %d", deletes, hookID)
+ } else {
+ log.Trace("No hook_task rows to delete for webhook %d", hookID)
+ }
+
+ return nil
+}
diff --git a/models/webhook/main_test.go b/models/webhook/main_test.go
new file mode 100644
index 0000000000..f94612a755
--- /dev/null
+++ b/models/webhook/main_test.go
@@ -0,0 +1,16 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package webhook
+
+import (
+ "path/filepath"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+)
+
+func TestMain(m *testing.M) {
+ db.MainTest(m, filepath.Join("..", ".."), "webhook.yml", "hook_task.yml")
+}
diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go
new file mode 100644
index 0000000000..de8bd5e338
--- /dev/null
+++ b/models/webhook/webhook.go
@@ -0,0 +1,582 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package webhook
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/util"
+
+ "xorm.io/builder"
+)
+
+// __ __ ___. .__ __
+// / \ / \ ____\_ |__ | |__ ____ ____ | | __
+// \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ /
+// \ /\ ___/| \_\ \ Y ( <_> | <_> ) <
+// \__/\ / \___ >___ /___| /\____/ \____/|__|_ \
+// \/ \/ \/ \/ \/
+
+// ErrWebhookNotExist represents a "WebhookNotExist" kind of error.
+type ErrWebhookNotExist struct {
+ ID int64
+}
+
+// IsErrWebhookNotExist checks if an error is a ErrWebhookNotExist.
+func IsErrWebhookNotExist(err error) bool {
+ _, ok := err.(ErrWebhookNotExist)
+ return ok
+}
+
+func (err ErrWebhookNotExist) Error() string {
+ return fmt.Sprintf("webhook does not exist [id: %d]", err.ID)
+}
+
+// HookContentType is the content type of a web hook
+type HookContentType int
+
+const (
+ // ContentTypeJSON is a JSON payload for web hooks
+ ContentTypeJSON HookContentType = iota + 1
+ // ContentTypeForm is an url-encoded form payload for web hook
+ ContentTypeForm
+)
+
+var hookContentTypes = map[string]HookContentType{
+ "json": ContentTypeJSON,
+ "form": ContentTypeForm,
+}
+
+// ToHookContentType returns HookContentType by given name.
+func ToHookContentType(name string) HookContentType {
+ return hookContentTypes[name]
+}
+
+// HookTaskCleanupType is the type of cleanup to perform on hook_task
+type HookTaskCleanupType int
+
+const (
+ // OlderThan hook_task rows will be cleaned up by the age of the row
+ OlderThan HookTaskCleanupType = iota
+ // PerWebhook hook_task rows will be cleaned up by leaving the most recent deliveries for each webhook
+ PerWebhook
+)
+
+var hookTaskCleanupTypes = map[string]HookTaskCleanupType{
+ "OlderThan": OlderThan,
+ "PerWebhook": PerWebhook,
+}
+
+// ToHookTaskCleanupType returns HookTaskCleanupType by given name.
+func ToHookTaskCleanupType(name string) HookTaskCleanupType {
+ return hookTaskCleanupTypes[name]
+}
+
+// Name returns the name of a given web hook's content type
+func (t HookContentType) Name() string {
+ switch t {
+ case ContentTypeJSON:
+ return "json"
+ case ContentTypeForm:
+ return "form"
+ }
+ return ""
+}
+
+// IsValidHookContentType returns true if given name is a valid hook content type.
+func IsValidHookContentType(name string) bool {
+ _, ok := hookContentTypes[name]
+ return ok
+}
+
+// HookEvents is a set of web hook events
+type HookEvents struct {
+ Create bool `json:"create"`
+ Delete bool `json:"delete"`
+ Fork bool `json:"fork"`
+ Issues bool `json:"issues"`
+ IssueAssign bool `json:"issue_assign"`
+ IssueLabel bool `json:"issue_label"`
+ IssueMilestone bool `json:"issue_milestone"`
+ IssueComment bool `json:"issue_comment"`
+ Push bool `json:"push"`
+ PullRequest bool `json:"pull_request"`
+ PullRequestAssign bool `json:"pull_request_assign"`
+ PullRequestLabel bool `json:"pull_request_label"`
+ PullRequestMilestone bool `json:"pull_request_milestone"`
+ PullRequestComment bool `json:"pull_request_comment"`
+ PullRequestReview bool `json:"pull_request_review"`
+ PullRequestSync bool `json:"pull_request_sync"`
+ Repository bool `json:"repository"`
+ Release bool `json:"release"`
+}
+
+// HookEvent represents events that will delivery hook.
+type HookEvent struct {
+ PushOnly bool `json:"push_only"`
+ SendEverything bool `json:"send_everything"`
+ ChooseEvents bool `json:"choose_events"`
+ BranchFilter string `json:"branch_filter"`
+
+ HookEvents `json:"events"`
+}
+
+// HookType is the type of a webhook
+type HookType = string
+
+// Types of webhooks
+const (
+ GITEA HookType = "gitea"
+ GOGS HookType = "gogs"
+ SLACK HookType = "slack"
+ DISCORD HookType = "discord"
+ DINGTALK HookType = "dingtalk"
+ TELEGRAM HookType = "telegram"
+ MSTEAMS HookType = "msteams"
+ FEISHU HookType = "feishu"
+ MATRIX HookType = "matrix"
+ WECHATWORK HookType = "wechatwork"
+)
+
+// HookStatus is the status of a web hook
+type HookStatus int
+
+// Possible statuses of a web hook
+const (
+ HookStatusNone = iota
+ HookStatusSucceed
+ HookStatusFail
+)
+
+// Webhook represents a web hook object.
+type Webhook struct {
+ ID int64 `xorm:"pk autoincr"`
+ RepoID int64 `xorm:"INDEX"` // An ID of 0 indicates either a default or system webhook
+ OrgID int64 `xorm:"INDEX"`
+ IsSystemWebhook bool
+ URL string `xorm:"url TEXT"`
+ HTTPMethod string `xorm:"http_method"`
+ ContentType HookContentType
+ Secret string `xorm:"TEXT"`
+ Events string `xorm:"TEXT"`
+ *HookEvent `xorm:"-"`
+ IsActive bool `xorm:"INDEX"`
+ Type HookType `xorm:"VARCHAR(16) 'type'"`
+ Meta string `xorm:"TEXT"` // store hook-specific attributes
+ LastStatus HookStatus // Last delivery status
+
+ CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
+ UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
+}
+
+func init() {
+ db.RegisterModel(new(Webhook))
+}
+
+// AfterLoad updates the webhook object upon setting a column
+func (w *Webhook) AfterLoad() {
+ w.HookEvent = &HookEvent{}
+ if err := json.Unmarshal([]byte(w.Events), w.HookEvent); err != nil {
+ log.Error("Unmarshal[%d]: %v", w.ID, err)
+ }
+}
+
+// History returns history of webhook by given conditions.
+func (w *Webhook) History(page int) ([]*HookTask, error) {
+ return HookTasks(w.ID, page)
+}
+
+// UpdateEvent handles conversion from HookEvent to Events.
+func (w *Webhook) UpdateEvent() error {
+ data, err := json.Marshal(w.HookEvent)
+ w.Events = string(data)
+ return err
+}
+
+// HasCreateEvent returns true if hook enabled create event.
+func (w *Webhook) HasCreateEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Create)
+}
+
+// HasDeleteEvent returns true if hook enabled delete event.
+func (w *Webhook) HasDeleteEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Delete)
+}
+
+// HasForkEvent returns true if hook enabled fork event.
+func (w *Webhook) HasForkEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Fork)
+}
+
+// HasIssuesEvent returns true if hook enabled issues event.
+func (w *Webhook) HasIssuesEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Issues)
+}
+
+// HasIssuesAssignEvent returns true if hook enabled issues assign event.
+func (w *Webhook) HasIssuesAssignEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.IssueAssign)
+}
+
+// HasIssuesLabelEvent returns true if hook enabled issues label event.
+func (w *Webhook) HasIssuesLabelEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.IssueLabel)
+}
+
+// HasIssuesMilestoneEvent returns true if hook enabled issues milestone event.
+func (w *Webhook) HasIssuesMilestoneEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.IssueMilestone)
+}
+
+// HasIssueCommentEvent returns true if hook enabled issue_comment event.
+func (w *Webhook) HasIssueCommentEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.IssueComment)
+}
+
+// HasPushEvent returns true if hook enabled push event.
+func (w *Webhook) HasPushEvent() bool {
+ return w.PushOnly || w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Push)
+}
+
+// HasPullRequestEvent returns true if hook enabled pull request event.
+func (w *Webhook) HasPullRequestEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequest)
+}
+
+// HasPullRequestAssignEvent returns true if hook enabled pull request assign event.
+func (w *Webhook) HasPullRequestAssignEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequestAssign)
+}
+
+// HasPullRequestLabelEvent returns true if hook enabled pull request label event.
+func (w *Webhook) HasPullRequestLabelEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequestLabel)
+}
+
+// HasPullRequestMilestoneEvent returns true if hook enabled pull request milestone event.
+func (w *Webhook) HasPullRequestMilestoneEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequestMilestone)
+}
+
+// HasPullRequestCommentEvent returns true if hook enabled pull_request_comment event.
+func (w *Webhook) HasPullRequestCommentEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequestComment)
+}
+
+// HasPullRequestApprovedEvent returns true if hook enabled pull request review event.
+func (w *Webhook) HasPullRequestApprovedEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequestReview)
+}
+
+// HasPullRequestRejectedEvent returns true if hook enabled pull request review event.
+func (w *Webhook) HasPullRequestRejectedEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequestReview)
+}
+
+// HasPullRequestReviewCommentEvent returns true if hook enabled pull request review event.
+func (w *Webhook) HasPullRequestReviewCommentEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequestReview)
+}
+
+// HasPullRequestSyncEvent returns true if hook enabled pull request sync event.
+func (w *Webhook) HasPullRequestSyncEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequestSync)
+}
+
+// HasReleaseEvent returns if hook enabled release event.
+func (w *Webhook) HasReleaseEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Release)
+}
+
+// HasRepositoryEvent returns if hook enabled repository event.
+func (w *Webhook) HasRepositoryEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Repository)
+}
+
+// EventCheckers returns event checkers
+func (w *Webhook) EventCheckers() []struct {
+ Has func() bool
+ Type HookEventType
+} {
+ return []struct {
+ Has func() bool
+ Type HookEventType
+ }{
+ {w.HasCreateEvent, HookEventCreate},
+ {w.HasDeleteEvent, HookEventDelete},
+ {w.HasForkEvent, HookEventFork},
+ {w.HasPushEvent, HookEventPush},
+ {w.HasIssuesEvent, HookEventIssues},
+ {w.HasIssuesAssignEvent, HookEventIssueAssign},
+ {w.HasIssuesLabelEvent, HookEventIssueLabel},
+ {w.HasIssuesMilestoneEvent, HookEventIssueMilestone},
+ {w.HasIssueCommentEvent, HookEventIssueComment},
+ {w.HasPullRequestEvent, HookEventPullRequest},
+ {w.HasPullRequestAssignEvent, HookEventPullRequestAssign},
+ {w.HasPullRequestLabelEvent, HookEventPullRequestLabel},
+ {w.HasPullRequestMilestoneEvent, HookEventPullRequestMilestone},
+ {w.HasPullRequestCommentEvent, HookEventPullRequestComment},
+ {w.HasPullRequestApprovedEvent, HookEventPullRequestReviewApproved},
+ {w.HasPullRequestRejectedEvent, HookEventPullRequestReviewRejected},
+ {w.HasPullRequestCommentEvent, HookEventPullRequestReviewComment},
+ {w.HasPullRequestSyncEvent, HookEventPullRequestSync},
+ {w.HasRepositoryEvent, HookEventRepository},
+ {w.HasReleaseEvent, HookEventRelease},
+ }
+}
+
+// EventsArray returns an array of hook events
+func (w *Webhook) EventsArray() []string {
+ events := make([]string, 0, 7)
+
+ for _, c := range w.EventCheckers() {
+ if c.Has() {
+ events = append(events, string(c.Type))
+ }
+ }
+ return events
+}
+
+// CreateWebhook creates a new web hook.
+func CreateWebhook(ctx context.Context, w *Webhook) error {
+ w.Type = strings.TrimSpace(w.Type)
+ return db.Insert(ctx, w)
+}
+
+// getWebhook uses argument bean as query condition,
+// ID must be specified and do not assign unnecessary fields.
+func getWebhook(bean *Webhook) (*Webhook, error) {
+ has, err := db.GetEngine(db.DefaultContext).Get(bean)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrWebhookNotExist{bean.ID}
+ }
+ return bean, nil
+}
+
+// GetWebhookByID returns webhook of repository by given ID.
+func GetWebhookByID(id int64) (*Webhook, error) {
+ return getWebhook(&Webhook{
+ ID: id,
+ })
+}
+
+// GetWebhookByRepoID returns webhook of repository by given ID.
+func GetWebhookByRepoID(repoID, id int64) (*Webhook, error) {
+ return getWebhook(&Webhook{
+ ID: id,
+ RepoID: repoID,
+ })
+}
+
+// GetWebhookByOrgID returns webhook of organization by given ID.
+func GetWebhookByOrgID(orgID, id int64) (*Webhook, error) {
+ return getWebhook(&Webhook{
+ ID: id,
+ OrgID: orgID,
+ })
+}
+
+// ListWebhookOptions are options to filter webhooks on ListWebhooksByOpts
+type ListWebhookOptions struct {
+ db.ListOptions
+ RepoID int64
+ OrgID int64
+ IsActive util.OptionalBool
+}
+
+func (opts *ListWebhookOptions) toCond() builder.Cond {
+ cond := builder.NewCond()
+ if opts.RepoID != 0 {
+ cond = cond.And(builder.Eq{"webhook.repo_id": opts.RepoID})
+ }
+ if opts.OrgID != 0 {
+ cond = cond.And(builder.Eq{"webhook.org_id": opts.OrgID})
+ }
+ if !opts.IsActive.IsNone() {
+ cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.IsTrue()})
+ }
+ return cond
+}
+
+func listWebhooksByOpts(e db.Engine, opts *ListWebhookOptions) ([]*Webhook, error) {
+ sess := e.Where(opts.toCond())
+
+ if opts.Page != 0 {
+ sess = db.SetSessionPagination(sess, opts)
+ webhooks := make([]*Webhook, 0, opts.PageSize)
+ err := sess.Find(&webhooks)
+ return webhooks, err
+ }
+
+ webhooks := make([]*Webhook, 0, 10)
+ err := sess.Find(&webhooks)
+ return webhooks, err
+}
+
+// ListWebhooksByOpts return webhooks based on options
+func ListWebhooksByOpts(opts *ListWebhookOptions) ([]*Webhook, error) {
+ return listWebhooksByOpts(db.GetEngine(db.DefaultContext), opts)
+}
+
+// CountWebhooksByOpts count webhooks based on options and ignore pagination
+func CountWebhooksByOpts(opts *ListWebhookOptions) (int64, error) {
+ return db.GetEngine(db.DefaultContext).Where(opts.toCond()).Count(&Webhook{})
+}
+
+// GetDefaultWebhooks returns all admin-default webhooks.
+func GetDefaultWebhooks() ([]*Webhook, error) {
+ return getDefaultWebhooks(db.DefaultContext)
+}
+
+func getDefaultWebhooks(ctx context.Context) ([]*Webhook, error) {
+ webhooks := make([]*Webhook, 0, 5)
+ return webhooks, db.GetEngine(ctx).
+ Where("repo_id=? AND org_id=? AND is_system_webhook=?", 0, 0, false).
+ Find(&webhooks)
+}
+
+// GetSystemOrDefaultWebhook returns admin system or default webhook by given ID.
+func GetSystemOrDefaultWebhook(id int64) (*Webhook, error) {
+ webhook := &Webhook{ID: id}
+ has, err := db.GetEngine(db.DefaultContext).
+ Where("repo_id=? AND org_id=?", 0, 0).
+ Get(webhook)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrWebhookNotExist{id}
+ }
+ return webhook, nil
+}
+
+// GetSystemWebhooks returns all admin system webhooks.
+func GetSystemWebhooks() ([]*Webhook, error) {
+ return getSystemWebhooks(db.GetEngine(db.DefaultContext))
+}
+
+func getSystemWebhooks(e db.Engine) ([]*Webhook, error) {
+ webhooks := make([]*Webhook, 0, 5)
+ return webhooks, e.
+ Where("repo_id=? AND org_id=? AND is_system_webhook=?", 0, 0, true).
+ Find(&webhooks)
+}
+
+// UpdateWebhook updates information of webhook.
+func UpdateWebhook(w *Webhook) error {
+ _, err := db.GetEngine(db.DefaultContext).ID(w.ID).AllCols().Update(w)
+ return err
+}
+
+// UpdateWebhookLastStatus updates last status of webhook.
+func UpdateWebhookLastStatus(w *Webhook) error {
+ _, err := db.GetEngine(db.DefaultContext).ID(w.ID).Cols("last_status").Update(w)
+ return err
+}
+
+// deleteWebhook uses argument bean as query condition,
+// ID must be specified and do not assign unnecessary fields.
+func deleteWebhook(bean *Webhook) (err error) {
+ sess := db.NewSession(db.DefaultContext)
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if count, err := sess.Delete(bean); err != nil {
+ return err
+ } else if count == 0 {
+ return ErrWebhookNotExist{ID: bean.ID}
+ } else if _, err = sess.Delete(&HookTask{HookID: bean.ID}); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
+
+// DeleteWebhookByRepoID deletes webhook of repository by given ID.
+func DeleteWebhookByRepoID(repoID, id int64) error {
+ return deleteWebhook(&Webhook{
+ ID: id,
+ RepoID: repoID,
+ })
+}
+
+// DeleteWebhookByOrgID deletes webhook of organization by given ID.
+func DeleteWebhookByOrgID(orgID, id int64) error {
+ return deleteWebhook(&Webhook{
+ ID: id,
+ OrgID: orgID,
+ })
+}
+
+// DeleteDefaultSystemWebhook deletes an admin-configured default or system webhook (where Org and Repo ID both 0)
+func DeleteDefaultSystemWebhook(id int64) error {
+ sess := db.NewSession(db.DefaultContext)
+ defer sess.Close()
+ if err := sess.Begin(); err != nil {
+ return err
+ }
+
+ count, err := sess.
+ Where("repo_id=? AND org_id=?", 0, 0).
+ Delete(&Webhook{ID: id})
+ if err != nil {
+ return err
+ } else if count == 0 {
+ return ErrWebhookNotExist{ID: id}
+ }
+
+ if _, err := sess.Delete(&HookTask{HookID: id}); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
+
+// CopyDefaultWebhooksToRepo creates copies of the default webhooks in a new repo
+func CopyDefaultWebhooksToRepo(ctx context.Context, repoID int64) error {
+ ws, err := getDefaultWebhooks(ctx)
+ if err != nil {
+ return fmt.Errorf("GetDefaultWebhooks: %v", err)
+ }
+
+ for _, w := range ws {
+ w.ID = 0
+ w.RepoID = repoID
+ if err := CreateWebhook(ctx, w); err != nil {
+ return fmt.Errorf("CreateWebhook: %v", err)
+ }
+ }
+ return nil
+}
diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go
new file mode 100644
index 0000000000..df2c37b355
--- /dev/null
+++ b/models/webhook/webhook_test.go
@@ -0,0 +1,328 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package webhook
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/json"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestHookContentType_Name(t *testing.T) {
+ assert.Equal(t, "json", ContentTypeJSON.Name())
+ assert.Equal(t, "form", ContentTypeForm.Name())
+}
+
+func TestIsValidHookContentType(t *testing.T) {
+ assert.True(t, IsValidHookContentType("json"))
+ assert.True(t, IsValidHookContentType("form"))
+ assert.False(t, IsValidHookContentType("invalid"))
+}
+
+func TestWebhook_History(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+ webhook := db.AssertExistsAndLoadBean(t, &Webhook{ID: 1}).(*Webhook)
+ tasks, err := webhook.History(0)
+ assert.NoError(t, err)
+ if assert.Len(t, tasks, 1) {
+ assert.Equal(t, int64(1), tasks[0].ID)
+ }
+
+ webhook = db.AssertExistsAndLoadBean(t, &Webhook{ID: 2}).(*Webhook)
+ tasks, err = webhook.History(0)
+ assert.NoError(t, err)
+ assert.Len(t, tasks, 0)
+}
+
+func TestWebhook_UpdateEvent(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+ webhook := db.AssertExistsAndLoadBean(t, &Webhook{ID: 1}).(*Webhook)
+ hookEvent := &HookEvent{
+ PushOnly: true,
+ SendEverything: false,
+ ChooseEvents: false,
+ HookEvents: HookEvents{
+ Create: false,
+ Push: true,
+ PullRequest: false,
+ },
+ }
+ webhook.HookEvent = hookEvent
+ assert.NoError(t, webhook.UpdateEvent())
+ assert.NotEmpty(t, webhook.Events)
+ actualHookEvent := &HookEvent{}
+ assert.NoError(t, json.Unmarshal([]byte(webhook.Events), actualHookEvent))
+ assert.Equal(t, *hookEvent, *actualHookEvent)
+}
+
+func TestWebhook_EventsArray(t *testing.T) {
+ assert.Equal(t, []string{
+ "create", "delete", "fork", "push",
+ "issues", "issue_assign", "issue_label", "issue_milestone", "issue_comment",
+ "pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone",
+ "pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected",
+ "pull_request_review_comment", "pull_request_sync", "repository", "release",
+ },
+ (&Webhook{
+ HookEvent: &HookEvent{SendEverything: true},
+ }).EventsArray(),
+ )
+
+ assert.Equal(t, []string{"push"},
+ (&Webhook{
+ HookEvent: &HookEvent{PushOnly: true},
+ }).EventsArray(),
+ )
+}
+
+func TestCreateWebhook(t *testing.T) {
+ hook := &Webhook{
+ RepoID: 3,
+ URL: "www.example.com/unit_test",
+ ContentType: ContentTypeJSON,
+ Events: `{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}`,
+ }
+ db.AssertNotExistsBean(t, hook)
+ assert.NoError(t, CreateWebhook(db.DefaultContext, hook))
+ db.AssertExistsAndLoadBean(t, hook)
+}
+
+func TestGetWebhookByRepoID(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+ hook, err := GetWebhookByRepoID(1, 1)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(1), hook.ID)
+
+ _, err = GetWebhookByRepoID(db.NonexistentID, db.NonexistentID)
+ assert.Error(t, err)
+ assert.True(t, IsErrWebhookNotExist(err))
+}
+
+func TestGetWebhookByOrgID(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+ hook, err := GetWebhookByOrgID(3, 3)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(3), hook.ID)
+
+ _, err = GetWebhookByOrgID(db.NonexistentID, db.NonexistentID)
+ assert.Error(t, err)
+ assert.True(t, IsErrWebhookNotExist(err))
+}
+
+func TestGetActiveWebhooksByRepoID(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+ hooks, err := ListWebhooksByOpts(&ListWebhookOptions{RepoID: 1, IsActive: util.OptionalBoolTrue})
+ assert.NoError(t, err)
+ if assert.Len(t, hooks, 1) {
+ assert.Equal(t, int64(1), hooks[0].ID)
+ assert.True(t, hooks[0].IsActive)
+ }
+}
+
+func TestGetWebhooksByRepoID(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+ hooks, err := ListWebhooksByOpts(&ListWebhookOptions{RepoID: 1})
+ assert.NoError(t, err)
+ if assert.Len(t, hooks, 2) {
+ assert.Equal(t, int64(1), hooks[0].ID)
+ assert.Equal(t, int64(2), hooks[1].ID)
+ }
+}
+
+func TestGetActiveWebhooksByOrgID(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+ hooks, err := ListWebhooksByOpts(&ListWebhookOptions{OrgID: 3, IsActive: util.OptionalBoolTrue})
+ assert.NoError(t, err)
+ if assert.Len(t, hooks, 1) {
+ assert.Equal(t, int64(3), hooks[0].ID)
+ assert.True(t, hooks[0].IsActive)
+ }
+}
+
+func TestGetWebhooksByOrgID(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+ hooks, err := ListWebhooksByOpts(&ListWebhookOptions{OrgID: 3})
+ assert.NoError(t, err)
+ if assert.Len(t, hooks, 1) {
+ assert.Equal(t, int64(3), hooks[0].ID)
+ assert.True(t, hooks[0].IsActive)
+ }
+}
+
+func TestUpdateWebhook(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+ hook := db.AssertExistsAndLoadBean(t, &Webhook{ID: 2}).(*Webhook)
+ hook.IsActive = true
+ hook.ContentType = ContentTypeForm
+ db.AssertNotExistsBean(t, hook)
+ assert.NoError(t, UpdateWebhook(hook))
+ db.AssertExistsAndLoadBean(t, hook)
+}
+
+func TestDeleteWebhookByRepoID(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+ db.AssertExistsAndLoadBean(t, &Webhook{ID: 2, RepoID: 1})
+ assert.NoError(t, DeleteWebhookByRepoID(1, 2))
+ db.AssertNotExistsBean(t, &Webhook{ID: 2, RepoID: 1})
+
+ err := DeleteWebhookByRepoID(db.NonexistentID, db.NonexistentID)
+ assert.Error(t, err)
+ assert.True(t, IsErrWebhookNotExist(err))
+}
+
+func TestDeleteWebhookByOrgID(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+ db.AssertExistsAndLoadBean(t, &Webhook{ID: 3, OrgID: 3})
+ assert.NoError(t, DeleteWebhookByOrgID(3, 3))
+ db.AssertNotExistsBean(t, &Webhook{ID: 3, OrgID: 3})
+
+ err := DeleteWebhookByOrgID(db.NonexistentID, db.NonexistentID)
+ assert.Error(t, err)
+ assert.True(t, IsErrWebhookNotExist(err))
+}
+
+func TestHookTasks(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+ hookTasks, err := HookTasks(1, 1)
+ assert.NoError(t, err)
+ if assert.Len(t, hookTasks, 1) {
+ assert.Equal(t, int64(1), hookTasks[0].ID)
+ }
+
+ hookTasks, err = HookTasks(db.NonexistentID, 1)
+ assert.NoError(t, err)
+ assert.Len(t, hookTasks, 0)
+}
+
+func TestCreateHookTask(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+ hookTask := &HookTask{
+ RepoID: 3,
+ HookID: 3,
+ Payloader: &api.PushPayload{},
+ }
+ db.AssertNotExistsBean(t, hookTask)
+ assert.NoError(t, CreateHookTask(hookTask))
+ db.AssertExistsAndLoadBean(t, hookTask)
+}
+
+func TestUpdateHookTask(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+
+ hook := db.AssertExistsAndLoadBean(t, &HookTask{ID: 1}).(*HookTask)
+ hook.PayloadContent = "new payload content"
+ hook.DeliveredString = "new delivered string"
+ hook.IsDelivered = true
+ db.AssertNotExistsBean(t, hook)
+ assert.NoError(t, UpdateHookTask(hook))
+ db.AssertExistsAndLoadBean(t, hook)
+}
+
+func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+ hookTask := &HookTask{
+ RepoID: 3,
+ HookID: 3,
+ Payloader: &api.PushPayload{},
+ IsDelivered: true,
+ Delivered: time.Now().UnixNano(),
+ }
+ db.AssertNotExistsBean(t, hookTask)
+ assert.NoError(t, CreateHookTask(hookTask))
+ db.AssertExistsAndLoadBean(t, hookTask)
+
+ assert.NoError(t, CleanupHookTaskTable(context.Background(), PerWebhook, 168*time.Hour, 0))
+ db.AssertNotExistsBean(t, hookTask)
+}
+
+func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+ hookTask := &HookTask{
+ RepoID: 2,
+ HookID: 4,
+ Payloader: &api.PushPayload{},
+ IsDelivered: false,
+ }
+ db.AssertNotExistsBean(t, hookTask)
+ assert.NoError(t, CreateHookTask(hookTask))
+ db.AssertExistsAndLoadBean(t, hookTask)
+
+ assert.NoError(t, CleanupHookTaskTable(context.Background(), PerWebhook, 168*time.Hour, 0))
+ db.AssertExistsAndLoadBean(t, hookTask)
+}
+
+func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+ hookTask := &HookTask{
+ RepoID: 2,
+ HookID: 4,
+ Payloader: &api.PushPayload{},
+ IsDelivered: true,
+ Delivered: time.Now().UnixNano(),
+ }
+ db.AssertNotExistsBean(t, hookTask)
+ assert.NoError(t, CreateHookTask(hookTask))
+ db.AssertExistsAndLoadBean(t, hookTask)
+
+ assert.NoError(t, CleanupHookTaskTable(context.Background(), PerWebhook, 168*time.Hour, 1))
+ db.AssertExistsAndLoadBean(t, hookTask)
+}
+
+func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+ hookTask := &HookTask{
+ RepoID: 3,
+ HookID: 3,
+ Payloader: &api.PushPayload{},
+ IsDelivered: true,
+ Delivered: time.Now().AddDate(0, 0, -8).UnixNano(),
+ }
+ db.AssertNotExistsBean(t, hookTask)
+ assert.NoError(t, CreateHookTask(hookTask))
+ db.AssertExistsAndLoadBean(t, hookTask)
+
+ assert.NoError(t, CleanupHookTaskTable(context.Background(), OlderThan, 168*time.Hour, 0))
+ db.AssertNotExistsBean(t, hookTask)
+}
+
+func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+ hookTask := &HookTask{
+ RepoID: 2,
+ HookID: 4,
+ Payloader: &api.PushPayload{},
+ IsDelivered: false,
+ }
+ db.AssertNotExistsBean(t, hookTask)
+ assert.NoError(t, CreateHookTask(hookTask))
+ db.AssertExistsAndLoadBean(t, hookTask)
+
+ assert.NoError(t, CleanupHookTaskTable(context.Background(), OlderThan, 168*time.Hour, 0))
+ db.AssertExistsAndLoadBean(t, hookTask)
+}
+
+func TestCleanupHookTaskTable_OlderThan_LeavesTaskEarlierThanAgeToDelete(t *testing.T) {
+ assert.NoError(t, db.PrepareTestDatabase())
+ hookTask := &HookTask{
+ RepoID: 2,
+ HookID: 4,
+ Payloader: &api.PushPayload{},
+ IsDelivered: true,
+ Delivered: time.Now().AddDate(0, 0, -6).UnixNano(),
+ }
+ db.AssertNotExistsBean(t, hookTask)
+ assert.NoError(t, CreateHookTask(hookTask))
+ db.AssertExistsAndLoadBean(t, hookTask)
+
+ assert.NoError(t, CleanupHookTaskTable(context.Background(), OlderThan, 168*time.Hour, 0))
+ db.AssertExistsAndLoadBean(t, hookTask)
+}