From 482c11a2a99b11e174a9c7a3c1ad49aaec7da7a9 Mon Sep 17 00:00:00 2001 From: jolheiser Date: Thu, 17 Feb 2022 23:26:49 -0600 Subject: [PATCH 01/10] WIP Signed-off-by: jolheiser --- modules/webhook/webhook.go | 69 ++++++++++++++++++++++++++ modules/webhook/webhook_test.go | 44 +++++++++++++++++ testdata/testdata.go | 8 +++ testdata/webhook/bad.yml | 1 + testdata/webhook/executable.go | 87 +++++++++++++++++++++++++++++++++ testdata/webhook/executable.yml | 17 +++++++ testdata/webhook/http.yml | 17 +++++++ 7 files changed, 243 insertions(+) create mode 100644 modules/webhook/webhook.go create mode 100644 modules/webhook/webhook_test.go create mode 100644 testdata/testdata.go create mode 100644 testdata/webhook/bad.yml create mode 100644 testdata/webhook/executable.go create mode 100644 testdata/webhook/executable.yml create mode 100644 testdata/webhook/http.yml diff --git a/modules/webhook/webhook.go b/modules/webhook/webhook.go new file mode 100644 index 000000000000..8f23a94f228d --- /dev/null +++ b/modules/webhook/webhook.go @@ -0,0 +1,69 @@ +// Copyright 2022 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 ( + "errors" + "io" + + "gopkg.in/yaml.v2" +) + +// Webhook is a custom webhook +type Webhook struct { + ID string `yaml:"id"` + HTTP string `yaml:"http"` + Exec []string `yaml:"exec"` + Form []Form `yaml:"form"` +} + +// Form is a webhook form +type Form struct { + ID string `yaml:"id"` + Label string `yaml:"label"` + Type string `yaml:"type"` + Required bool `yaml:"required"` + Default string `yaml:"default"` +} + +func (w *Webhook) validate() error { + if w.ID == "" { + return errors.New("webhook id is required") + } + if (w.HTTP == "" && len(w.Exec) == 0) || (w.HTTP != "" && len(w.Exec) > 0) { + return errors.New("webhook requires one of exec or http") + } + for _, form := range w.Form { + if form.ID == "" { + return errors.New("form id is required") + } + if form.Label == "" { + return errors.New("form label is required") + } + if form.Type == "" { + return errors.New("form type is required") + } + } + return nil +} + +// Parse parses a Webhooks from an io.Reader +func Parse(r io.Reader) (*Webhook, error) { + b, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + var w Webhook + if err := yaml.Unmarshal(b, &w); err != nil { + return nil, err + } + + if err := w.validate(); err != nil { + return nil, err + } + + return &w, nil +} diff --git a/modules/webhook/webhook_test.go b/modules/webhook/webhook_test.go new file mode 100644 index 000000000000..0f8f867ec5e7 --- /dev/null +++ b/modules/webhook/webhook_test.go @@ -0,0 +1,44 @@ +package webhook + +import ( + "bytes" + "code.gitea.io/gitea/testdata" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestWebhook(t *testing.T) { + tt := []struct { + Name string + File string + Err bool + }{ + { + Name: "Executable", + File: "executable.yml", + }, + { + Name: "HTTP", + File: "http.yml", + }, + { + Name: "Bad", + File: "bad.yml", + Err: true, + }, + } + + for _, tc := range tt { + t.Run(tc.Name, func(t *testing.T) { + contents, err := testdata.Webhook.ReadFile("webhook/" + tc.File) + assert.NoError(t, err, "expected to read file") + + _, err = Parse(bytes.NewReader(contents)) + if tc.Err { + assert.Error(t, err, "expected to get an error") + } else { + assert.NoError(t, err, "expected to not get an error") + } + }) + } +} diff --git a/testdata/testdata.go b/testdata/testdata.go new file mode 100644 index 000000000000..2c6095240c62 --- /dev/null +++ b/testdata/testdata.go @@ -0,0 +1,8 @@ +package testdata + +import "embed" + +var ( + //go:embed webhook + Webhook embed.FS +) diff --git a/testdata/webhook/bad.yml b/testdata/webhook/bad.yml new file mode 100644 index 000000000000..7ddc01604a03 --- /dev/null +++ b/testdata/webhook/bad.yml @@ -0,0 +1 @@ +# Malformed or no data diff --git a/testdata/webhook/executable.go b/testdata/webhook/executable.go new file mode 100644 index 000000000000..27afc86b927e --- /dev/null +++ b/testdata/webhook/executable.go @@ -0,0 +1,87 @@ +package webhook + +import ( + "encoding/json" + "fmt" + "io" + "os" +) + +type Payload struct { + Form map[string]struct { + Label string `json:"label"` + Type string `json:"type"` + Required bool `json:"required"` + Default interface{} `json:"default"` + Value interface{} `json:"value"` + } `json:"form"` +} + +func main() { + data, err := io.ReadAll(os.Stdin) + if err != nil { + panic(err) + } + + var p Payload + if err := json.Unmarshal(data, &p); err != nil { + panic(err) + } + + username, ok := p.Form["username"] + if !ok { + panic("username not found in form data") + } + checkType(username.Type, username.Value) + if username.Value.(string) != "jolheiser" { + panic("username should be jolheiser") + } + + favNum, ok := p.Form["favorite-number"] + if !ok { + panic("favorite-number not found in form data") + } + checkType(favNum.Type, favNum.Value) + if username.Value.(float64) != 12 { + panic("favNum should be 12") + } + + pineapple, ok := p.Form["pineapple"] + if !ok { + panic("pineapple not found in form data") + } + checkType(pineapple.Type, pineapple.Value) + if pineapple.Value.(bool) { + panic("pineapple should be false") + } + + secret, ok := p.Form["secret"] + if !ok { + panic("secret not found in form data") + } + checkType(secret.Type, secret.Value) + if secret.Value.(string) != "sn34ky" { + panic("secret should be sn34ky") + } +} + +func checkType(typ string, val interface{}) { + var ok bool + switch typ { + case "text", "secret": + _, ok = val.(string) + case "bool": + _, ok = val.(bool) + case "number": + _, ok = val.(float64) + } + if !ok { + panic(fmt.Sprintf("unexpected type %q for %v", typ, val)) + } +} + +// override panic +func panic(v interface{}) { + fmt.Println(v) + os.Exit(1) +} diff --git a/testdata/webhook/executable.yml b/testdata/webhook/executable.yml new file mode 100644 index 000000000000..18eec11851a1 --- /dev/null +++ b/testdata/webhook/executable.yml @@ -0,0 +1,17 @@ +id: executable +exec: ["go", "run", "executable.go"] +form: + - id: username # Required + label: Username # Required, the label for the input + type: text # Required, the type of input (text, number, bool, secret) + required: true # Optional, defaults to false + - id: favorite-number + label: Favorite Number + type: number + default: 12 # Optional, a default value + - id: pineapple + label: Pineapple on Pizza + type: bool + - id: secret + label: Passphrase + type: secret diff --git a/testdata/webhook/http.yml b/testdata/webhook/http.yml new file mode 100644 index 000000000000..e8a4f548af58 --- /dev/null +++ b/testdata/webhook/http.yml @@ -0,0 +1,17 @@ +id: http +http: "http://localhost:4665" +form: + - id: username # Required + label: Username # Required, the label for the input + type: text # Required, the type of input (text, number, bool, secret) + required: true # Optional, defaults to false + - id: favorite-number + label: Favorite Number + type: number + default: 12 # Optional, a default value + - id: pineapple + label: Pineapple on Pizza + type: bool + - id: secret + label: Passphrase + type: secret From eeae1f4557be23319e9dae84d7800fa52f74b821 Mon Sep 17 00:00:00 2001 From: jolheiser Date: Fri, 18 Mar 2022 21:04:32 -0500 Subject: [PATCH 02/10] More work Signed-off-by: jolheiser --- cmd/web.go | 8 ++++++ modules/webhook/webhook.go | 58 +++++++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/cmd/web.go b/cmd/web.go index 8c7c02617274..1b801b257041 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -5,11 +5,13 @@ package cmd import ( + "code.gitea.io/gitea/modules/webhook" "context" "fmt" "net" "net/http" "os" + "path/filepath" "strings" _ "net/http/pprof" // Used for debugging if enabled and a web server is running @@ -157,6 +159,12 @@ func runWeb(ctx *cli.Context) error { setting.LoadFromExisting() routers.GlobalInitInstalled(graceful.GetManager().HammerContext()) + if err := webhook.Init(filepath.Join(setting.CustomPath, "webhooks")); err != nil { + log.Fatal("Could not load custom webhooks: %v", err) + } + + log.Info("%#v\n", webhook.Webhooks) + // We check that AppDataPath exists here (it should have been created during installation) // We can't check it in `GlobalInitInstalled`, because some integration tests // use cmd -> GlobalInitInstalled, but the AppDataPath doesn't exist during those tests. diff --git a/modules/webhook/webhook.go b/modules/webhook/webhook.go index 8f23a94f228d..87819d85dca7 100644 --- a/modules/webhook/webhook.go +++ b/modules/webhook/webhook.go @@ -6,17 +6,34 @@ package webhook import ( "errors" + "fmt" "io" + "os" + "path/filepath" "gopkg.in/yaml.v2" ) +var Webhooks map[string]*Webhook + // Webhook is a custom webhook type Webhook struct { ID string `yaml:"id"` HTTP string `yaml:"http"` Exec []string `yaml:"exec"` Form []Form `yaml:"form"` + Path string `yaml:"-"` +} + +// Image returns a custom webhook image if it exists, else the default image +func (w *Webhook) Image() ([]byte, error) { + img, err := os.Open(filepath.Join(w.Path, "image.png")) + if err != nil { + return nil, fmt.Errorf("could not open custom webhook image: %w", err) + } + defer img.Close() + + return io.ReadAll(img) } // Form is a webhook form @@ -45,11 +62,16 @@ func (w *Webhook) validate() error { if form.Type == "" { return errors.New("form type is required") } + switch form.Type { + case "text", "secret", "bool", "number": + default: + return errors.New("form type is invalid; must be one of text, secret, bool, or number") + } } return nil } -// Parse parses a Webhooks from an io.Reader +// Parse parses a Webhook from an io.Reader func Parse(r io.Reader) (*Webhook, error) { b, err := io.ReadAll(r) if err != nil { @@ -67,3 +89,37 @@ func Parse(r io.Reader) (*Webhook, error) { return &w, nil } + +// Init initializes any custom webhooks found in path +func Init(path string) error { + dir, err := os.ReadDir(path) + if err != nil { + return fmt.Errorf("could not read dir %q: %w", path, err) + } + + for _, d := range dir { + if !d.IsDir() { + continue + } + + hookPath := filepath.Join(path, d.Name()) + cfg, err := os.Open(filepath.Join(hookPath, "config.yml")) + if err != nil { + return fmt.Errorf("could not open custom webhook config: %w", err) + } + + hook, err := Parse(cfg) + if err != nil { + return fmt.Errorf("could not parse custom webhook config: %w", err) + } + hook.Path = hookPath + + Webhooks[hook.ID] = hook + + if err := cfg.Close(); err != nil { + return fmt.Errorf("could not close custom webhook config: %w", err) + } + } + + return nil +} From 92e928160b0a8be4ddfff9c2c1fe7a2249ee8035 Mon Sep 17 00:00:00 2001 From: jolheiser Date: Fri, 1 Apr 2022 21:17:21 -0500 Subject: [PATCH 03/10] First implementation Signed-off-by: jolheiser --- cmd/web.go | 4 +- models/migrations/migrations.go | 2 + models/migrations/v217.go | 77 ++++++++++ models/webhook/webhook.go | 8 +- modules/webhook/webhook.go | 40 +++-- modules/webhook/webhook_test.go | 4 +- options/locale/locale_en-US.ini | 1 + routers/web/admin/hooks.go | 2 + routers/web/org/setting.go | 2 + routers/web/repo/webhook.go | 24 ++- routers/web/repo/webhook_custom.go | 145 ++++++++++++++++++ routers/web/web.go | 12 ++ services/forms/repo_form.go | 52 +++++++ services/webhook/custom.go | 51 ++++++ services/webhook/deliver.go | 74 ++++++--- services/webhook/webhook.go | 4 + .../repo/settings/webhook/base_list.tmpl | 5 + templates/repo/settings/webhook/custom.tmpl | 22 +++ templates/repo/settings/webhook/discord.tmpl | 2 +- templates/repo/settings/webhook/new.tmpl | 5 +- 20 files changed, 495 insertions(+), 41 deletions(-) create mode 100644 models/migrations/v217.go create mode 100644 routers/web/repo/webhook_custom.go create mode 100644 services/webhook/custom.go create mode 100644 templates/repo/settings/webhook/custom.tmpl diff --git a/cmd/web.go b/cmd/web.go index 1b801b257041..8983eb4f2561 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -5,7 +5,6 @@ package cmd import ( - "code.gitea.io/gitea/modules/webhook" "context" "fmt" "net" @@ -20,6 +19,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/routers" "code.gitea.io/gitea/routers/install" @@ -163,8 +163,6 @@ func runWeb(ctx *cli.Context) error { log.Fatal("Could not load custom webhooks: %v", err) } - log.Info("%#v\n", webhook.Webhooks) - // We check that AppDataPath exists here (it should have been created during installation) // We can't check it in `GlobalInitInstalled`, because some integration tests // use cmd -> GlobalInitInstalled, but the AppDataPath doesn't exist during those tests. diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 5a2297ac0d3c..c011f7f3501b 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -387,6 +387,8 @@ var migrations = []Migration{ NewMigration("Add auto merge table", addAutoMergeTable), // v215 -> v216 NewMigration("allow to view files in PRs", addReviewViewedFiles), + // v216 -> v217 + NewMigration("Add custom webhooks", addCustomWebhooks), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v217.go b/models/migrations/v217.go new file mode 100644 index 000000000000..0f27851e234c --- /dev/null +++ b/models/migrations/v217.go @@ -0,0 +1,77 @@ +// Copyright 2022 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 migrations + +import ( + "fmt" + + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func addCustomWebhooks(x *xorm.Engine) error { + type HookContentType int + + 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"` + } + + 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"` + } + + type HookType = string + + type HookStatus int + + type Webhook struct { + ID int64 `xorm:"pk autoincr"` + CustomID string `xorm:"VARCHAR(20) 'custom_id'"` + 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"` + } + + if err := x.Sync2(new(Webhook)); err != nil { + return fmt.Errorf("Sync2: %v", err) + } + return nil +} diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index 941a3f15c782..2dfc8d75998c 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -163,6 +163,7 @@ const ( MATRIX HookType = "matrix" WECHATWORK HookType = "wechatwork" PACKAGIST HookType = "packagist" + CUSTOM HookType = "custom" ) // HookStatus is the status of a web hook @@ -177,9 +178,10 @@ const ( // 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"` + ID int64 `xorm:"pk autoincr"` + CustomID string `xorm:"VARCHAR(20) 'custom_id'"` + 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"` diff --git a/modules/webhook/webhook.go b/modules/webhook/webhook.go index 87819d85dca7..29cd0813320c 100644 --- a/modules/webhook/webhook.go +++ b/modules/webhook/webhook.go @@ -8,32 +8,35 @@ import ( "errors" "fmt" "io" + "io/fs" "os" "path/filepath" "gopkg.in/yaml.v2" ) -var Webhooks map[string]*Webhook +var Webhooks = make(map[string]*Webhook) // Webhook is a custom webhook type Webhook struct { - ID string `yaml:"id"` - HTTP string `yaml:"http"` - Exec []string `yaml:"exec"` - Form []Form `yaml:"form"` - Path string `yaml:"-"` + ID string `yaml:"id"` + Label string `yaml:"label"` + URL string `yaml:"url"` + HTTP string `yaml:"http"` + Exec []string `yaml:"exec"` + Form []Form `yaml:"form"` + Path string `yaml:"-"` } // Image returns a custom webhook image if it exists, else the default image -func (w *Webhook) Image() ([]byte, error) { +// Image needs to be CLOSED +func (w *Webhook) Image() (io.ReadCloser, error) { img, err := os.Open(filepath.Join(w.Path, "image.png")) if err != nil { return nil, fmt.Errorf("could not open custom webhook image: %w", err) } - defer img.Close() - return io.ReadAll(img) + return img, nil } // Form is a webhook form @@ -45,6 +48,22 @@ type Form struct { Default string `yaml:"default"` } +// InputType returns the HTML input type of a Form.Type +func (f Form) InputType() string { + switch f.Type { + case "text": + return "text" + case "secret": + return "password" + case "number": + return "number" + case "bool": + return "checkbox" + default: + return "text" + } +} + func (w *Webhook) validate() error { if w.ID == "" { return errors.New("webhook id is required") @@ -94,6 +113,9 @@ func Parse(r io.Reader) (*Webhook, error) { func Init(path string) error { dir, err := os.ReadDir(path) if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return nil + } return fmt.Errorf("could not read dir %q: %w", path, err) } diff --git a/modules/webhook/webhook_test.go b/modules/webhook/webhook_test.go index 0f8f867ec5e7..d88c5a605ee8 100644 --- a/modules/webhook/webhook_test.go +++ b/modules/webhook/webhook_test.go @@ -2,9 +2,11 @@ package webhook import ( "bytes" + "testing" + "code.gitea.io/gitea/testdata" + "github.com/stretchr/testify/assert" - "testing" ) func TestWebhook(t *testing.T) { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index ba619b413cbc..077b0de5cf12 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1921,6 +1921,7 @@ 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.webhook.display_name = Display Name 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/admin/hooks.go b/routers/web/admin/hooks.go index 1483d0959dbe..9b026549756f 100644 --- a/routers/web/admin/hooks.go +++ b/routers/web/admin/hooks.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + cwebhook "code.gitea.io/gitea/modules/webhook" ) const ( @@ -25,6 +26,7 @@ func DefaultOrSystemWebhooks(ctx *context.Context) { ctx.Data["PageIsAdminSystemHooks"] = true ctx.Data["PageIsAdminDefaultHooks"] = true + ctx.Data["CustomWebhooks"] = cwebhook.Webhooks def := make(map[string]interface{}, len(ctx.Data)) sys := make(map[string]interface{}, len(ctx.Data)) diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 5cd245ef09a3..e0fce5905b6f 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -21,6 +21,7 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" + cwebhook "code.gitea.io/gitea/modules/webhook" user_setting "code.gitea.io/gitea/routers/web/user/setting" "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/org" @@ -206,6 +207,7 @@ func Webhooks(ctx *context.Context) { ctx.Data["BaseLink"] = ctx.Org.OrgLink + "/settings/hooks" ctx.Data["BaseLinkNew"] = ctx.Org.OrgLink + "/settings/hooks" ctx.Data["Description"] = ctx.Tr("org.settings.hooks_desc") + ctx.Data["CustomWebhooks"] = cwebhook.Webhooks ws, err := webhook.ListWebhooksByOpts(&webhook.ListWebhookOptions{OrgID: ctx.Org.Organization.ID}) if err != nil { diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go index d2e246118923..44799feb845c 100644 --- a/routers/web/repo/webhook.go +++ b/routers/web/repo/webhook.go @@ -25,6 +25,7 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + cwebhook "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/services/forms" webhook_service "code.gitea.io/gitea/services/webhook" ) @@ -50,6 +51,7 @@ func Webhooks(ctx *context.Context) { return } ctx.Data["Webhooks"] = ws + ctx.Data["CustomWebhooks"] = cwebhook.Webhooks ctx.HTML(http.StatusOK, tplHooks) } @@ -108,19 +110,21 @@ func getOrgRepoCtx(ctx *context.Context) (*orgRepoCtx, error) { return nil, errors.New("unable to set OrgRepo context") } -func checkHookType(ctx *context.Context) string { +func checkHookType(ctx *context.Context) (string, bool) { hookType := strings.ToLower(ctx.Params(":type")) - if !util.IsStringInSlice(hookType, setting.Webhook.Types, true) { + _, isCustom := cwebhook.Webhooks[hookType] + if !util.IsStringInSlice(hookType, setting.Webhook.Types, true) && !isCustom { ctx.NotFound("checkHookType", nil) - return "" + return "", false } - return hookType + return hookType, isCustom } // WebhooksNew render creating webhook page func WebhooksNew(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook") ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}} + ctx.Data["CustomWebhooks"] = cwebhook.Webhooks orCtx, err := getOrgRepoCtx(ctx) if err != nil { @@ -139,7 +143,7 @@ func WebhooksNew(ctx *context.Context) { ctx.Data["PageIsSettingsHooksNew"] = true } - hookType := checkHookType(ctx) + hookType, isCustom := checkHookType(ctx) ctx.Data["HookType"] = hookType if ctx.Written() { return @@ -149,6 +153,9 @@ func WebhooksNew(ctx *context.Context) { "Username": "Gitea", } } + if isCustom { + ctx.Data["CustomHook"] = cwebhook.Webhooks[hookType] + } ctx.Data["BaseLink"] = orCtx.LinkNew ctx.HTML(http.StatusOK, orCtx.NewTemplate) @@ -771,6 +778,13 @@ func checkWebhook(ctx *context.Context) (*orgRepoCtx, *webhook.Webhook) { ctx.Data["MatrixHook"] = webhook_service.GetMatrixHook(w) case webhook.PACKAGIST: ctx.Data["PackagistHook"] = webhook_service.GetPackagistHook(w) + case webhook.CUSTOM: + ctx.Data["CustomHook"] = cwebhook.Webhooks[w.CustomID] + hook := webhook_service.GetCustomHook(w) + ctx.Data["Webhook"] = hook + for key, val := range hook.Form { + ctx.Data["CustomHook_"+key] = val + } } ctx.Data["History"], err = w.History(1) diff --git a/routers/web/repo/webhook_custom.go b/routers/web/repo/webhook_custom.go new file mode 100644 index 000000000000..2e7a0a88cec4 --- /dev/null +++ b/routers/web/repo/webhook_custom.go @@ -0,0 +1,145 @@ +package repo + +import ( + "fmt" + "io" + "net/http" + + "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/web" + cwebhook "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/forms" + webhook_service "code.gitea.io/gitea/services/webhook" +) + +// CustomHooksNewPost response for creating custom hook +func CustomHooksNewPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.NewCustomWebhookForm) + ctx.Data["Title"] = ctx.Tr("repo.settings") + ctx.Data["PageIsSettingsHooks"] = true + ctx.Data["PageIsSettingsHooksNew"] = true + ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}} + ctx.Data["HookType"] = webhook.CUSTOM + + orCtx, err := getOrgRepoCtx(ctx) + if err != nil { + ctx.ServerError("getOrgRepoCtx", err) + return + } + + hookType, isCustom := checkHookType(ctx) + if !isCustom { + ctx.NotFound("checkHookType", nil) + return + } + hook := cwebhook.Webhooks[hookType] + if isCustom { + ctx.Data["CustomHook"] = hook + } + + if ctx.HasError() { + ctx.HTML(http.StatusOK, orCtx.NewTemplate) + return + } + + meta, err := json.Marshal(&webhook_service.CustomMeta{ + DisplayName: form.DisplayName, + Form: form.Form, + Secret: form.Secret, + }) + if err != nil { + ctx.ServerError("Marshal", err) + return + } + + w := &webhook.Webhook{ + URL: form.DisplayName, + Secret: form.Secret, + RepoID: orCtx.RepoID, + ContentType: webhook.ContentTypeJSON, + HookEvent: ParseHookEvent(form.WebhookForm), + IsActive: form.Active, + Type: webhook.CUSTOM, + CustomID: hook.ID, + Meta: string(meta), + OrgID: orCtx.OrgID, + IsSystemWebhook: orCtx.IsSystemWebhook, + } + if err := w.UpdateEvent(); err != nil { + ctx.ServerError("UpdateEvent", err) + return + } else if err := webhook.CreateWebhook(ctx, w); err != nil { + ctx.ServerError("CreateWebhook", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success")) + ctx.Redirect(orCtx.Link) +} + +// CustomHooksEditPost response for editing custom hook +func CustomHooksEditPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.NewCustomWebhookForm) + ctx.Data["Title"] = ctx.Tr("repo.settings") + ctx.Data["PageIsSettingsHooks"] = true + ctx.Data["PageIsSettingsHooksEdit"] = true + + orCtx, w := checkWebhook(ctx) + if ctx.Written() { + return + } + ctx.Data["Webhook"] = w + + if ctx.HasError() { + ctx.HTML(http.StatusOK, orCtx.NewTemplate) + return + } + + meta, err := json.Marshal(&webhook_service.CustomMeta{ + DisplayName: form.DisplayName, + Form: form.Form, + Secret: form.Secret, + }) + if err != nil { + ctx.ServerError("Marshal", err) + return + } + + w.Meta = string(meta) + w.URL = form.DisplayName + w.Secret = form.Secret + w.HookEvent = ParseHookEvent(form.WebhookForm) + w.IsActive = form.Active + if err := w.UpdateEvent(); err != nil { + ctx.ServerError("UpdateEvent", err) + return + } else if err := webhook.UpdateWebhook(w); err != nil { + ctx.ServerError("UpdateWebhook", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success")) + ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID)) +} + +// CustomWebhookImage gets the image for a custom webhook +func CustomWebhookImage(ctx *context.Context) { + id := ctx.Params("custom_id") + hook, ok := cwebhook.Webhooks[id] + if !ok { + ctx.NotFound("no webhook found", nil) + return + } + img, err := hook.Image() + if err != nil { + ctx.ServerError(fmt.Sprintf("webhook image not found for %q", id), err) + return + } + defer img.Close() + if _, err := io.Copy(ctx.Resp, img); err != nil { + ctx.ServerError(fmt.Sprintf("could not stream webhook image for %q", id), err) + return + } +} diff --git a/routers/web/web.go b/routers/web/web.go index 97ea1e90353f..20749986e643 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -529,6 +529,7 @@ func RegisterRoutes(m *web.Route) { m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) m.Post("/wechatwork/{id}", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) m.Post("/packagist/{id}", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) + m.Post("/{type}/{id}", bindIgnErr(forms.NewCustomWebhookForm{}), repo.CustomHooksEditPost) }, webhooksEnabled) m.Group("/{configType:default-hooks|system-hooks}", func() { @@ -544,6 +545,9 @@ func RegisterRoutes(m *web.Route) { m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) m.Post("/wechatwork/new", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) m.Post("/packagist/new", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost) + m.Post("/{type}/new", bindIgnErr(forms.NewCustomWebhookForm{}), repo.CustomHooksNewPost) + + m.Get("/{custom_id}/image", repo.CustomWebhookImage) }) m.Group("/auths", func() { @@ -662,6 +666,7 @@ 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.Post("/{type}/{id}", bindIgnErr(forms.NewCustomWebhookForm{}), repo.CustomHooksEditPost) m.Group("/{id}", func() { m.Get("", repo.WebHooksEdit) m.Post("/replay/{uuid}", repo.ReplayWebhook) @@ -676,6 +681,9 @@ func RegisterRoutes(m *web.Route) { m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost) m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) m.Post("/wechatwork/{id}", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) + m.Post("/{type}/new", bindIgnErr(forms.NewCustomWebhookForm{}), repo.CustomHooksNewPost) + + m.Get("/{custom_id}/image", repo.CustomWebhookImage) }, webhooksEnabled) m.Group("/labels", func() { @@ -781,6 +789,7 @@ func RegisterRoutes(m *web.Route) { m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) m.Post("/wechatwork/new", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) m.Post("/packagist/new", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost) + m.Post("/{type}/{id}", bindIgnErr(forms.NewCustomWebhookForm{}), repo.CustomHooksEditPost) m.Group("/{id}", func() { m.Get("", repo.WebHooksEdit) m.Post("/test", repo.TestWebhook) @@ -797,6 +806,9 @@ func RegisterRoutes(m *web.Route) { m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) m.Post("/wechatwork/{id}", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) m.Post("/packagist/{id}", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) + m.Post("/{type}/new", bindIgnErr(forms.NewCustomWebhookForm{}), repo.CustomHooksNewPost) + + m.Get("/{custom_id}/image", repo.CustomWebhookImage) }, webhooksEnabled) m.Group("/keys", func() { diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 18cbac751cd7..be7a4524bfa8 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -8,6 +8,7 @@ package forms import ( "net/http" "net/url" + "strconv" "strings" "code.gitea.io/gitea/models" @@ -16,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/routers/utils" "gitea.com/go-chi/binding" @@ -414,6 +416,56 @@ func (f *NewPackagistHookForm) Validate(req *http.Request, errs binding.Errors) return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } +// NewCustomWebhookForm form for creating custom web hook +type NewCustomWebhookForm struct { + DisplayName string `binding:"Required"` + Form map[string]interface{} + Secret string + WebhookForm +} + +// Validate validates the fields +func (f *NewCustomWebhookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { + ctx := context.GetContext(req) + hookType := ctx.Params("type") + hook, ok := webhook.Webhooks[hookType] + if !ok { + return errs + } + f.Form = make(map[string]interface{}) + for _, form := range hook.Form { + value := ctx.FormString(form.ID) + if form.Required && value == "" { + ctx.Data["Err_"+form.ID] = true + ctx.Data["HasError"] = true + ctx.Data["ErrorMsg"] = form.Label + ctx.Tr("form.require_error") + errs.Add([]string{form.ID}, binding.ERR_REQUIRED, "Required") + continue + } + if value == "" && form.Default != "" { + value = form.Default + } + switch form.Type { + case "number": + n, _ := strconv.Atoi(value) + f.Form[form.ID] = n + ctx.Data["CustomHook_"+form.ID] = n + case "bool": + b, _ := strconv.ParseBool(value) + f.Form[form.ID] = b + if b { + ctx.Data["CustomHook_"+form.ID] = b + } + case "text", "secret": + fallthrough + default: + f.Form[form.ID] = value + ctx.Data["CustomHook_"+form.ID] = value + } + } + return middleware.Validate(errs, ctx.Data, f, ctx.Locale) +} + // .___ // | | ______ ________ __ ____ // | |/ ___// ___/ | \_/ __ \ diff --git a/services/webhook/custom.go b/services/webhook/custom.go new file mode 100644 index 000000000000..00e7906bfe02 --- /dev/null +++ b/services/webhook/custom.go @@ -0,0 +1,51 @@ +package webhook + +import ( + "errors" + + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + api "code.gitea.io/gitea/modules/structs" +) + +type ( + // CustomPayload is a payload for a custom webhook + CustomPayload struct { + Form map[string]interface{} `json:"form"` + Payload api.Payloader `json:"payload"` + } + + // CustomMeta is the meta information for a custom webhook + CustomMeta struct { + DisplayName string + Form map[string]interface{} + Secret string + } +) + +// GetCustomHook returns custom metadata +func GetCustomHook(w *webhook_model.Webhook) *CustomMeta { + s := &CustomMeta{} + if err := json.Unmarshal([]byte(w.Meta), s); err != nil { + log.Error("webhook.GetCustomHook(%d): %v", w.ID, err) + } + return s +} + +func (c CustomPayload) JSONPayload() ([]byte, error) { + return json.Marshal(c) +} + +// GetCustomPayload converts a custom webhook into a CustomPayload +func GetCustomPayload(p api.Payloader, _ webhook_model.HookEventType, meta string) (api.Payloader, error) { + s := new(CustomPayload) + + custom := &CustomMeta{} + if err := json.Unmarshal([]byte(meta), &custom); err != nil { + return s, errors.New("GetPackagistPayload meta json:" + err.Error()) + } + s.Form = custom.Form + s.Payload = p + return s, nil +} diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go index 77744473f1ce..ed6f483de10c 100644 --- a/services/webhook/deliver.go +++ b/services/webhook/deliver.go @@ -11,10 +11,13 @@ import ( "crypto/sha256" "crypto/tls" "encoding/hex" + "errors" "fmt" "io" "net/http" "net/url" + "os" + "os/exec" "strings" "sync" "time" @@ -27,6 +30,7 @@ import ( "code.gitea.io/gitea/modules/proxy" "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" + cwebhook "code.gitea.io/gitea/modules/webhook" "github.com/gobwas/glob" ) @@ -50,6 +54,14 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error { t.IsDelivered = true var req *http.Request + var custom *cwebhook.Webhook + if w.CustomID != "" { + var ok bool + custom, ok = cwebhook.Webhooks[w.CustomID] + if !ok { + return fmt.Errorf("could not get custom webhook for %q", w.CustomID) + } + } switch w.HTTPMethod { case "": @@ -58,7 +70,11 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error { case http.MethodPost: switch w.ContentType { case webhook_model.ContentTypeJSON: - req, err = http.NewRequest("POST", w.URL, strings.NewReader(t.PayloadContent)) + u := w.URL + if custom != nil && custom.HTTP != "" { + u = custom.HTTP + } + req, err = http.NewRequest("POST", u, strings.NewReader(t.PayloadContent)) if err != nil { return err } @@ -179,26 +195,48 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error { return nil } - resp, err := webhookHTTPClient.Do(req.WithContext(ctx)) - if err != nil { - t.ResponseInfo.Body = fmt.Sprintf("Delivery: %v", err) - return err - } - defer resp.Body.Close() + if custom != nil && len(custom.Exec) > 0 { + graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) { + cmd := exec.Command(custom.Exec[0], custom.Exec[1:]...) + cmd.Stdin = strings.NewReader(t.PayloadContent) + cmd.Dir = custom.Path + env := os.Environ() + for key, vals := range req.Header { + env = append(env, fmt.Sprintf("%s=%s", key, vals[0])) + } + cmd.Env = env + out, err := cmd.CombinedOutput() + var exit *exec.ExitError + if err != nil && !errors.As(err, &exit) { + t.ResponseInfo.Body = fmt.Sprintf("Delivery: %v", err) + return + } + t.IsSucceed = cmd.ProcessState.Success() + t.ResponseInfo.Status = cmd.ProcessState.ExitCode() + t.ResponseInfo.Body = string(out) + }) + } else { + resp, err := webhookHTTPClient.Do(req.WithContext(graceful.GetManager().ShutdownContext())) + if err != nil { + t.ResponseInfo.Body = fmt.Sprintf("Delivery: %v", err) + return err + } + defer resp.Body.Close() - // Status code is 20x can be seen as succeed. - t.IsSucceed = resp.StatusCode/100 == 2 - t.ResponseInfo.Status = resp.StatusCode - for k, vals := range resp.Header { - t.ResponseInfo.Headers[k] = strings.Join(vals, ",") - } + // Status code is 20x can be seen as succeed. + t.IsSucceed = resp.StatusCode/100 == 2 + t.ResponseInfo.Status = resp.StatusCode + for k, vals := range resp.Header { + t.ResponseInfo.Headers[k] = strings.Join(vals, ",") + } - p, err := io.ReadAll(resp.Body) - if err != nil { - t.ResponseInfo.Body = fmt.Sprintf("read body: %s", err) - return err + p, err := io.ReadAll(resp.Body) + if err != nil { + t.ResponseInfo.Body = fmt.Sprintf("read body: %s", err) + return err + } + t.ResponseInfo.Body = string(p) } - t.ResponseInfo.Body = string(p) return nil } diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index b15b8173f51f..c74bdbc3a354 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -64,6 +64,10 @@ var webhooks = map[webhook_model.HookType]*webhook{ name: webhook_model.PACKAGIST, payloadCreator: GetPackagistPayload, }, + webhook_model.CUSTOM: { + name: webhook_model.CUSTOM, + payloadCreator: GetCustomPayload, + }, } // RegisterWebhook registers a webhook diff --git a/templates/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl index b1a3771bdba1..f52ab57984da 100644 --- a/templates/repo/settings/webhook/base_list.tmpl +++ b/templates/repo/settings/webhook/base_list.tmpl @@ -37,6 +37,11 @@ {{.i18n.Tr "repo.settings.web_hook_name_packagist"}} + {{range .CustomWebhooks}} + + {{.Label}} + + {{end}} diff --git a/templates/repo/settings/webhook/custom.tmpl b/templates/repo/settings/webhook/custom.tmpl new file mode 100644 index 000000000000..99db314ccefa --- /dev/null +++ b/templates/repo/settings/webhook/custom.tmpl @@ -0,0 +1,22 @@ +{{if .CustomHook}} +

