mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-08 18:04:14 +01:00
add Slack API webhook support
This commit is contained in:
parent
5e6091a30a
commit
2bce24068d
15 changed files with 485 additions and 79 deletions
|
@ -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
|
switch w.HookTaskType {
|
||||||
CreateHookTask(&HookTask{
|
case SLACK:
|
||||||
Type: WEBHOOK,
|
{
|
||||||
Url: w.Url,
|
s, err := GetSlackPayload(p, w.Meta)
|
||||||
Payload: p,
|
if err != nil {
|
||||||
ContentType: w.ContentType,
|
return errors.New("action.GetSlackPayload: " + err.Error())
|
||||||
IsSsl: w.IsSsl,
|
}
|
||||||
})
|
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
|
||||||
}
|
}
|
||||||
|
|
114
models/slack.go
Normal file
114
models/slack.go
Normal file
|
@ -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
|
Id int64
|
||||||
RepoId int64
|
RepoId int64
|
||||||
Url string `xorm:"TEXT"`
|
Url string `xorm:"TEXT"`
|
||||||
ContentType HookContentType
|
ContentType HookContentType
|
||||||
Secret string `xorm:"TEXT"`
|
Secret string `xorm:"TEXT"`
|
||||||
Events string `xorm:"TEXT"`
|
Events string `xorm:"TEXT"`
|
||||||
*HookEvent `xorm:"-"`
|
*HookEvent `xorm:"-"`
|
||||||
IsSsl bool
|
IsSsl bool
|
||||||
IsActive 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
|
GOGS HookTaskType = iota + 1
|
||||||
SERVICE
|
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 {
|
switch t.Type {
|
||||||
log.Error(4, "Delivery: %v", err)
|
case GOGS:
|
||||||
} else {
|
{
|
||||||
t.IsSucceed = true
|
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"`
|
HookTaskType string `form:"hook_type" binding:"Required"`
|
||||||
ContentType string `form:"content_type" binding:"Required"`
|
PayloadUrl string `form:"payload_url" binding:"Required;Url"`
|
||||||
Secret string `form:"secret"`
|
ContentType string `form:"content_type" binding:"Required"`
|
||||||
PushOnly bool `form:"push_only"`
|
Secret string `form:"secret"`
|
||||||
Active bool `form:"active"`
|
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))
|
||||||
|
}
|
||||||
|
|
23
templates/repo/settings/gogs_hook.tmpl
Normal file
23
templates/repo/settings/gogs_hook.tmpl
Normal file
|
@ -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">
|
{{template "repo/settings/hook_types" .}}
|
||||||
{{.CsrfTokenHtml}}
|
{{template "repo/settings/gogs_hook" .}}
|
||||||
<div class="text-center panel-desc">{{.i18n.Tr "repo.settings.add_webhook_desc" | Str2html}}</div>
|
{{template "repo/settings/slack_hook" .}}
|
||||||
<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>
|
|
||||||
</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" .}}
|
||||||
|
|
15
templates/repo/settings/hook_settings.tmpl
Normal file
15
templates/repo/settings/hook_settings.tmpl
Normal file
|
@ -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>
|
11
templates/repo/settings/hook_types.tmpl
Normal file
11
templates/repo/settings/hook_types.tmpl
Normal file
|
@ -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}}
|
20
templates/repo/settings/slack_hook.tmpl
Normal file
20
templates/repo/settings/slack_hook.tmpl
Normal file
|
@ -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>
|
Loading…
Reference in a new issue