Skip to content

Commit ced50e0

Browse files
authored
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
1 parent e41da38 commit ced50e0

File tree

11 files changed

+427
-14
lines changed

11 files changed

+427
-14
lines changed

models/webhook.go

+25-8
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,14 @@ import (
1313
"strings"
1414
"time"
1515

16-
"github.com/go-xorm/xorm"
17-
gouuid "github.com/satori/go.uuid"
18-
19-
api "code.gitea.io/sdk/gitea"
20-
2116
"code.gitea.io/gitea/modules/httplib"
2217
"code.gitea.io/gitea/modules/log"
2318
"code.gitea.io/gitea/modules/setting"
2419
"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"
2524
)
2625

2726
// HookQueue is a global queue of web hooks
@@ -150,6 +149,15 @@ func (w *Webhook) GetSlackHook() *SlackMeta {
150149
return s
151150
}
152151

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+
153161
// History returns history of webhook by given conditions.
154162
func (w *Webhook) History(page int) ([]*HookTask, error) {
155163
return HookTasks(w.ID, page)
@@ -314,12 +322,14 @@ const (
314322
GOGS HookTaskType = iota + 1
315323
SLACK
316324
GITEA
325+
DISCORD
317326
)
318327

319328
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,
323333
}
324334

325335
// ToHookTaskType returns HookTaskType by given name.
@@ -336,6 +346,8 @@ func (t HookTaskType) Name() string {
336346
return "gogs"
337347
case SLACK:
338348
return "slack"
349+
case DISCORD:
350+
return "discord"
339351
}
340352
return ""
341353
}
@@ -515,6 +527,11 @@ func PrepareWebhooks(repo *Repository, event HookEventType, p api.Payloader) err
515527
if err != nil {
516528
return fmt.Errorf("GetSlackPayload: %v", err)
517529
}
530+
case DISCORD:
531+
payloader, err = GetDiscordPayload(p, event, w.Meta)
532+
if err != nil {
533+
return fmt.Errorf("GetDiscordPayload: %v", err)
534+
}
518535
default:
519536
p.SetSecret(w.Secret)
520537
payloader = p

models/webhook_discord.go

+252
Original file line numberDiff line numberDiff line change
@@ -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+
}

modules/auth/repo_form.go

+13
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,19 @@ func (f *NewSlackHookForm) Validate(ctx *macaron.Context, errs binding.Errors) b
183183
return validate(errs, ctx.Data, f, ctx.Locale)
184184
}
185185

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+
186199
// .___
187200
// | | ______ ________ __ ____
188201
// | |/ ___// ___/ | \_/ __ \

modules/setting/setting.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1367,7 +1367,7 @@ func newWebhookService() {
13671367
Webhook.QueueLength = sec.Key("QUEUE_LENGTH").MustInt(1000)
13681368
Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5)
13691369
Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool()
1370-
Webhook.Types = []string{"gitea", "gogs", "slack"}
1370+
Webhook.Types = []string{"gitea", "gogs", "slack", "discord"}
13711371
Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10)
13721372
}
13731373

options/locale/locale_en-US.ini

+3
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,8 @@ settings.content_type = Content Type
879879
settings.secret = Secret
880880
settings.slack_username = Username
881881
settings.slack_icon_url = Icon URL
882+
settings.discord_username = Username
883+
settings.discord_icon_url = Icon URL
882884
settings.slack_color = Color
883885
settings.event_desc = When should this webhook be triggered?
884886
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
902904
settings.slack_token = Token
903905
settings.slack_domain = Domain
904906
settings.slack_channel = Channel
907+
settings.add_discord_hook_desc = Add <a href="%s">Discord</a> integration to your repository.
905908
settings.deploy_keys = Deploy Keys
906909
settings.add_deploy_key = Add Deploy Key
907910
settings.deploy_key_desc = Deploy keys have read-only access. They are not the same as personal account SSH keys.

public/img/discord.png

1.52 KB
Loading

0 commit comments

Comments
 (0)