Browse Source

Implementation of discord webhook (#2402)

* implementation of discord webhook

* fix webhooks

* fix typo and unnecessary color values

* fix typo

* fix imports and revert changes to webhook_slack.go
Lunny Xiao 2 years ago
parent
commit
ced50e0ec1

+ 25 - 8
models/webhook.go

@@ -13,15 +13,14 @@ import (
13 13
 	"strings"
14 14
 	"time"
15 15
 
16
-	"github.com/go-xorm/xorm"
17
-	gouuid "github.com/satori/go.uuid"
18
-
19
-	api "code.gitea.io/sdk/gitea"
20
-
21 16
 	"code.gitea.io/gitea/modules/httplib"
22 17
 	"code.gitea.io/gitea/modules/log"
23 18
 	"code.gitea.io/gitea/modules/setting"
24 19
 	"code.gitea.io/gitea/modules/sync"
20
+	api "code.gitea.io/sdk/gitea"
21
+
22
+	"github.com/go-xorm/xorm"
23
+	gouuid "github.com/satori/go.uuid"
25 24
 )
26 25
 
27 26
 // HookQueue is a global queue of web hooks
@@ -150,6 +149,15 @@ func (w *Webhook) GetSlackHook() *SlackMeta {
150 149
 	return s
151 150
 }
152 151
 
152
+// GetDiscordHook returns discord metadata
153
+func (w *Webhook) GetDiscordHook() *DiscordMeta {
154
+	s := &DiscordMeta{}
155
+	if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
156
+		log.Error(4, "webhook.GetDiscordHook(%d): %v", w.ID, err)
157
+	}
158
+	return s
159
+}
160
+
153 161
 // History returns history of webhook by given conditions.
154 162
 func (w *Webhook) History(page int) ([]*HookTask, error) {
155 163
 	return HookTasks(w.ID, page)
@@ -314,12 +322,14 @@ const (
314 322
 	GOGS HookTaskType = iota + 1
315 323
 	SLACK
316 324
 	GITEA
325
+	DISCORD
317 326
 )
318 327
 
319 328
 var hookTaskTypes = map[string]HookTaskType{
320
-	"gitea": GITEA,
321
-	"gogs":  GOGS,
322
-	"slack": SLACK,
329
+	"gitea":   GITEA,
330
+	"gogs":    GOGS,
331
+	"slack":   SLACK,
332
+	"discord": DISCORD,
323 333
 }
324 334
 
325 335
 // ToHookTaskType returns HookTaskType by given name.
@@ -336,6 +346,8 @@ func (t HookTaskType) Name() string {
336 346
 		return "gogs"
337 347
 	case SLACK:
338 348
 		return "slack"
349
+	case DISCORD:
350
+		return "discord"
339 351
 	}
340 352
 	return ""
341 353
 }
@@ -515,6 +527,11 @@ func PrepareWebhooks(repo *Repository, event HookEventType, p api.Payloader) err
515 527
 			if err != nil {
516 528
 				return fmt.Errorf("GetSlackPayload: %v", err)
517 529
 			}
530
+		case DISCORD:
531
+			payloader, err = GetDiscordPayload(p, event, w.Meta)
532
+			if err != nil {
533
+				return fmt.Errorf("GetDiscordPayload: %v", err)
534
+			}
518 535
 		default:
519 536
 			p.SetSecret(w.Secret)
520 537
 			payloader = p

+ 252 - 0
models/webhook_discord.go

