From 68cf72d65178b5e54c828af749284ca0605df312 Mon Sep 17 00:00:00 2001 From: Issei Horie Date: Sat, 15 Dec 2018 20:30:53 +0900 Subject: [PATCH] Support Webhook for Typetalk Signed-off-by: Issei Horie --- models/webhook.go | 9 + models/webhook_typetalk.go | 302 ++++++++++++++++++ modules/auth/repo_form.go | 11 + modules/setting/setting.go | 2 +- options/locale/locale_en-US.ini | 1 + public/img/typetalk.ico | Bin 0 -> 15086 bytes routers/repo/webhook.go | 72 +++++ routers/routes/routes.go | 4 + templates/org/settings/hook_new.tmpl | 3 + templates/repo/settings/webhook/list.tmpl | 3 + templates/repo/settings/webhook/new.tmpl | 3 + templates/repo/settings/webhook/typetalk.tmpl | 11 + 12 files changed, 420 insertions(+), 1 deletion(-) create mode 100644 models/webhook_typetalk.go create mode 100644 public/img/typetalk.ico create mode 100644 templates/repo/settings/webhook/typetalk.tmpl diff --git a/models/webhook.go b/models/webhook.go index 77662f52757a1..a2b6845102690 100644 --- a/models/webhook.go +++ b/models/webhook.go @@ -382,6 +382,7 @@ const ( GITEA DISCORD DINGTALK + TYPETALK ) var hookTaskTypes = map[string]HookTaskType{ @@ -390,6 +391,7 @@ var hookTaskTypes = map[string]HookTaskType{ "slack": SLACK, "discord": DISCORD, "dingtalk": DINGTALK, + "typetalk": TYPETALK, } // ToHookTaskType returns HookTaskType by given name. @@ -410,6 +412,8 @@ func (t HookTaskType) Name() string { return "discord" case DINGTALK: return "dingtalk" + case TYPETALK: + return "typetalk" } return "" } @@ -579,6 +583,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 diff --git a/models/webhook_typetalk.go b/models/webhook_typetalk.go new file mode 100644 index 0000000000000..276585c3bd197 --- /dev/null +++ b/models/webhook_typetalk.go @@ -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 +} diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go index a4a00d53b4f4c..875f918bb48a0 100644 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -256,6 +256,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) +} + // .___ // | | ______ ________ __ ____ // | |/ ___// ___/ | \_/ __ \ diff --git a/modules/setting/setting.go b/modules/setting/setting.go index f7da6baac4737..2123750372b4d 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -1606,7 +1606,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) } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index e499bb9fe4f5c..acbb9010cb424 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1117,6 +1117,7 @@ settings.slack_domain = Domain settings.slack_channel = Channel settings.add_discord_hook_desc = Integrate Discord into your repository. settings.add_dingtalk_hook_desc = Integrate Dingtalk into your repository. +settings.add_typetalk_hook_desc = Integrate Typetalk 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. diff --git a/public/img/typetalk.ico b/public/img/typetalk.ico new file mode 100644 index 0000000000000000000000000000000000000000..b9d9624ad2ce586b4830d04fa425fd7989c20445 GIT binary patch literal 15086 zcmeHN-ET}m6u%9gyhs}F`y%n!C(pe23tH4?*;V!3v;;{BArZk7i3s|Dh_9lQqCQeV zeY9vrMSP?Q#S*E{C%Ql9+_`h_)!n_EMsszRmts@$&il;z% zb`o_4oi?&<_BH{1R!Y7B?{i|w7jww|XVn5{E&^>=bI2s1{}ZjS)0Lay+P{1Qd`O0C z%d#7I3%njs-+sjg;PMUN`w!sjH=zAGz-4*Nr(I{`4j`5CXm}WC+Ma*=8H<70bwE!a zaQi;M^BBlGADs`FU#CxA3&H(oe>Yeex7S{%_D);521Bjij7DJ;g=}YHL{o{_K4nQ| zT!my+RvAxCqr8PENj4zV*fYu8=_^^4NO9lH!{*6+9p2-}#_iA4wh(2N37gkAIc}A_ z<#-$nQs$A#TK1o3K8?oCbE1pCt!oYV~N*YT)@x zv6u7j-a~5n_dfG@tLD$L=Dk>^7q5Wz+tiK!T)X59yn9c3`4V-!d7X9M_4r>W-BZ2x zI=ejn9i1iq|AYMF^+3ZWLETTh`M>i3sBaR)HmOjuICjS48T7<;bn-uXx`6y!0QNbU z{I-w(|M?^6@X5mQuO{DR!Ae07ee-|u8Zc+6Al?rPC4YavM@|9vdIarlR>RZJKdR>_ z{fv;9$S=_*zog#pZ~peTeLf{Rn?cUDFG%Ied_Nb1@HajGQzZt$+x9>(jSY| zFY)|uv^YM#-~T2E`Vya0&=ILsLzK@sgt`Z#6XV$B``8n`kq^!Lu5Z4JEPpTL^Y?TWcu&+U#Rbf+BG7k$mal@IA2)6vzREy!DmWe zh6HZ;YYP6FRY3ByMD69K;J-k32r?xT@Xw|7`SVvgdcL2)v5;!TXY!+66I*trW9NGn z^Ol=ocMcp)$A9XA8Q6KQ_zZDm+JCr&f3$iWwth+edUdTN|C0Q}u3Z!D=lRP({|k>k zei~{0MJ&IsACPqRl3pG0&(Gth>3DOMJfgdX{yua*hr!>|Yc_t4lj@YYpEv7*j31AT zAxk~t@BZJvJ?iQ%9{lWUB^o?x>yW6ue9u8({tAycF71~=F+I1mdAQ{h-z6(1jM=bk z-3{;<{KUR|wl<}A#>&U!Gf*c+X{_vJeAUpk<;I2$#V_L8M#k?%Jll@RQ~a0X+GfoG z-4l>8Y%}B6DK~y?$ARN1WA6`)n1|xMzrlA)=#GiR8 {{else if eq .HookType "dingtalk"}} + {{else if eq .HookType "typetalk"}} + {{end}} @@ -28,6 +30,7 @@ {{template "repo/settings/webhook/slack" .}} {{template "repo/settings/webhook/discord" .}} {{template "repo/settings/webhook/dingtalk" .}} + {{template "repo/settings/webhook/typetalk" .}} {{template "repo/settings/webhook/history" .}} diff --git a/templates/repo/settings/webhook/list.tmpl b/templates/repo/settings/webhook/list.tmpl index d98976cf5b400..660f573ae51fd 100644 --- a/templates/repo/settings/webhook/list.tmpl +++ b/templates/repo/settings/webhook/list.tmpl @@ -20,6 +20,9 @@ Dingtalk + + Typetalk + diff --git a/templates/repo/settings/webhook/new.tmpl b/templates/repo/settings/webhook/new.tmpl index 1b3d114577a4e..f03393c6751f8 100644 --- a/templates/repo/settings/webhook/new.tmpl +++ b/templates/repo/settings/webhook/new.tmpl @@ -17,6 +17,8 @@ {{else if eq .HookType "dingtalk"}} + {{else if eq .HookType "typetalk"}} + {{end}} @@ -26,6 +28,7 @@ {{template "repo/settings/webhook/slack" .}} {{template "repo/settings/webhook/discord" .}} {{template "repo/settings/webhook/dingtalk" .}} + {{template "repo/settings/webhook/typetalk" .}} {{template "repo/settings/webhook/history" .}} diff --git a/templates/repo/settings/webhook/typetalk.tmpl b/templates/repo/settings/webhook/typetalk.tmpl new file mode 100644 index 0000000000000..c690e73e5bc9d --- /dev/null +++ b/templates/repo/settings/webhook/typetalk.tmpl @@ -0,0 +1,11 @@ +{{if eq .HookType "typetalk"}} +

{{.i18n.Tr "repo.settings.add_typetalk_hook_desc" "https://www.typetalk.com" | Str2html}}

+
+ {{.CsrfTokenHtml}} +
+ + +
+ {{template "repo/settings/webhook/settings" .}} +
+{{end}}