{{.i18n.Tr "repo.settings.add_web_hook_desc" .CustomHook.URL .CustomHook.Label | Str2html}}

+
+ {{template "base/disable_form_autofill"}} + {{.CsrfTokenHtml}} +
+ + +
+ {{range .CustomHook.Form}} +
+ + +
+ {{end}} +
+ + +
+ {{template "repo/settings/webhook/settings" .}} +
+{{end}} diff --git a/templates/repo/settings/webhook/discord.tmpl b/templates/repo/settings/webhook/discord.tmpl index 1b58cfb6f02e..1c32c0d0b506 100644 --- a/templates/repo/settings/webhook/discord.tmpl +++ b/templates/repo/settings/webhook/discord.tmpl @@ -1,4 +1,4 @@ -{{if eq .HookType "discord"}} + {{if eq .HookType "discord"}}

{{.i18n.Tr "repo.settings.add_web_hook_desc" "https://discord.com" (.i18n.Tr "repo.settings.web_hook_name_discord") | Str2html}}

{{.CsrfTokenHtml}} diff --git a/templates/repo/settings/webhook/new.tmpl b/templates/repo/settings/webhook/new.tmpl index a438a4c71a3d..a9c8dce065b5 100644 --- a/templates/repo/settings/webhook/new.tmpl +++ b/templates/repo/settings/webhook/new.tmpl @@ -27,8 +27,10 @@ {{else if eq .HookType "wechatwork"}} - {{else if eq .HookType "packagist"}} + {{else if eq .HookType "packagist"}} + {{else}} + {{.Label}} {{end}} @@ -44,6 +46,7 @@ {{template "repo/settings/webhook/matrix" .}} {{template "repo/settings/webhook/wechatwork" .}} {{template "repo/settings/webhook/packagist" .}} + {{template "repo/settings/webhook/custom" .}} {{template "repo/settings/webhook/history" .}} From f6b40092c35cde7cd4859669be7589a6b5e31f7f Mon Sep 17 00:00:00 2001 From: jolheiser Date: Fri, 1 Apr 2022 21:40:27 -0500 Subject: [PATCH 04/10] Add license Signed-off-by: jolheiser --- modules/webhook/webhook_test.go | 4 ++ routers/web/repo/webhook_custom.go | 4 ++ services/webhook/custom.go | 4 ++ testdata/testdata.go | 4 ++ testdata/webhook/executable.go | 60 +++++++----------------------- 5 files changed, 30 insertions(+), 46 deletions(-) diff --git a/modules/webhook/webhook_test.go b/modules/webhook/webhook_test.go index d88c5a605ee8..9c7c044264ce 100644 --- a/modules/webhook/webhook_test.go +++ b/modules/webhook/webhook_test.go @@ -1,3 +1,7 @@ +// Copyright 2022 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 ( diff --git a/routers/web/repo/webhook_custom.go b/routers/web/repo/webhook_custom.go index 2e7a0a88cec4..4cf226f8531d 100644 --- a/routers/web/repo/webhook_custom.go +++ b/routers/web/repo/webhook_custom.go @@ -1,3 +1,7 @@ +// Copyright 2022 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 repo import ( diff --git a/services/webhook/custom.go b/services/webhook/custom.go index 00e7906bfe02..fcc13155d45c 100644 --- a/services/webhook/custom.go +++ b/services/webhook/custom.go @@ -1,3 +1,7 @@ +// Copyright 2022 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 ( diff --git a/testdata/testdata.go b/testdata/testdata.go index 2c6095240c62..a74483b220b5 100644 --- a/testdata/testdata.go +++ b/testdata/testdata.go @@ -1,3 +1,7 @@ +// Copyright 2022 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 testdata import "embed" diff --git a/testdata/webhook/executable.go b/testdata/webhook/executable.go index 27afc86b927e..00b0733b529c 100644 --- a/testdata/webhook/executable.go +++ b/testdata/webhook/executable.go @@ -1,4 +1,8 @@ -package webhook +// Copyright 2022 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 main import ( "encoding/json" @@ -8,12 +12,11 @@ import ( ) type Payload struct { - Form map[string]struct { - Label string `json:"label"` - Type string `json:"type"` - Required bool `json:"required"` - Default interface{} `json:"default"` - Value interface{} `json:"value"` + Form struct { + Username string `json:"username"` + FavoriteNumber int `json:"favorite-number"` + Pineapple bool `json:"pineapple"` + Passphrase string `json:"passphrase"` } `json:"form"` } @@ -28,58 +31,23 @@ func main() { panic(err) } - username, ok := p.Form["username"] - if !ok { - panic("username not found in form data") - } - checkType(username.Type, username.Value) - if username.Value.(string) != "jolheiser" { + if p.Form.Username != "jolheiser" { panic("username should be jolheiser") } - favNum, ok := p.Form["favorite-number"] - if !ok { - panic("favorite-number not found in form data") - } - checkType(favNum.Type, favNum.Value) - if username.Value.(float64) != 12 { + if p.Form.FavoriteNumber != 12 { panic("favNum should be 12") } - pineapple, ok := p.Form["pineapple"] - if !ok { - panic("pineapple not found in form data") - } - checkType(pineapple.Type, pineapple.Value) - if pineapple.Value.(bool) { + if p.Form.Pineapple { panic("pineapple should be false") } - secret, ok := p.Form["secret"] - if !ok { - panic("secret not found in form data") - } - checkType(secret.Type, secret.Value) - if secret.Value.(string) != "sn34ky" { + if p.Form.Passphrase != "sn34ky" { panic("secret should be sn34ky") } } -func checkType(typ string, val interface{}) { - var ok bool - switch typ { - case "text", "secret": - _, ok = val.(string) - case "bool": - _, ok = val.(bool) - case "number": - _, ok = val.(float64) - } - if !ok { - panic(fmt.Sprintf("unexpected type %q for %v", typ, val)) - } -} - // override panic func panic(v interface{}) { fmt.Println(v) From 62461dc2469cbcb5f8efb9986dc5edec51086f82 Mon Sep 17 00:00:00 2001 From: jolheiser Date: Sat, 2 Apr 2022 14:45:47 -0500 Subject: [PATCH 05/10] Fix image src Signed-off-by: jolheiser --- routers/web/repo/webhook_custom.go | 1 + templates/repo/settings/webhook/new.tmpl | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/routers/web/repo/webhook_custom.go b/routers/web/repo/webhook_custom.go index 4cf226f8531d..03620a9bb274 100644 --- a/routers/web/repo/webhook_custom.go +++ b/routers/web/repo/webhook_custom.go @@ -32,6 +32,7 @@ func CustomHooksNewPost(ctx *context.Context) { ctx.ServerError("getOrgRepoCtx", err) return } + ctx.Data["BaseLink"] = orCtx.LinkNew hookType, isCustom := checkHookType(ctx) if !isCustom { diff --git a/templates/repo/settings/webhook/new.tmpl b/templates/repo/settings/webhook/new.tmpl index a9c8dce065b5..554c0d79acb2 100644 --- a/templates/repo/settings/webhook/new.tmpl +++ b/templates/repo/settings/webhook/new.tmpl @@ -29,8 +29,8 @@ {{else if eq .HookType "packagist"}} - {{else}} - {{.Label}} + {{else if eq .HookType "custom"}} + {{end}} From 5487883adff4eabbcaa39ee7587de7a256e06092 Mon Sep 17 00:00:00 2001 From: jolheiser Date: Thu, 12 May 2022 20:55:28 -0500 Subject: [PATCH 06/10] Change URL to docs --- modules/webhook/webhook.go | 2 +- templates/repo/settings/webhook/custom.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/webhook/webhook.go b/modules/webhook/webhook.go index 29cd0813320c..3b8d62dd260b 100644 --- a/modules/webhook/webhook.go +++ b/modules/webhook/webhook.go @@ -21,7 +21,7 @@ var Webhooks = make(map[string]*Webhook) type Webhook struct { ID string `yaml:"id"` Label string `yaml:"label"` - URL string `yaml:"url"` + Docs string `yaml:"docs"` HTTP string `yaml:"http"` Exec []string `yaml:"exec"` Form []Form `yaml:"form"` diff --git a/templates/repo/settings/webhook/custom.tmpl b/templates/repo/settings/webhook/custom.tmpl index 99db314ccefa..47ace56d9846 100644 --- a/templates/repo/settings/webhook/custom.tmpl +++ b/templates/repo/settings/webhook/custom.tmpl @@ -1,5 +1,5 @@ {{if .CustomHook}} -

