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 b7e687a46..61840a981 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 000000000..bdb363af7 --- /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 681a478d3..70c9c8b4a 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 c25c2e0c6..a5908baca 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 4a8a39963..fbd96b3b0 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 zcmb7Edo&XY9G)H&d0dp;v2w8MeVH|3lGiqpVP>mYPyKh#`F_9ecYf!5|9t0sUz$G#t*#1E1pol* z-d>(q1+OYbx^1&!FO>{S6a@1>ghMKrnl}Ew5KRfpQ^>TUv6w)BYl7CprQ*R*+bP4d z{&iWJtIY@iU~7`MryGu;wD|n@yeeDG-!&?T(Y5fuTrQ(iMeiG5Xp|7A#B%9-@XptnMd3^YRW!BRa-Md=M|NLpK{9eS{qY# z#+lk91@5qxyHYr42->07qd8BIROvpgNvj)4AUg^$YEEz|%VJ*CO|U5SgN2xu)j-5u|JfpgU@0am_iI zdX8qG;>=E*)BI$sROL*@AH`G2&ih_@2nik7S{_|F%j~gU;kxx;yspl4m86!d(c#p4 zG0^d34V2LXY?TkM+rG4%tu3Q}>NUWpHI5`OJHiNIwzOtX8Z@3g2Q7s+b{^pBUz0;b z3cq!qslOK{pZgLAR>o@fV1Fu{ZCk0UFc ze(I|Br?qji^DFL-@(lxhNBLG*@SP}3$$048byr&3yO@|a-?i#VDpVH8yME=Ib49Y9 zrV~)7!rPV9+eI)9^CwtSgJCltUxzZc8%&`a()-QjB%(=$iUDp{S*C-P4VrT9uv_N% z%RASB{H~Pbup0Q~a3QfkIX1PEcM=C1oETp64U3x%jSaZM^n|F&`yLv9KjrK+VT3H2 z=-TF^Zh5-8!PNW>mvV+x)V#3GJ9V6L6ULEZ$C^y7>_qgDhd>Y=#gPgOJ@!+G+v9rs z^p^zz$!zJ}r?%NE!dzjh|9S?3FhOl%9W)YYES+}b)(88|lx*QY=h3ahlRoxqi%x0M zgzME?_+t$F`%Q-aJzoBiU}YP){7d4&BMB|HhDRH2uLPsny0_^1n(U*!nEj=A#}KeR zM~|JHKB*y|{1l!}#DRXe{@j5mr3#!VAj*Dh$ECNInQO89Hs!m5#~62Lgdvq5IeO%Xkx5BIYTG(SrV_cTr4=Y(nr#cn`J$2PFYMk5ZoWN zdV%Z$jL5`Rb2?EjNBetIblMxjB2IxBCBJ-D&y5lnO1TRRmi}I(<;@3>vGX4t;DxTA zW8^0Lo-GrBQf@YjXBlxQGyYZHkQUh(mfSrkw$sfw^C$m)87EeE*L3Kr=4MZ=ee%V)+ep>WE;)lU~aV+DuNAs{_e5mfpn5 zC~ipg1GEo?cbMt{LY)t*cg^9~_E?fNoWHp`$mHi;7dYDbssI20 literal 0 HcmV?d00001 diff --git a/routers/repo/webhook.go b/routers/repo/webhook.go index 5489d7185..ade40649b 100644 --- a/routers/repo/webhook.go +++ b/routers/repo/webhook.go @@ -11,16 +11,15 @@ import ( "fmt" "strings" - "github.com/Unknwon/com" - "code.gitea.io/git" - api "code.gitea.io/sdk/gitea" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/sdk/gitea" + + "github.com/Unknwon/com" ) const ( @@ -96,10 +95,18 @@ func WebhooksNew(ctx *context.Context) { return } - ctx.Data["HookType"] = checkHookType(ctx) + hookType := checkHookType(ctx) + ctx.Data["HookType"] = hookType if ctx.Written() { return } + if hookType == "discord" { + ctx.Data["DiscordHook"] = map[string]interface{}{ + "Username": "Gitea", + "IconURL": setting.AppURL + "img/favicon.png", + "Color": 16724530, + } + } ctx.Data["BaseLink"] = orCtx.Link ctx.HTML(200, orCtx.NewTemplate) @@ -213,6 +220,55 @@ func GogsHooksNewPost(ctx *context.Context, form auth.NewWebhookForm) { ctx.Redirect(orCtx.Link + "/settings/hooks") } +// DiscordHooksNewPost response for creating discord hook +func DiscordHooksNewPost(ctx *context.Context, form auth.NewDiscordHookForm) { + ctx.Data["Title"] = ctx.Tr("repo.settings") + ctx.Data["PageIsSettingsHooks"] = true + ctx.Data["PageIsSettingsHooksNew"] = true + ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}} + + orCtx, err := getOrgRepoCtx(ctx) + if err != nil { + ctx.Handle(500, "getOrgRepoCtx", err) + return + } + + if ctx.HasError() { + ctx.HTML(200, orCtx.NewTemplate) + return + } + + meta, err := json.Marshal(&models.DiscordMeta{ + Username: form.Username, + IconURL: form.IconURL, + }) + if err != nil { + ctx.Handle(500, "Marshal", err) + return + } + + w := &models.Webhook{ + RepoID: orCtx.RepoID, + URL: form.PayloadURL, + ContentType: models.ContentTypeJSON, + HookEvent: ParseHookEvent(form.WebhookForm), + IsActive: form.Active, + HookTaskType: models.DISCORD, + Meta: string(meta), + OrgID: orCtx.OrgID, + } + if err := w.UpdateEvent(); err != nil { + ctx.Handle(500, "UpdateEvent", err) + return + } else if err := models.CreateWebhook(w); err != nil { + ctx.Handle(500, "CreateWebhook", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success")) + ctx.Redirect(orCtx.Link + "/settings/hooks") +} + // SlackHooksNewPost response for creating slack hook func SlackHooksNewPost(ctx *context.Context, form auth.NewSlackHookForm) { ctx.Data["Title"] = ctx.Tr("repo.settings") @@ -295,6 +351,9 @@ func checkWebhook(ctx *context.Context) (*orgRepoCtx, *models.Webhook) { ctx.Data["HookType"] = "slack" case models.GOGS: ctx.Data["HookType"] = "gogs" + case models.DISCORD: + ctx.Data["DiscordHook"] = w.GetDiscordHook() + ctx.Data["HookType"] = "discord" default: ctx.Data["HookType"] = "gitea" } @@ -443,6 +502,48 @@ func SlackHooksEditPost(ctx *context.Context, form auth.NewSlackHookForm) { ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID)) } +// DiscordHooksEditPost response for editing discord hook +func DiscordHooksEditPost(ctx *context.Context, form auth.NewDiscordHookForm) { + ctx.Data["Title"] = ctx.Tr("repo.settings") + ctx.Data["PageIsSettingsHooks"] = true + ctx.Data["PageIsSettingsHooksEdit"] = true + + orCtx, w := checkWebhook(ctx) + if ctx.Written() { + return + } + ctx.Data["Webhook"] = w + + if ctx.HasError() { + ctx.HTML(200, orCtx.NewTemplate) + return + } + + meta, err := json.Marshal(&models.DiscordMeta{ + Username: form.Username, + IconURL: form.IconURL, + }) + if err != nil { + ctx.Handle(500, "Marshal", err) + return + } + + w.URL = form.PayloadURL + w.Meta = string(meta) + w.HookEvent = ParseHookEvent(form.WebhookForm) + w.IsActive = form.Active + if err := w.UpdateEvent(); err != nil { + ctx.Handle(500, "UpdateEvent", err) + return + } else if err := models.UpdateWebhook(w); err != nil { + ctx.Handle(500, "UpdateWebhook", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success")) + ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID)) +} + // TestWebhook test if web hook is work fine func TestWebhook(ctx *context.Context) { // Grab latest commit or fake one if it's empty repository. diff --git a/routers/routes/routes.go b/routers/routes/routes.go index d765c4c03..c619c8b5a 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -442,11 +442,13 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/gitea/new", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksNewPost) m.Post("/gogs/new", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost) m.Post("/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost) + m.Post("/discord/new", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksNewPost) m.Get("/:id", repo.WebHooksEdit) m.Post("/:id/test", repo.TestWebhook) m.Post("/gitea/:id", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksEditPost) m.Post("/gogs/:id", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost) m.Post("/slack/:id", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksEditPost) + m.Post("/discord/:id", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksEditPost) m.Group("/git", func() { m.Get("", repo.GitHooks) diff --git a/templates/repo/settings/hook_discord.tmpl b/templates/repo/settings/hook_discord.tmpl new file mode 100644 index 000000000..901e7e631 --- /dev/null +++ b/templates/repo/settings/hook_discord.tmpl @@ -0,0 +1,19 @@ +{{if eq .HookType "discord"}} +

{{.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 45ef70eca..dce343909 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 cbb52680c..a40eb9e42 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" .}}