From bf7b083cfe47cc922090ce7922b89f7a5030a44d Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 5 Jan 2022 22:00:20 +0100 Subject: [PATCH] Add replay of webhooks. (#18191) --- models/webhook/hooktask.go | 39 +++++++++++++++++--- models/webhook/webhook.go | 16 ++++++++ options/locale/locale_en-US.ini | 3 +- routers/web/repo/webhook.go | 24 +++++++++++- routers/web/web.go | 17 +++++++-- services/webhook/webhook.go | 12 ++++++ templates/admin/hook_new.tmpl | 2 +- templates/repo/settings/webhook/history.tmpl | 8 ++++ 8 files changed, 108 insertions(+), 13 deletions(-) diff --git a/models/webhook/hooktask.go b/models/webhook/hooktask.go index 1967ded298..1d19ebd24e 100644 --- a/models/webhook/hooktask.go +++ b/models/webhook/hooktask.go @@ -175,18 +175,13 @@ func HookTasks(hookID int64, page int) ([]*HookTask, error) { // 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 + return db.Insert(db.DefaultContext, t) } // UpdateHookTask updates information of hook task. @@ -195,6 +190,38 @@ func UpdateHookTask(t *HookTask) error { return err } +// ReplayHookTask copies a hook task to get re-delivered +func ReplayHookTask(hookID int64, uuid string) (*HookTask, error) { + var newTask *HookTask + + err := db.WithTx(func(ctx context.Context) error { + task := &HookTask{ + HookID: hookID, + UUID: uuid, + } + has, err := db.GetByBean(ctx, task) + if err != nil { + return err + } else if !has { + return ErrHookTaskNotExist{ + HookID: hookID, + UUID: uuid, + } + } + + newTask = &HookTask{ + UUID: gouuid.New().String(), + RepoID: task.RepoID, + HookID: task.HookID, + PayloadContent: task.PayloadContent, + EventType: task.EventType, + } + return db.Insert(ctx, newTask) + }) + + return newTask, err +} + // FindUndeliveredHookTasks represents find the undelivered hook tasks func FindUndeliveredHookTasks() ([]*HookTask, error) { tasks := make([]*HookTask, 0, 10) diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index d01f548eed..21c01d9289 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -41,6 +41,22 @@ func (err ErrWebhookNotExist) Error() string { return fmt.Sprintf("webhook does not exist [id: %d]", err.ID) } +// ErrHookTaskNotExist represents a "HookTaskNotExist" kind of error. +type ErrHookTaskNotExist struct { + HookID int64 + UUID string +} + +// IsErrWebhookNotExist checks if an error is a ErrWebhookNotExist. +func IsErrHookTaskNotExist(err error) bool { + _, ok := err.(ErrHookTaskNotExist) + return ok +} + +func (err ErrHookTaskNotExist) Error() string { + return fmt.Sprintf("hook task does not exist [hook: %d, uuid: %s]", err.HookID, err.UUID) +} + // HookContentType is the content type of a web hook type HookContentType int diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 7a3dbd50a8..3d60df5d68 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1831,12 +1831,13 @@ settings.webhook_deletion_desc = Removing a webhook deletes its settings and del settings.webhook_deletion_success = The webhook has been removed. settings.webhook.test_delivery = Test Delivery settings.webhook.test_delivery_desc = Test this webhook with a fake event. -settings.webhook.test_delivery_success = A fake event has been added to the delivery queue. It may take few seconds before it shows up in the delivery history. settings.webhook.request = Request settings.webhook.response = Response settings.webhook.headers = Headers settings.webhook.payload = Content settings.webhook.body = Body +settings.webhook.replay.description = Replay this webhook. +settings.webhook.delivery.success = An event has been added to the delivery queue. It may take few seconds before it shows up in the delivery history. settings.githooks_desc = "Git Hooks are powered by Git itself. You can edit hook files below to set up custom operations." settings.githook_edit_desc = If the hook is inactive, sample content will be presented. Leaving content to an empty value will disable this hook. settings.githook_name = Hook Name diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go index 47d8413671..2ec2e8911f 100644 --- a/routers/web/repo/webhook.go +++ b/routers/web/repo/webhook.go @@ -1189,11 +1189,33 @@ func TestWebhook(ctx *context.Context) { ctx.Flash.Error("PrepareWebhook: " + err.Error()) ctx.Status(500) } else { - ctx.Flash.Info(ctx.Tr("repo.settings.webhook.test_delivery_success")) + ctx.Flash.Info(ctx.Tr("repo.settings.webhook.delivery.success")) ctx.Status(200) } } +// ReplayWebhook replays a webhook +func ReplayWebhook(ctx *context.Context) { + hookTaskUUID := ctx.Params(":uuid") + + orCtx, w := checkWebhook(ctx) + if ctx.Written() { + return + } + + if err := webhook_service.ReplayHookTask(w, hookTaskUUID); err != nil { + if webhook.IsErrHookTaskNotExist(err) { + ctx.NotFound("ReplayHookTask", nil) + } else { + ctx.ServerError("ReplayHookTask", err) + } + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.webhook.delivery.success")) + ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID)) +} + // DeleteWebhook delete a webhook func DeleteWebhook(ctx *context.Context) { if err := webhook.DeleteWebhookByRepoID(ctx.Repo.Repository.ID, ctx.FormInt64("id")); err != nil { diff --git a/routers/web/web.go b/routers/web/web.go index e35ccb5aab..8abdf7c61f 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -435,7 +435,10 @@ func RegisterRoutes(m *web.Route) { m.Group("/hooks", func() { m.Get("", admin.DefaultOrSystemWebhooks) m.Post("/delete", admin.DeleteDefaultOrSystemWebhook) - m.Get("/{id}", repo.WebHooksEdit) + m.Group("/{id}", func() { + m.Get("", repo.WebHooksEdit) + m.Post("/replay/{uuid}", repo.ReplayWebhook) + }) m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost) m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost) m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) @@ -559,7 +562,10 @@ func RegisterRoutes(m *web.Route) { m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) m.Post("/wechatwork/new", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) - m.Get("/{id}", repo.WebHooksEdit) + m.Group("/{id}", func() { + m.Get("", repo.WebHooksEdit) + m.Post("/replay/{uuid}", repo.ReplayWebhook) + }) m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost) m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost) m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) @@ -653,8 +659,11 @@ func RegisterRoutes(m *web.Route) { m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) m.Post("/wechatwork/new", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) - m.Get("/{id}", repo.WebHooksEdit) - m.Post("/{id}/test", repo.TestWebhook) + m.Group("/{id}", func() { + m.Get("", repo.WebHooksEdit) + m.Post("/test", repo.TestWebhook) + m.Post("/replay/{uuid}", repo.ReplayWebhook) + }) m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost) m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost) m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index 9356f4ee11..f284a20c30 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -229,3 +229,15 @@ func prepareWebhooks(repo *repo_model.Repository, event webhook_model.HookEventT } return nil } + +// ReplayHookTask replays a webhook task +func ReplayHookTask(w *webhook_model.Webhook, uuid string) error { + t, err := webhook_model.ReplayHookTask(w.ID, uuid) + if err != nil { + return err + } + + go hookQueue.Add(t.RepoID) + + return nil +} diff --git a/templates/admin/hook_new.tmpl b/templates/admin/hook_new.tmpl index 4710498b20..2cd3fc826c 100644 --- a/templates/admin/hook_new.tmpl +++ b/templates/admin/hook_new.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/repo/settings/webhook/history.tmpl b/templates/repo/settings/webhook/history.tmpl index 8823d6c1dc..a719e4453a 100644 --- a/templates/repo/settings/webhook/history.tmpl +++ b/templates/repo/settings/webhook/history.tmpl @@ -40,6 +40,14 @@ N/A {{end}} + {{if or $.Permission.IsAdmin $.IsOrganizationOwner $.PageIsAdmin}} + + {{end}}
{{if .RequestInfo}}