@@ -284,9 +284,11 @@ func runWeb(*cli.Context) { | |||||
r.Route("/collaboration", "GET,POST", repo.SettingsCollaboration) | r.Route("/collaboration", "GET,POST", repo.SettingsCollaboration) | ||||
r.Get("/hooks", repo.Webhooks) | r.Get("/hooks", repo.Webhooks) | ||||
r.Get("/hooks/new", repo.WebHooksNew) | r.Get("/hooks/new", repo.WebHooksNew) | ||||
r.Post("/hooks/new", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksNewPost) | |||||
r.Post("/hooks/gogs/new", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksNewPost) | |||||
r.Post("/hooks/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost) | |||||
r.Get("/hooks/:id", repo.WebHooksEdit) | r.Get("/hooks/:id", repo.WebHooksEdit) | ||||
r.Post("/hooks/:id", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksEditPost) | |||||
r.Post("/hooks/gogs/:id", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksEditPost) | |||||
r.Post("/hooks/slack/:id", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksEditPost) | |||||
}) | }) | ||||
}, reqSignIn, middleware.RepoAssignment(true), reqTrueOwner) | }, reqSignIn, middleware.RepoAssignment(true), reqTrueOwner) | ||||
@@ -234,6 +234,11 @@ settings.update_webhook = Update Webhook | |||||
settings.update_hook_success = Webhook has been updated. | settings.update_hook_success = Webhook has been updated. | ||||
settings.delete_webhook = Delete Webhook | settings.delete_webhook = Delete Webhook | ||||
settings.recent_deliveries = Recent Deliveries | settings.recent_deliveries = Recent Deliveries | ||||
settings.hook_type = Hook Type | |||||
settings.add_slack_hook_desc = Add <a href="http://slack.com">Slack</a> integration to your repository. | |||||
settings.slack_token = Token | |||||
settings.slack_domain = Domain | |||||
settings.slack_channel = Channel | |||||
[org] | [org] | ||||
org_name_holder = Organization Name | org_name_holder = Organization Name | ||||
@@ -266,14 +266,33 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, | |||||
continue | continue | ||||
} | } | ||||
p.Secret = w.Secret | |||||
CreateHookTask(&HookTask{ | |||||
Type: WEBHOOK, | |||||
Url: w.Url, | |||||
Payload: p, | |||||
ContentType: w.ContentType, | |||||
IsSsl: w.IsSsl, | |||||
}) | |||||
switch w.HookTaskType { | |||||
case SLACK: | |||||
{ | |||||
s, err := GetSlackPayload(p, w.Meta) | |||||
if err != nil { | |||||
return errors.New("action.GetSlackPayload: " + err.Error()) | |||||
} | |||||
CreateHookTask(&HookTask{ | |||||
Type: w.HookTaskType, | |||||
Url: w.Url, | |||||
BasePayload: s, | |||||
ContentType: w.ContentType, | |||||
IsSsl: w.IsSsl, | |||||
}) | |||||
} | |||||
default: | |||||
{ | |||||
p.Secret = w.Secret | |||||
CreateHookTask(&HookTask{ | |||||
Type: w.HookTaskType, | |||||
Url: w.Url, | |||||
BasePayload: p, | |||||
ContentType: w.ContentType, | |||||
IsSsl: w.IsSsl, | |||||
}) | |||||
} | |||||
} | |||||
} | } | ||||
return nil | return nil | ||||
} | } | ||||
@@ -0,0 +1,114 @@ | |||||
// Copyright 2014 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" | |||||
"errors" | |||||
"fmt" | |||||
"strings" | |||||
) | |||||
const ( | |||||
SLACK_COLOR string = "#dd4b39" | |||||
) | |||||
type Slack struct { | |||||
Domain string `json:"domain"` | |||||
Token string `json:"token"` | |||||
Channel string `json:"channel"` | |||||
} | |||||
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 { | |||||
Color string `json:"color"` | |||||
Text string `json:"text"` | |||||
} | |||||
func GetSlackURL(domain string, token string) string { | |||||
return fmt.Sprintf( | |||||
"https://%s.slack.com/services/hooks/incoming-webhook?token=%s", | |||||
domain, | |||||
token, | |||||
) | |||||
} | |||||
func (p SlackPayload) GetJSONPayload() ([]byte, error) { | |||||
data, err := json.Marshal(p) | |||||
if err != nil { | |||||
return []byte{}, err | |||||
} | |||||
return data, nil | |||||
} | |||||
func GetSlackPayload(p *Payload, meta string) (*SlackPayload, error) { | |||||
slack := &Slack{} | |||||
slackPayload := &SlackPayload{} | |||||
if err := json.Unmarshal([]byte(meta), &slack); err != nil { | |||||
return slackPayload, errors.New("GetSlackPayload meta json:" + err.Error()) | |||||
} | |||||
// TODO: handle different payload types: push, new branch, delete branch etc. | |||||
// when they are added to gogs. Only handles push now | |||||
return getSlackPushPayload(p, slack) | |||||
} | |||||
func getSlackPushPayload(p *Payload, slack *Slack) (*SlackPayload, error) { | |||||
// n new commits | |||||
refSplit := strings.Split(p.Ref, "/") | |||||
branchName := refSplit[len(refSplit)-1] | |||||
var commitString string | |||||
// TODO: add commit compare before/after link when gogs adds it | |||||
if len(p.Commits) == 1 { | |||||
commitString = "1 new commit" | |||||
} else { | |||||
commitString = fmt.Sprintf("%d new commits", len(p.Commits)) | |||||
} | |||||
text := fmt.Sprintf("[%s:%s] %s pushed by %s", p.Repo.Name, branchName, commitString, p.Pusher.Name) | |||||
var attachmentText string | |||||
// for each commit, generate attachment text | |||||
for i, commit := range p.Commits { | |||||
attachmentText += fmt.Sprintf("<%s|%s>: %s - %s", commit.Url, commit.Id[:7], SlackFormatter(commit.Message), commit.Author.Name) | |||||
// add linebreak to each commit but the last | |||||
if i < len(p.Commits)-1 { | |||||
attachmentText += "\n" | |||||
} | |||||
} | |||||
slackAttachments := []SlackAttachment{{Color: SLACK_COLOR, Text: attachmentText}} | |||||
return &SlackPayload{ | |||||
Channel: slack.Channel, | |||||
Text: text, | |||||
Username: "gogs", | |||||
IconUrl: "https://raw.githubusercontent.com/gogits/gogs/master/public/img/favicon.png", | |||||
UnfurlLinks: 0, | |||||
LinkNames: 0, | |||||
Attachments: slackAttachments, | |||||
}, nil | |||||
} | |||||
// see: https://api.slack.com/docs/formatting | |||||
func SlackFormatter(s string) string { | |||||
// take only first line of commit | |||||
first := strings.Split(s, "\n")[0] | |||||
// replace & < > | |||||
first = strings.Replace(first, "&", "&", -1) | |||||
first = strings.Replace(first, "<", "<", -1) | |||||
first = strings.Replace(first, ">", ">", -1) | |||||
return first | |||||
} |
@@ -7,6 +7,7 @@ package models | |||||
import ( | import ( | ||||
"encoding/json" | "encoding/json" | ||||
"errors" | "errors" | ||||
"io/ioutil" | |||||
"time" | "time" | ||||
"github.com/gogits/gogs/modules/httplib" | "github.com/gogits/gogs/modules/httplib" | ||||
@@ -33,15 +34,17 @@ type HookEvent struct { | |||||
// Webhook represents a web hook object. | // Webhook represents a web hook object. | ||||
type Webhook struct { | type Webhook struct { | ||||
Id int64 | |||||
RepoId int64 | |||||
Url string `xorm:"TEXT"` | |||||
ContentType HookContentType | |||||
Secret string `xorm:"TEXT"` | |||||
Events string `xorm:"TEXT"` | |||||
*HookEvent `xorm:"-"` | |||||
IsSsl bool | |||||
IsActive bool | |||||
Id int64 | |||||
RepoId int64 | |||||
Url string `xorm:"TEXT"` | |||||
ContentType HookContentType | |||||
Secret string `xorm:"TEXT"` | |||||
Events string `xorm:"TEXT"` | |||||
*HookEvent `xorm:"-"` | |||||
IsSsl bool | |||||
IsActive bool | |||||
HookTaskType HookTaskType | |||||
Meta string `xorm:"TEXT"` // store hook-specific attributes | |||||
} | } | ||||
// GetEvent handles conversion from Events to HookEvent. | // GetEvent handles conversion from Events to HookEvent. | ||||
@@ -52,6 +55,14 @@ func (w *Webhook) GetEvent() { | |||||
} | } | ||||
} | } | ||||
func (w *Webhook) GetSlackHook() *Slack { | |||||
s := &Slack{} | |||||
if err := json.Unmarshal([]byte(w.Meta), s); err != nil { | |||||
log.Error(4, "webhook.GetSlackHook(%d): %v", w.Id, err) | |||||
} | |||||
return s | |||||
} | |||||
// UpdateEvent handles conversion from HookEvent to Events. | // UpdateEvent handles conversion from HookEvent to Events. | ||||
func (w *Webhook) UpdateEvent() error { | func (w *Webhook) UpdateEvent() error { | ||||
data, err := json.Marshal(w.HookEvent) | data, err := json.Marshal(w.HookEvent) | ||||
@@ -119,8 +130,8 @@ func DeleteWebhook(hookId int64) error { | |||||
type HookTaskType int | type HookTaskType int | ||||
const ( | const ( | ||||
WEBHOOK HookTaskType = iota + 1 | |||||
SERVICE | |||||
GOGS HookTaskType = iota + 1 | |||||
SLACK | |||||
) | ) | ||||
type HookEventType string | type HookEventType string | ||||
@@ -152,6 +163,10 @@ type PayloadRepo struct { | |||||
Private bool `json:"private"` | Private bool `json:"private"` | ||||
} | } | ||||
type BasePayload interface { | |||||
GetJSONPayload() ([]byte, error) | |||||
} | |||||
// Payload represents a payload information of hook. | // Payload represents a payload information of hook. | ||||
type Payload struct { | type Payload struct { | ||||
Secret string `json:"secret"` | Secret string `json:"secret"` | ||||
@@ -161,25 +176,33 @@ type Payload struct { | |||||
Pusher *PayloadAuthor `json:"pusher"` | Pusher *PayloadAuthor `json:"pusher"` | ||||
} | } | ||||
func (p Payload) GetJSONPayload() ([]byte, error) { | |||||
data, err := json.Marshal(p) | |||||
if err != nil { | |||||
return []byte{}, err | |||||
} | |||||
return data, nil | |||||
} | |||||
// HookTask represents a hook task. | // HookTask represents a hook task. | ||||
type HookTask struct { | type HookTask struct { | ||||
Id int64 | Id int64 | ||||
Uuid string | Uuid string | ||||
Type HookTaskType | Type HookTaskType | ||||
Url string | Url string | ||||
*Payload `xorm:"-"` | |||||
BasePayload `xorm:"-"` | |||||
PayloadContent string `xorm:"TEXT"` | PayloadContent string `xorm:"TEXT"` | ||||
ContentType HookContentType | ContentType HookContentType | ||||
EventType HookEventType | EventType HookEventType | ||||
IsSsl bool | IsSsl bool | ||||
IsDeliveried bool | |||||
IsDelivered bool | |||||
IsSucceed bool | IsSucceed bool | ||||
} | } | ||||
// CreateHookTask creates a new hook task, | // CreateHookTask creates a new hook task, | ||||
// it handles conversion from Payload to PayloadContent. | // it handles conversion from Payload to PayloadContent. | ||||
func CreateHookTask(t *HookTask) error { | func CreateHookTask(t *HookTask) error { | ||||
data, err := json.Marshal(t.Payload) | |||||
data, err := t.BasePayload.GetJSONPayload() | |||||
if err != nil { | if err != nil { | ||||
return err | return err | ||||
} | } | ||||
@@ -198,7 +221,7 @@ func UpdateHookTask(t *HookTask) error { | |||||
// DeliverHooks checks and delivers undelivered hooks. | // DeliverHooks checks and delivers undelivered hooks. | ||||
func DeliverHooks() { | func DeliverHooks() { | ||||
timeout := time.Duration(setting.WebhookDeliverTimeout) * time.Second | timeout := time.Duration(setting.WebhookDeliverTimeout) * time.Second | ||||
x.Where("is_deliveried=?", false).Iterate(new(HookTask), | |||||
x.Where("is_delivered=?", false).Iterate(new(HookTask), | |||||
func(idx int, bean interface{}) error { | func(idx int, bean interface{}) error { | ||||
t := bean.(*HookTask) | t := bean.(*HookTask) | ||||
req := httplib.Post(t.Url).SetTimeout(timeout, timeout). | req := httplib.Post(t.Url).SetTimeout(timeout, timeout). | ||||
@@ -212,13 +235,36 @@ func DeliverHooks() { | |||||
req.Param("payload", t.PayloadContent) | req.Param("payload", t.PayloadContent) | ||||
} | } | ||||
t.IsDeliveried = true | |||||
t.IsDelivered = true | |||||
// TODO: record response. | // TODO: record response. | ||||
if _, err := req.Response(); err != nil { | |||||
log.Error(4, "Delivery: %v", err) | |||||
} else { | |||||
t.IsSucceed = true | |||||
switch t.Type { | |||||
case GOGS: | |||||
{ | |||||
if _, err := req.Response(); err != nil { | |||||
log.Error(4, "Delivery: %v", err) | |||||
} else { | |||||
t.IsSucceed = true | |||||
} | |||||
} | |||||
case SLACK: | |||||
{ | |||||
if res, err := req.Response(); err != nil { | |||||
log.Error(4, "Delivery: %v", err) | |||||
} else { | |||||
defer res.Body.Close() | |||||
contents, err := ioutil.ReadAll(res.Body) | |||||
if err != nil { | |||||
log.Error(4, "%s", err) | |||||
} else { | |||||
if string(contents) != "ok" { | |||||
log.Error(4, "slack failed with: %s", string(contents)) | |||||
} else { | |||||
t.IsSucceed = true | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | } | ||||
if err := UpdateHookTask(t); err != nil { | if err := UpdateHookTask(t); err != nil { | ||||
@@ -69,17 +69,31 @@ func (f *RepoSettingForm) Validate(ctx *macaron.Context, errs *binding.Errors, l | |||||
// \/ \/ \/ \/ \/ \/ | // \/ \/ \/ \/ \/ \/ | ||||
type NewWebhookForm struct { | type NewWebhookForm struct { | ||||
PayloadUrl string `form:"payload_url" binding:"Required;Url"` | |||||
ContentType string `form:"content_type" binding:"Required"` | |||||
Secret string `form:"secret"` | |||||
PushOnly bool `form:"push_only"` | |||||
Active bool `form:"active"` | |||||
HookTaskType string `form:"hook_type" binding:"Required"` | |||||
PayloadUrl string `form:"payload_url" binding:"Required;Url"` | |||||
ContentType string `form:"content_type" binding:"Required"` | |||||
Secret string `form:"secret"` | |||||
PushOnly bool `form:"push_only"` | |||||
Active bool `form:"active"` | |||||
} | } | ||||
func (f *NewWebhookForm) Validate(ctx *macaron.Context, errs *binding.Errors, l i18n.Locale) { | func (f *NewWebhookForm) Validate(ctx *macaron.Context, errs *binding.Errors, l i18n.Locale) { | ||||
validate(errs, ctx.Data, f, l) | validate(errs, ctx.Data, f, l) | ||||
} | } | ||||
type NewSlackHookForm struct { | |||||
HookTaskType string `form:"hook_type" binding:"Required"` | |||||
Domain string `form:"domain" binding:"Required` | |||||
Token string `form:"token" binding:"Required"` | |||||
Channel string `form:"channel" binding:"Required"` | |||||
PushOnly bool `form:"push_only"` | |||||
Active bool `form:"active"` | |||||
} | |||||
func (f *NewSlackHookForm) Validate(ctx *macaron.Context, errs *binding.Errors, l i18n.Locale) { | |||||
validate(errs, ctx.Data, f, l) | |||||
} | |||||
// .___ | // .___ | ||||
// | | ______ ________ __ ____ | // | | ______ ________ __ ____ | ||||
// | |/ ___// ___/ | \_/ __ \ | // | |/ ___// ___/ | \_/ __ \ | ||||
@@ -1403,14 +1403,16 @@ The register and sign-in page style | |||||
#auth-setting-form, | #auth-setting-form, | ||||
#org-setting-form, | #org-setting-form, | ||||
#repo-setting-form, | #repo-setting-form, | ||||
#user-profile-form { | |||||
#user-profile-form, | |||||
.repo-setting-form { | |||||
background-color: #FFF; | background-color: #FFF; | ||||
padding: 30px 0; | padding: 30px 0; | ||||
} | } | ||||
#auth-setting-form textarea, | #auth-setting-form textarea, | ||||
#org-setting-form textarea, | #org-setting-form textarea, | ||||
#repo-setting-form textarea, | #repo-setting-form textarea, | ||||
#user-profile-form textarea { | |||||
#user-profile-form textarea, | |||||
.repo-setting-form textarea { | |||||
margin-left: 4px; | margin-left: 4px; | ||||
height: 100px; | height: 100px; | ||||
} | } | ||||
@@ -1418,24 +1420,38 @@ The register and sign-in page style | |||||
#org-setting-form label, | #org-setting-form label, | ||||
#repo-setting-form label, | #repo-setting-form label, | ||||
#user-profile-form label, | #user-profile-form label, | ||||
.repo-setting-form label, | |||||
#auth-setting-form .form-label, | #auth-setting-form .form-label, | ||||
#org-setting-form .form-label, | #org-setting-form .form-label, | ||||
#repo-setting-form .form-label, | #repo-setting-form .form-label, | ||||
#user-profile-form .form-label { | |||||
#user-profile-form .form-label, | |||||
.repo-setting-form .form-label { | |||||
width: 240px; | width: 240px; | ||||
} | } | ||||
#auth-setting-form .ipt, | #auth-setting-form .ipt, | ||||
#org-setting-form .ipt, | #org-setting-form .ipt, | ||||
#repo-setting-form .ipt, | #repo-setting-form .ipt, | ||||
#user-profile-form .ipt { | |||||
#user-profile-form .ipt, | |||||
.repo-setting-form .ipt { | |||||
width: 360px; | width: 360px; | ||||
} | } | ||||
#auth-setting-form .field, | #auth-setting-form .field, | ||||
#org-setting-form .field, | #org-setting-form .field, | ||||
#repo-setting-form .field, | #repo-setting-form .field, | ||||
#user-profile-form .field { | |||||
#user-profile-form .field, | |||||
.repo-setting-form .field { | |||||
margin-bottom: 24px; | margin-bottom: 24px; | ||||
} | } | ||||
#hook-type { | |||||
padding: 10px 0 0 0; | |||||
background-color: #fff; | |||||
} | |||||
#hook-type .field { | |||||
margin-bottom: 24px; | |||||
} | |||||
#hook-type label { | |||||
width: 240px; | |||||
} | |||||
#repo-hooks-panel, | #repo-hooks-panel, | ||||
#repo-hooks-history-panel, | #repo-hooks-history-panel, | ||||
#user-social-panel, | #user-social-panel, | ||||
@@ -359,6 +359,22 @@ function initRepoSetting() { | |||||
return true; | return true; | ||||
} | } | ||||
}); | }); | ||||
// web hook type change | |||||
$('select#hook-type').on("change", function () { | |||||
hookTypes = ['Gogs','Slack']; | |||||
var curHook = $(this).val(); | |||||
hookTypes.forEach(function(hookType) { | |||||
if (curHook === hookType) { | |||||
$('div#'+hookType.toLowerCase()).toggleShow(); | |||||
} | |||||
else { | |||||
$('div#'+hookType.toLowerCase()).toggleHide(); | |||||
} | |||||
}); | |||||
}); | |||||
$('#transfer-button').click(function () { | $('#transfer-button').click(function () { | ||||
$('#transfer-form').show(); | $('#transfer-form').show(); | ||||
}); | }); | ||||
@@ -594,4 +610,4 @@ function homepage() { | |||||
} | } | ||||
$('#promo-form').attr('action', '/user/sign_up'); | $('#promo-form').attr('action', '/user/sign_up'); | ||||
}); | }); | ||||
} | |||||
} |
@@ -34,7 +34,8 @@ | |||||
#auth-setting-form, | #auth-setting-form, | ||||
#org-setting-form, | #org-setting-form, | ||||
#repo-setting-form, | #repo-setting-form, | ||||
#user-profile-form { | |||||
#user-profile-form, | |||||
.repo-setting-form { | |||||
background-color: #FFF; | background-color: #FFF; | ||||
padding: 30px 0; | padding: 30px 0; | ||||
textarea { | textarea { | ||||
@@ -53,6 +54,17 @@ | |||||
} | } | ||||
} | } | ||||
#hook-type { | |||||
padding: 10px 0 0 0; | |||||
background-color: #fff; | |||||
.field { | |||||
margin-bottom: 24px; | |||||
} | |||||
label { | |||||
width: 240px; | |||||
} | |||||
} | |||||
#repo-hooks-panel, | #repo-hooks-panel, | ||||
#repo-hooks-history-panel, | #repo-hooks-history-panel, | ||||
#user-social-panel, | #user-social-panel, | ||||
@@ -109,4 +121,4 @@ | |||||
.field { | .field { | ||||
margin-bottom: 24px; | margin-bottom: 24px; | ||||
} | } | ||||
} | |||||
} |
@@ -5,6 +5,7 @@ | |||||
package repo | package repo | ||||
import ( | import ( | ||||
"encoding/json" | |||||
"fmt" | "fmt" | ||||
"strings" | "strings" | ||||
"time" | "time" | ||||
@@ -272,11 +273,17 @@ func Webhooks(ctx *middleware.Context) { | |||||
ctx.HTML(200, HOOKS) | ctx.HTML(200, HOOKS) | ||||
} | } | ||||
func renderHookTypes(ctx *middleware.Context) { | |||||
ctx.Data["HookTypes"] = []string{"Gogs", "Slack"} | |||||
ctx.Data["HookType"] = "Gogs" | |||||
} | |||||
func WebHooksNew(ctx *middleware.Context) { | func WebHooksNew(ctx *middleware.Context) { | ||||
ctx.Data["Title"] = ctx.Tr("repo.settings") | ctx.Data["Title"] = ctx.Tr("repo.settings") | ||||
ctx.Data["PageIsSettingsHooks"] = true | ctx.Data["PageIsSettingsHooks"] = true | ||||
ctx.Data["PageIsSettingsHooksNew"] = true | ctx.Data["PageIsSettingsHooksNew"] = true | ||||
ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}} | ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}} | ||||
renderHookTypes(ctx) | |||||
ctx.HTML(200, HOOK_NEW) | ctx.HTML(200, HOOK_NEW) | ||||
} | } | ||||
@@ -304,8 +311,11 @@ func WebHooksNewPost(ctx *middleware.Context, form auth.NewWebhookForm) { | |||||
HookEvent: &models.HookEvent{ | HookEvent: &models.HookEvent{ | ||||
PushOnly: form.PushOnly, | PushOnly: form.PushOnly, | ||||
}, | }, | ||||
IsActive: form.Active, | |||||
IsActive: form.Active, | |||||
HookTaskType: models.GOGS, | |||||
Meta: "", | |||||
} | } | ||||
if err := w.UpdateEvent(); err != nil { | if err := w.UpdateEvent(); err != nil { | ||||
ctx.Handle(500, "UpdateEvent", err) | ctx.Handle(500, "UpdateEvent", err) | ||||
return | return | ||||
@@ -338,6 +348,19 @@ func WebHooksEdit(ctx *middleware.Context) { | |||||
} | } | ||||
return | return | ||||
} | } | ||||
// set data per HookTaskType | |||||
switch w.HookTaskType { | |||||
case models.SLACK: | |||||
{ | |||||
ctx.Data["SlackHook"] = w.GetSlackHook() | |||||
ctx.Data["HookType"] = "slack" | |||||
} | |||||
default: | |||||
{ | |||||
ctx.Data["HookType"] = "gogs" | |||||
} | |||||
} | |||||
w.GetEvent() | w.GetEvent() | ||||
ctx.Data["Webhook"] = w | ctx.Data["Webhook"] = w | ||||
ctx.HTML(200, HOOK_NEW) | ctx.HTML(200, HOOK_NEW) | ||||
@@ -394,3 +417,104 @@ func WebHooksEditPost(ctx *middleware.Context, form auth.NewWebhookForm) { | |||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success")) | ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success")) | ||||
ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", ctx.Repo.RepoLink, hookId)) | ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", ctx.Repo.RepoLink, hookId)) | ||||
} | } | ||||
func SlackHooksNewPost(ctx *middleware.Context, form auth.NewSlackHookForm) { | |||||
ctx.Data["Title"] = ctx.Tr("repo.settings") | |||||
ctx.Data["PageIsSettingsHooks"] = true | |||||
ctx.Data["PageIsSettingsHooksNew"] = true | |||||
ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}} | |||||
if ctx.HasError() { | |||||
ctx.HTML(200, HOOK_NEW) | |||||
return | |||||
} | |||||
meta, err := json.Marshal(&models.Slack{ | |||||
Domain: form.Domain, | |||||
Channel: form.Channel, | |||||
Token: form.Token, | |||||
}) | |||||
if err != nil { | |||||
ctx.Handle(500, "SlackHooksNewPost: JSON marshal failed: ", err) | |||||
return | |||||
} | |||||
w := &models.Webhook{ | |||||
RepoId: ctx.Repo.Repository.Id, | |||||
Url: models.GetSlackURL(form.Domain, form.Token), | |||||
ContentType: models.JSON, | |||||
Secret: "", | |||||
HookEvent: &models.HookEvent{ | |||||
PushOnly: form.PushOnly, | |||||
}, | |||||
IsActive: form.Active, | |||||
HookTaskType: models.SLACK, | |||||
Meta: string(meta), | |||||
} | |||||
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(ctx.Repo.RepoLink + "/settings/hooks") | |||||
} | |||||
func SlackHooksEditPost(ctx *middleware.Context, form auth.NewSlackHookForm) { | |||||
ctx.Data["Title"] = ctx.Tr("repo.settings") | |||||
ctx.Data["PageIsSettingsHooks"] = true | |||||
ctx.Data["PageIsSettingsHooksEdit"] = true | |||||
hookId := com.StrTo(ctx.Params(":id")).MustInt64() | |||||
fmt.Println("hookId slack=%d", hookId) | |||||
if hookId == 0 { | |||||
ctx.Handle(404, "setting.WebHooksEditPost", nil) | |||||
return | |||||
} | |||||
w, err := models.GetWebhookById(hookId) | |||||
if err != nil { | |||||
if err == models.ErrWebhookNotExist { | |||||
ctx.Handle(404, "GetWebhookById", nil) | |||||
} else { | |||||
ctx.Handle(500, "GetWebhookById", err) | |||||
} | |||||
return | |||||
} | |||||
w.GetEvent() | |||||
ctx.Data["Webhook"] = w | |||||
if ctx.HasError() { | |||||
ctx.HTML(200, HOOK_NEW) | |||||
return | |||||
} | |||||
meta, err := json.Marshal(&models.Slack{ | |||||
Domain: form.Domain, | |||||
Channel: form.Channel, | |||||
Token: form.Token, | |||||
}) | |||||
if err != nil { | |||||
ctx.Handle(500, "SlackHooksNewPost: JSON marshal failed: ", err) | |||||
return | |||||
} | |||||
w.Url = models.GetSlackURL(form.Domain, form.Token) | |||||
w.Meta = string(meta) | |||||
w.HookEvent = &models.HookEvent{ | |||||
PushOnly: form.PushOnly, | |||||
} | |||||
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, "SlackHooksEditPost", err) | |||||
return | |||||
} | |||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success")) | |||||
ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", ctx.Repo.RepoLink, hookId)) | |||||
} |
@@ -0,0 +1,23 @@ | |||||
<div id="gogs" class="{{if (and .PageIsSettingsHooksEdit (not (eq .HookType "gogs")))}}hidden{{end}}"> | |||||
<form class="form form-align panel-body repo-setting-form" id="repo-setting-form-gogs" action="{{.RepoLink}}/settings/hooks/gogs/{{if .PageIsSettingsHooksNew}}new{{else}}{{.Webhook.Id}}{{end}}" method="post"> | |||||
{{.CsrfTokenHtml}} | |||||
<input type="hidden" name="hook_type" value="gogs"> | |||||
<div class="text-center panel-desc">{{.i18n.Tr "repo.settings.add_webhook_desc" | Str2html}}</div> | |||||
<div class="field"> | |||||
<label class="req" for="payload-url">{{.i18n.Tr "repo.settings.payload_url"}}</label> | |||||
<input class="ipt ipt-large ipt-radius {{if .Err_UserName}}ipt-error{{end}}" id="payload-url" name="payload_url" type="url" value="{{.Webhook.Url}}" required /> | |||||
</div> | |||||
<div class="field"> | |||||
<label class="req">{{.i18n.Tr "repo.settings.content_type"}}</label> | |||||
<select name="content_type"> | |||||
<option value="1" {{if or .PageIsSettingsHooksNew (eq .Webhook.ContentType 1)}}selected{{end}}>application/json</option> | |||||
<option value="2" {{if eq .Webhook.ContentType 2}}selected{{end}}>application/x-www-form-urlencoded</option> | |||||
</select> | |||||
</div> | |||||
<div class="field"> | |||||
<label for="secret">{{.i18n.Tr "repo.settings.secret"}}</label> | |||||
<input class="ipt ipt-large ipt-radius {{if .Err_UserName}}ipt-error{{end}}" id="secret" name="secret" type="password" value="{{.Webhook.Secret}}" autocomplete="off" /> | |||||
</div> | |||||
{{template "repo/settings/hook_settings" .}} | |||||
</form> | |||||
</div> |
@@ -13,40 +13,9 @@ | |||||
<div class="panel-header"> | <div class="panel-header"> | ||||
<strong>{{if .PageIsSettingsHooksNew}}{{.i18n.Tr "repo.settings.add_webhook"}}{{else}}{{.i18n.Tr "repo.settings.update_webhook"}}{{end}}</strong> | <strong>{{if .PageIsSettingsHooksNew}}{{.i18n.Tr "repo.settings.add_webhook"}}{{else}}{{.i18n.Tr "repo.settings.update_webhook"}}{{end}}</strong> | ||||
</div> | </div> | ||||
<form class="form form-align panel-body" id="repo-setting-form" action="{{.RepoLink}}/settings/hooks/{{if .PageIsSettingsHooksNew}}new{{else}}{{.Webhook.Id}}{{end}}" method="post"> | |||||
{{.CsrfTokenHtml}} | |||||
<div class="text-center panel-desc">{{.i18n.Tr "repo.settings.add_webhook_desc" | Str2html}}</div> | |||||
<div class="field"> | |||||
<label class="req" for="payload-url">{{.i18n.Tr "repo.settings.payload_url"}}</label> | |||||
<input class="ipt ipt-large ipt-radius {{if .Err_UserName}}ipt-error{{end}}" id="payload-url" name="payload_url" type="url" value="{{.Webhook.Url}}" required /> | |||||
</div> | |||||
<div class="field"> | |||||
<label class="req">{{.i18n.Tr "repo.settings.content_type"}}</label> | |||||
<select name="content_type"> | |||||
<option value="1" {{if or .PageIsSettingsHooksNew (eq .Webhook.ContentType 1)}}selected{{end}}>application/json</option> | |||||
<option value="2" {{if eq .Webhook.ContentType 2}}selected{{end}}>application/x-www-form-urlencoded</option> | |||||
</select> | |||||
</div> | |||||
<div class="field"> | |||||
<label for="secret">{{.i18n.Tr "repo.settings.secret"}}</label> | |||||
<input class="ipt ipt-large ipt-radius {{if .Err_UserName}}ipt-error{{end}}" id="secret" name="secret" type="password" value="{{.Webhook.Secret}}" autocomplete="off" /> | |||||
</div> | |||||
<div class="field"> | |||||
<h4 class="text-center">{{.i18n.Tr "repo.settings.event_desc"}}</h4> | |||||
<label></label> | |||||
<input name="push_only" type="radio" {{if or .PageIsSettingsHooksNew .Webhook.PushOnly}}checked{{end}}> {{.i18n.Tr "repo.settings.event_push_only" | Str2html}} | |||||
</div> | |||||
<div class="field"> | |||||
<label for="active">{{.i18n.Tr "repo.settings.active"}}</label> | |||||
<input class="ipt-chk" id="active" name="active" type="checkbox" {{if or .PageIsSettingsHooksNew .Webhook.IsActive}}checked{{end}} /> | |||||
<span>{{.i18n.Tr "repo.settings.active_helper"}}</span> | |||||
</div> | |||||
<div class="field"> | |||||
<label></label> | |||||
<button class="btn btn-green btn-large btn-radius">{{if .PageIsSettingsHooksNew}}{{.i18n.Tr "repo.settings.add_webhook"}}{{else}}{{.i18n.Tr "repo.settings.update_webhook"}}{{end}}</button> | |||||
{{if .PageIsSettingsHooksEdit}}<a class="btn btn-red btn-large btn-link btn-radius" href="{{.RepoLink}}/settings/hooks?remove={{.Webhook.Id}}"><strong>{{.i18n.Tr "repo.settings.delete_webhook"}}</strong></a>{{end}} | |||||
</div> | |||||
</form> | |||||
{{template "repo/settings/hook_types" .}} | |||||
{{template "repo/settings/gogs_hook" .}} | |||||
{{template "repo/settings/slack_hook" .}} | |||||
</div> | </div> | ||||
</div> | </div> | ||||
{{if .PageIsSettingsHooksEdit}} | {{if .PageIsSettingsHooksEdit}} | ||||
@@ -67,4 +36,4 @@ | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
{{template "ng/base/footer" .}} | |||||
{{template "ng/base/footer" .}} |
@@ -0,0 +1,15 @@ | |||||
<div class="field"> | |||||
<h4 class="text-center">{{.i18n.Tr "repo.settings.event_desc"}}</h4> | |||||
<label></label> | |||||
<input name="push_only" type="radio" {{if or .PageIsSettingsHooksNew .Webhook.PushOnly}}checked{{end}}> {{.i18n.Tr "repo.settings.event_push_only" | Str2html}} | |||||
</div> | |||||
<div class="field"> | |||||
<label for="active">{{.i18n.Tr "repo.settings.active"}}</label> | |||||
<input class="ipt-chk" id="active" name="active" type="checkbox" {{if or .PageIsSettingsHooksNew .Webhook.IsActive}}checked{{end}} /> | |||||
<span>{{.i18n.Tr "repo.settings.active_helper"}}</span> | |||||
</div> | |||||
<div class="field"> | |||||
<label></label> | |||||
<button class="btn btn-green btn-large btn-radius">{{if .PageIsSettingsHooksNew}}{{.i18n.Tr "repo.settings.add_webhook"}}{{else}}{{.i18n.Tr "repo.settings.update_webhook"}}{{end}}</button> | |||||
{{if .PageIsSettingsHooksEdit}}<a class="btn btn-red btn-large btn-link btn-radius" href="{{.RepoLink}}/settings/hooks?remove={{.Webhook.Id}}"><strong>{{.i18n.Tr "repo.settings.delete_webhook"}}</strong></a>{{end}} | |||||
</div> |
@@ -0,0 +1,11 @@ | |||||
{{if .PageIsSettingsHooksNew}} | |||||
<div id="hook-type" class="form-align"> | |||||
<label class="req">{{.i18n.Tr "repo.settings.hook_type"}}</label> | |||||
<select name="hook_type" id="hook-type" class="form-control"> | |||||
{{if .HookType}}<option value="{{.HookType}}">{{.HookType}}</option>{{end}} | |||||
{{range .HookTypes}} | |||||
{{if not (eq $.HookType .)}}<option value="{{.}}" >{{.}}</option>{{end}} | |||||
{{end}} | |||||
</select> | |||||
</div> | |||||
{{end}} |
@@ -0,0 +1,20 @@ | |||||
<div id="slack" class="{{if or .PageIsSettingsHooksNew (and .PageIsSettingsHooksEdit (not (eq .HookType "slack")))}}hidden{{end}}"> | |||||
<form class="form form-align panel-body repo-setting-form" id="repo-setting-form-slack" action="{{.RepoLink}}/settings/hooks/slack/{{if .PageIsSettingsHooksNew}}new{{else}}{{.Webhook.Id}}{{end}}" method="post"> | |||||
{{.CsrfTokenHtml}} | |||||
<input type="hidden" name="hook_type" value="slack"> | |||||
<div class="text-center panel-desc">{{.i18n.Tr "repo.settings.add_slack_hook_desc" | Str2html}}</div> | |||||
<div class="field"> | |||||
<label class="req" for="domain">{{.i18n.Tr "repo.settings.slack_domain"}}</label> | |||||
<input class="ipt ipt-large ipt-radius {{if .Err_UserName}}ipt-error{{end}}" id="domain" name="domain" type="text" value="{{.SlackHook.Domain}}" placeholde="myslack" required /> | |||||
</div> | |||||
<div class="field"> | |||||
<label class="req" for="token">{{.i18n.Tr "repo.settings.slack_token"}}</label> | |||||
<input class="ipt ipt-large ipt-radius {{if .Err_UserName}}ipt-error{{end}}" id="token" name="token" type="text" value="{{.SlackHook.Token}}" autocomplete="off" required /> | |||||
</div> | |||||
<div class="field"> | |||||
<label class="req" for="channel">{{.i18n.Tr "repo.settings.slack_channel"}}</label> | |||||
<input class="ipt ipt-large ipt-radius {{if .Err_UserName}}ipt-error{{end}}" id="channel" name="channel" type="text" value="{{.SlackHook.Channel}}" placeholder="#general" required /> | |||||
</div> | |||||
{{template "repo/settings/hook_settings" .}} | |||||
</form> | |||||
</div> |