[REFACTOR] webhook matrix endpoints

This commit is contained in:
oliverpool 2024-03-21 13:23:27 +01:00
parent e41e18f87e
commit 8dfbbfef07
16 changed files with 134 additions and 49 deletions

View file

@ -79,8 +79,8 @@ func GetInclude(field reflect.StructField) string {
return getRuleBody(field, "Include(") return getRuleBody(field, "Include(")
} }
// Validate validate TODO: // Validate populates the data with validation error (if any).
func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Locale) binding.Errors { func Validate(errs binding.Errors, data map[string]any, f any, l translation.Locale) binding.Errors {
if errs.Len() == 0 { if errs.Len() == 0 {
return errs return errs
} }

View file

@ -24,11 +24,14 @@ import (
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
webhook_module "code.gitea.io/gitea/modules/webhook" webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
webhook_service "code.gitea.io/gitea/services/webhook" webhook_service "code.gitea.io/gitea/services/webhook"
"gitea.com/go-chi/binding"
) )
const ( const (
@ -201,6 +204,29 @@ type webhookParams struct {
Meta any 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) { func createWebhook(ctx *context.Context, params webhookParams) {
ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook") ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
ctx.Data["PageIsSettingsHooks"] = true ctx.Data["PageIsSettingsHooks"] = true
@ -260,6 +286,29 @@ func createWebhook(ctx *context.Context, params webhookParams) {
ctx.Redirect(orCtx.Link) 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) { func editWebhook(ctx *context.Context, params webhookParams) {
ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook") ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook")
ctx.Data["PageIsSettingsHooks"] = true 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 // MSTeamsHooksNewPost response for creating MSTeams webhook
func MSTeamsHooksNewPost(ctx *context.Context) { func MSTeamsHooksNewPost(ctx *context.Context) {
createWebhook(ctx, mSTeamsHookParams(ctx)) createWebhook(ctx, mSTeamsHookParams(ctx))

View file

@ -409,11 +409,11 @@ func registerRoutes(m *web.Route) {
m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo_setting.DiscordHooksNewPost) m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo_setting.DiscordHooksNewPost)
m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo_setting.DingtalkHooksNewPost) m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo_setting.DingtalkHooksNewPost)
m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo_setting.TelegramHooksNewPost) 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("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo_setting.MSTeamsHooksNewPost)
m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo_setting.FeishuHooksNewPost) m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo_setting.FeishuHooksNewPost)
m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo_setting.WechatworkHooksNewPost) m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo_setting.WechatworkHooksNewPost)
m.Post("/packagist/new", web.Bind(forms.NewPackagistHookForm{}), repo_setting.PackagistHooksNewPost) m.Post("/packagist/new", web.Bind(forms.NewPackagistHookForm{}), repo_setting.PackagistHooksNewPost)
m.Post("/{type}/new", repo_setting.WebhookCreate)
} }
addWebhookEditRoutes := func() { addWebhookEditRoutes := func() {
@ -424,11 +424,11 @@ func registerRoutes(m *web.Route) {
m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo_setting.DiscordHooksEditPost) m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo_setting.DiscordHooksEditPost)
m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo_setting.DingtalkHooksEditPost) m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo_setting.DingtalkHooksEditPost)
m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo_setting.TelegramHooksEditPost) 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("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo_setting.MSTeamsHooksEditPost)
m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo_setting.FeishuHooksEditPost) m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo_setting.FeishuHooksEditPost)
m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo_setting.WechatworkHooksEditPost) m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo_setting.WechatworkHooksEditPost)
m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo_setting.PackagistHooksEditPost) m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo_setting.PackagistHooksEditPost)
m.Post("/{type}/{id:[0-9]+}", repo_setting.WebhookUpdate)
} }
addSettingsVariablesRoutes := func() { addSettingsVariablesRoutes := func() {

View file

@ -371,20 +371,6 @@ func (f *NewTelegramHookForm) Validate(req *http.Request, errs binding.Errors) b
return middleware.Validate(errs, ctx.Data, f, ctx.Locale) 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 // NewMSTeamsHookForm form for creating MS Teams hook
type NewMSTeamsHookForm struct { type NewMSTeamsHookForm struct {
PayloadURL string `binding:"Required;ValidUrl"` PayloadURL string `binding:"Required;ValidUrl"`

View file

@ -35,6 +35,10 @@ func (dh defaultHandler) Type() webhook_module.HookType {
func (defaultHandler) Metadata(*webhook_model.Webhook) any { return nil } 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) { func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) {
switch w.HTTPMethod { switch w.HTTPMethod {
case "": case "":

View file

@ -23,6 +23,9 @@ type dingtalkHandler struct{}
func (dingtalkHandler) Type() webhook_module.HookType { return webhook_module.DINGTALK } func (dingtalkHandler) Type() webhook_module.HookType { return webhook_module.DINGTALK }
func (dingtalkHandler) Metadata(*webhook_model.Webhook) any { return nil } func (dingtalkHandler) Metadata(*webhook_model.Webhook) any { return nil }
func (dingtalkHandler) FormFields(bind func(any)) FormFields {
panic("TODO")
}
type ( type (
// DingtalkPayload represents // DingtalkPayload represents

View file

@ -26,6 +26,10 @@ type discordHandler struct{}
func (discordHandler) Type() webhook_module.HookType { return webhook_module.DISCORD } func (discordHandler) Type() webhook_module.HookType { return webhook_module.DISCORD }
func (discordHandler) FormFields(bind func(any)) FormFields {
panic("TODO")
}
type ( type (
// DiscordEmbedFooter for Embed Footer Structure. // DiscordEmbedFooter for Embed Footer Structure.
DiscordEmbedFooter struct { DiscordEmbedFooter struct {

View file

@ -17,7 +17,12 @@ import (
type feishuHandler struct{} 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 } func (feishuHandler) Metadata(*webhook_model.Webhook) any { return nil }
type ( type (

View file

@ -22,12 +22,40 @@ import (
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook" webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/services/forms"
) )
type matrixHandler struct{} type matrixHandler struct{}
func (matrixHandler) Type() webhook_module.HookType { return webhook_module.MATRIX } 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) { func (matrixHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
meta := &MatrixMeta{} meta := &MatrixMeta{}
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {

View file

@ -22,6 +22,10 @@ type msteamsHandler struct{}
func (msteamsHandler) Type() webhook_module.HookType { return webhook_module.MSTEAMS } func (msteamsHandler) Type() webhook_module.HookType { return webhook_module.MSTEAMS }
func (msteamsHandler) Metadata(*webhook_model.Webhook) any { return nil } func (msteamsHandler) Metadata(*webhook_model.Webhook) any { return nil }
func (msteamsHandler) FormFields(bind func(any)) FormFields {
panic("TODO")
}
type ( type (
// MSTeamsFact for Fact Structure // MSTeamsFact for Fact Structure
MSTeamsFact struct { MSTeamsFact struct {

View file

@ -18,6 +18,10 @@ type packagistHandler struct{}
func (packagistHandler) Type() webhook_module.HookType { return webhook_module.PACKAGIST } func (packagistHandler) Type() webhook_module.HookType { return webhook_module.PACKAGIST }
func (packagistHandler) FormFields(bind func(any)) FormFields {
panic("TODO")
}
type ( type (
// PackagistPayload represents a packagist payload // PackagistPayload represents a packagist payload
// as expected by https://packagist.org/about // as expected by https://packagist.org/about

View file

@ -23,6 +23,10 @@ type slackHandler struct{}
func (slackHandler) Type() webhook_module.HookType { return webhook_module.SLACK } func (slackHandler) Type() webhook_module.HookType { return webhook_module.SLACK }
func (slackHandler) FormFields(bind func(any)) FormFields {
panic("TODO")
}
// SlackMeta contains the slack metadata // SlackMeta contains the slack metadata
type SlackMeta struct { type SlackMeta struct {
Channel string `json:"channel"` Channel string `json:"channel"`

View file

@ -21,6 +21,10 @@ type telegramHandler struct{}
func (telegramHandler) Type() webhook_module.HookType { return webhook_module.TELEGRAM } func (telegramHandler) Type() webhook_module.HookType { return webhook_module.TELEGRAM }
func (telegramHandler) FormFields(bind func(any)) FormFields {
panic("TODO")
}
type ( type (
// TelegramPayload represents // TelegramPayload represents
TelegramPayload struct { TelegramPayload struct {

View file

@ -23,14 +23,27 @@ import (
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook" webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/services/forms"
"github.com/gobwas/glob" "github.com/gobwas/glob"
) )
type Handler interface { type Handler interface {
Type() webhook_module.HookType 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 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{ var webhookHandlers = []Handler{

View file

@ -20,6 +20,10 @@ type wechatworkHandler struct{}
func (wechatworkHandler) Type() webhook_module.HookType { return webhook_module.WECHATWORK } func (wechatworkHandler) Type() webhook_module.HookType { return webhook_module.WECHATWORK }
func (wechatworkHandler) Metadata(*webhook_model.Webhook) any { return nil } func (wechatworkHandler) Metadata(*webhook_model.Webhook) any { return nil }
func (wechatworkHandler) FormFields(bind func(any)) FormFields {
panic("TODO")
}
type ( type (
// WechatworkPayload represents // WechatworkPayload represents
WechatworkPayload struct { WechatworkPayload struct {

View file

@ -203,8 +203,8 @@ func TestWebhookForms(t *testing.T) {
"homeserver_url": "https://matrix.example.com", "homeserver_url": "https://matrix.example.com",
"room_id": "123", "room_id": "123",
"authorization_header": "Bearer 123456", "authorization_header": "Bearer 123456",
// }, map[string]string{ // authorization_header is actually required, but not enforced (yet) }, map[string]string{
// "authorization_header": "", "authorization_header": "",
})) }))
t.Run("matrix/optional", testWebhookForms("matrix", session, map[string]string{ t.Run("matrix/optional", testWebhookForms("matrix", session, map[string]string{
"homeserver_url": "https://matrix.example.com", "homeserver_url": "https://matrix.example.com",