Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feishu webhook support #10229

Merged
merged 3 commits into from
Feb 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions models/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ const (
DINGTALK
TELEGRAM
MSTEAMS
FEISHU
)

var hookTaskTypes = map[string]HookTaskType{
Expand All @@ -455,6 +456,7 @@ var hookTaskTypes = map[string]HookTaskType{
"dingtalk": DINGTALK,
"telegram": TELEGRAM,
"msteams": MSTEAMS,
"feishu": FEISHU,
}

// ToHookTaskType returns HookTaskType by given name.
Expand All @@ -479,6 +481,8 @@ func (t HookTaskType) Name() string {
return "telegram"
case MSTEAMS:
return "msteams"
case FEISHU:
return "feishu"
}
return ""
}
Expand Down
11 changes: 11 additions & 0 deletions modules/auth/repo_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,17 @@ func (f *NewMSTeamsHookForm) Validate(ctx *macaron.Context, errs binding.Errors)
return validate(errs, ctx.Data, f, ctx.Locale)
}

// NewFeishuHookForm form for creating feishu hook
type NewFeishuHookForm struct {
PayloadURL string `binding:"Required;ValidUrl"`
WebhookForm
}

// Validate validates the fields
func (f *NewFeishuHookForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}

// .___
// | | ______ ________ __ ____
// | |/ ___// ___/ | \_/ __ \
Expand Down
2 changes: 1 addition & 1 deletion modules/setting/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func newWebhookService() {
Webhook.QueueLength = sec.Key("QUEUE_LENGTH").MustInt(1000)
Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5)
Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool()
Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams"}
Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams", "feishu"}
Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10)
Webhook.ProxyURL = sec.Key("PROXY_URL").MustString("")
if Webhook.ProxyURL != "" {
Expand Down
2 changes: 1 addition & 1 deletion modules/structs/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type CreateHookOptionConfig map[string]string
// CreateHookOption options when create a hook
type CreateHookOption struct {
// required: true
// enum: dingtalk,discord,gitea,gogs,msteams,slack,telegram
// enum: dingtalk,discord,gitea,gogs,msteams,slack,telegram,feishu
Type string `json:"type" binding:"Required"`
// required: true
Config CreateHookOptionConfig `json:"config" binding:"Required"`
Expand Down
201 changes: 201 additions & 0 deletions modules/webhook/feishu.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// Copyright 2020 The Gitea 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 webhook

import (
"encoding/json"
"fmt"
"strings"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
)

type (
// FeishuPayload represents
FeishuPayload struct {
Title string `json:"title"`
Text string `json:"text"`
}
)

// SetSecret sets the Feishu secret
func (p *FeishuPayload) SetSecret(_ string) {}

// JSONPayload Marshals the FeishuPayload to json
func (p *FeishuPayload) JSONPayload() ([]byte, error) {
data, err := json.MarshalIndent(p, "", " ")
if err != nil {
return []byte{}, err
}
return data, nil
}

func getFeishuCreatePayload(p *api.CreatePayload) (*FeishuPayload, error) {
// created tag/branch
refName := git.RefEndName(p.Ref)
title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)

return &FeishuPayload{
Text: title,
Title: title,
}, nil
}

func getFeishuDeletePayload(p *api.DeletePayload) (*FeishuPayload, error) {
// created tag/branch
refName := git.RefEndName(p.Ref)
title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)

return &FeishuPayload{
Text: title,
Title: title,
}, nil
}

func getFeishuForkPayload(p *api.ForkPayload) (*FeishuPayload, error) {
title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)

return &FeishuPayload{
Text: title,
Title: title,
}, nil
}

func getFeishuPushPayload(p *api.PushPayload) (*FeishuPayload, error) {
var (
branchName = git.RefEndName(p.Ref)
commitDesc string
)

title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc)

var text string
// for each commit, generate attachment text
for i, commit := range p.Commits {
var authorName string
if commit.Author != nil {
authorName = " - " + commit.Author.Name
}
text += fmt.Sprintf("[%s](%s) %s", commit.ID[:7], commit.URL,
strings.TrimRight(commit.Message, "\r\n")) + authorName
lunny marked this conversation as resolved.
Show resolved Hide resolved
// add linebreak to each commit but the last
if i < len(p.Commits)-1 {
text += "\n"
}
}

return &FeishuPayload{
Text: text,
Title: title,
}, nil
}

func getFeishuIssuesPayload(p *api.IssuePayload) (*FeishuPayload, error) {
text, issueTitle, attachmentText, _ := getIssuesPayloadInfo(p, noneLinkFormatter, true)

return &FeishuPayload{
Text: text + "\r\n\r\n" + attachmentText,
Title: issueTitle,
}, nil
}

func getFeishuIssueCommentPayload(p *api.IssueCommentPayload) (*FeishuPayload, error) {
text, issueTitle, _ := getIssueCommentPayloadInfo(p, noneLinkFormatter, true)

return &FeishuPayload{
Text: text + "\r\n\r\n" + p.Comment.Body,
Title: issueTitle,
}, nil
}

func getFeishuPullRequestPayload(p *api.PullRequestPayload) (*FeishuPayload, error) {
text, issueTitle, attachmentText, _ := getPullRequestPayloadInfo(p, noneLinkFormatter, true)

return &FeishuPayload{
Text: text + "\r\n\r\n" + attachmentText,
Title: issueTitle,
}, nil
}

