Browse Source

Add dingtalk webhook (#2777)

* add dingtalk webhook type

* add vendor

* some fixes

* fix name check

* fix name check & improvment
Lunny Xiao 2 years ago
parent
commit
10b54df2b2

+ 13 - 4
models/webhook.go

@@ -332,13 +332,15 @@ const (
332 332
 	SLACK
333 333
 	GITEA
334 334
 	DISCORD
335
+	DINGTALK
335 336
 )
336 337
 
337 338
 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,
342 344
 }
343 345
 
344 346
 // ToHookTaskType returns HookTaskType by given name.
@@ -357,6 +359,8 @@ func (t HookTaskType) Name() string {
357 359
 		return "slack"
358 360
 	case DISCORD:
359 361
 		return "discord"
362
+	case DINGTALK:
363
+		return "dingtalk"
360 364
 	}
361 365
 	return ""
362 366
 }
@@ -520,6 +524,11 @@ func prepareWebhook(e Engine, w *Webhook, repo *Repository, event HookEventType,
520 524
 		if err != nil {
521 525
 			return fmt.Errorf("GetDiscordPayload: %v", err)
522 526
 		}
527
+	case DINGTALK:
528
+		payloader, err = GetDingtalkPayload(p, event, w.Meta)
529
+		if err != nil {
530
+			return fmt.Errorf("GetDingtalkPayload: %v", err)
531
+		}
523 532
 	default:
524 533
 		p.SetSecret(w.Secret)
525 534
 		payloader = p

+ 197 - 0
models/webhook_dingtalk.go

@@ -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
+}

+ 11 - 0
modules/auth/repo_form.go

@@ -222,6 +222,17 @@ func (f *NewDiscordHookForm) Validate(ctx *macaron.Context, errs binding.Errors)
222 222
 	return validate(errs, ctx.Data, f, ctx.Locale)
223 223
 }
224 224
 
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
+
225 236
 // .___
226 237
 // |   | ______ ________ __   ____
227 238
 // |   |/  ___//  ___/  |  \_/ __ \

+ 1 - 1
modules/setting/setting.go

@@ -1509,7 +1509,7 @@ func newWebhookService() {
1509 1509
 	Webhook.QueueLength = sec.Key("QUEUE_LENGTH").MustInt(1000)
1510 1510
 	Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5)
1511 1511
 	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"}
1513 1513
 	Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10)
1514 1514
 }
1515 1515
 

+ 1 - 0
options/locale/locale_en-US.ini

@@ -978,6 +978,7 @@ settings.slack_token = Token
978 978
 settings.slack_domain = Domain
979 979
 settings.slack_channel = Channel
980 980
 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.
981 982
 settings.deploy_keys = Deploy Keys
982 983
 settings.add_deploy_key = Add Deploy Key
983 984
 settings.deploy_key_desc = Deploy keys have read-only access. They are not the same as personal account SSH keys.

BIN
public/img/dingtalk.ico


+ 73 - 6
routers/repo/webhook.go

@@ -269,6 +269,46 @@ func DiscordHooksNewPost(ctx *context.Context, form auth.NewDiscordHookForm) {
269 269
 	ctx.Redirect(orCtx.Link + "/settings/hooks")
270 270
 }
271 271
 
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
+
272 312
 // SlackHooksNewPost response for creating slack hook
273 313
 func SlackHooksNewPost(ctx *context.Context, form auth.NewSlackHookForm) {
274 314
 	ctx.Data["Title"] = ctx.Tr("repo.settings")
@@ -345,17 +385,12 @@ func checkWebhook(ctx *context.Context) (*orgRepoCtx, *models.Webhook) {
345 385
 		return nil, nil
346 386
 	}
347 387
 
388
+	ctx.Data["HookType"] = w.HookTaskType.Name()
348 389
 	switch w.HookTaskType {
349 390
 	case models.SLACK:
350 391
 		ctx.Data["SlackHook"] = w.GetSlackHook()
351
-		ctx.Data["HookType"] = "slack"
352
-	case models.GOGS:
353
-		ctx.Data["HookType"] = "gogs"
354 392
 	case models.DISCORD:
355 393
 		ctx.Data["DiscordHook"] = w.GetDiscordHook()
356
-		ctx.Data["HookType"] = "discord"
357
-	default:
358
-		ctx.Data["HookType"] = "gitea"
359 394
 	}
360 395
 
361 396
 	ctx.Data["History"], err = w.History(1)
@@ -544,6 +579,38 @@ func DiscordHooksEditPost(ctx *context.Context, form auth.NewDiscordHookForm) {
544 579
 	ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID))
