diff --git a/models/webhook.go b/models/webhook.go
index 508fea9977..1b601b4e62 100644
--- a/models/webhook.go
+++ b/models/webhook.go
@@ -332,13 +332,15 @@ const (
SLACK
GITEA
DISCORD
+ DINGTALK
)
var hookTaskTypes = map[string]HookTaskType{
- "gitea": GITEA,
- "gogs": GOGS,
- "slack": SLACK,
- "discord": DISCORD,
+ "gitea": GITEA,
+ "gogs": GOGS,
+ "slack": SLACK,
+ "discord": DISCORD,
+ "dingtalk": DINGTALK,
}
// ToHookTaskType returns HookTaskType by given name.
@@ -357,6 +359,8 @@ func (t HookTaskType) Name() string {
return "slack"
case DISCORD:
return "discord"
+ case DINGTALK:
+ return "dingtalk"
}
return ""
}
@@ -520,6 +524,11 @@ func prepareWebhook(e Engine, w *Webhook, repo *Repository, event HookEventType,
if err != nil {
return fmt.Errorf("GetDiscordPayload: %v", err)
}
+ case DINGTALK:
+ payloader, err = GetDingtalkPayload(p, event, w.Meta)
+ if err != nil {
+ return fmt.Errorf("GetDingtalkPayload: %v", err)
+ }
default:
p.SetSecret(w.Secret)
payloader = p
diff --git a/models/webhook_dingtalk.go b/models/webhook_dingtalk.go
new file mode 100644
index 0000000000..e25e989084
--- /dev/null
+++ b/models/webhook_dingtalk.go
@@ -0,0 +1,197 @@
+// Copyright 2017 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 models
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ "code.gitea.io/git"
+ api "code.gitea.io/sdk/gitea"
+
+ dingtalk "github.com/lunny/dingtalk_webhook"
+)
+
+type (
+ // DingtalkPayload represents
+ DingtalkPayload dingtalk.Payload
+)
+
+// SetSecret sets the dingtalk secret
+func (p *DingtalkPayload) SetSecret(_ string) {}
+
+// JSONPayload Marshals the DingtalkPayload to json
+func (p *DingtalkPayload) JSONPayload() ([]byte, error) {
+ data, err := json.MarshalIndent(p, "", " ")
+ if err != nil {
+ return []byte{}, err
+ }
+ return data, nil
+}
+
+func getDingtalkCreatePayload(p *api.CreatePayload) (*DingtalkPayload, error) {
+ // created tag/branch
+ refName := git.RefEndName(p.Ref)
+ title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
+
+ return &DingtalkPayload{
+ MsgType: "actionCard",
+ ActionCard: dingtalk.ActionCard{
+ Text: title,
+ Title: title,
+ HideAvatar: "0",
+ SingleTitle: fmt.Sprintf("view branch %s", refName),
+ SingleURL: p.Repo.HTMLURL + "/src/" + refName,
+ },
+ }, nil
+}
+
+func getDingtalkPushPayload(p *api.PushPayload) (*DingtalkPayload, error) {
+ var (
+ branchName = git.RefEndName(p.Ref)
+ commitDesc string
+ )
+
+ var titleLink, linkText string
+ if len(p.Commits) == 1 {
+ commitDesc = "1 new commit"
+ titleLink = p.Commits[0].URL
+ linkText = fmt.Sprintf("view commit %s", p.Commits[0].ID[:7])
+ } else {
+ commitDesc = fmt.Sprintf("%d new commits", len(p.Commits))
+ titleLink = p.CompareURL
+ linkText = fmt.Sprintf("view commit %s...%s", p.Commits[0].ID[:7], p.Commits[len(p.Commits)-1].ID[:7])
+ }
+ if titleLink == "" {
+ titleLink = p.Repo.HTMLURL + "/src/" + branchName
+ }
+
+ title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc)
+
+ var text string
+ // for each commit, generate attachment text
+ for i, commit := range p.Commits {
+ var authorName string
+ if commit.Author != nil {
+ authorName = " - " + commit.Author.Name
+ }
+ text += fmt.Sprintf("[%s](%s) %s", commit.ID[:7], commit.URL,
+ strings.TrimRight(commit.Message, "\r\n")) + authorName
+ // add linebreak to each commit but the last
+ if i < len(p.Commits)-1 {
+ text += "\n"
+ }
+ }
+
+ return &DingtalkPayload{
+ MsgType: "actionCard",
+ ActionCard: dingtalk.ActionCard{
+ Text: text,
+ Title: title,
+ HideAvatar: "0",
+ SingleTitle: linkText,
+ SingleURL: titleLink,
+ },
+ }, nil
+}
+
+func getDingtalkPullRequestPayload(p *api.PullRequestPayload) (*DingtalkPayload, error) {
+ var text, title string
+ switch p.Action {
+ case api.HookIssueOpened:
+ title = fmt.Sprintf("[%s] Pull request opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
+ text = p.PullRequest.Body
+ case api.HookIssueClosed:
+ if p.PullRequest.HasMerged {
+ title = fmt.Sprintf("[%s] Pull request merged: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
+ } else {
+ title = fmt.Sprintf("[%s] Pull request closed: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
+ }
+ text = p.PullRequest.Body
+ case api.HookIssueReOpened:
+ title = fmt.Sprintf("[%s] Pull request re-opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
+ text = p.PullRequest.Body
+ case api.HookIssueEdited:
+ title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
+ text = p.PullRequest.Body
+ case api.HookIssueAssigned:
+ title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d %s", p.Repository.FullName,
+ p.PullRequest.Assignee.UserName, p.Index, p.PullRequest.Title)
+ text = p.PullRequest.Body
+ case api.HookIssueUnassigned:
+ title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
+ text = p.PullRequest.Body
+ case api.HookIssueLabelUpdated:
+ title = fmt.Sprintf("[%s] Pull request labels updated: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
+ text = p.PullRequest.Body
+ case api.HookIssueLabelCleared:
+ title = fmt.Sprintf("[%s] Pull request labels cleared: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
+ text = p.PullRequest.Body
+ case api.HookIssueSynchronized:
+ title = fmt.Sprintf("[%s] Pull request synchronized: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
+ text = p.PullRequest.Body
+ }
+
+ return &DingtalkPayload{
+ MsgType: "actionCard",
+ ActionCard: dingtalk.ActionCard{
+ Text: text,
+ Title: title,
+ HideAvatar: "0",
+ SingleTitle: "view pull request",
+ SingleURL: p.PullRequest.HTMLURL,
+ },
+ }, nil
+}
+
+func getDingtalkRepositoryPayload(p *api.RepositoryPayload) (*DingtalkPayload, error) {
+ var title, url string
+ switch p.Action {
+ case api.HookRepoCreated:
+ title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
+ url = p.Repository.HTMLURL
+ return &DingtalkPayload{
+ MsgType: "actionCard",
+ ActionCard: dingtalk.ActionCard{
+ Text: title,
+ Title: title,
+ HideAvatar: "0",
+ SingleTitle: "view repository",
+ SingleURL: url,
+ },
+ }, nil
+ case api.HookRepoDeleted:
+ title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
+ return &DingtalkPayload{
+ MsgType: "text",
+ Text: struct {
+ Content string `json:"content"`
+ }{
+ Content: title,
+ },
+ }, nil
+ }
+
+ return nil, nil
+}
+
+// GetDingtalkPayload converts a ding talk webhook into a DingtalkPayload
+func GetDingtalkPayload(p api.Payloader, event HookEventType, meta string) (*DingtalkPayload, error) {
+ s := new(DingtalkPayload)
+
+ switch event {
+ case HookEventCreate:
+ return getDingtalkCreatePayload(p.(*api.CreatePayload))
+ case HookEventPush:
+ return getDingtalkPushPayload(p.(*api.PushPayload))
+ case HookEventPullRequest:
+ return getDingtalkPullRequestPayload(p.(*api.PullRequestPayload))
+ case HookEventRepository:
+ return getDingtalkRepositoryPayload(p.(*api.RepositoryPayload))
+ }
+
+ return s, nil
+}
diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go
index 6fe826f574..bb917a9114 100644
--- a/modules/auth/repo_form.go
+++ b/modules/auth/repo_form.go
@@ -222,6 +222,17 @@ func (f *NewDiscordHookForm) Validate(ctx *macaron.Context, errs binding.Errors)
return validate(errs, ctx.Data, f, ctx.Locale)
}
+// NewDingtalkHookForm form for creating dingtalk hook
+type NewDingtalkHookForm struct {
+ PayloadURL string `binding:"Required;ValidUrl"`
+ WebhookForm
+}
+
+// Validate validates the fields
+func (f *NewDingtalkHookForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
+ return validate(errs, ctx.Data, f, ctx.Locale)
+}
+
// .___
// | | ______ ________ __ ____
// | |/ ___// ___/ | \_/ __ \
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index d15a76f9bf..d4f92dee39 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -1509,7 +1509,7 @@ func newWebhookService() {
Webhook.QueueLength = sec.Key("QUEUE_LENGTH").MustInt(1000)
Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5)
Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool()
- Webhook.Types = []string{"gitea", "gogs", "slack", "discord"}
+ Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk"}
Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10)
}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 834ec49e53..4410b63cd0 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -978,6 +978,7 @@ settings.slack_token = Token
settings.slack_domain = Domain
settings.slack_channel = Channel
settings.add_discord_hook_desc = Add Discord integration to your repository.
+settings.add_dingtalk_hook_desc = Add Dingtalk integration to your repository.
settings.deploy_keys = Deploy Keys
settings.add_deploy_key = Add Deploy Key
settings.deploy_key_desc = Deploy keys have read-only access. They are not the same as personal account SSH keys.
diff --git a/public/img/dingtalk.ico b/public/img/dingtalk.ico
new file mode 100644
index 0000000000..a2682bbbc4
Binary files /dev/null and b/public/img/dingtalk.ico differ
diff --git a/routers/repo/webhook.go b/routers/repo/webhook.go
index 81f79582de..1bff139528 100644
--- a/routers/repo/webhook.go
+++ b/routers/repo/webhook.go
@@ -269,6 +269,46 @@ func DiscordHooksNewPost(ctx *context.Context, form auth.NewDiscordHookForm) {
ctx.Redirect(orCtx.Link + "/settings/hooks")
}
+// DingtalkHooksNewPost response for creating dingtalk hook
+func DingtalkHooksNewPost(ctx *context.Context, form auth.NewDingtalkHookForm) {
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksNew"] = true
+ ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+
+ orCtx, err := getOrgRepoCtx(ctx)
+ if err != nil {
+ ctx.Handle(500, "getOrgRepoCtx", err)
+ return
+ }
+
+ if ctx.HasError() {
+ ctx.HTML(200, orCtx.NewTemplate)
+ return
+ }
+
+ w := &models.Webhook{
+ RepoID: orCtx.RepoID,
+ URL: form.PayloadURL,
+ ContentType: models.ContentTypeJSON,
+ HookEvent: ParseHookEvent(form.WebhookForm),
+ IsActive: form.Active,
+ HookTaskType: models.DINGTALK,
+ Meta: "",
+ OrgID: orCtx.OrgID,
+ }
+ if err := w.UpdateEvent(); err != nil {
+ ctx.Handle(500, "UpdateEvent", err)
+ return
+ } else if err := models.CreateWebhook(w); err != nil {
+ ctx.Handle(500, "CreateWebhook", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+ ctx.Redirect(orCtx.Link + "/settings/hooks")
+}
+
// SlackHooksNewPost response for creating slack hook
func SlackHooksNewPost(ctx *context.Context, form auth.NewSlackHookForm) {
ctx.Data["Title"] = ctx.Tr("repo.settings")
@@ -345,17 +385,12 @@ func checkWebhook(ctx *context.Context) (*orgRepoCtx, *models.Webhook) {
return nil, nil
}
+ ctx.Data["HookType"] = w.HookTaskType.Name()
switch w.HookTaskType {
case models.SLACK:
ctx.Data["SlackHook"] = w.GetSlackHook()
- ctx.Data["HookType"] = "slack"
- case models.GOGS:
- ctx.Data["HookType"] = "gogs"
case models.DISCORD:
ctx.Data["DiscordHook"] = w.GetDiscordHook()
- ctx.Data["HookType"] = "discord"
- default:
- ctx.Data["HookType"] = "gitea"
}
ctx.Data["History"], err = w.History(1)
@@ -544,6 +579,38 @@ func DiscordHooksEditPost(ctx *context.Context, form auth.NewDiscordHookForm) {
ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID))
}
+// DingtalkHooksEditPost response for editing discord hook
+func DingtalkHooksEditPost(ctx *context.Context, form auth.NewDingtalkHookForm) {
+ 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(200, orCtx.NewTemplate)
+ return
+ }
+
+ w.URL = form.PayloadURL
+ w.HookEvent = ParseHookEvent(form.WebhookForm)
+ w.IsActive = form.Active
+ if err := w.UpdateEvent(); err != nil {
+ ctx.Handle(500, "UpdateEvent", err)
+ return
+ } else if err := models.UpdateWebhook(w); err != nil {
+ ctx.Handle(500, "UpdateWebhook", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
+ ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID))
+}
+
// TestWebhook test if web hook is work fine
func TestWebhook(ctx *context.Context) {
hookID := ctx.ParamsInt64(":id")
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index 5a76dddb66..ece2565683 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -396,11 +396,13 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Post("/gogs/new", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost)
m.Post("/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost)
m.Post("/discord/new", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
+ m.Post("/dingtalk/new", bindIgnErr(auth.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
m.Get("/:id", repo.WebHooksEdit)
m.Post("/gitea/:id", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksEditPost)
m.Post("/gogs/:id", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksEditPost)
m.Post("/slack/:id", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksEditPost)
m.Post("/discord/:id", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
+ m.Post("/dingtalk/:id", bindIgnErr(auth.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
})
m.Route("/delete", "GET,POST", org.SettingsDelete)
@@ -444,12 +446,14 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Post("/gogs/new", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost)
m.Post("/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost)
m.Post("/discord/new", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
+ m.Post("/dingtalk/new", bindIgnErr(auth.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
m.Get("/:id", repo.WebHooksEdit)
m.Post("/:id/test", repo.TestWebhook)
m.Post("/gitea/:id", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksEditPost)
m.Post("/gogs/:id", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost)
m.Post("/slack/:id", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksEditPost)
m.Post("/discord/:id", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
+ m.Post("/dingtalk/:id", bindIgnErr(auth.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
m.Group("/git", func() {
m.Get("", repo.GitHooks)
diff --git a/templates/org/settings/hook_new.tmpl b/templates/org/settings/hook_new.tmpl
index 15c6943185..bedb2dc142 100644
--- a/templates/org/settings/hook_new.tmpl
+++ b/templates/org/settings/hook_new.tmpl
@@ -17,6 +17,8 @@
{{else if eq .HookType "discord"}}
+ {{else if eq .HookType "dingtalk"}}
+
{{end}}
@@ -25,6 +27,7 @@
{{template "repo/settings/hook_gogs" .}}
{{template "repo/settings/hook_slack" .}}
{{template "repo/settings/hook_discord" .}}
+ {{template "repo/settings/hook_dingtalk" .}}
{{template "repo/settings/hook_history" .}}
diff --git a/templates/repo/settings/hook_dingtalk.tmpl b/templates/repo/settings/hook_dingtalk.tmpl
new file mode 100644
index 0000000000..37271a7db5
--- /dev/null
+++ b/templates/repo/settings/hook_dingtalk.tmpl
@@ -0,0 +1,11 @@
+{{if eq .HookType "dingtalk"}}
+
{{.i18n.Tr "repo.settings.add_dingtalk_hook_desc" "https://dingtalk.com" | Str2html}}
+ +{{end}} diff --git a/templates/repo/settings/hook_list.tmpl b/templates/repo/settings/hook_list.tmpl index dce3439096..4e61ba7a07 100644 --- a/templates/repo/settings/hook_list.tmpl +++ b/templates/repo/settings/hook_list.tmpl @@ -17,6 +17,9 @@