{{.i18n.Tr "repo.settings.add_web_hook_desc" .CustomHook.URL .CustomHook.Label | Str2html}}

+

{{.i18n.Tr "repo.settings.add_web_hook_desc" .CustomHook.Docs .CustomHook.Label | Str2html}}

{{template "base/disable_form_autofill"}} {{.CsrfTokenHtml}} From 8de2a6b7b10587aa1cf026ebae986641a0e78266 Mon Sep 17 00:00:00 2001 From: jolheiser Date: Thu, 12 May 2022 21:00:23 -0500 Subject: [PATCH 07/10] Appease linter --- templates/repo/settings/webhook/new.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/repo/settings/webhook/new.tmpl b/templates/repo/settings/webhook/new.tmpl index 554c0d79acb2..b6930bdeb2e5 100644 --- a/templates/repo/settings/webhook/new.tmpl +++ b/templates/repo/settings/webhook/new.tmpl @@ -27,9 +27,9 @@ {{else if eq .HookType "wechatwork"}} - {{else if eq .HookType "packagist"}} + {{else if eq .HookType "packagist"}} - {{else if eq .HookType "custom"}} + {{else if eq .HookType "custom"}} {{end}} From cdec7a686c8eb1ba74a64ec59aa1cc0e864ffb15 Mon Sep 17 00:00:00 2001 From: jolheiser Date: Thu, 12 May 2022 23:19:45 -0500 Subject: [PATCH 08/10] Pattern and template spacing Signed-off-by: jolheiser --- modules/webhook/webhook.go | 1 + templates/repo/settings/webhook/custom.tmpl | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/modules/webhook/webhook.go b/modules/webhook/webhook.go index 3b8d62dd260b..d25427a40591 100644 --- a/modules/webhook/webhook.go +++ b/modules/webhook/webhook.go @@ -46,6 +46,7 @@ type Form struct { Type string `yaml:"type"` Required bool `yaml:"required"` Default string `yaml:"default"` + Pattern string `yaml:"pattern"` } // InputType returns the HTML input type of a Form.Type diff --git a/templates/repo/settings/webhook/custom.tmpl b/templates/repo/settings/webhook/custom.tmpl index 47ace56d9846..00c8fad9c28d 100644 --- a/templates/repo/settings/webhook/custom.tmpl +++ b/templates/repo/settings/webhook/custom.tmpl @@ -10,7 +10,16 @@ {{range .CustomHook.Form}}
- +
{{end}}
From b4eda5e7ad5c19744781d182901167cabca485ee Mon Sep 17 00:00:00 2001 From: jolheiser Date: Thu, 12 May 2022 23:36:15 -0500 Subject: [PATCH 09/10] Add WIP schema Signed-off-by: jolheiser --- modules/webhook/schema.json | 78 +++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 modules/webhook/schema.json diff --git a/modules/webhook/schema.json b/modules/webhook/schema.json new file mode 100644 index 000000000000..83f7f27eee12 --- /dev/null +++ b/modules/webhook/schema.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://gitea.com/gitea/gitea/modules/webhook/schema.json", + "title": "Custom webhook configuration", + "description": "A custom webhook for Gitea", + "type": "object", + "required": ["id", "label", "docs", "form"], + "oneOf": [ + { + "required": ["exec"] + }, + { + "required": ["http"] + } + ], + "additionalProperties": false, + "properties": { + "id": { + "description": "Webhook ID", + "type": "string" + }, + "label": { + "description": "Webhook Label", + "type": "string" + }, + "docs": { + "description": "Webhook Docs URL", + "type": "string" + }, + "http": { + "description": "HTTP Endpoint", + "type": "string" + }, + "exec": { + "description": "Command to execute", + "type": "array", + "items": { + "type": "string" + } + }, + "form": { + "description": "Webhook form", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "label", "type"], + "properties": { + "id": { + "description": "Form ID", + "type": "string" + }, + "label": { + "description": "Form label", + "type": "string" + }, + "type": { + "description": "Form type", + "type": "string", + "enum": ["text", "number", "bool", "secret"] + }, + "required": { + "description": "Input required", + "type": "boolean" + }, + "default": { + "description": "Input default", + "type": "string" + }, + "pattern": { + "description": "Input pattern", + "type": "string" + } + } + } + } + } +} From 8bf9a79e9ff024f2ba5981b0d8b4836c480d0f87 Mon Sep 17 00:00:00 2001 From: jolheiser Date: Tue, 26 Jul 2022 23:31:11 -0500 Subject: [PATCH 10/10] WIP jsonnet Signed-off-by: jolheiser --- go.mod | 1 + go.sum | 5 +++ modules/webhook/webhook.go | 17 +++++----- services/webhook/custom.go | 50 ++++++++++++++++++++++++---- services/webhook/deliver.go | 60 ++++++++++------------------------ services/webhook/dingtalk.go | 2 +- services/webhook/discord.go | 4 +-- services/webhook/feishu.go | 2 +- services/webhook/matrix.go | 4 +-- services/webhook/msteams.go | 2 +- services/webhook/packagist.go | 4 +-- services/webhook/slack.go | 4 +-- services/webhook/telegram.go | 2 +- services/webhook/webhook.go | 4 +-- services/webhook/wechatwork.go | 2 +- 15 files changed, 91 insertions(+), 72 deletions(-) diff --git a/go.mod b/go.mod index 5ef996769d57..fc943184925b 100644 --- a/go.mod +++ b/go.mod @@ -42,6 +42,7 @@ require ( github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 github.com/golang-jwt/jwt/v4 v4.4.1 github.com/google/go-github/v39 v39.2.0 + github.com/google/go-jsonnet v0.18.0 github.com/google/pprof v0.0.0-20220509035851-59ca7ad80af3 github.com/google/uuid v1.3.0 github.com/gorilla/feeds v1.1.1 diff --git a/go.sum b/go.sum index ae6b0ad83a27..d5bec55eb34f 100644 --- a/go.sum +++ b/go.sum @@ -426,6 +426,7 @@ github.com/ethantkoenig/rupture v1.0.1 h1:6aAXghmvtnngMgQzy7SMGdicMvkV86V4n9fT0m github.com/ethantkoenig/rupture v1.0.1/go.mod h1:Sjqo/nbffZp1pVVXNGhpugIjsWmuS9KiIB4GtpEBur4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= @@ -745,6 +746,8 @@ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8 github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ= github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= +github.com/google/go-jsonnet v0.18.0 h1:/6pTy6g+Jh1a1I2UMoAODkqELFiVIdOxbNwv0DDzoOg= +github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0= github.com/google/go-licenses v0.0.0-20210329231322-ce1d9163b77d/go.mod h1:+TYOmkVoJOpwnS0wfdsJCV9CoD5nJYsHoFk/0CrTK4M= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -1120,6 +1123,7 @@ github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcncea github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= @@ -2317,6 +2321,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/modules/webhook/webhook.go b/modules/webhook/webhook.go index d25427a40591..e8be38c606c5 100644 --- a/modules/webhook/webhook.go +++ b/modules/webhook/webhook.go @@ -19,13 +19,12 @@ var Webhooks = make(map[string]*Webhook) // Webhook is a custom webhook type Webhook struct { - ID string `yaml:"id"` - Label string `yaml:"label"` - Docs string `yaml:"docs"` - HTTP string `yaml:"http"` - Exec []string `yaml:"exec"` - Form []Form `yaml:"form"` - Path string `yaml:"-"` + ID string `yaml:"id"` + Label string `yaml:"label"` + Docs string `yaml:"docs"` + HTTP string `yaml:"http"` + Form []Form `yaml:"form"` + Path string `yaml:"-"` } // Image returns a custom webhook image if it exists, else the default image @@ -69,8 +68,8 @@ func (w *Webhook) validate() error { if w.ID == "" { return errors.New("webhook id is required") } - if (w.HTTP == "" && len(w.Exec) == 0) || (w.HTTP != "" && len(w.Exec) > 0) { - return errors.New("webhook requires one of exec or http") + if w.HTTP == "" { + return errors.New("webhook http is required") } for _, form := range w.Form { if form.ID == "" { diff --git a/services/webhook/custom.go b/services/webhook/custom.go index fcc13155d45c..eabcd735bbae 100644 --- a/services/webhook/custom.go +++ b/services/webhook/custom.go @@ -5,12 +5,17 @@ package webhook import ( - "errors" + "fmt" + "os" + "path/filepath" webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" + cwebhook "code.gitea.io/gitea/modules/webhook" + + "github.com/google/go-jsonnet" ) type ( @@ -42,14 +47,47 @@ func (c CustomPayload) JSONPayload() ([]byte, error) { } // GetCustomPayload converts a custom webhook into a CustomPayload -func GetCustomPayload(p api.Payloader, _ webhook_model.HookEventType, meta string) (api.Payloader, error) { +func GetCustomPayload(p api.Payloader, event webhook_model.HookEventType, w *webhook_model.Webhook) (api.Payloader, error) { s := new(CustomPayload) - custom := &CustomMeta{} - if err := json.Unmarshal([]byte(meta), &custom); err != nil { - return s, errors.New("GetPackagistPayload meta json:" + err.Error()) + var custom CustomMeta + if err := json.Unmarshal([]byte(w.Meta), &custom); err != nil { + return s, fmt.Errorf("GetCustomPayload meta json: %v", err) } s.Form = custom.Form s.Payload = p - return s, nil + + payload, err := json.Marshal(s) + if err != nil { + return nil, fmt.Errorf("GetCustomPayload marshal json: %v", err) + } + + webhook, ok := cwebhook.Webhooks[w.CustomID] + if !ok { + return nil, fmt.Errorf("GetCustomPayload no custom webhook %q", w.CustomID) + } + + vm := jsonnet.MakeVM() + vm.Importer(&jsonnet.MemoryImporter{ + Data: map[string]jsonnet.Contents{ + fmt.Sprintf("%s.libsonnet", event): jsonnet.MakeContents(string(payload)), + }, + }) + + filename := fmt.Sprintf("%s.jsonnet", event) + snippet, err := os.ReadFile(filepath.Join(webhook.Path, filename)) + if err != nil { + return nil, fmt.Errorf("GetCustomPayload read jsonnet: %v", err) + } + + out, err := vm.EvaluateAnonymousSnippet(filename, string(snippet)) + return stringPayloader{out}, err +} + +type stringPayloader struct { + payload string +} + +func (s stringPayloader) JSONPayload() ([]byte, error) { + return []byte(s.payload), nil } diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go index ed6f483de10c..f95658d413f0 100644 --- a/services/webhook/deliver.go +++ b/services/webhook/deliver.go @@ -11,13 +11,10 @@ import ( "crypto/sha256" "crypto/tls" "encoding/hex" - "errors" "fmt" "io" "net/http" "net/url" - "os" - "os/exec" "strings" "sync" "time" @@ -195,48 +192,27 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error { return nil } - if custom != nil && len(custom.Exec) > 0 { - graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) { - cmd := exec.Command(custom.Exec[0], custom.Exec[1:]...) - cmd.Stdin = strings.NewReader(t.PayloadContent) - cmd.Dir = custom.Path - env := os.Environ() - for key, vals := range req.Header { - env = append(env, fmt.Sprintf("%s=%s", key, vals[0])) - } - cmd.Env = env - out, err := cmd.CombinedOutput() - var exit *exec.ExitError - if err != nil && !errors.As(err, &exit) { - t.ResponseInfo.Body = fmt.Sprintf("Delivery: %v", err) - return - } - t.IsSucceed = cmd.ProcessState.Success() - t.ResponseInfo.Status = cmd.ProcessState.ExitCode() - t.ResponseInfo.Body = string(out) - }) - } else { - resp, err := webhookHTTPClient.Do(req.WithContext(graceful.GetManager().ShutdownContext())) - if err != nil { - t.ResponseInfo.Body = fmt.Sprintf("Delivery: %v", err) - return err - } - defer resp.Body.Close() + resp, err := webhookHTTPClient.Do(req.WithContext(ctx)) + if err != nil { + t.ResponseInfo.Body = fmt.Sprintf("Delivery: %v", err) + return err + } + defer resp.Body.Close() - // Status code is 20x can be seen as succeed. - t.IsSucceed = resp.StatusCode/100 == 2 - t.ResponseInfo.Status = resp.StatusCode - for k, vals := range resp.Header { - t.ResponseInfo.Headers[k] = strings.Join(vals, ",") - } + // Status code is 20x can be seen as succeed. + t.IsSucceed = resp.StatusCode/100 == 2 + t.ResponseInfo.Status = resp.StatusCode + for k, vals := range resp.Header { + t.ResponseInfo.Headers[k] = strings.Join(vals, ",") + } - p, err := io.ReadAll(resp.Body) - if err != nil { - t.ResponseInfo.Body = fmt.Sprintf("read body: %s", err) - return err - } - t.ResponseInfo.Body = string(p) + p, err := io.ReadAll(resp.Body) + if err != nil { + t.ResponseInfo.Body = fmt.Sprintf("read body: %s", err) + return err } + t.ResponseInfo.Body = string(p) + return nil } diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go index 642cf6f2fda1..054561a327ae 100644 --- a/services/webhook/dingtalk.go +++ b/services/webhook/dingtalk.go @@ -183,6 +183,6 @@ func createDingtalkPayload(title, text, singleTitle, singleURL string) *Dingtalk } // GetDingtalkPayload converts a ding talk webhook into a DingtalkPayload -func GetDingtalkPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) { +func GetDingtalkPayload(p api.Payloader, event webhook_model.HookEventType, _ *webhook_model.Webhook) (api.Payloader, error) { return convertPayloader(new(DingtalkPayload), p, event) } diff --git a/services/webhook/discord.go b/services/webhook/discord.go index ae5460b9a7a1..946c088a34d2 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -243,11 +243,11 @@ func (d *DiscordPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { } // GetDiscordPayload converts a discord webhook into a DiscordPayload -func GetDiscordPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) { +func GetDiscordPayload(p api.Payloader, event webhook_model.HookEventType, w *webhook_model.Webhook) (api.Payloader, error) { s := new(DiscordPayload) discord := &DiscordMeta{} - if err := json.Unmarshal([]byte(meta), &discord); err != nil { + if err := json.Unmarshal([]byte(w.Meta), &discord); err != nil { return s, errors.New("GetDiscordPayload meta json:" + err.Error()) } s.Username = discord.Username diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go index 5b20c7dda7e5..2f7482c552a4 100644 --- a/services/webhook/feishu.go +++ b/services/webhook/feishu.go @@ -153,6 +153,6 @@ func (f *FeishuPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { } // GetFeishuPayload converts a ding talk webhook into a FeishuPayload -func GetFeishuPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) { +func GetFeishuPayload(p api.Payloader, event webhook_model.HookEventType, _ *webhook_model.Webhook) (api.Payloader, error) { return convertPayloader(new(FeishuPayload), p, event) } diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go index a42ab2a93e06..bc59b27cdc3a 100644 --- a/services/webhook/matrix.go +++ b/services/webhook/matrix.go @@ -222,11 +222,11 @@ func (m *MatrixPayloadUnsafe) Repository(p *api.RepositoryPayload) (api.Payloade } // GetMatrixPayload converts a Matrix webhook into a MatrixPayloadUnsafe -func GetMatrixPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) { +func GetMatrixPayload(p api.Payloader, event webhook_model.HookEventType, w *webhook_model.Webhook) (api.Payloader, error) { s := new(MatrixPayloadUnsafe) matrix := &MatrixMeta{} - if err := json.Unmarshal([]byte(meta), &matrix); err != nil { + if err := json.Unmarshal([]byte(w.Meta), &matrix); err != nil { return s, errors.New("GetMatrixPayload meta json:" + err.Error()) } diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go index 59e2e9349398..ab1520a42a6d 100644 --- a/services/webhook/msteams.go +++ b/services/webhook/msteams.go @@ -282,7 +282,7 @@ func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { } // GetMSTeamsPayload converts a MSTeams webhook into a MSTeamsPayload -func GetMSTeamsPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) { +func GetMSTeamsPayload(p api.Payloader, event webhook_model.HookEventType, _ *webhook_model.Webhook) (api.Payloader, error) { return convertPayloader(new(MSTeamsPayload), p, event) } diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go index ace93b13ff05..1910f89facc1 100644 --- a/services/webhook/packagist.go +++ b/services/webhook/packagist.go @@ -100,11 +100,11 @@ func (f *PackagistPayload) Release(p *api.ReleasePayload) (api.Payloader, error) } // GetPackagistPayload converts a packagist webhook into a PackagistPayload -func GetPackagistPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) { +func GetPackagistPayload(p api.Payloader, event webhook_model.HookEventType, w *webhook_model.Webhook) (api.Payloader, error) { s := new(PackagistPayload) packagist := &PackagistMeta{} - if err := json.Unmarshal([]byte(meta), &packagist); err != nil { + if err := json.Unmarshal([]byte(w.Meta), &packagist); err != nil { return s, errors.New("GetPackagistPayload meta json:" + err.Error()) } s.PackagistRepository.URL = packagist.PackageURL diff --git a/services/webhook/slack.go b/services/webhook/slack.go index 11e1d3c081c2..26fd738f6478 100644 --- a/services/webhook/slack.go +++ b/services/webhook/slack.go @@ -271,11 +271,11 @@ func (s *SlackPayload) createPayload(text string, attachments []SlackAttachment) } // GetSlackPayload converts a slack webhook into a SlackPayload -func GetSlackPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) { +func GetSlackPayload(p api.Payloader, event webhook_model.HookEventType, w *webhook_model.Webhook) (api.Payloader, error) { s := new(SlackPayload) slack := &SlackMeta{} - if err := json.Unmarshal([]byte(meta), &slack); err != nil { + if err := json.Unmarshal([]byte(w.Meta), &slack); err != nil { return s, errors.New("GetSlackPayload meta json:" + err.Error()) } diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go index 64211493ec62..ece41a6bb828 100644 --- a/services/webhook/telegram.go +++ b/services/webhook/telegram.go @@ -179,7 +179,7 @@ func (t *TelegramPayload) Release(p *api.ReleasePayload) (api.Payloader, error) } // GetTelegramPayload converts a telegram webhook into a TelegramPayload -func GetTelegramPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) { +func GetTelegramPayload(p api.Payloader, event webhook_model.HookEventType, _ *webhook_model.Webhook) (api.Payloader, error) { return convertPayloader(new(TelegramPayload), p, event) } diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index c74bdbc3a354..77e192fe3147 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -24,7 +24,7 @@ import ( type webhook struct { name webhook_model.HookType - payloadCreator func(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) + payloadCreator func(p api.Payloader, event webhook_model.HookEventType, w *webhook_model.Webhook) (api.Payloader, error) } var webhooks = map[webhook_model.HookType]*webhook{ @@ -201,7 +201,7 @@ func prepareWebhook(w *webhook_model.Webhook, repo *repo_model.Repository, event var err error webhook, ok := webhooks[w.Type] if ok { - payloader, err = webhook.payloadCreator(p, event, w.Meta) + payloader, err = webhook.payloadCreator(p, event, w) if err != nil { return fmt.Errorf("create payload for %s[%s]: %v", w.Type, event, err) } diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go index de8b77706657..4c79df2f2094 100644 --- a/services/webhook/wechatwork.go +++ b/services/webhook/wechatwork.go @@ -174,6 +174,6 @@ func (f *WechatworkPayload) Release(p *api.ReleasePayload) (api.Payloader, error } // GetWechatworkPayload GetWechatworkPayload converts a ding talk webhook into a WechatworkPayload -func GetWechatworkPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) { +func GetWechatworkPayload(p api.Payloader, event webhook_model.HookEventType, _ *webhook_model.Webhook) (api.Payloader, error) { return convertPayloader(new(WechatworkPayload), p, event) }