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

+
+ {{.CsrfTokenHtml}} +
+ + +
+
+ + +
+
+ + +
+ {{template "repo/settings/hook_settings" .}} +
+{{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" .}}