From 8dfbbfef07bb35f921e4ab4228c8006ce26dd183 Mon Sep 17 00:00:00 2001 From: oliverpool Date: Thu, 21 Mar 2024 13:23:27 +0100 Subject: [PATCH] [REFACTOR] webhook matrix endpoints --- modules/web/middleware/binding.go | 4 +- routers/web/repo/setting/webhook.go | 76 +++++++++++++++++--------- routers/web/web.go | 4 +- services/forms/repo_form.go | 14 ----- services/webhook/default.go | 4 ++ services/webhook/dingtalk.go | 3 + services/webhook/discord.go | 4 ++ services/webhook/feishu.go | 7 ++- services/webhook/matrix.go | 28 ++++++++++ services/webhook/msteams.go | 4 ++ services/webhook/packagist.go | 4 ++ services/webhook/slack.go | 4 ++ services/webhook/telegram.go | 4 ++ services/webhook/webhook.go | 15 ++++- services/webhook/wechatwork.go | 4 ++ tests/integration/repo_webhook_test.go | 4 +- 16 files changed, 134 insertions(+), 49 deletions(-) diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index 4891e43f2..8fa71a81b 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -79,8 +79,8 @@ func GetInclude(field reflect.StructField) string { return getRuleBody(field, "Include(") } -// Validate validate TODO: -func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Locale) binding.Errors { +// Validate populates the data with validation error (if any). +func Validate(errs binding.Errors, data map[string]any, f any, l translation.Locale) binding.Errors { if errs.Len() == 0 { return errs } diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index 1f78681da..f41752e47 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -24,11 +24,14 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/modules/web/middleware" webhook_module "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/forms" webhook_service "code.gitea.io/gitea/services/webhook" + + "gitea.com/go-chi/binding" ) const ( @@ -201,6 +204,29 @@ type webhookParams struct { Meta any } +func WebhookCreate(ctx *context.Context) { + typ := ctx.Params(":type") + handler := webhook_service.GetWebhookHandler(typ) + if handler == nil { + ctx.NotFound("GetWebhookHandler", nil) + return + } + + fields := handler.FormFields(func(form any) { + errs := binding.Bind(ctx.Req, form) + middleware.Validate(errs, ctx.Data, form, ctx.Locale) // error will be checked later in ctx.HasError + }) + createWebhook(ctx, webhookParams{ + Type: typ, + URL: fields.URL, + ContentType: fields.ContentType, + Secret: fields.Secret, + HTTPMethod: fields.HTTPMethod, + WebhookForm: fields.WebhookForm, + Meta: fields.Metadata, + }) +} + func createWebhook(ctx *context.Context, params webhookParams) { ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook") ctx.Data["PageIsSettingsHooks"] = true @@ -260,6 +286,29 @@ func createWebhook(ctx *context.Context, params webhookParams) { ctx.Redirect(orCtx.Link) } +func WebhookUpdate(ctx *context.Context) { + typ := ctx.Params(":type") + handler := webhook_service.GetWebhookHandler(typ) + if handler == nil { + ctx.NotFound("GetWebhookHandler", nil) + return + } + + fields := handler.FormFields(func(form any) { + errs := binding.Bind(ctx.Req, form) + middleware.Validate(errs, ctx.Data, form, ctx.Locale) // error will be checked later in ctx.HasError + }) + editWebhook(ctx, webhookParams{ + Type: typ, + URL: fields.URL, + ContentType: fields.ContentType, + Secret: fields.Secret, + HTTPMethod: fields.HTTPMethod, + WebhookForm: fields.WebhookForm, + Meta: fields.Metadata, + }) +} + func editWebhook(ctx *context.Context, params webhookParams) { ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook") ctx.Data["PageIsSettingsHooks"] = true @@ -467,33 +516,6 @@ func telegramHookParams(ctx *context.Context) webhookParams { } } -// MatrixHooksNewPost response for creating Matrix webhook -func MatrixHooksNewPost(ctx *context.Context) { - createWebhook(ctx, matrixHookParams(ctx)) -} - -// MatrixHooksEditPost response for editing Matrix webhook -func MatrixHooksEditPost(ctx *context.Context) { - editWebhook(ctx, matrixHookParams(ctx)) -} - -func matrixHookParams(ctx *context.Context) webhookParams { - form := web.GetForm(ctx).(*forms.NewMatrixHookForm) - - return webhookParams{ - Type: webhook_module.MATRIX, - URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)), - ContentType: webhook.ContentTypeJSON, - HTTPMethod: http.MethodPut, - WebhookForm: form.WebhookForm, - Meta: &webhook_service.MatrixMeta{ - HomeserverURL: form.HomeserverURL, - Room: form.RoomID, - MessageType: form.MessageType, - }, - } -} - // MSTeamsHooksNewPost response for creating MSTeams webhook func MSTeamsHooksNewPost(ctx *context.Context) { createWebhook(ctx, mSTeamsHookParams(ctx)) diff --git a/routers/web/web.go b/routers/web/web.go index 50a73e9b8..b23068a29 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -409,11 +409,11 @@ func registerRoutes(m *web.Route) { m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo_setting.DiscordHooksNewPost) m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo_setting.DingtalkHooksNewPost) m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo_setting.TelegramHooksNewPost) - m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo_setting.MatrixHooksNewPost) m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo_setting.MSTeamsHooksNewPost) m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo_setting.FeishuHooksNewPost) m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo_setting.WechatworkHooksNewPost) m.Post("/packagist/new", web.Bind(forms.NewPackagistHookForm{}), repo_setting.PackagistHooksNewPost) + m.Post("/{type}/new", repo_setting.WebhookCreate) } addWebhookEditRoutes := func() { @@ -424,11 +424,11 @@ func registerRoutes(m *web.Route) { m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo_setting.DiscordHooksEditPost) m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo_setting.DingtalkHooksEditPost) m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo_setting.TelegramHooksEditPost) - m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo_setting.MatrixHooksEditPost) m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo_setting.MSTeamsHooksEditPost) m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo_setting.FeishuHooksEditPost) m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo_setting.WechatworkHooksEditPost) m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo_setting.PackagistHooksEditPost) + m.Post("/{type}/{id:[0-9]+}", repo_setting.WebhookUpdate) } addSettingsVariablesRoutes := func() { diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index f9ebb6eba..35d465bdd 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -371,20 +371,6 @@ func (f *NewTelegramHookForm) Validate(req *http.Request, errs binding.Errors) b return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } -// NewMatrixHookForm form for creating Matrix hook -type NewMatrixHookForm struct { - HomeserverURL string `binding:"Required;ValidUrl"` - RoomID string `binding:"Required"` - MessageType int - WebhookForm -} - -// Validate validates the fields -func (f *NewMatrixHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - // NewMSTeamsHookForm form for creating MS Teams hook type NewMSTeamsHookForm struct { PayloadURL string `binding:"Required;ValidUrl"` diff --git a/services/webhook/default.go b/services/webhook/default.go index 874668bb4..d61e316a6 100644 --- a/services/webhook/default.go +++ b/services/webhook/default.go @@ -35,6 +35,10 @@ func (dh defaultHandler) Type() webhook_module.HookType { func (defaultHandler) Metadata(*webhook_model.Webhook) any { return nil } +func (defaultHandler) FormFields(bind func(any)) FormFields { + panic("TODO") +} + func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) { switch w.HTTPMethod { case "": diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go index 4e7bef89f..12c04d225 100644 --- a/services/webhook/dingtalk.go +++ b/services/webhook/dingtalk.go @@ -23,6 +23,9 @@ type dingtalkHandler struct{} func (dingtalkHandler) Type() webhook_module.HookType { return webhook_module.DINGTALK } func (dingtalkHandler) Metadata(*webhook_model.Webhook) any { return nil } +func (dingtalkHandler) FormFields(bind func(any)) FormFields { + panic("TODO") +} type ( // DingtalkPayload represents diff --git a/services/webhook/discord.go b/services/webhook/discord.go index a84aa9d7b..8ddbb1373 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -26,6 +26,10 @@ type discordHandler struct{} func (discordHandler) Type() webhook_module.HookType { return webhook_module.DISCORD } +func (discordHandler) FormFields(bind func(any)) FormFields { + panic("TODO") +} + type ( // DiscordEmbedFooter for Embed Footer Structure. DiscordEmbedFooter struct { diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go index 2c3508e3c..19a3a0cc0 100644 --- a/services/webhook/feishu.go +++ b/services/webhook/feishu.go @@ -17,7 +17,12 @@ import ( type feishuHandler struct{} -func (feishuHandler) Type() webhook_module.HookType { return webhook_module.FEISHU } +func (feishuHandler) Type() webhook_module.HookType { return webhook_module.FEISHU } + +func (feishuHandler) FormFields(bind func(any)) FormFields { + panic("TODO") +} + func (feishuHandler) Metadata(*webhook_model.Webhook) any { return nil } type ( diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go index d1c0ec33f..d04f0f367 100644 --- a/services/webhook/matrix.go +++ b/services/webhook/matrix.go @@ -22,12 +22,40 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/forms" ) type matrixHandler struct{} func (matrixHandler) Type() webhook_module.HookType { return webhook_module.MATRIX } +func (matrixHandler) FormFields(bind func(any)) FormFields { + var form struct { + forms.WebhookForm + HomeserverURL string `binding:"Required;ValidUrl"` + RoomID string `binding:"Required"` + MessageType int + + // enforce requirement of authorization_header + // (value will still be set in the embedded WebhookForm) + AuthorizationHeader string `binding:"Required"` + } + bind(&form) + + return FormFields{ + WebhookForm: form.WebhookForm, + URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)), + ContentType: webhook_model.ContentTypeJSON, + Secret: "", + HTTPMethod: http.MethodPut, + Metadata: &MatrixMeta{ + HomeserverURL: form.HomeserverURL, + Room: form.RoomID, + MessageType: form.MessageType, + }, + } +} + func (matrixHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { meta := &MatrixMeta{} if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go index 6d9070d08..579988489 100644 --- a/services/webhook/msteams.go +++ b/services/webhook/msteams.go @@ -22,6 +22,10 @@ type msteamsHandler struct{} func (msteamsHandler) Type() webhook_module.HookType { return webhook_module.MSTEAMS } func (msteamsHandler) Metadata(*webhook_model.Webhook) any { return nil } +func (msteamsHandler) FormFields(bind func(any)) FormFields { + panic("TODO") +} + type ( // MSTeamsFact for Fact Structure MSTeamsFact struct { diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go index d8d1937e6..9b07fe025 100644 --- a/services/webhook/packagist.go +++ b/services/webhook/packagist.go @@ -18,6 +18,10 @@ type packagistHandler struct{} func (packagistHandler) Type() webhook_module.HookType { return webhook_module.PACKAGIST } +func (packagistHandler) FormFields(bind func(any)) FormFields { + panic("TODO") +} + type ( // PackagistPayload represents a packagist payload // as expected by https://packagist.org/about diff --git a/services/webhook/slack.go b/services/webhook/slack.go index 9b1c367e3..fdc10730d 100644 --- a/services/webhook/slack.go +++ b/services/webhook/slack.go @@ -23,6 +23,10 @@ type slackHandler struct{} func (slackHandler) Type() webhook_module.HookType { return webhook_module.SLACK } +func (slackHandler) FormFields(bind func(any)) FormFields { + panic("TODO") +} + // SlackMeta contains the slack metadata type SlackMeta struct { Channel string `json:"channel"` diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go index 84e2c038f..4179437a8 100644 --- a/services/webhook/telegram.go +++ b/services/webhook/telegram.go @@ -21,6 +21,10 @@ type telegramHandler struct{} func (telegramHandler) Type() webhook_module.HookType { return webhook_module.TELEGRAM } +func (telegramHandler) FormFields(bind func(any)) FormFields { + panic("TODO") +} + type ( // TelegramPayload represents TelegramPayload struct { diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index 62e5374fc..a7802d27d 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -23,14 +23,27 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/forms" "github.com/gobwas/glob" ) type Handler interface { Type() webhook_module.HookType - NewRequest(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error) Metadata(*webhook_model.Webhook) any + // FormFields provides a function to bind the request to the form. + // If form implements the [binding.Validator] interface, the Validate method will be called + FormFields(bind func(form any)) FormFields + NewRequest(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error) +} + +type FormFields struct { + forms.WebhookForm + URL string + ContentType webhook_model.HookContentType + Secret string + HTTPMethod string + Metadata any } var webhookHandlers = []Handler{ diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go index 184d83308..6d0b12b84 100644 --- a/services/webhook/wechatwork.go +++ b/services/webhook/wechatwork.go @@ -20,6 +20,10 @@ type wechatworkHandler struct{} func (wechatworkHandler) Type() webhook_module.HookType { return webhook_module.WECHATWORK } func (wechatworkHandler) Metadata(*webhook_model.Webhook) any { return nil } +func (wechatworkHandler) FormFields(bind func(any)) FormFields { + panic("TODO") +} + type ( // WechatworkPayload represents WechatworkPayload struct { diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index 3a2fa48c9..47c7a1e43 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -203,8 +203,8 @@ func TestWebhookForms(t *testing.T) { "homeserver_url": "https://matrix.example.com", "room_id": "123", "authorization_header": "Bearer 123456", - // }, map[string]string{ // authorization_header is actually required, but not enforced (yet) - // "authorization_header": "", + }, map[string]string{ + "authorization_header": "", })) t.Run("matrix/optional", testWebhookForms("matrix", session, map[string]string{ "homeserver_url": "https://matrix.example.com",