Skip to content

Commit 10b54df

Browse files
lunnylafriks
authored andcommitted
Add dingtalk webhook (#2777)
* add dingtalk webhook type * add vendor * some fixes * fix name check * fix name check & improvment
1 parent 420fc8e commit 10b54df

File tree

16 files changed

+725
-11
lines changed

16 files changed

+725
-11
lines changed

models/webhook.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -332,13 +332,15 @@ const (
332332
SLACK
333333
GITEA
334334
DISCORD
335+
DINGTALK
335336
)
336337

337338
var hookTaskTypes = map[string]HookTaskType{
338-
"gitea": GITEA,
339-
"gogs": GOGS,
340-
"slack": SLACK,
341-
"discord": DISCORD,
339+
"gitea": GITEA,
340+
"gogs": GOGS,
341+
"slack": SLACK,
342+
"discord": DISCORD,
343+
"dingtalk": DINGTALK,
342344
}
343345

344346
// ToHookTaskType returns HookTaskType by given name.
@@ -357,6 +359,8 @@ func (t HookTaskType) Name() string {
357359
return "slack"
358360
case DISCORD:
359361
return "discord"
362+
case DINGTALK:
363+
return "dingtalk"
360364
}
361365
return ""
362366
}
@@ -520,6 +524,11 @@ func prepareWebhook(e Engine, w *Webhook, repo *Repository, event HookEventType,
520524
if err != nil {
521525
return fmt.Errorf("GetDiscordPayload: %v", err)
522526
}
527+
case DINGTALK:
528+
payloader, err = GetDingtalkPayload(p, event, w.Meta)
529+
if err != nil {
530+
return fmt.Errorf("GetDingtalkPayload: %v", err)
531+
}
523532
default:
524533
p.SetSecret(w.Secret)
525534
payloader = p

models/webhook_dingtalk.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// Copyright 2017 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package models
6+
7+
import (
8+
"encoding/json"
9+
"fmt"
10+
"strings"
11+
12+
"code.gitea.io/git"
13+
api "code.gitea.io/sdk/gitea"
14+
15+
dingtalk "github.com/lunny/dingtalk_webhook"
16+
)
17+
18+
type (
19+
// DingtalkPayload represents
20+
DingtalkPayload dingtalk.Payload
21+
)
22+
23+
// SetSecret sets the dingtalk secret
24+
func (p *DingtalkPayload) SetSecret(_ string) {}
25+
26+
// JSONPayload Marshals the DingtalkPayload to json
27+
func (p *DingtalkPayload) JSONPayload() ([]byte, error) {
28+
data, err := json.MarshalIndent(p, "", " ")
29+
if err != nil {
30+
return []byte{}, err
31+
}
32+
return data, nil
33+
}
34+
35+
func getDingtalkCreatePayload(p *api.CreatePayload) (*DingtalkPayload, error) {
36+
// created tag/branch
37+
refName := git.RefEndName(p.Ref)
38+
title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
39+
40+
return &DingtalkPayload{
41+
MsgType: "actionCard",
42+
ActionCard: dingtalk.ActionCard{
43+
Text: title,
44+
Title: title,
45+
HideAvatar: "0",
46+
SingleTitle: fmt.Sprintf("view branch %s", refName),
47+
SingleURL: p.Repo.HTMLURL + "/src/" + refName,
48+
},
49+
}, nil
50+
}
51+
52+
func getDingtalkPushPayload(p *api.PushPayload) (*DingtalkPayload, error) {
53+
var (
54+
branchName = git.RefEndName(p.Ref)
55+
commitDesc string
56+
)
57+
58+
var titleLink, linkText string
59+
if len(p.Commits) == 1 {
60+
commitDesc = "1 new commit"
61+
titleLink = p.Commits[0].URL
62+
linkText = fmt.Sprintf("view commit %s", p.Commits[0].ID[:7])
63+
} else {
64+
commitDesc = fmt.Sprintf("%d new commits", len(p.Commits))
65+
titleLink = p.CompareURL
66+
linkText = fmt.Sprintf("view commit %s...%s", p.Commits[0].ID[:7], p.Commits[len(p.Commits)-1].ID[:7])
67+
}
68+
if titleLink == "" {
69+
titleLink = p.Repo.HTMLURL + "/src/" + branchName
70+
}
71+
72+
title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc)
73+
74+
var text string
75+
// for each commit, generate attachment text
76+
for i, commit := range p.Commits {
77+
var authorName string
78+
if commit.Author != nil {
79+
authorName = " - " + commit.Author.Name
80+
}
81+
text += fmt.Sprintf("[%s](%s) %s", commit.ID[:7], commit.URL,
82+
strings.TrimRight(commit.Message, "\r\n")) + authorName
83+
// add linebreak to each commit but the last
84+
if i < len(p.Commits)-1 {
85+
text += "\n"
86+
}
87+
}
88+
89+
return &DingtalkPayload{
90+
MsgType: "actionCard",
91+
ActionCard: dingtalk.ActionCard{
92+
Text: text,
93+
Title: title,
94+
HideAvatar: "0",
95+
SingleTitle: linkText,
96+
SingleURL: titleLink,
97+
},
98+
}, nil
99+
}
100+
101+
func getDingtalkPullRequestPayload(p *api.PullRequestPayload) (*DingtalkPayload, error) {
102+
var text, title string
103+
switch p.Action {
104+
case api.HookIssueOpened:
105+
title = fmt.Sprintf("[%s] Pull request opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
106+
text = p.PullRequest.Body
107+
case api.HookIssueClosed:
108+
if p.PullRequest.HasMerged {
109+
title = fmt.Sprintf("[%s] Pull request merged: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
110+
} else {
111+
title = fmt.Sprintf("[%s] Pull request closed: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
112+
}
113+
text = p.PullRequest.Body
114+
case api.HookIssueReOpened:
115+
title = fmt.Sprintf("[%s] Pull request re-opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
116+
text = p.PullRequest.Body
117+
case api.HookIssueEdited:
118+
title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
119+
text = p.PullRequest.Body
120+
case api.HookIssueAssigned:
121+
title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d %s", p.Repository.FullName,
122+
p.PullRequest.Assignee.UserName, p.Index, p.PullRequest.Title)
123+
text = p.PullRequest.Body
124+
case api.HookIssueUnassigned:
125+
title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
126+
text = p.PullRequest.Body
127+
case api.HookIssueLabelUpdated:
128+
title = fmt.Sprintf("[%s] Pull request labels updated: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
129+
text = p.PullRequest.Body
130+
case api.HookIssueLabelCleared:
131+
title = fmt.Sprintf("[%s] Pull request labels cleared: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
132+
text = p.PullRequest.Body
133+
case api.HookIssueSynchronized:
134+
title = fmt.Sprintf("[%s] Pull request synchronized: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
135+
text = p.PullRequest.Body
136+
}
137+
138+
return &DingtalkPayload{
139+
MsgType: "actionCard",
140+
ActionCard: dingtalk.ActionCard{
141+
Text: text,
142+
Title: title,
143+
HideAvatar: "0",
144+
SingleTitle: "view pull request",
145+
SingleURL: p.PullRequest.HTMLURL,
146+
},
147+
}, nil
148+
}
149+
150+
func getDingtalkRepositoryPayload(p *api.RepositoryPayload) (*DingtalkPayload, error) {
151+
var title, url string
152+
switch p.Action {
153+
case api.HookRepoCreated:
154+
title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
155+
url = p.Repository.HTMLURL
156+
return &DingtalkPayload{
157+
MsgType: "actionCard",
158+
ActionCard: dingtalk.ActionCard{
159+
Text: title,
160+
Title: title,
161+
HideAvatar: "0",
162+
SingleTitle: "view repository",
163+
SingleURL: url,
164+
},
165+
}, nil
166+
case api.HookRepoDeleted:
167+
title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
168+
return &DingtalkPayload{
169+
MsgType: "text",
170+
Text: struct {
171+
Content string `json:"content"`
172+
}{
173+
Content: title,
174+
},
175+
}, nil
176+
}
177+
178+
return nil, nil
179+
}
180+
181+
// GetDingtalkPayload converts a ding talk webhook into a DingtalkPayload
182+
func GetDingtalkPayload(p api.Payloader, event HookEventType, meta string) (*DingtalkPayload, error) {
183+
s := new(DingtalkPayload)
184+
185+
switch event {
186+
case HookEventCreate:
187+
return getDingtalkCreatePayload(p.(*api.CreatePayload))
188+
case HookEventPush:
189+
return getDingtalkPushPayload(p.(*api.PushPayload))
190+
case HookEventPullRequest:
191+
return getDingtalkPullRequestPayload(p.(*api.PullRequestPayload))
192+
case HookEventRepository:
193+
return getDingtalkRepositoryPayload(p.(*api.RepositoryPayload))
194+
}
195+
196+
return s, nil
197+
}

modules/auth/repo_form.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,17 @@ func (f *NewDiscordHookForm) Validate(ctx *macaron.Context, errs binding.Errors)
222222
return validate(errs, ctx.Data, f, ctx.Locale)
223223
}
224224

225+
// NewDingtalkHookForm form for creating dingtalk hook
226+
type NewDingtalkHookForm struct {
227+
PayloadURL string `binding:"Required;ValidUrl"`
228+
WebhookForm
229+
}
230+
231+
// Validate validates the fields
232+
func (f *NewDingtalkHookForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
233+
return validate(errs, ctx.Data, f, ctx.Locale)
234+
}
235+
225236
// .___
226237
// | | ______ ________ __ ____
227238
// | |/ ___// ___/ | \_/ __ \

modules/setting/setting.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1509,7 +1509,7 @@ func newWebhookService() {
15091509
Webhook.QueueLength = sec.Key("QUEUE_LENGTH").MustInt(1000)
15101510
Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5)
15111511
Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool()
1512-
Webhook.Types = []string{"gitea", "gogs", "slack", "discord"}
1512+
Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk"}
15131513
Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10)
15141514
}
15151515

options/locale/locale_en-US.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,7 @@ settings.slack_token = Token
978978
settings.slack_domain = Domain
979979
settings.slack_channel = Channel
980980
settings.add_discord_hook_desc = Add <a href="%s">Discord</a> integration to your repository.
981+
settings.add_dingtalk_hook_desc = Add <a href="%s">Dingtalk</a> integration to your repository.
981982
settings.deploy_keys = Deploy Keys
982983
settings.add_deploy_key = Add Deploy Key
983984
settings.deploy_key_desc = Deploy keys have read-only access. They are not the same as personal account SSH keys.

public/img/dingtalk.ico

7.7 KB
Binary file not shown.

routers/repo/webhook.go

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,46 @@ func DiscordHooksNewPost(ctx *context.Context, form auth.NewDiscordHookForm) {
269269
ctx.Redirect(orCtx.Link + "/settings/hooks")
270270
}
271271

272+
// DingtalkHooksNewPost response for creating dingtalk hook
273+
func DingtalkHooksNewPost(ctx *context.Context, form auth.NewDingtalkHookForm) {
274+
ctx.Data["Title"] = ctx.Tr("repo.settings")
275+
ctx.Data["PageIsSettingsHooks"] = true
276+
ctx.Data["PageIsSettingsHooksNew"] = true
277+
ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
278+
279+
orCtx, err := getOrgRepoCtx(ctx)
280+
if err != nil {
281+
ctx.Handle(500, "getOrgRepoCtx", err)
282+
return
283+
}
284+
285+
if ctx.HasError() {
286+
ctx.HTML(200, orCtx.NewTemplate)
287+
return
288+
}
289+
290+
w := &models.Webhook{
291+
RepoID: orCtx.RepoID,
292+
URL: form.PayloadURL,
293+
ContentType: models.ContentTypeJSON,
294+
HookEvent: ParseHookEvent(form.WebhookForm),
295+
IsActive: form.Active,
296+
HookTaskType: models.DINGTALK,
297+
Meta: "",
298+
OrgID: orCtx.OrgID,
299+
}
300+
if err := w.UpdateEvent(); err != nil {
301+
ctx.Handle(500, "UpdateEvent", err)
302+
return
303+
} else if err := models.CreateWebhook(w); err != nil {
304+
ctx.Handle(500, "CreateWebhook", err)
305+
return
306+
}
307+
308+
ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
309+
ctx.Redirect(orCtx.Link + "/settings/hooks")
310+
}
311+
272312
// SlackHooksNewPost response for creating slack hook
273313
func SlackHooksNewPost(ctx *context.Context, form auth.NewSlackHookForm) {
274314
ctx.Data["Title"] = ctx.Tr("repo.settings")
@@ -345,17 +385,12 @@ func checkWebhook(ctx *context.Context) (*orgRepoCtx, *models.Webhook) {
345385
return nil, nil
346386
}
347387

388+
ctx.Data["HookType"] = w.HookTaskType.Name()
348389
switch w.HookTaskType {
349390
case models.SLACK:
350391
ctx.Data["SlackHook"] = w.GetSlackHook()
351-
ctx.Data["HookType"] = "slack"
352-
case models.GOGS:
353-
ctx.Data["HookType"] = "gogs"
354392
case models.DISCORD:
355393
ctx.Data["DiscordHook"] = w.GetDiscordHook()
356-
ctx.Data["HookType"] = "discord"
357-
default:
358-
ctx.Data["HookType"] = "gitea"
359394
}
360395

361396
ctx.Data["History"], err = w.History(1)
@@ -544,6 +579,38 @@ func DiscordHooksEditPost(ctx *context.Context, form auth.NewDiscordHookForm) {
544579
ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID))
545580
}
546581

582+
// DingtalkHooksEditPost response for editing discord hook
583+
func DingtalkHooksEditPost(ctx *context.Context, form auth.NewDingtalkHookForm) {
584+
ctx.Data["Title"] = ctx.Tr("repo.settings")
585+
ctx.Data["PageIsSettingsHooks"] = true
586+
ctx.Data["PageIsSettingsHooksEdit"] = true
587+
588+
orCtx, w := checkWebhook(ctx)
589+
if ctx.Written() {
590+
return
591+
}
592+
ctx.Data["Webhook"] = w
593+
594+
if ctx.HasError() {
595+
ctx.HTML(200, orCtx.NewTemplate)
596+
return
597+
}
598+
599+
w.URL = form.PayloadURL
600+
w.HookEvent = ParseHookEvent(form.WebhookForm)
601+
w.IsActive = form.Active
602+
if err := w.UpdateEvent(); err != nil {
603+
ctx.Handle(500, "UpdateEvent", err)
604+
return
605+
} else if err := models.UpdateWebhook(w); err != nil {
606+
ctx.Handle(500, "UpdateWebhook", err)
607+
return
608+
}
609+
610+
ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
611+
ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID))
612+
}
613+
547614
// TestWebhook test if web hook is work fine
548615
func TestWebhook(ctx *context.Context) {
549616
hookID := ctx.ParamsInt64(":id")

0 commit comments

Comments
 (0)