func getFeishuPullRequestApprovalPayload(p *api.PullRequestPayload, event models.HookEventType) (*FeishuPayload, error) {
var text, title string
switch p.Action {
case api.HookIssueSynchronized:
action, err := parseHookPullRequestEventType(event)
if err != nil {
return nil, err
}

title = fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
text = p.Review.Content

}

return &FeishuPayload{
Text: title + "\r\n\r\n" + text,
Title: title,
}, nil
}

func getFeishuRepositoryPayload(p *api.RepositoryPayload) (*FeishuPayload, error) {
var title string
switch p.Action {
case api.HookRepoCreated:
title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
return &FeishuPayload{
Text: title,
Title: title,
}, nil
case api.HookRepoDeleted:
title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
return &FeishuPayload{
Title: title,
Text: title,
}, nil
}

return nil, nil
}

func getFeishuReleasePayload(p *api.ReleasePayload) (*FeishuPayload, error) {
text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)

return &FeishuPayload{
Text: text,
Title: text,
}, nil
}

// GetFeishuPayload converts a ding talk webhook into a FeishuPayload
func GetFeishuPayload(p api.Payloader, event models.HookEventType, meta string) (*FeishuPayload, error) {
s := new(FeishuPayload)

switch event {
case models.HookEventCreate:
return getFeishuCreatePayload(p.(*api.CreatePayload))
case models.HookEventDelete:
return getFeishuDeletePayload(p.(*api.DeletePayload))
case models.HookEventFork:
return getFeishuForkPayload(p.(*api.ForkPayload))
case models.HookEventIssues:
return getFeishuIssuesPayload(p.(*api.IssuePayload))
case models.HookEventIssueComment:
return getFeishuIssueCommentPayload(p.(*api.IssueCommentPayload))
case models.HookEventPush:
return getFeishuPushPayload(p.(*api.PushPayload))
case models.HookEventPullRequest:
return getFeishuPullRequestPayload(p.(*api.PullRequestPayload))
case models.HookEventPullRequestApproved, models.HookEventPullRequestRejected, models.HookEventPullRequestComment:
return getFeishuPullRequestApprovalPayload(p.(*api.PullRequestPayload), event)
case models.HookEventRepository:
return getFeishuRepositoryPayload(p.(*api.RepositoryPayload))
case models.HookEventRelease:
return getFeishuReleasePayload(p.(*api.ReleasePayload))
}

return s, nil
}
5 changes: 5 additions & 0 deletions modules/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ func prepareWebhook(w *models.Webhook, repo *models.Repository, event models.Hoo
if err != nil {
return fmt.Errorf("GetMSTeamsPayload: %v", err)
}
case models.FEISHU:
payloader, err = GetFeishuPayload(p, event, w.Meta)
if err != nil {
return fmt.Errorf("GetFeishuPayload: %v", err)
}
default:
p.SetSecret(w.Secret)
payloader = p
Expand Down
1 change: 1 addition & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1398,6 +1398,7 @@ settings.add_discord_hook_desc = Integrate <a href="%s">Discord</a> into your re
settings.add_dingtalk_hook_desc = Integrate <a href="%s">Dingtalk</a> into your repository.
settings.add_telegram_hook_desc = Integrate <a href="%s">Telegram</a> into your repository.
settings.add_msteams_hook_desc = Integrate <a href="%s">Microsoft Teams</a> into your repository.
settings.add_feishu_hook_desc = Integrate <a href="%s">Feishu</a> into your repository.
settings.deploy_keys = Deploy Keys
settings.add_deploy_key = Add Deploy Key
settings.deploy_key_desc = Deploy keys have read-only pull access to the repository.
Expand Down
Binary file added public/img/feishu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 72 additions & 0 deletions routers/repo/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,46 @@ func SlackHooksNewPost(ctx *context.Context, form auth.NewSlackHookForm) {
ctx.Redirect(orCtx.Link)
}

// FeishuHooksNewPost response for creating feishu hook
func FeishuHooksNewPost(ctx *context.Context, form auth.NewFeishuHookForm) {
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.ServerError("getOrgRepoCtx", err)
return
}

if ctx.HasError() {
ctx.HTML(200, orCtx.NewTemplate)
return
}

w := &models.Webhook{
RepoID: orCtx.RepoID,
URL: form.PayloadURL,
ContentType: models.ContentTypeJSON,
HookEvent: ParseHookEvent(form.WebhookForm),
IsActive: form.Active,
HookTaskType: models.FEISHU,
Meta: "",
OrgID: orCtx.OrgID,
}
if err := w.UpdateEvent(); err != nil {
ctx.ServerError("UpdateEvent", err)
return
} else if err := models.CreateWebhook(w); err != nil {
ctx.ServerError("CreateWebhook", err)
return
}

ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
ctx.Redirect(orCtx.Link)
}

func checkWebhook(ctx *context.Context) (*orgRepoCtx, *models.Webhook) {
ctx.Data["RequireHighlightJS"] = true

Expand Down Expand Up @@ -819,6 +859,38 @@ func MSTeamsHooksEditPost(ctx *context.Context, form auth.NewMSTeamsHookForm) {
ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
}

// FeishuHooksEditPost response for editing feishu hook
func FeishuHooksEditPost(ctx *context.Context, form auth.NewFeishuHookForm) {
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
}

w.URL = form.PayloadURL
w.HookEvent = ParseHookEvent(form.WebhookForm)
w.IsActive = form.Active
if err := w.UpdateEvent(); err != nil {
ctx.ServerError("UpdateEvent", err)
return
} else if err := models.UpdateWebhook(w); err != nil {
ctx.ServerError("UpdateWebhook", err)
return
}

ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
}

// TestWebhook test if web hook is work fine
func TestWebhook(ctx *context.Context) {
hookID := ctx.ParamsInt64(":id")
Expand Down
Loading