ソースを参照

webhook: add native Discord support

Unknwon 7 年 前
コミット
a07b1f630a

+ 4 - 0
cmd/web.go

@@ -407,9 +407,11 @@ func runWeb(ctx *cli.Context) error {
 					m.Get("/:type/new", repo.WebhooksNew)
 					m.Post("/gogs/new", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksNewPost)
 					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("/gogs/:id", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksEditPost)
 					m.Post("/slack/:id", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksEditPost)
+					m.Post("/discord/:id", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
 				})
 
 				m.Route("/delete", "GET,POST", org.SettingsDelete)
@@ -457,10 +459,12 @@ func runWeb(ctx *cli.Context) error {
 				m.Get("/:type/new", repo.WebhooksNew)
 				m.Post("/gogs/new", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksNewPost)
 				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("/gogs/:id", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksEditPost)
 				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.SettingsGitHooks)

+ 1 - 0
conf/locale/locale_en-US.ini

@@ -761,6 +761,7 @@ settings.delete_webhook = Delete Webhook
 settings.recent_deliveries = Recent Deliveries
 settings.hook_type = Hook Type
 settings.add_slack_hook_desc = Add <a href="%s">Slack</a> integration to your repository.
+settings.add_discord_hook_desc = Add <a href="%s">Discord</a> integration to your repository.
 settings.slack_token = Token
 settings.slack_domain = Domain
 settings.slack_channel = Channel

+ 1 - 1
gogs.go

@@ -16,7 +16,7 @@ import (
 	"github.com/gogits/gogs/modules/setting"
 )
 
