diff --git a/cmd/web.go b/cmd/web.go
index 8c7c026172745..8983eb4f2561b 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -10,6 +10,7 @@ import (
"net"
"net/http"
"os"
+ "path/filepath"
"strings"
_ "net/http/pprof" // Used for debugging if enabled and a web server is running
@@ -18,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"
@@ -157,6 +159,10 @@ 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)
+ }
+
// 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/go.mod b/go.mod
index 5ef996769d57b..fc943184925b6 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 ae6b0ad83a272..d5bec55eb34f5 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/models/migrations/migrations.go b/models/migrations/migrations.go
index 5a2297ac0d3c2..c011f7f3501bc 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 0000000000000..0f27851e234c1
--- /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 941a3f15c782f..2dfc8d75998cc 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/schema.json b/modules/webhook/schema.json
new file mode 100644
index 0000000000000..83f7f27eee126
--- /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"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/modules/webhook/webhook.go b/modules/webhook/webhook.go
new file mode 100644
index 0000000000000..e8be38c606c54
--- /dev/null
+++ b/modules/webhook/webhook.go
@@ -0,0 +1,147 @@
+// 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"
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "path/filepath"
+
+ "gopkg.in/yaml.v2"
+)
+
+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"`
+ Form []Form `yaml:"form"`
+ Path string `yaml:"-"`
+}
+
+// Image returns a custom webhook image if it exists, else the default image
+// 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)
+ }
+
+ return img, nil
+}
+
+// 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"`
+ Pattern string `yaml:"pattern"`
+}
+
+// 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")
+ }
+ if w.HTTP == "" {
+ return errors.New("webhook http is required")
+ }
+ 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")
+ }
+ 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 Webhook 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
+}
+
+// Init initializes any custom webhooks found in path
+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)
+ }
+
+ 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
+}
diff --git a/modules/webhook/webhook_test.go b/modules/webhook/webhook_test.go
new file mode 100644
index 0000000000000..9c7c044264cea
--- /dev/null
+++ b/modules/webhook/webhook_test.go
@@ -0,0 +1,50 @@
+// 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 (
+ "bytes"
+ "testing"
+
+ "code.gitea.io/gitea/testdata"
+
+ "github.com/stretchr/testify/assert"
+)
+
+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/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index ba619b413cbc9..077b0de5cf124 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 1483d0959dbe3..9b026549756f3 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 5cd245ef09a3b..e0fce5905b6f3 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 d2e246118923d..44799feb845c0 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 0000000000000..03620a9bb274a
--- /dev/null
+++ b/routers/web/repo/webhook_custom.go
@@ -0,0 +1,150 @@
+// 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 (
+ "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
+ }
+ ctx.Data["BaseLink"] = orCtx.LinkNew
+
+ 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 97ea1e90353f4..20749986e6438 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 18cbac751cd7c..be7a4524bfa87 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 0000000000000..eabcd735bbae2
--- /dev/null
+++ b/services/webhook/custom.go
@@ -0,0 +1,93 @@
+// 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 (
+ "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 (
+ // 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, event webhook_model.HookEventType, w *webhook_model.Webhook) (api.Payloader, error) {
+ s := new(CustomPayload)
+
+ 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
+
+ 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 77744473f1ce3..f95658d413f0b 100644
--- a/services/webhook/deliver.go
+++ b/services/webhook/deliver.go
@@ -27,6 +27,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 +51,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 +67,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
}
@@ -199,6 +212,7 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
return err
}
t.ResponseInfo.Body = string(p)
+
return nil
}
diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go
index 642cf6f2fda16..054561a327ae1 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 ae5460b9a7a1b..946c088a34d2f 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 5b20c7dda7e5f..2f7482c552a48 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 a42ab2a93e063..bc59b27cdc3aa 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 59e2e93493982..ab1520a42a6d0 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 ace93b13ff056..1910f89facc1d 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 11e1d3c081c28..26fd738f64784 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 64211493ec62c..ece41a6bb828f 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 b15b8173f51fe..77e192fe3147b 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{
@@ -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
@@ -197,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 de8b777066576..4c79df2f20949 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)
}
diff --git a/templates/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl
index b1a3771bdba11..f52ab57984daa 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 0000000000000..00c8fad9c28d0
--- /dev/null
+++ b/templates/repo/settings/webhook/custom.tmpl
@@ -0,0 +1,31 @@
+{{if .CustomHook}}
+
{{.i18n.Tr "repo.settings.add_web_hook_desc" .CustomHook.Docs .CustomHook.Label | Str2html}}
+ +{{end}} diff --git a/templates/repo/settings/webhook/discord.tmpl b/templates/repo/settings/webhook/discord.tmpl index 1b58cfb6f02e9..1c32c0d0b5067 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}}