From ced50e0ec13085504fa19c82f018a2eecb70ff68 Mon Sep 17 00:00:00 2001
From: Lunny Xiao
Date: Mon, 28 Aug 2017 13:06:45 +0800
Subject: [PATCH] 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
---
models/webhook.go | 33 ++-
models/webhook_discord.go | 252 ++++++++++++++++++++++
modules/auth/repo_form.go | 13 ++
modules/setting/setting.go | 2 +-
options/locale/locale_en-US.ini | 3 +
public/img/discord.png | Bin 0 -> 1559 bytes
routers/repo/webhook.go | 111 +++++++++-
routers/routes/routes.go | 2 +
templates/repo/settings/hook_discord.tmpl | 19 ++
templates/repo/settings/hook_list.tmpl | 3 +
templates/repo/settings/hook_new.tmpl | 3 +
11 files changed, 427 insertions(+), 14 deletions(-)
create mode 100644 models/webhook_discord.go
create mode 100644 public/img/discord.png
create mode 100644 templates/repo/settings/hook_discord.tmpl
diff --git a/models/webhook.go b/models/webhook.go
index b7e687a461..61840a9811 100644
--- a/models/webhook.go
+++ b/models/webhook.go
@@ -13,15 +13,14 @@ import (
"strings"
"time"
- "github.com/go-xorm/xorm"
- gouuid "github.com/satori/go.uuid"
-
- api "code.gitea.io/sdk/gitea"
-
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/sync"
+ api "code.gitea.io/sdk/gitea"
+
+ "github.com/go-xorm/xorm"
+ gouuid "github.com/satori/go.uuid"
)
// HookQueue is a global queue of web hooks
@@ -150,6 +149,15 @@ func (w *Webhook) GetSlackHook() *SlackMeta {
return s
}
+// GetDiscordHook returns discord metadata
+func (w *Webhook) GetDiscordHook() *DiscordMeta {
+ s := &DiscordMeta{}
+ if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
+ log.Error(4, "webhook.GetDiscordHook(%d): %v", w.ID, err)
+ }
+ return s
+}
+
// History returns history of webhook by given conditions.
func (w *Webhook) History(page int) ([]*HookTask, error) {
return HookTasks(w.ID, page)
@@ -314,12 +322,14 @@ const (
GOGS HookTaskType = iota + 1
SLACK
GITEA
+ DISCORD
)
var hookTaskTypes = map[string]HookTaskType{
- "gitea": GITEA,
- "gogs": GOGS,
- "slack": SLACK,
+ "gitea": GITEA,
+ "gogs": GOGS,
+ "slack": SLACK,
+ "discord": DISCORD,
}
// ToHookTaskType returns HookTaskType by given name.
@@ -336,6 +346,8 @@ func (t HookTaskType) Name() string {
return "gogs"
case SLACK:
return "slack"
+ case DISCORD:
+ return "discord"
}
return ""
}
@@ -515,6 +527,11 @@ func PrepareWebhooks(repo *Repository, event HookEventType, p api.Payloader) err
if err != nil {
return fmt.Errorf("GetSlackPayload: %v", err)
}
+ case DISCORD:
+ payloader, err = GetDiscordPayload(p, event, w.Meta)
+ if err != nil {
+ return fmt.Errorf("GetDiscordPayload: %v", err)
+ }
default:
p.SetSecret(w.Secret)
payloader = p
diff --git a/models/webhook_discord.go b/models/webhook_discord.go
new file mode 100644
index 0000000000..bdb363af73
--- /dev/null
+++ b/models/webhook_discord.go
@@ -0,0 +1,252 @@
+package models
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "code.gitea.io/git"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/sdk/gitea"
+)
+
+type (
+ // DiscordEmbedFooter for Embed Footer Structure.
+ DiscordEmbedFooter struct {
+ Text string `json:"text"`
+ }
+
+ // DiscordEmbedAuthor for Embed Author Structure
+ DiscordEmbedAuthor struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ IconURL string `json:"icon_url"`
+ }
+
+ // DiscordEmbedField for Embed Field Structure
+ DiscordEmbedField struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+ }
+
+ // DiscordEmbed is for Embed Structure
+ DiscordEmbed struct {
+ Title string `json:"title"`
+ Description string `json:"description"`
+ URL string `json:"url"`
+ Color int `json:"color"`
+ Footer DiscordEmbedFooter `json:"footer"`
+ Author DiscordEmbedAuthor `json:"author"`
+ Fields []DiscordEmbedField `json:"fields"`
+ }
+
+ // DiscordPayload represents
+ DiscordPayload struct {
+ Wait bool `json:"wait"`
+ Content string `json:"content"`
+ Username string `json:"username"`
+ AvatarURL string `json:"avatar_url"`
+ TTS bool `json:"tts"`
+ Embeds []DiscordEmbed `json:"embeds"`
+ }
+
+ // DiscordMeta contains the discord metadata
+ DiscordMeta struct {
+ Username string `json:"username"`
+ IconURL string `json:"icon_url"`
+ }
+)
+
+func color(clr string) int {
+ if clr != "" {
+ clr = strings.TrimLeft(clr, "#")
+ if s, err := strconv.ParseInt(clr, 16, 32); err == nil {
+ return int(s)
+ }
+ }
+
+ return 0
+}
+
+var (
+ successColor = color("1ac600")
+ warnColor = color("ffd930")
+ failedColor = color("ff3232")
+)
+
+// SetSecret sets the discord secret
+func (p *DiscordPayload) SetSecret(_ string) {}
+
+// JSONPayload Marshals the DiscordPayload to json
+func (p *DiscordPayload) JSONPayload() ([]byte, error) {
+ data, err := json.MarshalIndent(p, "", " ")
+ if err != nil {
+ return []byte{}, err
+ }
+ return data, nil
+}
+
+func getDiscordCreatePayload(p *api.CreatePayload, meta *DiscordMeta) (*DiscordPayload, error) {
+ // created tag/branch
+ refName := git.RefEndName(p.Ref)
+ title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
+
+ return &DiscordPayload{
+ Username: meta.Username,
+ AvatarURL: meta.IconURL,
+ Embeds: []DiscordEmbed{
+ {
+ Title: title,
+ URL: p.Repo.HTMLURL + "/src/" + refName,
+ Color: successColor,
+ Author: DiscordEmbedAuthor{
+ Name: p.Sender.UserName,
+ URL: setting.AppURL + p.Sender.UserName,
+ IconURL: p.Sender.AvatarURL,
+ },
+ },
+ },
+ }, nil
+}
+
+func getDiscordPushPayload(p *api.PushPayload, meta *DiscordMeta) (*DiscordPayload, error) {
+ var (
+ branchName = git.RefEndName(p.Ref)
+ commitDesc string
+ )
+
+ var titleLink string
+ if len(p.Commits) == 1 {
+ commitDesc = "1 new commit"
+ titleLink = p.Commits[0].URL
+ } else {
+ commitDesc = fmt.Sprintf("%d new commits", len(p.Commits))
+ titleLink = p.CompareURL
+ }
+ 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 {
+ text += fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL,
+ strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name)
+ // add linebreak to each commit but the last
+ if i < len(p.Commits)-1 {
+ text += "\n"
+ }
+ }
+
+ fmt.Println(text)
+
+ return &DiscordPayload{
+ Username: meta.Username,
+ AvatarURL: meta.IconURL,
+ Embeds: []DiscordEmbed{
+ {
+ Title: title,
+ Description: text,
+ URL: titleLink,
+ Color: successColor,
+ Author: DiscordEmbedAuthor{
+ Name: p.Sender.UserName,
+ URL: setting.AppURL + p.Sender.UserName,
+ IconURL: p.Sender.AvatarURL,
+ },
+ },
+ },
+ }, nil
+}
+
+func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta) (*DiscordPayload, error) {
+ var text, title string
+ var color int
+ 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
+ color = warnColor
+ case api.HookIssueClosed:
+ if p.PullRequest.HasMerged {
+ title = fmt.Sprintf("[%s] Pull request merged: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
+ color = successColor
+ } else {
+ title = fmt.Sprintf("[%s] Pull request closed: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
+ color = failedColor
+ }
+ 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
+ color = warnColor
+ case api.HookIssueEdited:
+ title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
+ text = p.PullRequest.Body
+ color = warnColor
+ 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
+ color = successColor
+ case api.HookIssueUnassigned:
+ title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
+ text = p.PullRequest.Body
+ color = warnColor
+ 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
+ color = warnColor
+ 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
+ color = warnColor
+ case api.HookIssueSynchronized:
+ title = fmt.Sprintf("[%s] Pull request synchronized: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
+ text = p.PullRequest.Body
+ color = warnColor
+ }
+
+ return &DiscordPayload{
+ Username: meta.Username,
+ AvatarURL: meta.IconURL,
+ Embeds: []DiscordEmbed{
+ {
+ Title: title,
+ Description: text,
+ URL: p.PullRequest.HTMLURL,
+ Color: color,
+ Author: DiscordEmbedAuthor{
+ Name: p.Sender.UserName,
+ URL: setting.AppURL + p.Sender.UserName,
+ IconURL: p.Sender.AvatarURL,
+ },
+ },
+ },
+ }, nil
+}
+
+// GetDiscordPayload converts a discord webhook into a DiscordPayload
+func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*DiscordPayload, error) {
+ s := new(DiscordPayload)
+
+ discord := &DiscordMeta{}
+ if err := json.Unmarshal([]byte(meta), &discord); err != nil {
+ return s, errors.New("GetDiscordPayload meta json:" + err.Error())
+ }
+
+ switch event {
+ case HookEventCreate:
+ return getDiscordCreatePayload(p.(*api.CreatePayload), discord)
+ case HookEventPush:
+ return getDiscordPushPayload(p.(*api.PushPayload), discord)
+ case HookEventPullRequest:
+ return getDiscordPullRequestPayload(p.(*api.PullRequestPayload), discord)
+ }
+
+ return s, nil
+}
diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go
index 681a478d3b..70c9c8b4af 100644
--- a/modules/auth/repo_form.go
+++ b/modules/auth/repo_form.go
@@ -183,6 +183,19 @@ func (f *NewSlackHookForm) Validate(ctx *macaron.Context, errs binding.Errors) b
return validate(errs, ctx.Data, f, ctx.Locale)
}
+// NewDiscordHookForm form for creating discord hook
+type NewDiscordHookForm struct {
+ PayloadURL string `binding:"Required;ValidUrl"`
+ Username string
+ IconURL string
+ WebhookForm
+}
+
+// Validate validates the fields
+func (f *NewDiscordHookForm) 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 c25c2e0c65..a5908bacaa 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -1367,7 +1367,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"}
+ Webhook.Types = []string{"gitea", "gogs", "slack", "discord"}
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 4a8a39963f..fbd96b3b00 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -879,6 +879,8 @@ settings.content_type = Content Type
settings.secret = Secret
settings.slack_username = Username
settings.slack_icon_url = Icon URL
+settings.discord_username = Username
+settings.discord_icon_url = Icon URL
settings.slack_color = Color
settings.event_desc = When should this webhook be triggered?
settings.event_push_only = Just the push
event.
@@ -902,6 +904,7 @@ settings.add_slack_hook_desc = Add Slack integration to your re
settings.slack_token = Token
settings.slack_domain = Domain
settings.slack_channel = Channel
+settings.add_discord_hook_desc = Add Discord 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/discord.png b/public/img/discord.png
new file mode 100644
index 0000000000000000000000000000000000000000..db0e70d5d42d5a4e9df0db6491f647b3f33bea76
GIT binary patch
literal 1559
zcmb7^do&Yz0LOp5DCKcQ+_7@7JHpM(gh^i8NQRlMW-YgLJ7nasHH(Pmk&t9=!<5$@
zqP%a^%A+B#uywskSaFMtB4s!C+WKzbb}VgLZ)DWm_3NV*Rf0H790!21A)IJJih1^xc!6MAR8
z?qsMgH=+U9lHlgzNMvo8fBrkS!dx|RvY(6g_!wIo+Xbm{C+XI9#A$!vdq&Ml9EL1C
z{-e%56G3t>8Li~1Hl#H`?R@_P6TjYs6Zk%ebDI_32
zx999FbiZ*&vnhaB8RT0*k(86_%YFZisBcQ#>B?nl<&oy!R-MJL;vJwfVrYKFE-^fe
zp{r=ei=R>bWWJ%ojz&603a8oaf8{J9w-KrYOoa@i$69%7X8o}mJ0~loRi!Fzw%r$e
zY))4qSY6u9^1(IpmnPFy#mrCLx}=oap*VJ106D;%(dfcJ#PVhkMX1{LgQYrG4Dj|(jZIh&!K^Ts$M?3?9`v*Le-a_w^^Mj-^jyMKzENY
zoaOmO*$n-Sn3_@WNG%oi!RX$#MWbybG%!cp@)N=A_J&Io9PqOg3lL{Wf>xizl-vH)
zaoda0!q3bpYZ$KA^YIw2Hzh#tM&Jua{b#N@Fk0S4MZNj1SxZ``I7i#{D?iKu!?T@?
z!v+SmZcuI&BUOx_5a7ClM(!Tg#g5n6B1eqdn@b54!!ku(;;!O!Ytwx=de{-i^wF1h
zuW8qJBqat^qAmrAD7gyJ$?byEL}dTi;DSd$%(Q>B_hq&VOiAAJ(BS(CJKHgRO#WEM
zR(EBSGq-Et#&1gLA)Nfixvg%=qx2g{zKk$Z2RF44Glw2(Lzq~;OsMC4fKJ&K)7_)9
zDD+O`$r_%TXD*4dMaf=kX=w6Tcpc}EzF1}9j7@2^pXXHJ=KALXrdd$J$6j@DpejSO
zc6)RE2+Qhzot{^ht5+ygVINBVCH~OSxTc$f!!@^-{BS&tn@k;5-mz}{fg+O4ai|qv
zit|A%#6qHIKLVmdR+?pZ_7uwPx^aF&pi*GNnSEB1%6dDX(?i`DCGUy^F@C`$C
z?DtJ7L_f*9BbrGZuvvKRBDfy^eqdw=_4ot#NOu&T-0WB3OIQ4G#)4JmN}RDM^hwBF
zLla^nGP47LlrL;UvZF$)dz}kH?LD#YU2N_HD$tnRpPwYhp>
z^gKFpRsD4F<)d6SoiAL6;gb#)N~IZT)6V*K+b86#7gt|*Uy1UKN8Mvbluq7->!5SC
zr=KG0uAi)$)z1FPUPj|L`u?qVUn4QWv#$dw=6LqC?-U~o#$6X#jH#>$2WzTFG_#JA
zR#21d30en&+TdEi-|mQV#|&v@j|pw+;-MFhu#Kh#W68LzgTyq}XW?7#m89}N0<7w5
z2db&7J*6w_n^qAK=(_bivkXj4ka}9^f
zTJqE=BdaYVe&v}VW!R{{.i18n.Tr "repo.settings.add_discord_hook_desc" "https://discordapp.com" | Str2html}}
+
+{{end}}
diff --git a/templates/repo/settings/hook_list.tmpl b/templates/repo/settings/hook_list.tmpl
index 45ef70ecad..dce3439096 100644
--- a/templates/repo/settings/hook_list.tmpl
+++ b/templates/repo/settings/hook_list.tmpl
@@ -14,6 +14,9 @@
Slack
+
+ Discord
+
diff --git a/templates/repo/settings/hook_new.tmpl b/templates/repo/settings/hook_new.tmpl
index cbb52680cd..a40eb9e428 100644
--- a/templates/repo/settings/hook_new.tmpl
+++ b/templates/repo/settings/hook_new.tmpl
@@ -13,6 +13,8 @@
{{else if eq .HookType "slack"}}
+ {{else if eq .HookType "discord"}}
+
{{end}}
@@ -20,6 +22,7 @@
{{template "repo/settings/hook_gitea" .}}
{{template "repo/settings/hook_gogs" .}}
{{template "repo/settings/hook_slack" .}}
+ {{template "repo/settings/hook_discord" .}}
{{template "repo/settings/hook_history" .}}