-const APP_VER = "0.9.156.0217"
+const APP_VER = "0.9.157.0218"
 
 func init() {
 	setting.AppVer = APP_VER

+ 15 - 11
models/action.go

@@ -505,26 +505,30 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
 	apiRepo := repo.APIFormat(nil)
 	switch opType {
 	case ACTION_COMMIT_REPO: // Push
+		compareURL := setting.AppUrl + opts.Commits.CompareURL
+		if isNewBranch {
+			compareURL = ""
+			if err = PrepareWebhooks(repo, HOOK_EVENT_CREATE, &api.CreatePayload{
+				Ref:     refName,
+				RefType: "branch",
+				Repo:    apiRepo,
+				Sender:  apiPusher,
+			}); err != nil {
+				return fmt.Errorf("PrepareWebhooks (new branch): %v", err)
+			}
+		}
+
 		if err = PrepareWebhooks(repo, HOOK_EVENT_PUSH, &api.PushPayload{
 			Ref:        opts.RefFullName,
 			Before:     opts.OldCommitID,
 			After:      opts.NewCommitID,
-			CompareURL: setting.AppUrl + opts.Commits.CompareURL,
+			CompareURL: compareURL,
 			Commits:    opts.Commits.ToApiPayloadCommits(repo.HTMLURL()),
 			Repo:       apiRepo,
 			Pusher:     apiPusher,
 			Sender:     apiPusher,
 		}); err != nil {
-			return fmt.Errorf("PrepareWebhooks: %v", err)
-		}
-
-		if isNewBranch {
-			return PrepareWebhooks(repo, HOOK_EVENT_CREATE, &api.CreatePayload{
-				Ref:     refName,
-				RefType: "branch",
-				Repo:    apiRepo,
-				Sender:  apiPusher,
-			})
+			return fmt.Errorf("PrepareWebhooks (new commit): %v", err)
 		}
 
 	case ACTION_PUSH_TAG: // Create

+ 11 - 3
models/webhook.go

@@ -292,6 +292,7 @@ type HookTaskType int
 const (
 	GOGS HookTaskType = iota + 1
 	SLACK
+	DISCORD
 )
 
 var hookTaskTypes = map[string]HookTaskType{
@@ -458,6 +459,10 @@ func PrepareWebhooks(repo *Repository, event HookEventType, p api.Payloader) err
 
 	var payloader api.Payloader
 	for _, w := range ws {
+		if !w.IsActive {
+			continue
+		}
+
 		switch event {
 		case HOOK_EVENT_CREATE:
 			if !w.HasCreateEvent() {
@@ -476,12 +481,15 @@ func PrepareWebhooks(repo *Repository, event HookEventType, p api.Payloader) err
 		// Use separate objects so modifcations won't be made on payload on non-Gogs type hooks.
 		switch w.HookTaskType {
 		case SLACK:
-			// FIXME: dirty fix for buggy support of Discord for Slack-type webhook.
-			// Should remove this if we want to support Discord fully as its own.
-			payloader, err = GetSlackPayload(strings.Contains(w.URL, ".discordapp.com/"), p, event, w.Meta)
+			payloader, err = GetSlackPayload(p, event, w.Meta)
 			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

+ 213 - 0
models/webhook_discord.go

@@ -0,0 +1,213 @@
+// Copyright 2017 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package models
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"github.com/gogits/git-module"
+	api "github.com/gogits/go-gogs-client"
+)
+
+type DiscordEmbedFooterObject struct {
+	Text string `json:"text"`
+}
+
+type DiscordEmbedAuthorObject struct {
+	Name    string `json:"name"`
+	URL     string `json:"url"`
+	IconURL string `json:"icon_url"`
+}
+
+type DiscordEmbedFieldObject struct {
+	Name  string `json:"name"`
+	Value string `json:"value"`
+}
+
+type DiscordEmbedObject struct {
+	Title       string                     `json:"title"`
+	Description string                     `json:"description"`
+	URL         string                     `json:"url"`
+	Footer      *DiscordEmbedFooterObject  `json:"footer"`
+	Author      *DiscordEmbedAuthorObject  `json:"author"`
+	Fields      []*DiscordEmbedFieldObject `json:"fields"`
+}
+
+type DiscordPayload struct {
+	Content   string                `json:"content"`
+	Username  string                `json:"username"`
+	AvatarURL string                `json:"avatar_url"`
+	Embeds    []*DiscordEmbedObject `json:"embeds"`
+}
+
+func (p *DiscordPayload) SetSecret(_ string) {}
+
+func (p *DiscordPayload) JSONPayload() ([]byte, error) {
+	data, err := json.MarshalIndent(p, "", "  ")
+	if err != nil {
+		return []byte{}, err
+	}
+	return data, nil
+}
+
+func DiscordLinkFormatter(url string, text string) string {
+	return fmt.Sprintf("[%s](%s)", text, url)
+}
+
+func DiscordSHALinkFormatter(url string, text string) string {
+	return fmt.Sprintf("[`%s`](%s)", text, url)
+}
+
+func getDiscordCreatePayload(p *api.CreatePayload, slack *SlackMeta) (*DiscordPayload, error) {
+	// Created tag/branch
+	refName := git.RefEndName(p.Ref)
+
+	repoLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
+	refLink := DiscordLinkFormatter(p.Repo.HTMLURL+"/src/"+refName, refName)
+	content := fmt.Sprintf("Created new %s: %s/%s", p.RefType, repoLink, refLink)
+
+	return &DiscordPayload{
+		Username:  slack.Username,
+		AvatarURL: slack.IconURL,
+		Embeds: []*DiscordEmbedObject{{
+			Description: content,
+			Author: &DiscordEmbedAuthorObject{
+				Name:    p.Sender.UserName,
+				IconURL: p.Sender.AvatarUrl,
+			},
+		}},
+	}, nil
+}
+
+func getDiscordPushPayload(p *api.PushPayload, slack *SlackMeta) (*DiscordPayload, error) {
+	// n new commits
+	var (
+		branchName   = git.RefEndName(p.Ref)
+		commitDesc   string
+		commitString string
+	)
+
+	if len(p.Commits) == 1 {
+		commitDesc = "1 new commit"
+	} else {
+		commitDesc = fmt.Sprintf("%d new commits", len(p.Commits))
+	}
+
+	if len(p.CompareURL) > 0 {
+		commitString = DiscordLinkFormatter(p.CompareURL, commitDesc)
+	} else {
+		commitString = commitDesc
+	}
+
+	repoLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
+	branchLink := DiscordLinkFormatter(p.Repo.HTMLURL+"/src/"+branchName, branchName)
+	content := fmt.Sprintf("Pushed %s to %s/%s:\n", commitString, repoLink, branchLink)
+
+	// for each commit, generate attachment text
+	for i, commit := range p.Commits {
+		content += fmt.Sprintf("%s %s - %s", DiscordSHALinkFormatter(commit.URL, commit.ID[:7]), SlackShortTextFormatter(commit.Message), commit.Author.Name)
+		// add linebreak to each commit but the last
+		if i < len(p.Commits)-1 {
+			content += "\n"
+		}
+	}
+
+	return &DiscordPayload{
+		Username:  slack.Username,
+		AvatarURL: slack.IconURL,
+		Embeds: []*DiscordEmbedObject{{
+			Description: content,
+			Author: &DiscordEmbedAuthorObject{
+				Name:    p.Sender.UserName,
+				IconURL: p.Sender.AvatarUrl,
+			},
+		}},
+	}, nil
+}
+
+func getDiscordPullRequestPayload(p *api.PullRequestPayload, slack *SlackMeta) (*DiscordPayload, error) {
+	title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
+	url := fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index)
+	content := ""
+	fields := make([]*DiscordEmbedFieldObject, 0, 1)
+	switch p.Action {
+	case api.HOOK_ISSUE_OPENED:
+		title = "New pull request: " + title
+		content = p.PullRequest.Body
+	case api.HOOK_ISSUE_CLOSED:
+		if p.PullRequest.HasMerged {
+			title = "Pull request merged: " + title
+		} else {
+			title = "Pull request closed: " + title
+		}
+	case api.HOOK_ISSUE_REOPENED:
+		title = "Pull request re-opened: " + title
+	case api.HOOK_ISSUE_EDITED:
+		title = "Pull request edited: " + title
+		content = p.PullRequest.Body
+	case api.HOOK_ISSUE_ASSIGNED:
+		title = "Pull request assigned: " + title
+		fields = []*DiscordEmbedFieldObject{{
+			Name:  "New Assignee",
+			Value: p.PullRequest.Assignee.UserName,
+		}}
+	case api.HOOK_ISSUE_UNASSIGNED:
+		title = "Pull request unassigned: " + title
+	case api.HOOK_ISSUE_LABEL_UPDATED:
+		title = "Pull request labels updated: " + title
+		labels := make([]string, len(p.PullRequest.Labels))
+		for i := range p.PullRequest.Labels {
+			labels[i] = p.PullRequest.Labels[i].Name
+		}
+		fields = []*DiscordEmbedFieldObject{{
+			Name:  "Labels",
+			Value: strings.Join(labels, ", "),
+		}}
+	case api.HOOK_ISSUE_LABEL_CLEARED:
+		title = "Pull request labels cleared: " + title
+	case api.HOOK_ISSUE_SYNCHRONIZED:
+		title = "Pull request synchronized: " + title
+	}
+
+	return &DiscordPayload{
+		Username:  slack.Username,
+		AvatarURL: slack.IconURL,
+		Embeds: []*DiscordEmbedObject{{
+			Title:       title,
+			Description: content,
+			URL:         url,
+			Footer: &DiscordEmbedFooterObject{
+				Text: p.Repository.FullName,
+			},
+			Author: &DiscordEmbedAuthorObject{
+				Name:    p.Sender.UserName,
+				IconURL: p.Sender.AvatarUrl,
+			},
+			Fields: fields,
+		}},
+	}, nil
+}
+
+func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*DiscordPayload, error) {
+	d := new(DiscordPayload)
+
+	slack := &SlackMeta{}
+	if err := json.Unmarshal([]byte(meta), &slack); err != nil {
+		return d, fmt.Errorf("GetDiscordPayload meta json: %v", err)
+	}
+
+	switch event {
+	case HOOK_EVENT_CREATE:
+		return getDiscordCreatePayload(p.(*api.CreatePayload), slack)
+	case HOOK_EVENT_PUSH:
+		return getDiscordPushPayload(p.(*api.PushPayload), slack)
+	case HOOK_EVENT_PULL_REQUEST:
+		return getDiscordPullRequestPayload(p.(*api.PullRequestPayload), slack)
+	}
+
+	return d, nil
+}

+ 22 - 35
models/webhook_slack.go

@@ -6,7 +6,6 @@ package models
 
 import (
 	"encoding/json"
-	"errors"
 	"fmt"
 	"strings"
 
@@ -23,16 +22,6 @@ type SlackMeta struct {
 	Color    string `json:"color"`
 }
 
-type SlackPayload struct {
-	Channel     string            `json:"channel"`
-	Text        string            `json:"text"`
-	Username    string            `json:"username"`
-	IconURL     string            `json:"icon_url"`
-	UnfurlLinks int               `json:"unfurl_links"`
-	LinkNames   int               `json:"link_names"`
-	Attachments []SlackAttachment `json:"attachments"`
-}
-
 type SlackAttachment struct {
 	Fallback string `json:"fallback"`
 	Color    string `json:"color"`
@@ -40,6 +29,16 @@ type SlackAttachment struct {
 	Text     string `json:"text"`
 }
 
+type SlackPayload struct {
+	Channel     string             `json:"channel"`
+	Text        string             `json:"text"`
+	Username    string             `json:"username"`
+	IconURL     string             `json:"icon_url"`
+	UnfurlLinks int                `json:"unfurl_links"`
+	LinkNames   int                `json:"link_names"`
+	Attachments []*SlackAttachment `json:"attachments"`
+}
+
 func (p *SlackPayload) SetSecret(_ string) {}
 
 func (p *SlackPayload) JSONPayload() ([]byte, error) {
@@ -72,21 +71,13 @@ func SlackLinkFormatter(url string, text string) string {
 	return fmt.Sprintf("<%s|%s>", url, SlackTextFormatter(text))
 }
 
-func replaceBadCharsForDiscord(in string) string {
-	return strings.NewReplacer("[", "", "]", ":", ":", "/").Replace(in)
-}
-
-func getSlackCreatePayload(isDiscord bool, p *api.CreatePayload, slack *SlackMeta) (*SlackPayload, error) {
+func getSlackCreatePayload(p *api.CreatePayload, slack *SlackMeta) (*SlackPayload, error) {
 	// Created tag/branch
 	refName := git.RefEndName(p.Ref)
 
 	repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
 	refLink := SlackLinkFormatter(p.Repo.HTMLURL+"/src/"+refName, refName)
-	format := "[%s:%s] %s created by %s"
-	if isDiscord {
-		format = replaceBadCharsForDiscord(format)
-	}
-	text := fmt.Sprintf(format, repoLink, refLink, p.RefType, p.Sender.UserName)
+	text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName)
 
 	return &SlackPayload{
 		Channel:  slack.Channel,
@@ -96,7 +87,7 @@ func getSlackCreatePayload(isDiscord bool, p *api.CreatePayload, slack *SlackMet
 	}, nil
 }
 
-func getSlackPushPayload(isDiscord bool, p *api.PushPayload, slack *SlackMeta) (*SlackPayload, error) {
+func getSlackPushPayload(p *api.PushPayload, slack *SlackMeta) (*SlackPayload, error) {
 	// n new commits
 	var (
 		branchName   = git.RefEndName(p.Ref)
@@ -117,11 +108,7 @@ func getSlackPushPayload(isDiscord bool, p *api.PushPayload, slack *SlackMeta) (
 
 	repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
 	branchLink := SlackLinkFormatter(p.Repo.HTMLURL+"/src/"+branchName, branchName)
-	format := "[%s:%s] %s pushed by %s"
-	if isDiscord {
-		format = replaceBadCharsForDiscord(format)
-	}
-	text := fmt.Sprintf(format, repoLink, branchLink, commitString, p.Pusher.UserName)
+	text := fmt.Sprintf("[%s:%s] %s pushed by %s", repoLink, branchLink, commitString, p.Pusher.UserName)
 
 	var attachmentText string
 	// for each commit, generate attachment text
@@ -138,14 +125,14 @@ func getSlackPushPayload(isDiscord bool, p *api.PushPayload, slack *SlackMeta) (
 		Text:     text,
 		Username: slack.Username,
 		IconURL:  slack.IconURL,
-		Attachments: []SlackAttachment{{
+		Attachments: []*SlackAttachment{{
 			Color: slack.Color,
 			Text:  attachmentText,
 		}},
 	}, nil
 }
 
-func getSlackPullRequestPayload(isDiscord bool, p *api.PullRequestPayload, slack *SlackMeta) (*SlackPayload, error) {
+func getSlackPullRequestPayload(p *api.PullRequestPayload, slack *SlackMeta) (*SlackPayload, error) {
 	senderLink := SlackLinkFormatter(setting.AppUrl+p.Sender.UserName, p.Sender.UserName)
 	titleLink := SlackLinkFormatter(fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index),
 		fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title))
@@ -185,7 +172,7 @@ func getSlackPullRequestPayload(isDiscord bool, p *api.PullRequestPayload, slack
 		Text:     text,
 		Username: slack.Username,
 		IconURL:  slack.IconURL,
-		Attachments: []SlackAttachment{{
+		Attachments: []*SlackAttachment{{
 			Color: slack.Color,
 			Title: title,
 			Text:  attachmentText,
@@ -193,21 +180,21 @@ func getSlackPullRequestPayload(isDiscord bool, p *api.PullRequestPayload, slack
 	}, nil
 }
 
-func GetSlackPayload(isDiscord bool, p api.Payloader, event HookEventType, meta string) (*SlackPayload, error) {
+func GetSlackPayload(p api.Payloader, event HookEventType, meta string) (*SlackPayload, error) {
 	s := new(SlackPayload)
 
 	slack := &SlackMeta{}
 	if err := json.Unmarshal([]byte(meta), &slack); err != nil {
-		return s, errors.New("GetSlackPayload meta json:" + err.Error())
+		return s, fmt.Errorf("GetSlackPayload meta json: %v", err)
 	}
 
 	switch event {
 	case HOOK_EVENT_CREATE:
-		return getSlackCreatePayload(isDiscord, p.(*api.CreatePayload), slack)
+		return getSlackCreatePayload(p.(*api.CreatePayload), slack)
 	case HOOK_EVENT_PUSH:
-		return getSlackPushPayload(isDiscord, p.(*api.PushPayload), slack)
+		return getSlackPushPayload(p.(*api.PushPayload), slack)
 	case HOOK_EVENT_PULL_REQUEST:
-		return getSlackPullRequestPayload(isDiscord, p.(*api.PullRequestPayload), slack)
+		return getSlackPullRequestPayload(p.(*api.PullRequestPayload), slack)
 	}
 
 	return s, nil

+ 11 - 0
modules/auth/repo_form.go

@@ -173,6 +173,17 @@ func (f *NewSlackHookForm) Validate(ctx *macaron.Context, errs binding.Errors) b
 	return validate(errs, ctx.Data, f, ctx.Locale)
 }
 
+type NewDiscordHookForm struct {
+	PayloadURL string `binding:"Required;Url"`
+	Username   string
+	IconURL    string
+	WebhookForm
+}
+
+func (f *NewDiscordHookForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
+	return validate(errs, ctx.Data, f, ctx.Locale)
+}
+
 // .___
 // |   | ______ ________ __   ____
 // |   |/  ___//  ___/  |  \_/ __ \

ファイルの差分が大きいため隠しています
+ 0 - 0
modules/bindata/bindata.go


+ 1 - 1
modules/setting/setting.go

@@ -821,7 +821,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{"gogs", "slack"}
+	Webhook.Types = []string{"gogs", "slack", "discord"}
 	Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10)
 }
 

+ 11 - 0
public/config.codekit

@@ -84,6 +84,17 @@
 		"outputPathIsSetByUser": 0,
 		"processed": 1
 		},
+	"\/img\/discord.png": {
+		"fileType": 32768,
+		"ignore": 0,
+		"ignoreWasSetByUser": 0,
+		"initialSize": 1559,
+		"inputAbbreviatedPath": "\/img\/discord.png",
+		"outputAbbreviatedPath": "\/img\/discord.png",
+		"outputPathIsOutsideProject": 0,
+		"outputPathIsSetByUser": 0,
+		"processed": 0
+		},
 	"\/img\/favicon.png": {
 		"fileType": 32768,
 		"ignore": 0,

+ 3 - 0
public/css/gogs.css

@@ -2291,6 +2291,9 @@ footer .ui.language .menu {
   margin-left: 20px;
   display: block;
 }
+.repository.settings.webhooks .types .menu .item {
+  padding: 10px !important;
+}
 .repository.settings.webhook .events .column {
   padding-bottom: 0;
 }

BIN
public/img/discord.png


+ 8 - 0
public/less/_repository.less

@@ -1329,6 +1329,14 @@
 			}
 		}
 
+		&.webhooks {
+			.types {
+				.menu .item {
+					padding: 10px !important;
+				}
+			}
+		}
+
 		&.webhook {
 			.events {
 				.column {

+ 94 - 0
routers/repo/webhook.go

@@ -211,6 +211,55 @@ func SlackHooksNewPost(ctx *context.Context, form auth.NewSlackHookForm) {
 	ctx.Redirect(orCtx.Link + "/settings/hooks")
 }
 
+// FIXME: merge logic to Slack
+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.SlackMeta{
+		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.JSON,
+		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")
+}
+
 func checkWebhook(ctx *context.Context) (*OrgRepoCtx, *models.Webhook) {
 	ctx.Data["RequireHighlightJS"] = true
 
@@ -240,6 +289,9 @@ func checkWebhook(ctx *context.Context) (*OrgRepoCtx, *models.Webhook) {
 	case models.SLACK:
 		ctx.Data["SlackHook"] = w.GetSlackHook()
 		ctx.Data["HookType"] = "slack"
+	case models.DISCORD:
+		ctx.Data["SlackHook"] = w.GetSlackHook()
+		ctx.Data["HookType"] = "discord"
 	default:
 		ctx.Data["HookType"] = "gogs"
 	}
@@ -346,6 +398,48 @@ func SlackHooksEditPost(ctx *context.Context, form auth.NewSlackHookForm) {
 	ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID))
 }
 
+// FIXME: merge logic to Slack
+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.SlackMeta{
+		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))
+}
+
 func TestWebhook(ctx *context.Context) {
 	var authorUsername, committerUsername string
 

+ 1 - 1
templates/.VERSION

@@ -1 +1 @@
-0.9.156.0217
+0.9.157.0218

+ 20 - 0
templates/repo/settings/hook_discord.tmpl

@@ -0,0 +1,20 @@
+{{if eq .HookType "discord"}}
+	<p>{{.i18n.Tr "repo.settings.add_discord_hook_desc" "https://discordapp.com/" | Str2html}}</p>
+	<form class="ui form" action="{{.BaseLink}}/settings/hooks/discord/{{if .PageIsSettingsHooksNew}}new{{else}}{{.Webhook.ID}}{{end}}" method="post">
+		{{.CsrfTokenHtml}}
+		<div class="required field {{if .Err_PayloadURL}}error{{end}}">
+			<label for="payload_url">{{.i18n.Tr "repo.settings.payload_url"}}</label>
+			<input id="payload_url" name="payload_url" type="url" value="{{.Webhook.URL}}" autofocus required>
+		</div>
+
+		<div class="field">
+			<label for="username">{{.i18n.Tr "repo.settings.slack_username"}}</label>
+			<input id="username" name="username" value="{{.SlackHook.Username}}" placeholder="e.g. Gogs">
+		</div>
+		<div class="field">
+			<label for="icon_url">{{.i18n.Tr "repo.settings.slack_icon_url"}}</label>
+			<input id="icon_url" name="icon_url" value="{{.SlackHook.IconURL}}" placeholder="e.g. https://example.com/img/favicon.png">
+		</div>
+		{{template "repo/settings/hook_settings" .}}
+	</form>
+{{end}}

+ 6 - 3
templates/repo/settings/hook_list.tmpl

@@ -3,14 +3,17 @@
 	<h4 class="ui top attached header">
 		{{.i18n.Tr "repo.settings.hooks"}}
 		<div class="ui right">
-			<div class="ui floating1 jump dropdown">
+			<div class="ui types jump dropdown">
 				<div class="ui blue tiny button">{{.i18n.Tr "repo.settings.add_webhook"}}</div>
 				<div class="menu">
 					<a class="item" href="{{.BaseLink}}/settings/hooks/gogs/new">
-						<img class="img-10" src="{{AppSubUrl}}/img/favicon.png">Gogs
+						<img class="img-12" src="{{AppSubUrl}}/img/favicon.png">Gogs
 					</a>
 					<a class="item" href="{{.BaseLink}}/settings/hooks/slack/new">
-						<img class="img-10" src="{{AppSubUrl}}/img/slack.png">Slack
+						<img class="img-12" src="{{AppSubUrl}}/img/slack.png">Slack
+					</a>
+					<a class="item" href="{{.BaseLink}}/settings/hooks/discord/new">
+						<img class="img-12" src="{{AppSubUrl}}/img/discord.png">Discord
 					</a>
 				</div>
 			</div>

+ 3 - 2
templates/repo/settings/hook_new.tmpl

@@ -11,14 +11,15 @@
 					<div class="ui right">
 						{{if eq .HookType "gogs"}}
 							<img class="img-13" src="{{AppSubUrl}}/img/favicon.png">
-						{{else if eq .HookType "slack"}}
-							<img class="img-13" src="{{AppSubUrl}}/img/slack.png">
+						{{else}}
+							<img class="img-13" src="{{AppSubUrl}}/img/{{.HookType}}.png">
 						{{end}}
 					</div>
 				</h4>
 				<div class="ui attached segment">
 					{{template "repo/settings/hook_gogs" .}}
 					{{template "repo/settings/hook_slack" .}}
+					{{template "repo/settings/hook_discord" .}}
 				</div>
 
 				{{template "repo/settings/hook_history" .}}

+ 1 - 1
templates/repo/settings/hook_slack.tmpl

@@ -1,5 +1,5 @@
 {{if eq .HookType "slack"}}
-	<p>{{.i18n.Tr "repo.settings.add_slack_hook_desc" "http://slack.com" | Str2html}}</p>
+	<p>{{.i18n.Tr "repo.settings.add_slack_hook_desc" "https://slack.com" | Str2html}}</p>
 	<form class="ui form" action="{{.BaseLink}}/settings/hooks/slack/{{if .PageIsSettingsHooksNew}}new{{else}}{{.Webhook.ID}}{{end}}" method="post">
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_PayloadURL}}error{{end}}">

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません