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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Webhook for Typetalk #5548

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions models/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ const (
GITEA
DISCORD
DINGTALK
TYPETALK
)

var hookTaskTypes = map[string]HookTaskType{
Expand All @@ -389,6 +390,7 @@ var hookTaskTypes = map[string]HookTaskType{
"slack": SLACK,
"discord": DISCORD,
"dingtalk": DINGTALK,
"typetalk": TYPETALK,
}

// ToHookTaskType returns HookTaskType by given name.
Expand All @@ -409,6 +411,8 @@ func (t HookTaskType) Name() string {
return "discord"
case DINGTALK:
return "dingtalk"
case TYPETALK:
return "typetalk"
}
return ""
}
Expand Down Expand Up @@ -581,6 +585,11 @@ func prepareWebhook(e Engine, w *Webhook, repo *Repository, event HookEventType,
if err != nil {
return fmt.Errorf("GetDingtalkPayload: %v", err)
}
case TYPETALK:
payloader, err = GetTypetalkPayload(p, event, w.Meta)
if err != nil {
return fmt.Errorf("GetTypetalkPayload: %v", err)
}
default:
p.SetSecret(w.Secret)
payloader = p
Expand Down
302 changes: 302 additions & 0 deletions models/webhook_typetalk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
// Copyright 2018 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 models

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

"code.gitea.io/git"
api "code.gitea.io/sdk/gitea"
)

// TypetalkPayload contains information for posting messages on Typetalk
type TypetalkPayload struct {
Message string `json:"message"`
}

// SetSecret sets the Typetalk secret
func (p *TypetalkPayload) SetSecret(_ string) {}

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

func formatRepositoryLinkForTypetalk(name, url string) string {
return fmt.Sprintf("[%s](%s)", name, url)
}

func formatIssueLinkForTypetalk(issueTitle, repositoryURL string, issueNumber int64) string {
return fmt.Sprintf("[#%d %s](%s/issues/%d)", issueNumber, issueTitle, repositoryURL, issueNumber)
}

func formatIssueCommentLinkForTypetalk(repositoryURL string, issueID, commentID int64) string {
return fmt.Sprintf("[#%d](%s/issues/%d#%s)", commentID, repositoryURL, issueID, CommentHashTag(commentID))
}

func formatPullRequestLinkForTypetalk(pullRequestTitle, pullRequestyURL string, pullRequestID int64) string {
return fmt.Sprintf("[#%d %s](%s)", pullRequestID, pullRequestTitle, pullRequestyURL)
}

func formatTagLinkForTypetalk(name, url, tag string) string {
return fmt.Sprintf("[%s:%s](%s/src/tag/%s)", name, tag, url, tag)
}

func getTypetalkCreatePayload(p *api.CreatePayload) (*TypetalkPayload, error) {
repoLink := formatRepositoryLinkForTypetalk(p.Repo.FullName, p.Repo.HTMLURL)
refName := git.RefEndName(p.Ref)
message := fmt.Sprintf("[%s] %s %s created", repoLink, p.RefType, refName)

return &TypetalkPayload{
Message: message,
}, nil
}

func getTypetalkDeletePayload(p *api.DeletePayload) (*TypetalkPayload, error) {
repoLink := formatRepositoryLinkForTypetalk(p.Repo.FullName, p.Repo.HTMLURL)
refName := git.RefEndName(p.Ref)
message := fmt.Sprintf("[%s] %s %s deleted", repoLink, p.RefType, refName)

return &TypetalkPayload{
Message: message,
}, nil
}

func getTypetalkForkPayload(p *api.ForkPayload) (*TypetalkPayload, error) {
origin := fmt.Sprintf("[%s](%s)", p.Forkee.FullName, p.Forkee.HTMLURL)
forked := fmt.Sprintf("[%s](%s)", p.Repo.FullName, p.Repo.HTMLURL)
message := fmt.Sprintf("%s is forked to %s", origin, forked)

return &TypetalkPayload{
Message: message,
}, nil
}

func getTypetalkIssuesPayload(p *api.IssuePayload) (*TypetalkPayload, error) {

repoLink := formatRepositoryLinkForTypetalk(p.Repository.FullName, p.Repository.HTMLURL)
issueLink := formatIssueLinkForTypetalk(p.Issue.Title, p.Repository.HTMLURL, p.Index)

var title, text string
switch p.Action {
case api.HookIssueOpened:
title = fmt.Sprintf("[%s] %s Issue opened by %s", repoLink, issueLink, p.Sender.UserName)
text = p.Issue.Body
case api.HookIssueClosed:
title = fmt.Sprintf("[%s] %s Issue closed by %s", repoLink, issueLink, p.Sender.UserName)
case api.HookIssueReOpened:
title = fmt.Sprintf("[%s] %s Issue re-opened by %s", repoLink, issueLink, p.Sender.UserName)
case api.HookIssueEdited:
title = fmt.Sprintf("[%s] %s Issue edited by %s", repoLink, issueLink, p.Sender.UserName)
text = p.Issue.Body
case api.HookIssueAssigned:
title = fmt.Sprintf("[%s] %s Issue assigned to %s", repoLink, issueLink, p.Issue.Assignee.UserName)
case api.HookIssueUnassigned:
title = fmt.Sprintf("[%s] %s Issue unassigned by %s", repoLink, issueLink, p.Sender.UserName)
case api.HookIssueLabelUpdated:
title = fmt.Sprintf("[%s] %s Issue labels updated by %s", repoLink, issueLink, p.Sender.UserName)
case api.HookIssueLabelCleared:
title = fmt.Sprintf("[%s] %s Issue labels cleared by %s", repoLink, issueLink, p.Sender.UserName)
case api.HookIssueSynchronized:
title = fmt.Sprintf("[%s] %s Issue synchronized by %s", repoLink, issueLink, p.Sender.UserName)
case api.HookIssueMilestoned:
title = fmt.Sprintf("[%s] %s Issue milestones updated by %s", repoLink, issueLink, p.Sender.UserName)
case api.HookIssueDemilestoned:
title = fmt.Sprintf("[%s] %s Issue milestone cleared by %s", repoLink, issueLink, p.Sender.UserName)
}
message := fmt.Sprintf("%s\n%s", title, text)

return &TypetalkPayload{
Message: message,
}, nil
}

func getTypetalkIssueCommentPayload(p *api.IssueCommentPayload) (*TypetalkPayload, error) {

repoLink := formatRepositoryLinkForTypetalk(p.Repository.FullName, p.Repository.HTMLURL)
issueLink := formatIssueLinkForTypetalk(p.Issue.Title, p.Repository.HTMLURL, p.Issue.Index)
issueCommentLink := formatIssueCommentLinkForTypetalk(p.Repository.HTMLURL, p.Issue.Index, p.Comment.ID)

var title, text string
switch p.Action {
case api.HookIssueCommentCreated:
title = fmt.Sprintf("[%s] %s New comment %s created by %s ", repoLink, issueLink, issueCommentLink, p.Sender.UserName)
text = p.Comment.Body
case api.HookIssueCommentEdited:
title = fmt.Sprintf("[%s] %s Comment %s edited by %s", repoLink, issueLink, issueCommentLink, p.Sender.UserName)
text = p.Comment.Body
case api.HookIssueCommentDeleted:
title = fmt.Sprintf("[%s] %s Comment #%d deleted by %s", repoLink, issueLink, p.Comment.ID, p.Sender.UserName)
}

message := fmt.Sprintf("%s\n%s", title, text)
return &TypetalkPayload{
Message: message,
}, nil
}

func getTypetalkPushPayload(p *api.PushPayload) (*TypetalkPayload, error) {

branchName := git.RefEndName(p.Ref)

var titleLink, commitDesc string
if len(p.Commits) == 1 {
commitDesc = "1 new commit"
titleLink = p.Commits[0].URL
} else {
commitDesc = fmt.Sprintf("%d new commits", len(p.Commits))
titleLink = p.CompareURL
}
if titleLink == "" {
titleLink = p.Repo.HTMLURL + "/src/" + branchName
}

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

var text string
for i, commit := range p.Commits {
var authorName string
if commit.Author != nil {
authorName = " - " + commit.Author.Name
}
t := fmt.Sprintf("[%s](%s) %s", commit.ID[:7], commit.URL,
strings.TrimRight(commit.Message, "\r\n")) + authorName

// Typetalk accepts message shorter than 4000 characters.
if utf8.RuneCountInString(text)+utf8.RuneCountInString(t) < 4000 {
text += t
// Add linebreak to each commit but the last
if i < len(p.Commits)-1 {
text += "\n"
}
} else {
text += "..."
break
}
}

message := fmt.Sprintf("%s\n%s", title, text)

return &TypetalkPayload{
Message: message,
}, nil
}

