From 702152bfdee617e30589122d24b345d34bdf81b8 Mon Sep 17 00:00:00 2001 From: oliverpool Date: Wed, 20 Mar 2024 15:44:01 +0100 Subject: [PATCH] [REFACTOR] webhook.Handler interface --- .deadcode-out | 1 + models/fixtures/webhook.yml | 1 + modules/structs/hook.go | 12 +- routers/web/repo/setting/webhook.go | 14 +- services/webhook/default.go | 136 ++++++++++++++++++ services/webhook/deliver.go | 115 +-------------- services/webhook/dingtalk.go | 7 +- services/webhook/dingtalk_test.go | 2 +- services/webhook/discord.go | 14 +- services/webhook/discord_test.go | 2 +- services/webhook/feishu.go | 7 +- services/webhook/feishu_test.go | 2 +- services/webhook/general.go | 24 ++-- services/webhook/gogs.go | 12 ++ services/webhook/matrix.go | 14 +- services/webhook/matrix_test.go | 2 +- services/webhook/msteams.go | 7 +- services/webhook/msteams_test.go | 2 +- services/webhook/packagist.go | 15 +- services/webhook/packagist_test.go | 2 +- services/webhook/slack.go | 14 +- services/webhook/slack_test.go | 53 ++++++- services/webhook/telegram.go | 12 +- services/webhook/telegram_test.go | 2 +- services/webhook/webhook.go | 51 +++++-- services/webhook/webhook_test.go | 12 -- services/webhook/wechatwork.go | 7 +- templates/repo/settings/webhook/discord.tmpl | 4 +- templates/repo/settings/webhook/matrix.tmpl | 6 +- .../repo/settings/webhook/packagist.tmpl | 6 +- templates/repo/settings/webhook/slack.tmpl | 8 +- templates/repo/settings/webhook/telegram.tmpl | 6 +- templates/swagger/v1_json.tmpl | 12 ++ tests/integration/api_repo_hook_test.go | 1 + tests/integration/repo_webhook_test.go | 3 +- 35 files changed, 378 insertions(+), 210 deletions(-) create mode 100644 services/webhook/default.go create mode 100644 services/webhook/gogs.go diff --git a/.deadcode-out b/.deadcode-out index c728ea81a0..fc65cb2a28 100644 --- a/.deadcode-out +++ b/.deadcode-out @@ -342,4 +342,5 @@ package "code.gitea.io/gitea/services/repository/files" package "code.gitea.io/gitea/services/webhook" func NewNotifier + func List diff --git a/models/fixtures/webhook.yml b/models/fixtures/webhook.yml index 989bb5657a..f0b64cd57b 100644 --- a/models/fixtures/webhook.yml +++ b/models/fixtures/webhook.yml @@ -3,6 +3,7 @@ repo_id: 1 url: http://www.example.com/url1 http_method: POST + type: forgejo content_type: 1 # json events: '{"push_only":true,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":false}}' is_active: false # disable to prevent sending hook task during unrelated tests diff --git a/modules/structs/hook.go b/modules/structs/hook.go index 9c2bbe8738..784e69ea84 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -17,13 +17,17 @@ var ErrInvalidReceiveHook = errors.New("Invalid JSON payload received over webho // Hook a hook is a web hook when one repository changed type Hook struct { - ID int64 `json:"id"` - Type string `json:"type"` - BranchFilter string `json:"branch_filter"` - URL string `json:"-"` + ID int64 `json:"id"` + Type string `json:"type"` + BranchFilter string `json:"branch_filter"` + URL string `json:"url"` + + // Deprecated: use Metadata instead Config map[string]string `json:"config"` Events []string `json:"events"` AuthorizationHeader string `json:"authorization_header"` + ContentType string `json:"content_type"` + Metadata any `json:"metadata"` Active bool `json:"active"` // swagger:strfmt date-time Updated time.Time `json:"updated_at"` diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index 08e45b5137..1f78681dae 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -637,17 +637,9 @@ func checkWebhook(ctx *context.Context) (*ownerRepoCtx, *webhook.Webhook) { } ctx.Data["HookType"] = w.Type - switch w.Type { - case webhook_module.SLACK: - ctx.Data["SlackHook"] = webhook_service.GetSlackHook(w) - case webhook_module.DISCORD: - ctx.Data["DiscordHook"] = webhook_service.GetDiscordHook(w) - case webhook_module.TELEGRAM: - ctx.Data["TelegramHook"] = webhook_service.GetTelegramHook(w) - case webhook_module.MATRIX: - ctx.Data["MatrixHook"] = webhook_service.GetMatrixHook(w) - case webhook_module.PACKAGIST: - ctx.Data["PackagistHook"] = webhook_service.GetPackagistHook(w) + + if handler := webhook_service.GetWebhookHandler(w.Type); handler != nil { + ctx.Data["HookMetadata"] = handler.Metadata(w) } ctx.Data["History"], err = w.History(ctx, 1) diff --git a/services/webhook/default.go b/services/webhook/default.go new file mode 100644 index 0000000000..874668bb40 --- /dev/null +++ b/services/webhook/default.go @@ -0,0 +1,136 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package webhook + +import ( + "context" + "crypto/hmac" + "crypto/sha1" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/log" + webhook_module "code.gitea.io/gitea/modules/webhook" +) + +var _ Handler = defaultHandler{} + +type defaultHandler struct { + forgejo bool +} + +func (dh defaultHandler) Type() webhook_module.HookType { + if dh.forgejo { + return webhook_module.FORGEJO + } + return webhook_module.GITEA +} + +func (defaultHandler) Metadata(*webhook_model.Webhook) any { return nil } + +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 "": + log.Info("HTTP Method for %s webhook %s [ID: %d] is not set, defaulting to POST", w.Type, w.URL, w.ID) + fallthrough + case http.MethodPost: + switch w.ContentType { + case webhook_model.ContentTypeJSON: + req, err = http.NewRequest("POST", w.URL, strings.NewReader(t.PayloadContent)) + if err != nil { + return nil, nil, err + } + + req.Header.Set("Content-Type", "application/json") + case webhook_model.ContentTypeForm: + forms := url.Values{ + "payload": []string{t.PayloadContent}, + } + + req, err = http.NewRequest("POST", w.URL, strings.NewReader(forms.Encode())) + if err != nil { + return nil, nil, err + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + default: + return nil, nil, fmt.Errorf("invalid content type: %v", w.ContentType) + } + case http.MethodGet: + u, err := url.Parse(w.URL) + if err != nil { + return nil, nil, fmt.Errorf("invalid URL: %w", err) + } + vals := u.Query() + vals["payload"] = []string{t.PayloadContent} + u.RawQuery = vals.Encode() + req, err = http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, nil, err + } + case http.MethodPut: + switch w.Type { + case webhook_module.MATRIX: // used when t.Version == 1 + txnID, err := getMatrixTxnID([]byte(t.PayloadContent)) + if err != nil { + return nil, nil, err + } + url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID)) + req, err = http.NewRequest("PUT", url, strings.NewReader(t.PayloadContent)) + if err != nil { + return nil, nil, err + } + default: + return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod) + } + default: + return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod) + } + + body = []byte(t.PayloadContent) + return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) +} + +func addDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTask, payloadContent []byte) error { + var signatureSHA1 string + var signatureSHA256 string + if len(secret) > 0 { + sig1 := hmac.New(sha1.New, secret) + sig256 := hmac.New(sha256.New, secret) + _, err := io.MultiWriter(sig1, sig256).Write(payloadContent) + if err != nil { + // this error should never happen, since the hashes are writing to []byte and always return a nil error. + return fmt.Errorf("prepareWebhooks.sigWrite: %w", err) + } + signatureSHA1 = hex.EncodeToString(sig1.Sum(nil)) + signatureSHA256 = hex.EncodeToString(sig256.Sum(nil)) + } + + event := t.EventType.Event() + eventType := string(t.EventType) + req.Header.Add("X-Forgejo-Delivery", t.UUID) + req.Header.Add("X-Forgejo-Event", event) + req.Header.Add("X-Forgejo-Event-Type", eventType) + req.Header.Add("X-Forgejo-Signature", signatureSHA256) + req.Header.Add("X-Gitea-Delivery", t.UUID) + req.Header.Add("X-Gitea-Event", event) + req.Header.Add("X-Gitea-Event-Type", eventType) + req.Header.Add("X-Gitea-Signature", signatureSHA256) + req.Header.Add("X-Gogs-Delivery", t.UUID) + req.Header.Add("X-Gogs-Event", event) + req.Header.Add("X-Gogs-Event-Type", eventType) + req.Header.Add("X-Gogs-Signature", signatureSHA256) + req.Header.Add("X-Hub-Signature", "sha1="+signatureSHA1) + req.Header.Add("X-Hub-Signature-256", "sha256="+signatureSHA256) + req.Header["X-GitHub-Delivery"] = []string{t.UUID} + req.Header["X-GitHub-Event"] = []string{event} + req.Header["X-GitHub-Event-Type"] = []string{eventType} + return nil +} diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go index 32f1a3de45..d488b1725a 100644 --- a/services/webhook/deliver.go +++ b/services/webhook/deliver.go @@ -5,11 +5,7 @@ package webhook import ( "context" - "crypto/hmac" - "crypto/sha1" - "crypto/sha256" "crypto/tls" - "encoding/hex" "fmt" "io" "net/http" @@ -32,106 +28,6 @@ import ( "github.com/gobwas/glob" ) -func newDefaultRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) { - switch w.HTTPMethod { - case "": - log.Info("HTTP Method for %s webhook %s [ID: %d] is not set, defaulting to POST", w.Type, w.URL, w.ID) - fallthrough - case http.MethodPost: - switch w.ContentType { - case webhook_model.ContentTypeJSON: - req, err = http.NewRequest("POST", w.URL, strings.NewReader(t.PayloadContent)) - if err != nil { - return nil, nil, err - } - - req.Header.Set("Content-Type", "application/json") - case webhook_model.ContentTypeForm: - forms := url.Values{ - "payload": []string{t.PayloadContent}, - } - - req, err = http.NewRequest("POST", w.URL, strings.NewReader(forms.Encode())) - if err != nil { - return nil, nil, err - } - - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - default: - return nil, nil, fmt.Errorf("invalid content type: %v", w.ContentType) - } - case http.MethodGet: - u, err := url.Parse(w.URL) - if err != nil { - return nil, nil, fmt.Errorf("invalid URL: %w", err) - } - vals := u.Query() - vals["payload"] = []string{t.PayloadContent} - u.RawQuery = vals.Encode() - req, err = http.NewRequest("GET", u.String(), nil) - if err != nil { - return nil, nil, err - } - case http.MethodPut: - switch w.Type { - case webhook_module.MATRIX: // used when t.Version == 1 - txnID, err := getMatrixTxnID([]byte(t.PayloadContent)) - if err != nil { - return nil, nil, err - } - url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID)) - req, err = http.NewRequest("PUT", url, strings.NewReader(t.PayloadContent)) - if err != nil { - return nil, nil, err - } - default: - return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod) - } - default: - return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod) - } - - body = []byte(t.PayloadContent) - return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) -} - -func addDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTask, payloadContent []byte) error { - var signatureSHA1 string - var signatureSHA256 string - if len(secret) > 0 { - sig1 := hmac.New(sha1.New, secret) - sig256 := hmac.New(sha256.New, secret) - _, err := io.MultiWriter(sig1, sig256).Write(payloadContent) - if err != nil { - // this error should never happen, since the hashes are writing to []byte and always return a nil error. - return fmt.Errorf("prepareWebhooks.sigWrite: %w", err) - } - signatureSHA1 = hex.EncodeToString(sig1.Sum(nil)) - signatureSHA256 = hex.EncodeToString(sig256.Sum(nil)) - } - - event := t.EventType.Event() - eventType := string(t.EventType) - req.Header.Add("X-Forgejo-Delivery", t.UUID) - req.Header.Add("X-Forgejo-Event", event) - req.Header.Add("X-Forgejo-Event-Type", eventType) - req.Header.Add("X-Forgejo-Signature", signatureSHA256) - req.Header.Add("X-Gitea-Delivery", t.UUID) - req.Header.Add("X-Gitea-Event", event) - req.Header.Add("X-Gitea-Event-Type", eventType) - req.Header.Add("X-Gitea-Signature", signatureSHA256) - req.Header.Add("X-Gogs-Delivery", t.UUID) - req.Header.Add("X-Gogs-Event", event) - req.Header.Add("X-Gogs-Event-Type", eventType) - req.Header.Add("X-Gogs-Signature", signatureSHA256) - req.Header.Add("X-Hub-Signature", "sha1="+signatureSHA1) - req.Header.Add("X-Hub-Signature-256", "sha256="+signatureSHA256) - req.Header["X-GitHub-Delivery"] = []string{t.UUID} - req.Header["X-GitHub-Event"] = []string{event} - req.Header["X-GitHub-Event-Type"] = []string{eventType} - return nil -} - // Deliver creates the [http.Request] (depending on the webhook type), sends it // and records the status and response. func Deliver(ctx context.Context, t *webhook_model.HookTask) error { @@ -151,12 +47,15 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error { t.IsDelivered = true - newRequest := webhookRequesters[w.Type] - if t.PayloadVersion == 1 || newRequest == nil { - newRequest = newDefaultRequest + handler := GetWebhookHandler(w.Type) + if handler == nil { + return fmt.Errorf("GetWebhookHandler %q", w.Type) + } + if t.PayloadVersion == 1 { + handler = defaultHandler{true} } - req, body, err := newRequest(ctx, w, t) + req, body, err := handler.NewRequest(ctx, w, t) if err != nil { return fmt.Errorf("cannot create http request for webhook %s[%d %s]: %w", w.Type, w.ID, w.URL, err) } diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go index c57d04415a..4e7bef89f8 100644 --- a/services/webhook/dingtalk.go +++ b/services/webhook/dingtalk.go @@ -19,6 +19,11 @@ import ( dingtalk "gitea.com/lunny/dingtalk_webhook" ) +type dingtalkHandler struct{} + +func (dingtalkHandler) Type() webhook_module.HookType { return webhook_module.DINGTALK } +func (dingtalkHandler) Metadata(*webhook_model.Webhook) any { return nil } + type ( // DingtalkPayload represents DingtalkPayload dingtalk.Payload @@ -190,6 +195,6 @@ type dingtalkConvertor struct{} var _ payloadConvertor[DingtalkPayload] = dingtalkConvertor{} -func newDingtalkRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { +func (dingtalkHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { return newJSONRequest(dingtalkConvertor{}, w, t, true) } diff --git a/services/webhook/dingtalk_test.go b/services/webhook/dingtalk_test.go index 25f47347d0..073904f660 100644 --- a/services/webhook/dingtalk_test.go +++ b/services/webhook/dingtalk_test.go @@ -236,7 +236,7 @@ func TestDingTalkJSONPayload(t *testing.T) { PayloadVersion: 2, } - req, reqBody, err := newDingtalkRequest(context.Background(), hook, task) + req, reqBody, err := dingtalkHandler{}.NewRequest(context.Background(), hook, task) require.NotNil(t, req) require.NotNil(t, reqBody) require.NoError(t, err) diff --git a/services/webhook/discord.go b/services/webhook/discord.go index 659754d5e0..a84aa9d7b2 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -22,6 +22,10 @@ import ( webhook_module "code.gitea.io/gitea/modules/webhook" ) +type discordHandler struct{} + +func (discordHandler) Type() webhook_module.HookType { return webhook_module.DISCORD } + type ( // DiscordEmbedFooter for Embed Footer Structure. DiscordEmbedFooter struct { @@ -69,11 +73,11 @@ type ( } ) -// GetDiscordHook returns discord metadata -func GetDiscordHook(w *webhook_model.Webhook) *DiscordMeta { +// Metadata returns discord metadata +func (discordHandler) Metadata(w *webhook_model.Webhook) any { s := &DiscordMeta{} if err := json.Unmarshal([]byte(w.Meta), s); err != nil { - log.Error("webhook.GetDiscordHook(%d): %v", w.ID, err) + log.Error("discordHandler.Metadata(%d): %v", w.ID, err) } return s } @@ -260,10 +264,10 @@ type discordConvertor struct { var _ payloadConvertor[DiscordPayload] = discordConvertor{} -func newDiscordRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { +func (discordHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { meta := &DiscordMeta{} if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { - return nil, nil, fmt.Errorf("newDiscordRequest meta json: %w", err) + return nil, nil, fmt.Errorf("discordHandler.NewRequest meta json: %w", err) } sc := discordConvertor{ Username: meta.Username, diff --git a/services/webhook/discord_test.go b/services/webhook/discord_test.go index c04b95383b..895914ab2f 100644 --- a/services/webhook/discord_test.go +++ b/services/webhook/discord_test.go @@ -275,7 +275,7 @@ func TestDiscordJSONPayload(t *testing.T) { PayloadVersion: 2, } - req, reqBody, err := newDiscordRequest(context.Background(), hook, task) + req, reqBody, err := discordHandler{}.NewRequest(context.Background(), hook, task) require.NotNil(t, req) require.NotNil(t, reqBody) require.NoError(t, err) diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go index 1ec436894b..2c3508e3cc 100644 --- a/services/webhook/feishu.go +++ b/services/webhook/feishu.go @@ -15,6 +15,11 @@ import ( webhook_module "code.gitea.io/gitea/modules/webhook" ) +type feishuHandler struct{} + +func (feishuHandler) Type() webhook_module.HookType { return webhook_module.FEISHU } +func (feishuHandler) Metadata(*webhook_model.Webhook) any { return nil } + type ( // FeishuPayload represents FeishuPayload struct { @@ -168,6 +173,6 @@ type feishuConvertor struct{} var _ payloadConvertor[FeishuPayload] = feishuConvertor{} -func newFeishuRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { +func (feishuHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { return newJSONRequest(feishuConvertor{}, w, t, true) } diff --git a/services/webhook/feishu_test.go b/services/webhook/feishu_test.go index ef18333fd4..f591133cbd 100644 --- a/services/webhook/feishu_test.go +++ b/services/webhook/feishu_test.go @@ -177,7 +177,7 @@ func TestFeishuJSONPayload(t *testing.T) { PayloadVersion: 2, } - req, reqBody, err := newFeishuRequest(context.Background(), hook, task) + req, reqBody, err := feishuHandler{}.NewRequest(context.Background(), hook, task) require.NotNil(t, req) require.NotNil(t, reqBody) require.NoError(t, err) diff --git a/services/webhook/general.go b/services/webhook/general.go index 69b944f4bd..c41f58fe8d 100644 --- a/services/webhook/general.go +++ b/services/webhook/general.go @@ -314,33 +314,41 @@ func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, w // ToHook convert models.Webhook to api.Hook // This function is not part of the convert package to prevent an import cycle func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) { + // config is deprecated, but kept for compatibility config := map[string]string{ "url": w.URL, "content_type": w.ContentType.Name(), } if w.Type == webhook_module.SLACK { - s := GetSlackHook(w) - config["channel"] = s.Channel - config["username"] = s.Username - config["icon_url"] = s.IconURL - config["color"] = s.Color + if s, ok := (slackHandler{}.Metadata(w)).(*SlackMeta); ok { + config["channel"] = s.Channel + config["username"] = s.Username + config["icon_url"] = s.IconURL + config["color"] = s.Color + } } authorizationHeader, err := w.HeaderAuthorization() if err != nil { return nil, err } + var metadata any + if handler := GetWebhookHandler(w.Type); handler != nil { + metadata = handler.Metadata(w) + } return &api.Hook{ ID: w.ID, Type: w.Type, - URL: fmt.Sprintf("%s/settings/hooks/%d", repoLink, w.ID), - Active: w.IsActive, + BranchFilter: w.BranchFilter, + URL: w.URL, Config: config, Events: w.EventsArray(), AuthorizationHeader: authorizationHeader, + ContentType: w.ContentType.Name(), + Metadata: metadata, + Active: w.IsActive, Updated: w.UpdatedUnix.AsTime(), Created: w.CreatedUnix.AsTime(), - BranchFilter: w.BranchFilter, }, nil } diff --git a/services/webhook/gogs.go b/services/webhook/gogs.go new file mode 100644 index 0000000000..2ecd9e6d09 --- /dev/null +++ b/services/webhook/gogs.go @@ -0,0 +1,12 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package webhook + +import ( + webhook_module "code.gitea.io/gitea/modules/webhook" +) + +type gogsHandler struct{ defaultHandler } + +func (gogsHandler) Type() webhook_module.HookType { return webhook_module.GOGS } diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go index 0329804a8b..d1c0ec33fe 100644 --- a/services/webhook/matrix.go +++ b/services/webhook/matrix.go @@ -24,10 +24,14 @@ import ( webhook_module "code.gitea.io/gitea/modules/webhook" ) -func newMatrixRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { +type matrixHandler struct{} + +func (matrixHandler) Type() webhook_module.HookType { return webhook_module.MATRIX } + +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 { - return nil, nil, fmt.Errorf("GetMatrixPayload meta json: %w", err) + return nil, nil, fmt.Errorf("matrixHandler.NewRequest meta json: %w", err) } mc := matrixConvertor{ MsgType: messageTypeText[meta.MessageType], @@ -69,11 +73,11 @@ var messageTypeText = map[int]string{ 2: "m.text", } -// GetMatrixHook returns Matrix metadata -func GetMatrixHook(w *webhook_model.Webhook) *MatrixMeta { +// Metadata returns Matrix metadata +func (matrixHandler) Metadata(w *webhook_model.Webhook) any { s := &MatrixMeta{} if err := json.Unmarshal([]byte(w.Meta), s); err != nil { - log.Error("webhook.GetMatrixHook(%d): %v", w.ID, err) + log.Error("matrixHandler.Metadata(%d): %v", w.ID, err) } return s } diff --git a/services/webhook/matrix_test.go b/services/webhook/matrix_test.go index 058f8e3c5f..0269ccccea 100644 --- a/services/webhook/matrix_test.go +++ b/services/webhook/matrix_test.go @@ -211,7 +211,7 @@ func TestMatrixJSONPayload(t *testing.T) { PayloadVersion: 2, } - req, reqBody, err := newMatrixRequest(context.Background(), hook, task) + req, reqBody, err := matrixHandler{}.NewRequest(context.Background(), hook, task) require.NotNil(t, req) require.NotNil(t, reqBody) require.NoError(t, err) diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go index 99d0106184..6d9070d088 100644 --- a/services/webhook/msteams.go +++ b/services/webhook/msteams.go @@ -17,6 +17,11 @@ import ( webhook_module "code.gitea.io/gitea/modules/webhook" ) +type msteamsHandler struct{} + +func (msteamsHandler) Type() webhook_module.HookType { return webhook_module.MSTEAMS } +func (msteamsHandler) Metadata(*webhook_model.Webhook) any { return nil } + type ( // MSTeamsFact for Fact Structure MSTeamsFact struct { @@ -347,6 +352,6 @@ type msteamsConvertor struct{} var _ payloadConvertor[MSTeamsPayload] = msteamsConvertor{} -func newMSTeamsRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { +func (msteamsHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { return newJSONRequest(msteamsConvertor{}, w, t, true) } diff --git a/services/webhook/msteams_test.go b/services/webhook/msteams_test.go index 01e08b918e..c63ad1f89a 100644 --- a/services/webhook/msteams_test.go +++ b/services/webhook/msteams_test.go @@ -439,7 +439,7 @@ func TestMSTeamsJSONPayload(t *testing.T) { PayloadVersion: 2, } - req, reqBody, err := newMSTeamsRequest(context.Background(), hook, task) + req, reqBody, err := msteamsHandler{}.NewRequest(context.Background(), hook, task) require.NotNil(t, req) require.NotNil(t, reqBody) require.NoError(t, err) diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go index 0a2b62c89a..d8d1937e62 100644 --- a/services/webhook/packagist.go +++ b/services/webhook/packagist.go @@ -11,8 +11,13 @@ import ( webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + webhook_module "code.gitea.io/gitea/modules/webhook" ) +type packagistHandler struct{} + +func (packagistHandler) Type() webhook_module.HookType { return webhook_module.PACKAGIST } + type ( // PackagistPayload represents a packagist payload // as expected by https://packagist.org/about @@ -30,20 +35,20 @@ type ( } ) -// GetPackagistHook returns packagist metadata -func GetPackagistHook(w *webhook_model.Webhook) *PackagistMeta { +// Metadata returns packagist metadata +func (packagistHandler) Metadata(w *webhook_model.Webhook) any { s := &PackagistMeta{} if err := json.Unmarshal([]byte(w.Meta), s); err != nil { - log.Error("webhook.GetPackagistHook(%d): %v", w.ID, err) + log.Error("packagistHandler.Metadata(%d): %v", w.ID, err) } return s } // newPackagistRequest creates a request with the [PackagistPayload] for packagist (same payload for all events). -func newPackagistRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { +func (packagistHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { meta := &PackagistMeta{} if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { - return nil, nil, fmt.Errorf("newpackagistRequest meta json: %w", err) + return nil, nil, fmt.Errorf("packagistHandler.NewRequest meta json: %w", err) } payload := PackagistPayload{ diff --git a/services/webhook/packagist_test.go b/services/webhook/packagist_test.go index e6a963a9dd..d7374fde09 100644 --- a/services/webhook/packagist_test.go +++ b/services/webhook/packagist_test.go @@ -53,7 +53,7 @@ func TestPackagistPayload(t *testing.T) { PayloadVersion: 2, } - req, reqBody, err := newPackagistRequest(context.Background(), hook, task) + req, reqBody, err := packagistHandler{}.NewRequest(context.Background(), hook, task) require.NotNil(t, req) require.NotNil(t, reqBody) require.NoError(t, err) diff --git a/services/webhook/slack.go b/services/webhook/slack.go index ba8bac27d9..9b1c367e34 100644 --- a/services/webhook/slack.go +++ b/services/webhook/slack.go @@ -19,6 +19,10 @@ import ( webhook_module "code.gitea.io/gitea/modules/webhook" ) +type slackHandler struct{} + +func (slackHandler) Type() webhook_module.HookType { return webhook_module.SLACK } + // SlackMeta contains the slack metadata type SlackMeta struct { Channel string `json:"channel"` @@ -27,11 +31,11 @@ type SlackMeta struct { Color string `json:"color"` } -// GetSlackHook returns slack metadata -func GetSlackHook(w *webhook_model.Webhook) *SlackMeta { +// Metadata returns slack metadata +func (slackHandler) Metadata(w *webhook_model.Webhook) any { s := &SlackMeta{} if err := json.Unmarshal([]byte(w.Meta), s); err != nil { - log.Error("webhook.GetSlackHook(%d): %v", w.ID, err) + log.Error("slackHandler.Metadata(%d): %v", w.ID, err) } return s } @@ -283,10 +287,10 @@ type slackConvertor struct { var _ payloadConvertor[SlackPayload] = slackConvertor{} -func newSlackRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { +func (slackHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { meta := &SlackMeta{} if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { - return nil, nil, fmt.Errorf("newSlackRequest meta json: %w", err) + return nil, nil, fmt.Errorf("slackHandler.NewRequest meta json: %w", err) } sc := slackConvertor{ Channel: meta.Channel, diff --git a/services/webhook/slack_test.go b/services/webhook/slack_test.go index 7ebf16aba2..58f4e78878 100644 --- a/services/webhook/slack_test.go +++ b/services/webhook/slack_test.go @@ -178,7 +178,7 @@ func TestSlackJSONPayload(t *testing.T) { PayloadVersion: 2, } - req, reqBody, err := newSlackRequest(context.Background(), hook, task) + req, reqBody, err := slackHandler{}.NewRequest(context.Background(), hook, task) require.NotNil(t, req) require.NotNil(t, reqBody) require.NoError(t, err) @@ -211,3 +211,54 @@ func TestIsValidSlackChannel(t *testing.T) { assert.Equal(t, v.expected, IsValidSlackChannel(v.channelName)) } } + +func TestSlackMetadata(t *testing.T) { + w := &webhook_model.Webhook{ + Meta: `{"channel": "foo", "username": "username", "color": "blue"}`, + } + slackHook := slackHandler{}.Metadata(w) + assert.Equal(t, *slackHook.(*SlackMeta), SlackMeta{ + Channel: "foo", + Username: "username", + Color: "blue", + }) +} + +func TestSlackToHook(t *testing.T) { + w := &webhook_model.Webhook{ + Type: webhook_module.SLACK, + ContentType: webhook_model.ContentTypeJSON, + URL: "https://slack.example.com", + Meta: `{"channel": "foo", "username": "username", "color": "blue"}`, + HookEvent: &webhook_module.HookEvent{ + PushOnly: true, + SendEverything: false, + ChooseEvents: false, + HookEvents: webhook_module.HookEvents{ + Create: false, + Push: true, + PullRequest: false, + }, + }, + } + h, err := ToHook("repoLink", w) + assert.NoError(t, err) + + assert.Equal(t, h.Config, map[string]string{ + "url": "https://slack.example.com", + "content_type": "json", + + "channel": "foo", + "color": "blue", + "icon_url": "", + "username": "username", + }) + assert.Equal(t, h.URL, "https://slack.example.com") + assert.Equal(t, h.ContentType, "json") + assert.Equal(t, h.Metadata, &SlackMeta{ + Channel: "foo", + Username: "username", + IconURL: "", + Color: "blue", + }) +} diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go index e4a5b5a424..1541822a76 100644 --- a/services/webhook/telegram.go +++ b/services/webhook/telegram.go @@ -17,6 +17,10 @@ import ( webhook_module "code.gitea.io/gitea/modules/webhook" ) +type telegramHandler struct{} + +func (telegramHandler) Type() webhook_module.HookType { return webhook_module.TELEGRAM } + type ( // TelegramPayload represents TelegramPayload struct { @@ -33,11 +37,11 @@ type ( } ) -// GetTelegramHook returns telegram metadata -func GetTelegramHook(w *webhook_model.Webhook) *TelegramMeta { +// Metadata returns telegram metadata +func (telegramHandler) Metadata(w *webhook_model.Webhook) any { s := &TelegramMeta{} if err := json.Unmarshal([]byte(w.Meta), s); err != nil { - log.Error("webhook.GetTelegramHook(%d): %v", w.ID, err) + log.Error("telegramHandler.Metadata(%d): %v", w.ID, err) } return s } @@ -189,6 +193,6 @@ type telegramConvertor struct{} var _ payloadConvertor[TelegramPayload] = telegramConvertor{} -func newTelegramRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { +func (telegramHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { return newJSONRequest(telegramConvertor{}, w, t, true) } diff --git a/services/webhook/telegram_test.go b/services/webhook/telegram_test.go index 27ab96cd09..54e7f9fed1 100644 --- a/services/webhook/telegram_test.go +++ b/services/webhook/telegram_test.go @@ -177,7 +177,7 @@ func TestTelegramJSONPayload(t *testing.T) { PayloadVersion: 2, } - req, reqBody, err := newTelegramRequest(context.Background(), hook, task) + req, reqBody, err := telegramHandler{}.NewRequest(context.Background(), hook, task) require.NotNil(t, req) require.NotNil(t, reqBody) require.NoError(t, err) diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index e6646501da..62e5374fcb 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -27,25 +27,46 @@ import ( "github.com/gobwas/glob" ) -var webhookRequesters = map[webhook_module.HookType]func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error){ - webhook_module.SLACK: newSlackRequest, - webhook_module.DISCORD: newDiscordRequest, - webhook_module.DINGTALK: newDingtalkRequest, - webhook_module.TELEGRAM: newTelegramRequest, - webhook_module.MSTEAMS: newMSTeamsRequest, - webhook_module.FEISHU: newFeishuRequest, - webhook_module.MATRIX: newMatrixRequest, - webhook_module.WECHATWORK: newWechatworkRequest, - webhook_module.PACKAGIST: newPackagistRequest, +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 +} + +var webhookHandlers = []Handler{ + defaultHandler{true}, + defaultHandler{false}, + gogsHandler{}, + + slackHandler{}, + discordHandler{}, + dingtalkHandler{}, + telegramHandler{}, + msteamsHandler{}, + feishuHandler{}, + matrixHandler{}, + wechatworkHandler{}, + packagistHandler{}, +} + +// GetWebhookHandler return the handler for a given webhook type (nil if not found) +func GetWebhookHandler(name webhook_module.HookType) Handler { + for _, h := range webhookHandlers { + if h.Type() == name { + return h + } + } + return nil +} + +// List provides a list of the supported webhooks +func List() []Handler { + return webhookHandlers } // 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 := webhookRequesters[name] - return ok + return GetWebhookHandler(name) != nil } // hookQueue is a global queue of web hooks diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go index 1ef152838d..2436b5a4db 100644 --- a/services/webhook/webhook_test.go +++ b/services/webhook/webhook_test.go @@ -16,18 +16,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestWebhook_GetSlackHook(t *testing.T) { - w := &webhook_model.Webhook{ - Meta: `{"channel": "foo", "username": "username", "color": "blue"}`, - } - slackHook := GetSlackHook(w) - assert.Equal(t, *slackHook, SlackMeta{ - Channel: "foo", - Username: "username", - Color: "blue", - }) -} - func activateWebhook(t *testing.T, hookID int64) { t.Helper() updated, err := db.GetEngine(db.DefaultContext).ID(hookID).Cols("is_active").Update(webhook_model.Webhook{IsActive: true}) diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go index 46e7856ecf..184d83308d 100644 --- a/services/webhook/wechatwork.go +++ b/services/webhook/wechatwork.go @@ -15,6 +15,11 @@ import ( webhook_module "code.gitea.io/gitea/modules/webhook" ) +type wechatworkHandler struct{} + +func (wechatworkHandler) Type() webhook_module.HookType { return webhook_module.WECHATWORK } +func (wechatworkHandler) Metadata(*webhook_model.Webhook) any { return nil } + type ( // WechatworkPayload represents WechatworkPayload struct { @@ -177,6 +182,6 @@ type wechatworkConvertor struct{} var _ payloadConvertor[WechatworkPayload] = wechatworkConvertor{} -func newWechatworkRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { +func (wechatworkHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { return newJSONRequest(wechatworkConvertor{}, w, t, true) } diff --git a/templates/repo/settings/webhook/discord.tmpl b/templates/repo/settings/webhook/discord.tmpl index b623a6d8d3..d390e739be 100644 --- a/templates/repo/settings/webhook/discord.tmpl +++ b/templates/repo/settings/webhook/discord.tmpl @@ -8,11 +8,11 @@
- +
- +
{{template "repo/settings/webhook/settings" .}} diff --git a/templates/repo/settings/webhook/matrix.tmpl b/templates/repo/settings/webhook/matrix.tmpl index 7f1c9f08e6..43df8fd5bd 100644 --- a/templates/repo/settings/webhook/matrix.tmpl +++ b/templates/repo/settings/webhook/matrix.tmpl @@ -4,16 +4,16 @@ {{.CsrfTokenHtml}}
- +
- +