545 580
 }
546 581
 
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
+
547 614
 // TestWebhook test if web hook is work fine
548 615
 func TestWebhook(ctx *context.Context) {
549 616
 	hookID := ctx.ParamsInt64(":id")

+ 4 - 0
routers/routes/routes.go

@@ -396,11 +396,13 @@ func RegisterRoutes(m *macaron.Macaron) {
396 396
 					m.Post("/gogs/new", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost)
397 397
 					m.Post("/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost)
398 398
 					m.Post("/discord/new", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
399
+					m.Post("/dingtalk/new", bindIgnErr(auth.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
399 400
 					m.Get("/:id", repo.WebHooksEdit)
400 401
 					m.Post("/gitea/:id", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksEditPost)
401 402
 					m.Post("/gogs/:id", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksEditPost)
402 403
 					m.Post("/slack/:id", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksEditPost)
403 404
 					m.Post("/discord/:id", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
405
+					m.Post("/dingtalk/:id", bindIgnErr(auth.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
404 406
 				})
405 407
 
406 408
 				m.Route("/delete", "GET,POST", org.SettingsDelete)
@@ -444,12 +446,14 @@ func RegisterRoutes(m *macaron.Macaron) {
444 446
 				m.Post("/gogs/new", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost)
445 447
 				m.Post("/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost)
446 448
 				m.Post("/discord/new", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
449
+				m.Post("/dingtalk/new", bindIgnErr(auth.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
447 450
 				m.Get("/:id", repo.WebHooksEdit)
448 451
 				m.Post("/:id/test", repo.TestWebhook)
449 452
 				m.Post("/gitea/:id", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksEditPost)
450 453
 				m.Post("/gogs/:id", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost)
451 454
 				m.Post("/slack/:id", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksEditPost)
452 455
 				m.Post("/discord/:id", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
456
+				m.Post("/dingtalk/:id", bindIgnErr(auth.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
453 457
 
454 458
 				m.Group("/git", func() {
455 459
 					m.Get("", repo.GitHooks)

+ 3 - 0
templates/org/settings/hook_new.tmpl

@@ -17,6 +17,8 @@
17 17
 							<img class="img-13" src="{{AppSubUrl}}/img/slack.png">
18 18
 						{{else if eq .HookType "discord"}}
19 19
 							<img class="img-13" src="{{AppSubUrl}}/img/discord.png">
20
+						{{else if eq .HookType "dingtalk"}}
21
+							<img class="img-13" src="{{AppSubUrl}}/img/dingtalk.png">
20 22
 						{{end}}
21 23
 					</div>
22 24
 				</h4>
@@ -25,6 +27,7 @@
25 27
 					{{template "repo/settings/hook_gogs" .}}
26 28
 					{{template "repo/settings/hook_slack" .}}
27 29
 					{{template "repo/settings/hook_discord" .}}
30
+					{{template "repo/settings/hook_dingtalk" .}}
28 31
 				</div>
29 32
 
30 33
 				{{template "repo/settings/hook_history" .}}

+ 11 - 0
templates/repo/settings/hook_dingtalk.tmpl

@@ -0,0 +1,11 @@
1
+{{if eq .HookType "dingtalk"}}
2
+	<p>{{.i18n.Tr "repo.settings.add_dingtalk_hook_desc" "https://dingtalk.com" | Str2html}}</p>
3
+	<form class="ui form" action="{{.BaseLink}}/settings/hooks/dingtalk/{{if .PageIsSettingsHooksNew}}new{{else}}{{.Webhook.ID}}{{end}}" method="post">
4
+		{{.CsrfTokenHtml}}
5
+		<div class="required field {{if .Err_PayloadURL}}error{{end}}">
6
+			<label for="payload_url">{{.i18n.Tr "repo.settings.payload_url"}}</label>
7
+			<input id="payload_url" name="payload_url" type="url" value="{{.Webhook.URL}}" autofocus required>
8
+		</div>
9
+		{{template "repo/settings/hook_settings" .}}
10
+	</form>
11
+{{end}}

+ 3 - 0
templates/repo/settings/hook_list.tmpl

@@ -17,6 +17,9 @@
17 17
 				<a class="item" href="{{.BaseLink}}/settings/hooks/discord/new">
18 18
 					<img class="img-10" src="{{AppSubUrl}}/img/discord.png">Discord
19 19
 				</a>
20
+				<a class="item" href="{{.BaseLink}}/settings/hooks/dingtalk/new">
21
+					<img class="img-10" src="{{AppSubUrl}}/img/dingtalk.ico">Dingtalk
22
+				</a>
20 23
 			</div>
21 24
 		</div>
22 25
 	</div>

+ 3 - 0
templates/repo/settings/hook_new.tmpl

@@ -15,6 +15,8 @@
15 15
 					<img class="img-13" src="{{AppSubUrl}}/img/slack.png">
16 16
 				{{else if eq .HookType "discord"}}
17 17
 					<img class="img-13" src="{{AppSubUrl}}/img/discord.png">
18
+				{{else if eq .HookType "dingtalk"}}
19
+					<img class="img-13" src="{{AppSubUrl}}/img/dingtalk.ico">
18 20
 				{{end}}
19 21
 			</div>
20 22
 		</h4>
@@ -23,6 +25,7 @@
23 25
 			{{template "repo/settings/hook_gogs" .}}
24 26
 			{{template "repo/settings/hook_slack" .}}
25 27
 			{{template "repo/settings/hook_discord" .}}
28
+			{{template "repo/settings/hook_dingtalk" .}}
26 29
 		</div>
27 30
 
28 31
 		{{template "repo/settings/hook_history" .}}

+ 20 - 0
vendor/github.com/lunny/dingtalk_webhook/LICENSE

@@ -0,0 +1,20 @@
1
+Copyright (c) 2016 The Gitea Authors
2
+Copyright (c) 2015 The Gogs Authors
3
+
4
+Permission is hereby granted, free of charge, to any person obtaining a copy
5
+of this software and associated documentation files (the "Software"), to deal
6
+in the Software without restriction, including without limitation the rights
7
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+copies of the Software, and to permit persons to whom the Software is
9
+furnished to do so, subject to the following conditions:
10
+
11
+The above copyright notice and this permission notice shall be included in
12
+all copies or substantial portions of the Software.
13
+
14
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+THE SOFTWARE.

+ 18 - 0
vendor/github.com/lunny/dingtalk_webhook/README.md

@@ -0,0 +1,18 @@
1
+# 非官方 Dingtalk webhook Golang SDK
2
+
3
+## 此工程仅封装了 Dingtalk 的 webhook 部分的请求
4
+
5
+## 使用
6
+
7
+首先在dingtalk中创建一个机器人,将accessToken拷贝出来,然后执行下面方法即可
8
+
9
+```Go
10
+webhook := dingtalk.Webhook(accessToken)
11
+webhook.SendTextMsg("这是一个没有AT的文本消息", false)
12
+```
13
+
14
+## License
15
+
16
+This project is licensed under the MIT License.
17
+See the [LICENSE](https://github.com/lunny/webhook_dingtalk/blob/master/LICENSE) file
18
+for the full license text.

+ 361 - 0
vendor/github.com/lunny/dingtalk_webhook/webhook.go

@@ -0,0 +1,361 @@
1
+// Copyright 2017 Lunny Xiao. 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 dingtalk
6
+
7
+import (
8
+	"bytes"
9
+	"encoding/json"
10
+	"errors"
11
+	"fmt"
12
+	"io/ioutil"
13
+	"net/http"
14
+)
15
+
16
+/*
17
+{
18
+	"msgtype": "text",
19
+	"text": {
20
+		"content": "我就是我, 是不一样的烟火"
21
+	},
22
+	"at": {
23
+		"atMobiles": [
24
+			"156xxxx8827",
25
+			"189xxxx8325"
26
+		],
27
+		"isAtAll": false
28
+	}
29
+}
30
+
31
+{
32
+	"msgtype": "link",
33
+	"link": {
34
+		"text": "这个即将发布的新版本,创始人陈航(花名“无招”)称它为“红树林”。
35
+而在此之前,每当面临重大升级,产品经理们都会取一个应景的代号,这一次,为什么是“红树林”?",
36
+		"title": "时代的火车向前开",
37
+		"picUrl": "",
38
+		"messageUrl": "https://mp.weixin.qq.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI"
39
+	}
40
+}
41
+
42
+{
43
+	"msgtype": "markdown",
44
+	"markdown": {
45
+		"title":"杭州天气",
46
+		"text": "#### 杭州天气 @156xxxx8827\n" +
47
+				"> 9度,西北风1级,空气良89,相对温度73%\n\n" +
48
+				"> ![screenshot](http://image.jpg)\n"  +
49
+				"> ###### 10点20分发布 [天气](http://www.thinkpage.cn/) \n"
50
+	},
51
+	"at": {
52
+		"atMobiles": [
53
+			"156xxxx8827",
54
+			"189xxxx8325"
55
+		],
56
+		"isAtAll": false
57
+	}
58
+}
59
+
60
+{
61
+    "actionCard": {
62
+        "title": "乔布斯 20 年前想打造一间苹果咖啡厅,而它正是 Apple Store 的前身",
63
+        "text": "![screenshot](@lADOpwk3K80C0M0FoA)
64
+ ### 乔布斯 20 年前想打造的苹果咖啡厅
65
+ Apple Store 的设计正从原来满满的科技感走向生活化,而其生活化的走向其实可以追溯到 20 年前苹果一个建立咖啡馆的计划",
66
+        "hideAvatar": "0",
67
+        "btnOrientation": "0",
68
+        "singleTitle" : "阅读全文",
69
+		"singleURL" : "https://www.dingtalk.com/",
70
+		"btns": [
71
+            {
72
+                "title": "内容不错",
73
+                "actionURL": "https://www.dingtalk.com/"
74
+            },
75
+            {
76
+                "title": "不感兴趣",
77
+                "actionURL": "https://www.dingtalk.com/"
78
+            }
79
+        ]
80
+    },
81
+    "msgtype": "actionCard"
82
+}
83
+
84
+{
85
+    "feedCard": {
86
+        "links": [
87
+            {
88
+                "title": "时代的火车向前开",
89
+                "messageURL": "https://mp.weixin.qq.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI",
90
+                "picURL": "https://www.dingtalk.com/"
91
+            },
92
+            {
93
+                "title": "时代的火车向前开2",
94
+                "messageURL": "https://mp.weixin.qq.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI",
95
+                "picURL": "https://www.dingtalk.com/"
96
+            }
97
+        ]
98
+    },
99
+    "msgtype": "feedCard"
100
+}
101
+*/
102
+
103
+type LinkMsg struct {
104
+	Title      string `json:"title"`
105
+	MessageURL string `json:"messageURL"`
106
+	PicURL     string `json:"picURL"`
107
+}
108
+
109
+type ActionCard struct {
110
+	Text           string `json:"text"`
111
+	Title          string `json:"title"`
112
+	HideAvatar     string `json:"hideAvatar"`
113
+	BtnOrientation string `json:"btnOrientation"`
114
+	SingleTitle    string `json:"singleTitle"`
115
+	SingleURL      string `json:"singleURL"`
116
+	Buttons        []struct {
117
+		Title     string `json:"title"`
118
+		ActionURL string `json:"actionURL"`
119
+	} `json:"btns"`
120
+}
121
+
122
+// Payload struct
123
+type Payload struct {
124
+	MsgType string `json:"msgtype"`
125
+	Text    struct {
126
+		Content string `json:"content"`
127
+	} `json:"text"`
128
+	Link struct {
129
+		Text       string `json:"text"`
130
+		Title      string `json:"title"`
131
+		PicURL     string `json:"picUrl"`
132
+		MessageURL string `json:"messageUrl"`
133
+	} `json:"link"`
134
+	Markdown struct {
135
+		Text  string `json:"text"`
136
+		Title string `json:"title"`
137
+	} `json:"markdown"`
138
+	ActionCard ActionCard `json:"actionCard"`
139
+	FeedCard   struct {
140
+		Links []LinkMsg `json:"links"`
141
+	} `json:"feedCard"`
142
+	At struct {
143
+		AtMobiles []string `json:"atMobiles"`
144
+		IsAtAll   bool     `json:"isAtAll"`
145
+	} `json:"at"`
146
+}
147
+
148
+type Webhook struct {
149
+	accessToken string
150
+}
151
+
152
+func NewWebhook(accessToken string) *Webhook {
153
+	return &Webhook{accessToken}
154
+}
155
+
156
+type Response struct {
157
+	ErrorCode    int    `json:"errcode"`
158
+	ErrorMessage string `json:"errmsg"`
159
+}
160
+
161
+// SendPayload 发送消息
162
+func (w *Webhook) SendPayload(payload *Payload) error {
163
+	bs, err := json.Marshal(payload)
164
+	if err != nil {
165
+		return err
166
+	}
167
+
168
+	resp, err := http.Post("https://oapi.dingtalk.com/robot/send?access_token="+w.accessToken, "application/json", bytes.NewReader(bs))
169
+	if err != nil {
170
+		return err
171
+	}
172
+
173
+	bs, err = ioutil.ReadAll(resp.Body)
174
+	if err != nil {
175
+		return err
176
+	}
177
+
178
+	if resp.StatusCode != 200 {
179
+		return fmt.Errorf("%d: %s", resp.StatusCode, string(bs))
180
+	}
181
+
182
+	var result Response
183
+	err = json.Unmarshal(bs, &result)
184
+	if err != nil {
185
+		return err
186
+	}
187
+	if result.ErrorCode != 0 {
188
+		return fmt.Errorf("%d: %s", result.ErrorCode, result.ErrorMessage)
189
+	}
190
+
191
+	return nil
192
+}
193
+
194
+// SendTextMsg 发送文本消息
195
+func (w *Webhook) SendTextMsg(content string, isAtAll bool, mobiles ...string) error {
196
+	return w.SendPayload(&Payload{
197
+		MsgType: "text",
198
+		Text: struct {
199
+			Content string `json:"content"`
200
+		}{
201
+			Content: content,
202
+		},
203
+		At: struct {
204
+			AtMobiles []string `json:"atMobiles"`
205
+			IsAtAll   bool     `json:"isAtAll"`
206
+		}{
207
+			AtMobiles: mobiles,
208
+			IsAtAll:   isAtAll,
209
+		},
210
+	})
211
+}
212
+
213
+// SendLinkMsg 发送链接消息
214
+func (w *Webhook) SendLinkMsg(title, content, picURL, msgURL string) error {
215
+	return w.SendPayload(&Payload{
216
+		MsgType: "link",
217
+		Link: struct {
218
+			Text       string `json:"text"`
219
+			Title      string `json:"title"`
220
+			PicURL     string `json:"picUrl"`
221
+			MessageURL string `json:"messageUrl"`
222
+		}{
223
+			Text:       content,
224
+			Title:      title,
225
+			PicURL:     picURL,
226
+			MessageURL: msgURL,
227
+		},
228
+	})
229
+}
230
+
231
+// SendMarkdownMsg 发送markdown消息,仅支持以下格式
232
+/*
233
+标题
234
+# 一级标题
235
+## 二级标题
236
+### 三级标题
237
+#### 四级标题
238
+##### 五级标题
239
+###### 六级标题
240
+
241
+引用
242
+> A man who stands for nothing will fall for anything.
243
+
244
+文字加粗、斜体
245
+**bold**
246
+*italic*
247
+
248
+链接
249
+[this is a link](http://name.com)
250
+
251
+图片
252
+![](http://name.com/pic.jpg)
253
+
254
+无序列表
255
+- item1
256
+- item2
257
+
258
+有序列表
259
+1. item1
260
+2. item2
261
+*/
262
+func (w *Webhook) SendMarkdownMsg(title, content string, isAtAll bool, mobiles ...string) error {
263
+	return w.SendPayload(&Payload{
264
+		MsgType: "markdown",
265
+		Markdown: struct {
266
+			Text  string `json:"text"`
267
+			Title string `json:"title"`
268
+		}{
269
+			Text:  content,
270
+			Title: title,
271
+		},
272
+		At: struct {
273
+			AtMobiles []string `json:"atMobiles"`
274
+			IsAtAll   bool     `json:"isAtAll"`
275
+		}{
276
+			AtMobiles: mobiles,
277
+			IsAtAll:   isAtAll,
278
+		},
279
+	})
280
+}
281
+
282
+// SendSingleActionCardMsg 发送整体跳转ActionCard类型消息
283
+func (w *Webhook) SendSingleActionCardMsg(title, content, linkTitle, linkURL string, hideAvatar, btnOrientation bool) error {
284
+	var strHideAvatar = "0"
285
+	if hideAvatar {
286
+		strHideAvatar = "1"
287
+	}
288
+	var strBtnOrientation = "0"
289
+	if btnOrientation {
290
+		strBtnOrientation = "1"
291
+	}
292
+
293
+	return w.SendPayload(&Payload{
294
+		MsgType: "actionCard",
295
+		ActionCard: ActionCard{
296
+			Text:           content,
297
+			Title:          title,
298
+			HideAvatar:     strHideAvatar,
299
+			BtnOrientation: strBtnOrientation,
300
+			SingleTitle:    linkTitle,
301
+			SingleURL:      linkURL,
302
+		},
303
+	})
304
+}
305
+
306
+// SendActionCardMsg 独立跳转ActionCard类型
307
+func (w *Webhook) SendActionCardMsg(title, content string, linkTitles, linkURLs []string, hideAvatar, btnOrientation bool) error {
308
+	if len(linkTitles) == 0 || len(linkURLs) == 0 {
309
+		return errors.New("链接参数不能为空")
310
+	}
311
+	if len(linkTitles) != len(linkURLs) {
312
+		return errors.New("链接数量不匹配")
313
+	}
314
+
315
+	var strHideAvatar = "0"
316
+	if hideAvatar {
317
+		strHideAvatar = "1"
318
+	}
319
+	var strBtnOrientation = "0"
320
+	if btnOrientation {
321
+		strBtnOrientation = "1"
322
+	}
323
+
324
+	var btns []struct {
325
+		Title     string `json:"title"`
326
+		ActionURL string `json:"actionURL"`
327
+	}
328
+
329
+	for i := 0; i < len(linkTitles); i++ {
330
+		btns = append(btns, struct {
331
+			Title     string `json:"title"`
332
+			ActionURL string `json:"actionURL"`
333
+		}{
334
+			Title:     linkTitles[i],
335
+			ActionURL: linkURLs[i],
336
+		})
337
+	}
338
+
339
+	return w.SendPayload(&Payload{
340
+		MsgType: "actionCard",
341
+		ActionCard: ActionCard{
342
+			Text:           content,
343
+			Title:          title,
344
+			HideAvatar:     strHideAvatar,
345
+			BtnOrientation: strBtnOrientation,
346
+			Buttons:        btns,
347
+		},
348
+	})
349
+}
350
+
351
+// SendLinkCardMsg 发送链接消息
352
+func (w *Webhook) SendLinkCardMsg(msgs []LinkMsg) error {
353
+	return w.SendPayload(&Payload{
354
+		MsgType: "feedCard",
355
+		FeedCard: struct {
356
+			Links []LinkMsg `json:"links"`
357
+		}{
358
+			Links: msgs,
359
+		},
360
+	})
361
+}

+ 6 - 0
vendor/vendor.json

@@ -648,6 +648,12 @@
648 648
 			"revisionTime": "2017-10-19T22:30:07Z"
649 649
 		},
650 650
 		{
651
+			"checksumSHA1": "gVEVVVLsFxLE+ADLuzkmzMxlmMA=",
652
+			"path": "github.com/lunny/dingtalk_webhook",
653
+			"revision": "e3534c89ef969912856dfa39e56b09e58c5f5daf",
654
+			"revisionTime": "2017-10-25T03:15:54Z"
655
+		},
656
+		{
651 657
 			"checksumSHA1": "O3KUfEXQPfdQ+tCMpP2RAIRJJqY=",
652 658
 			"path": "github.com/markbates/goth",
653 659
 			"revision": "90362394a367f9d77730911973462a53d69662ba",