func getTypetalkPullRequestPayload(p *api.PullRequestPayload) (*TypetalkPayload, error) {

repoLink := formatRepositoryLinkForTypetalk(p.Repository.FullName, p.Repository.HTMLURL)
pullRequestLink := formatPullRequestLinkForTypetalk(p.PullRequest.Title, p.PullRequest.HTMLURL, p.Index)

var text, title string
switch p.Action {
case api.HookIssueOpened:
title = fmt.Sprintf("[%s] %s Pull request opened by %s", repoLink, pullRequestLink, p.Sender.UserName)
text = p.PullRequest.Body
case api.HookIssueClosed:
if p.PullRequest.HasMerged {
title = fmt.Sprintf("[%s] %s Pull request merged by %s", repoLink, pullRequestLink, p.Sender.UserName)
} else {
title = fmt.Sprintf("[%s] %s Pull request closed by %s", repoLink, pullRequestLink, p.Sender.UserName)
}
case api.HookIssueReOpened:
title = fmt.Sprintf("[%s] %s Pull request re-opened by %s", repoLink, pullRequestLink, p.Sender.UserName)
case api.HookIssueEdited:
title = fmt.Sprintf("[%s] %s Pull request edited by %s", repoLink, pullRequestLink, p.Sender.UserName)
text = p.PullRequest.Body
case api.HookIssueAssigned:
list, err := MakeAssigneeList(&Issue{ID: p.PullRequest.ID})
if err != nil {
return &TypetalkPayload{}, err
}
title = fmt.Sprintf("[%s] %s Pull request assigned to %s", repoLink, pullRequestLink, list)
case api.HookIssueUnassigned:
title = fmt.Sprintf("[%s] %s Pull request unassigned by %s", repoLink, pullRequestLink, p.Sender.UserName)
case api.HookIssueLabelUpdated:
title = fmt.Sprintf("[%s] %s Pull request labels updated by %s", repoLink, pullRequestLink, p.Sender.UserName)
case api.HookIssueLabelCleared:
title = fmt.Sprintf("[%s] %s Pull request labels cleared by %s", repoLink, pullRequestLink, p.Sender.UserName)
case api.HookIssueSynchronized:
title = fmt.Sprintf("[%s] %s Pull request synchronized by %s", repoLink, pullRequestLink, p.Sender.UserName)
case api.HookIssueMilestoned:
title = fmt.Sprintf("[%s] %s Pull request milestones updated by %s", repoLink, pullRequestLink, p.Sender.UserName)
case api.HookIssueDemilestoned:
title = fmt.Sprintf("[%s] %s Pull request milestones cleared by %s", repoLink, pullRequestLink, p.Sender.UserName)
}

message := fmt.Sprintf("%s\n%s", title, text)

return &TypetalkPayload{
Message: message,
}, nil
}

func getTypetalkRepositoryPayload(p *api.RepositoryPayload) (*TypetalkPayload, error) {

var message string
switch p.Action {
case api.HookRepoCreated:
message = fmt.Sprintf("[%s] Repository created", formatRepositoryLinkForTypetalk(p.Repository.FullName, p.Repository.HTMLURL))
case api.HookRepoDeleted:
message = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
}

return &TypetalkPayload{
Message: message,
}, nil
}

func getTypetalkReleasePayload(p *api.ReleasePayload) (*TypetalkPayload, error) {

tagLink := formatTagLinkForTypetalk(p.Repository.FullName, p.Repository.HTMLURL, p.Release.TagName)

var message string
switch p.Action {
case api.HookReleasePublished:
message = fmt.Sprintf("[%s] Release created", tagLink)
case api.HookReleaseUpdated:
message = fmt.Sprintf("[%s] Release updated", tagLink)
case api.HookReleaseDeleted:
message = fmt.Sprintf("[%s:%s] Release deleted", p.Repository.FullName, p.Release.TagName)
}

return &TypetalkPayload{
Message: message,
}, nil
}

// GetTypetalkPayload converts a Typetalk webhook into a TypetalkPayload
func GetTypetalkPayload(p api.Payloader, event HookEventType, meta string) (*TypetalkPayload, error) {
s := new(TypetalkPayload)

switch event {
case HookEventCreate:
return getTypetalkCreatePayload(p.(*api.CreatePayload))
case HookEventDelete:
return getTypetalkDeletePayload(p.(*api.DeletePayload))
case HookEventFork:
return getTypetalkForkPayload(p.(*api.ForkPayload))
case HookEventIssues:
return getTypetalkIssuesPayload(p.(*api.IssuePayload))
case HookEventIssueComment:
return getTypetalkIssueCommentPayload(p.(*api.IssueCommentPayload))
case HookEventPush:
return getTypetalkPushPayload(p.(*api.PushPayload))
case HookEventPullRequest:
return getTypetalkPullRequestPayload(p.(*api.PullRequestPayload))
case HookEventRepository:
return getTypetalkRepositoryPayload(p.(*api.RepositoryPayload))
case HookEventRelease:
return getTypetalkReleasePayload(p.(*api.ReleasePayload))
}

return s, nil
}
11 changes: 11 additions & 0 deletions modules/auth/repo_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,17 @@ func (f *NewDingtalkHookForm) Validate(ctx *macaron.Context, errs binding.Errors
return validate(errs, ctx.Data, f, ctx.Locale)
}

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

// Validate validates the fields
func (f *NewTypetalkHookForm) 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/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -1614,7 +1614,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"}
Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk", "typetalk"}
Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10)
}

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 @@ -1127,6 +1127,7 @@ settings.slack_domain = Domain
settings.slack_channel = Channel
settings.add_discord_hook_desc = Integrate <a href="%s">Discord</a> into your repository.
settings.add_dingtalk_hook_desc = Integrate <a href="%s">Dingtalk</a> into your repository.
settings.add_typetalk_hook_desc = Integrate <a href="%s">Typetalk</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/typetalk.ico
Binary file not shown.