@@ -0,0 +1,252 @@
1
+package models
2
+
3
+import (
4
+	"encoding/json"
5
+	"errors"
6
+	"fmt"
7
+	"strconv"
8
+	"strings"
9
+
10
+	"code.gitea.io/git"
11
+	"code.gitea.io/gitea/modules/setting"
12
+	api "code.gitea.io/sdk/gitea"
13
+)
14
+
15
+type (
16
+	// DiscordEmbedFooter for Embed Footer Structure.
17
+	DiscordEmbedFooter struct {
18
+		Text string `json:"text"`
19
+	}
20
+
21
+	// DiscordEmbedAuthor for Embed Author Structure
22
+	DiscordEmbedAuthor struct {
23
+		Name    string `json:"name"`
24
+		URL     string `json:"url"`
25
+		IconURL string `json:"icon_url"`
26
+	}
27
+
28
+	// DiscordEmbedField for Embed Field Structure
29
+	DiscordEmbedField struct {
30
+		Name  string `json:"name"`
31
+		Value string `json:"value"`
32
+	}
33
+
34
+	// DiscordEmbed is for Embed Structure
35
+	DiscordEmbed struct {
36
+		Title       string              `json:"title"`
37
+		Description string              `json:"description"`
38
+		URL         string              `json:"url"`
39
+		Color       int                 `json:"color"`
40
+		Footer      DiscordEmbedFooter  `json:"footer"`
41
+		Author      DiscordEmbedAuthor  `json:"author"`
42
+		Fields      []DiscordEmbedField `json:"fields"`
43
+	}
44
+
45
+	// DiscordPayload represents
46
+	DiscordPayload struct {
47
+		Wait      bool           `json:"wait"`
48
+		Content   string         `json:"content"`
49
+		Username  string         `json:"username"`
50
+		AvatarURL string         `json:"avatar_url"`
51
+		TTS       bool           `json:"tts"`
52
+		Embeds    []DiscordEmbed `json:"embeds"`
53
+	}
54
+
55
+	// DiscordMeta contains the discord metadata
56
+	DiscordMeta struct {
57
+		Username string `json:"username"`
58
+		IconURL  string `json:"icon_url"`
59
+	}
60
+)
61
+
62
+func color(clr string) int {
63
+	if clr != "" {
64
+		clr = strings.TrimLeft(clr, "#")
65
+		if s, err := strconv.ParseInt(clr, 16, 32); err == nil {
66
+			return int(s)
67
+		}
68
+	}
69
+
70
+	return 0
71
+}
72
+
73
+var (
74
+	successColor = color("1ac600")
75
+	warnColor    = color("ffd930")
76
+	failedColor  = color("ff3232")
77
+)
78
+
79
+// SetSecret sets the discord secret
80
+func (p *DiscordPayload) SetSecret(_ string) {}
81
+
82
+// JSONPayload Marshals the DiscordPayload to json
83
+func (p *DiscordPayload) JSONPayload() ([]byte, error) {
84
+	data, err := json.MarshalIndent(p, "", "  ")
85
+	if err != nil {
86
+		return []byte{}, err
87
+	}
88
+	return data, nil
89
+}
90
+
91
+func getDiscordCreatePayload(p *api.CreatePayload, meta *DiscordMeta) (*DiscordPayload, error) {
92
+	// created tag/branch
93
+	refName := git.RefEndName(p.Ref)
94
+	title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
95
+
96
+	return &DiscordPayload{
97
+		Username:  meta.Username,
98
+		AvatarURL: meta.IconURL,
99
+		Embeds: []DiscordEmbed{
100
+			{
101
+				Title: title,
102
+				URL:   p.Repo.HTMLURL + "/src/" + refName,
103
+				Color: successColor,
104
+				Author: DiscordEmbedAuthor{
105
+					Name:    p.Sender.UserName,
106
+					URL:     setting.AppURL + p.Sender.UserName,
107
+					IconURL: p.Sender.AvatarURL,
108
+				},
109
+			},
110
+		},
111
+	}, nil
112
+}
113
+
114
+func getDiscordPushPayload(p *api.PushPayload, meta *DiscordMeta) (*DiscordPayload, error) {
115
+	var (
116
+		branchName = git.RefEndName(p.Ref)
117
+		commitDesc string
118
+	)
119
+
120
+	var titleLink string
121
+	if len(p.Commits) == 1 {
122
+		commitDesc = "1 new commit"
123
+		titleLink = p.Commits[0].URL
124
+	} else {
125
+		commitDesc = fmt.Sprintf("%d new commits", len(p.Commits))
126
+		titleLink = p.CompareURL
127
+	}
128
+	if titleLink == "" {
129
+		titleLink = p.Repo.HTMLURL + "/src/" + branchName
130
+	}
131
+
132
+	title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc)
133
+
134
+	var text string
135
+	// for each commit, generate attachment text
136
+	for i, commit := range p.Commits {
137
+		text += fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL,
138
+			strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name)
139
+		// add linebreak to each commit but the last
140
+		if i < len(p.Commits)-1 {
141
+			text += "\n"
142
+		}
143
+	}
144
+
145
+	fmt.Println(text)
146
+
147
+	return &DiscordPayload{
148
+		Username:  meta.Username,
149
+		AvatarURL: meta.IconURL,
150
+		Embeds: []DiscordEmbed{
151
+			{
152
+				Title:       title,
153
+				Description: text,
154
+				URL:         titleLink,
155
+				Color:       successColor,
156
+				Author: DiscordEmbedAuthor{
157
+					Name:    p.Sender.UserName,
158
+					URL:     setting.AppURL + p.Sender.UserName,
159
+					IconURL: p.Sender.AvatarURL,
160
+				},
161
+			},
162
+		},
163
+	}, nil
164
+}
165
+
166
+func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta) (*DiscordPayload, error) {
167
+	var text, title string
168
+	var color int
169
+	switch p.Action {
170
+	case api.HookIssueOpened:
171
+		title = fmt.Sprintf("[%s] Pull request opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
172
+		text = p.PullRequest.Body
173
+		color = warnColor
174
+	case api.HookIssueClosed:
175
+		if p.PullRequest.HasMerged {
176
+			title = fmt.Sprintf("[%s] Pull request merged: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
177
+			color = successColor
178
+		} else {
179
+			title = fmt.Sprintf("[%s] Pull request closed: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
180
+			color = failedColor
181
+		}
182
+		text = p.PullRequest.Body
183
+	case api.HookIssueReOpened:
184
+		title = fmt.Sprintf("[%s] Pull request re-opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
185
+		text = p.PullRequest.Body
186
+		color = warnColor
187
+	case api.HookIssueEdited:
188
+		title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
189
+		text = p.PullRequest.Body
190
+		color = warnColor
191
+	case api.HookIssueAssigned:
192
+		title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d %s", p.Repository.FullName,
193
+			p.PullRequest.Assignee.UserName, p.Index, p.PullRequest.Title)
194
+		text = p.PullRequest.Body
195
+		color = successColor
196
+	case api.HookIssueUnassigned:
197
+		title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
198
+		text = p.PullRequest.Body
199
+		color = warnColor
200
+	case api.HookIssueLabelUpdated:
201
+		title = fmt.Sprintf("[%s] Pull request labels updated: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
202
+		text = p.PullRequest.Body
203
+		color = warnColor
204
+	case api.HookIssueLabelCleared:
205
+		title = fmt.Sprintf("[%s] Pull request labels cleared: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
206
+		text = p.PullRequest.Body
207
+		color = warnColor
208
+	case api.HookIssueSynchronized:
209
+		title = fmt.Sprintf("[%s] Pull request synchronized: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
210
+		text = p.PullRequest.Body
211
+		color = warnColor
212
+	}
213
+
214
+	return &DiscordPayload{
215
+		Username:  meta.Username,
216
+		AvatarURL: meta.IconURL,
217
+		Embeds: []DiscordEmbed{
218
+			{
219
+				Title:       title,
220
+				Description: text,
221
+				URL:         p.PullRequest.HTMLURL,
222
+				Color:       color,
223
+				Author: DiscordEmbedAuthor{
224
+					Name:    p.Sender.UserName,
225
+					URL:     setting.AppURL + p.Sender.UserName,
226
+					IconURL: p.Sender.AvatarURL,
227
+				},
228
+			},
229
+		},
230
+	}, nil
231
+}
232
+
233
+// GetDiscordPayload converts a discord webhook into a DiscordPayload
234
+func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*DiscordPayload, error) {
235
+	s := new(DiscordPayload)
236
+
237
+	discord := &DiscordMeta{}
238
+	if err := json.Unmarshal([]byte(meta), &discord); err != nil {
239
+		return s, errors.New("GetDiscordPayload meta json:" + err.Error())
240
+	}
241
+
242
+	switch event {
243
+	case HookEventCreate:
244
+		return getDiscordCreatePayload(p.(*api.CreatePayload), discord)
245
+	case HookEventPush:
246
+		return getDiscordPushPayload(p.(*api.PushPayload), discord)
247
+	case HookEventPullRequest:
248
+		return getDiscordPullRequestPayload(p.(*api.PullRequestPayload), discord)
249
+	}
250
+
251
+	return s, nil
252
+}

+ 13 - 0
modules/auth/repo_form.go

@@ -183,6 +183,19 @@ func (f *NewSlackHookForm) Validate(ctx *macaron.Context, errs binding.Errors) b
183 183
 	return validate(errs, ctx.Data, f, ctx.Locale)
184 184
 }
185 185
 
186
+// NewDiscordHookForm form for creating discord hook
187
+type NewDiscordHookForm struct {
188
+	PayloadURL string `binding:"Required;ValidUrl"`
189
+	Username   string
190
+	IconURL    string
191
+	WebhookForm
192
+}
193
+
194
+// Validate validates the fields
195
+func (f *NewDiscordHookForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
196
+	return validate(errs, ctx.Data, f, ctx.Locale)
197
+}
198
+
186 199
 // .___
187 200
 // |   | ______ ________ __   ____
188 201
 // |   |/  ___//  ___/  |  \_/ __ \

+ 1 - 1
modules/setting/setting.go

@@ -1367,7 +1367,7 @@ func newWebhookService() {
1367 1367
 	Webhook.QueueLength = sec.Key("QUEUE_LENGTH").MustInt(1000)
1368 1368
 	Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5)
1369 1369
 	Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool()
1370
-	Webhook.Types = []string{"gitea", "gogs", "slack"}
1370
+	Webhook.Types = []string{"gitea", "gogs", "slack", "discord"}
1371 1371
 	Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10)
1372 1372
 }
1373 1373
 

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

@@ -879,6 +879,8 @@ settings.content_type = Content Type
879 879
 settings.secret = Secret
880 880
 settings.slack_username = Username
881 881
 settings.slack_icon_url = Icon URL
882
+settings.discord_username = Username
883
+settings.discord_icon_url = Icon URL
882 884
 settings.slack_color = Color
883 885
 settings.event_desc = When should this webhook be triggered?
884 886
 settings.event_push_only = Just the <code>push</code> event.
@@ -902,6 +904,7 @@ settings.add_slack_hook_desc = Add <a href="%s">Slack</a> integration to your re
902 904
 settings.slack_token = Token
903 905
 settings.slack_domain = Domain
904 906
 settings.slack_channel = Channel
907
+settings.add_discord_hook_desc = Add <a href="%s">Discord</a> integration to your repository.
905 908
 settings.deploy_keys = Deploy Keys
906 909
 settings.add_deploy_key = Add Deploy Key
907 910
 settings.deploy_key_desc = Deploy keys have read-only access. They are not the same as personal account SSH keys.

BIN
public/img/discord.png


+ 106 - 5
routers/repo/webhook.go

@@ -11,16 +11,15 @@ import (
11 11
 	"fmt"
12 12
 	"strings"
13 13
 
14
-	"github.com/Unknwon/com"
15
-
16 14
 	"code.gitea.io/git"
17
-	api "code.gitea.io/sdk/gitea"
18
-
19 15
 	"code.gitea.io/gitea/models"
20 16
 	"code.gitea.io/gitea/modules/auth"
21 17
 	"code.gitea.io/gitea/modules/base"
22 18
 	"code.gitea.io/gitea/modules/context"
23 19
 	"code.gitea.io/gitea/modules/setting"
20
+	api "code.gitea.io/sdk/gitea"
21
+
22
+	"github.com/Unknwon/com"
24 23
 )
25 24
 
26 25
 const (
@@ -96,10 +95,18 @@ func WebhooksNew(ctx *context.Context) {
96 95
 		return
97 96
 	}
98 97
 
99
-	ctx.Data["HookType"] = checkHookType(ctx)
98
+	hookType := checkHookType(ctx)
99
+	ctx.Data["HookType"] = hookType
100 100
 	if ctx.Written() {
101 101
 		return
102 102
 	}
103
+	if hookType == "discord" {
104
+		ctx.Data["DiscordHook"] = map[string]interface{}{
105
+			"Username": "Gitea",
106
+			"IconURL":  setting.AppURL + "img/favicon.png",
107
+			"Color":    16724530,
108
+		}
109
+	}
103 110
 	ctx.Data["BaseLink"] = orCtx.Link
104 111
 
105 112
 	ctx.HTML(200, orCtx.NewTemplate)
@@ -213,6 +220,55 @@ func GogsHooksNewPost(ctx *context.Context, form auth.NewWebhookForm) {
213 220
 	ctx.Redirect(orCtx.Link + "/settings/hooks")
214 221
 }
215 222
 
223
+// DiscordHooksNewPost response for creating discord hook
224
+func DiscordHooksNewPost(ctx *context.Context, form auth.NewDiscordHookForm) {
225
+	ctx.Data["Title"] = ctx.Tr("repo.settings")
226
+	ctx.Data["PageIsSettingsHooks"] = true
227
+	ctx.Data["PageIsSettingsHooksNew"] = true
228
+	ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
229
+
230
+	orCtx, err := getOrgRepoCtx(ctx)
231
+	if err != nil {
232
+		ctx.Handle(500, "getOrgRepoCtx", err)
233
+		return
234
+	}
235
+
236
+	if ctx.HasError() {
237
+		ctx.HTML(200, orCtx.NewTemplate)
238
+		return
239
+	}
240
+
241
+	meta, err := json.Marshal(&models.DiscordMeta{
242
+		Username: form.Username,
243
+		IconURL:  form.IconURL,
244
+	})
245
+	if err != nil {
246
+		ctx.Handle(500, "Marshal", err)
247
+		return
248
+	}
249
+
250
+	w := &models.Webhook{
251
+		RepoID:       orCtx.RepoID,
252
+		URL:          form.PayloadURL,
253
+		ContentType:  models.ContentTypeJSON,
254
+		HookEvent:    ParseHookEvent(form.WebhookForm),
255
+		IsActive:     form.Active,
256
+		HookTaskType: models.DISCORD,
257
+		Meta:         string(meta),
258
+		OrgID:        orCtx.OrgID,
259
+	}
260
+	if err := w.UpdateEvent(); err != nil {
261
+		ctx.Handle(500, "UpdateEvent", err)
262
+		return
263
+	} else if err := models.CreateWebhook(w); err != nil {
264
+		ctx.Handle(500, "CreateWebhook", err)
265
+		return
266
+	}
267
+
268
+	ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
269
+	ctx.Redirect(orCtx.Link + "/settings/hooks")
270
+}
271
+
216 272
 // SlackHooksNewPost response for creating slack hook
217 273
 func SlackHooksNewPost(ctx *context.Context, form auth.NewSlackHookForm) {
218 274
 	ctx.Data["Title"] = ctx.Tr("repo.settings")
@@ -295,6 +351,9 @@ func checkWebhook(ctx *context.Context) (*orgRepoCtx, *models.Webhook) {
295 351
 		ctx.Data["HookType"] = "slack"
296 352
 	case models.GOGS:
297 353
 		ctx.Data["HookType"] = "gogs"
354
+	case models.DISCORD:
355
+		ctx.Data["DiscordHook"] = w.GetDiscordHook()
356
+		ctx.Data["HookType"] = "discord"
298 357
 	default:
299 358
 		ctx.Data["HookType"] = "gitea"
300 359
 	}
@@ -443,6 +502,48 @@ func SlackHooksEditPost(ctx *context.Context, form auth.NewSlackHookForm) {
443 502
 	ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID))
444 503
 }
445 504
 
505
+// DiscordHooksEditPost response for editing discord hook
506
+func DiscordHooksEditPost(ctx *context.Context, form auth.NewDiscordHookForm) {
507
+	ctx.Data["Title"] = ctx.Tr("repo.settings")
508
+	ctx.Data["PageIsSettingsHooks"] = true
509
+	ctx.Data["PageIsSettingsHooksEdit"] = true
510
+
511
+	orCtx, w := checkWebhook(ctx)
512
+	if ctx.Written() {
513
+		return
514
+	}
515
+	ctx.Data["Webhook"] = w
516
+
517
+	if ctx.HasError() {
518
+		ctx.HTML(200, orCtx.NewTemplate)
519
+		return
520
+	}
521
+
522
+	meta, err := json.Marshal(&models.DiscordMeta{
523
+		Username: form.Username,
524
+		IconURL:  form.IconURL,
525
+	})
526
+	if err != nil {
527
+		ctx.Handle(500, "Marshal", err)
528
+		return
529
+	}
530
+
531
+	w.URL = form.PayloadURL
532
+	w.Meta = string(meta)
533
+	w.HookEvent = ParseHookEvent(form.WebhookForm)
534
+	w.IsActive = form.Active
535
+	if err := w.UpdateEvent(); err != nil {
536
+		ctx.Handle(500, "UpdateEvent", err)
537
+		return
538
+	} else if err := models.UpdateWebhook(w); err != nil {
539
+		ctx.Handle(500, "UpdateWebhook", err)
540
+		return
541
+	}
542
+
543
+	ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
544
+	ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID))
545
+}
546
+
446 547
 // TestWebhook test if web hook is work fine
