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 000000000..db0e70d5d
Binary files /dev/null and b/public/img/discord.png differ
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}}
+ +{{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" .}}