447 548
 func TestWebhook(ctx *context.Context) {
448 549
 	// Grab latest commit or fake one if it's empty repository.

+ 2 - 0
routers/routes/routes.go

@@ -442,11 +442,13 @@ func RegisterRoutes(m *macaron.Macaron) {
442 442
 				m.Post("/gitea/new", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksNewPost)
443 443
 				m.Post("/gogs/new", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost)
444 444
 				m.Post("/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost)
445
+				m.Post("/discord/new", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
445 446
 				m.Get("/:id", repo.WebHooksEdit)
446 447
 				m.Post("/:id/test", repo.TestWebhook)
447 448
 				m.Post("/gitea/:id", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksEditPost)
448 449
 				m.Post("/gogs/:id", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost)
449 450
 				m.Post("/slack/:id", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksEditPost)
451
+				m.Post("/discord/:id", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
450 452
 
451 453
 				m.Group("/git", func() {
452 454
 					m.Get("", repo.GitHooks)

+ 19 - 0
templates/repo/settings/hook_discord.tmpl

@@ -0,0 +1,19 @@
1
+{{if eq .HookType "discord"}}
2
+	<p>{{.i18n.Tr "repo.settings.add_discord_hook_desc" "https://discordapp.com" | Str2html}}</p>
3
+	<form class="ui form" action="{{.BaseLink}}/settings/hooks/discord/{{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
+		<div class="field">
10
+			<label for="username">{{.i18n.Tr "repo.settings.discord_username"}}</label>
11
+			<input id="username" name="username" value="{{.DiscordHook.Username}}" placeholder="e.g. Gitea">
12
+		</div>
13
+		<div class="field">
14
+			<label for="icon_url">{{.i18n.Tr "repo.settings.discord_icon_url"}}</label>
15
+			<input id="icon_url" name="icon_url" value="{{.DiscordHook.IconURL}}" placeholder="e.g. https://example.com/img/favicon.png">
16
+		</div>
17
+		{{template "repo/settings/hook_settings" .}}
18
+	</form>
19
+{{end}}

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

@@ -14,6 +14,9 @@
14 14
 				<a class="item" href="{{.BaseLink}}/settings/hooks/slack/new">
15 15
 					<img class="img-10" src="{{AppSubUrl}}/img/slack.png">Slack
16 16
 				</a>
17
+				<a class="item" href="{{.BaseLink}}/settings/hooks/discord/new">
18
+					<img class="img-10" src="{{AppSubUrl}}/img/discord.png">Discord
19
+				</a>
17 20
 			</div>
18 21
 		</div>
19 22
 	</div>

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

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