diff --git a/models/webhook.go b/models/webhook.go index 508fea9977..1b601b4e62 100644 --- a/models/webhook.go +++ b/models/webhook.go @@ -332,13 +332,15 @@ const ( SLACK GITEA DISCORD + DINGTALK ) var hookTaskTypes = map[string]HookTaskType{ - "gitea": GITEA, - "gogs": GOGS, - "slack": SLACK, - "discord": DISCORD, + "gitea": GITEA, + "gogs": GOGS, + "slack": SLACK, + "discord": DISCORD, + "dingtalk": DINGTALK, } // ToHookTaskType returns HookTaskType by given name. @@ -357,6 +359,8 @@ func (t HookTaskType) Name() string { return "slack" case DISCORD: return "discord" + case DINGTALK: + return "dingtalk" } return "" } @@ -520,6 +524,11 @@ func prepareWebhook(e Engine, w *Webhook, repo *Repository, event HookEventType, if err != nil { return fmt.Errorf("GetDiscordPayload: %v", err) } + case DINGTALK: + payloader, err = GetDingtalkPayload(p, event, w.Meta) + if err != nil { + return fmt.Errorf("GetDingtalkPayload: %v", err) + } default: p.SetSecret(w.Secret) payloader = p diff --git a/models/webhook_dingtalk.go b/models/webhook_dingtalk.go new file mode 100644 index 0000000000..e25e989084 --- /dev/null +++ b/models/webhook_dingtalk.go @@ -0,0 +1,197 @@ +// Copyright 2017 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" + + "code.gitea.io/git" + api "code.gitea.io/sdk/gitea" + + dingtalk "github.com/lunny/dingtalk_webhook" +) + +type ( + // DingtalkPayload represents + DingtalkPayload dingtalk.Payload +) + +// SetSecret sets the dingtalk secret +func (p *DingtalkPayload) SetSecret(_ string) {} + +// JSONPayload Marshals the DingtalkPayload to json +func (p *DingtalkPayload) JSONPayload() ([]byte, error) { + data, err := json.MarshalIndent(p, "", " ") + if err != nil { + return []byte{}, err + } + return data, nil +} + +func getDingtalkCreatePayload(p *api.CreatePayload) (*DingtalkPayload, error) { + // created tag/branch + refName := git.RefEndName(p.Ref) + title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) + + return &DingtalkPayload{ + MsgType: "actionCard", + ActionCard: dingtalk.ActionCard{ + Text: title, + Title: title, + HideAvatar: "0", + SingleTitle: fmt.Sprintf("view branch %s", refName), + SingleURL: p.Repo.HTMLURL + "/src/" + refName, + }, + }, nil +} + +func getDingtalkPushPayload(p *api.PushPayload) (*DingtalkPayload, error) { + var ( + branchName = git.RefEndName(p.Ref) + commitDesc string + ) + + var titleLink, linkText string + if len(p.Commits) == 1 { + commitDesc = "1 new commit" + titleLink = p.Commits[0].URL + linkText = fmt.Sprintf("view commit %s", p.Commits[0].ID[:7]) + } else { + commitDesc = fmt.Sprintf("%d new commits", len(p.Commits)) + titleLink = p.CompareURL + linkText = fmt.Sprintf("view commit %s...%s", p.Commits[0].ID[:7], p.Commits[len(p.Commits)-1].ID[:7]) + } + if titleLink == "" { + titleLink = p.Repo.HTMLURL + "/src/" + branchName + } + + 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 + // add linebreak to each commit but the last + if i < len(p.Commits)-1 { + text += "\n" + } + } + + return &DingtalkPayload{ + MsgType: "actionCard", + ActionCard: dingtalk.ActionCard{ + Text: text, + Title: title, + HideAvatar: "0", + SingleTitle: linkText, + SingleURL: titleLink, + }, + }, nil +} + +func getDingtalkPullRequestPayload(p *api.PullRequestPayload) (*DingtalkPayload, error) { + var text, title string + switch p.Action { + case api.HookIssueOpened: + title = fmt.Sprintf("[%s] Pull request opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueClosed: + if p.PullRequest.HasMerged { + title = fmt.Sprintf("[%s] Pull request merged: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + } else { + title = fmt.Sprintf("[%s] Pull request closed: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + } + text = p.PullRequest.Body + case api.HookIssueReOpened: + title = fmt.Sprintf("[%s] Pull request re-opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueEdited: + title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueAssigned: + title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d %s", p.Repository.FullName, + p.PullRequest.Assignee.UserName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueUnassigned: + title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueLabelUpdated: + title = fmt.Sprintf("[%s] Pull request labels updated: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueLabelCleared: + title = fmt.Sprintf("[%s] Pull request labels cleared: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueSynchronized: + title = fmt.Sprintf("[%s] Pull request synchronized: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + } + + return &DingtalkPayload{ + MsgType: "actionCard", + ActionCard: dingtalk.ActionCard{ + Text: text, + Title: title, + HideAvatar: "0", + SingleTitle: "view pull request", + SingleURL: p.PullRequest.HTMLURL, + }, + }, nil +} + +func getDingtalkRepositoryPayload(p *api.RepositoryPayload) (*DingtalkPayload, error) { + var title, url string + switch p.Action { + case api.HookRepoCreated: + title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName) + url = p.Repository.HTMLURL + return &DingtalkPayload{ + MsgType: "actionCard", + ActionCard: dingtalk.ActionCard{ + Text: title, + Title: title, + HideAvatar: "0", + SingleTitle: "view repository", + SingleURL: url, + }, + }, nil + case api.HookRepoDeleted: + title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName) + return &DingtalkPayload{ + MsgType: "text", + Text: struct { + Content string `json:"content"` + }{ + Content: title, + }, + }, nil + } + + return nil, nil +} + +// GetDingtalkPayload converts a ding talk webhook into a DingtalkPayload +func GetDingtalkPayload(p api.Payloader, event HookEventType, meta string) (*DingtalkPayload, error) { + s := new(DingtalkPayload) + + switch event { + case HookEventCreate: + return getDingtalkCreatePayload(p.(*api.CreatePayload)) + case HookEventPush: + return getDingtalkPushPayload(p.(*api.PushPayload)) + case HookEventPullRequest: + return getDingtalkPullRequestPayload(p.(*api.PullRequestPayload)) + case HookEventRepository: + return getDingtalkRepositoryPayload(p.(*api.RepositoryPayload)) + } + + return s, nil +} diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go index 6fe826f574..bb917a9114 100644 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -222,6 +222,17 @@ func (f *NewDiscordHookForm) Validate(ctx *macaron.Context, errs binding.Errors) return validate(errs, ctx.Data, f, ctx.Locale) } +// NewDingtalkHookForm form for creating dingtalk hook +type NewDingtalkHookForm struct { + PayloadURL string `binding:"Required;ValidUrl"` + WebhookForm +} + +// Validate validates the fields +func (f *NewDingtalkHookForm) 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 d15a76f9bf..d4f92dee39 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -1509,7 +1509,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"} + Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk"} 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 834ec49e53..4410b63cd0 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -978,6 +978,7 @@ settings.slack_token = Token settings.slack_domain = Domain settings.slack_channel = Channel settings.add_discord_hook_desc = Add Discord integration to your repository. +settings.add_dingtalk_hook_desc = Add Dingtalk integration to your repository. settings.deploy_keys = Deploy Keys settings.add_deploy_key = Add Deploy Key settings.deploy_key_desc = Deploy keys have read-only access. They are not the same as personal account SSH keys. diff --git a/public/img/dingtalk.ico b/public/img/dingtalk.ico new file mode 100644 index 0000000000..a2682bbbc4 Binary files /dev/null and b/public/img/dingtalk.ico differ diff --git a/routers/repo/webhook.go b/routers/repo/webhook.go index 81f79582de..1bff139528 100644 --- a/routers/repo/webhook.go +++ b/routers/repo/webhook.go @@ -269,6 +269,46 @@ func DiscordHooksNewPost(ctx *context.Context, form auth.NewDiscordHookForm) { ctx.Redirect(orCtx.Link + "/settings/hooks") } +// DingtalkHooksNewPost response for creating dingtalk hook +func DingtalkHooksNewPost(ctx *context.Context, form auth.NewDingtalkHookForm) { + 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.Handle(500, "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.DINGTALK, + Meta: "", + OrgID: orCtx.OrgID, + } + 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(orCtx.Link + "/settings/hooks") +} + // SlackHooksNewPost response for creating slack hook func SlackHooksNewPost(ctx *context.Context, form auth.NewSlackHookForm) { ctx.Data["Title"] = ctx.Tr("repo.settings") @@ -345,17 +385,12 @@ func checkWebhook(ctx *context.Context) (*orgRepoCtx, *models.Webhook) { return nil, nil } + ctx.Data["HookType"] = w.HookTaskType.Name() switch w.HookTaskType { case models.SLACK: ctx.Data["SlackHook"] = w.GetSlackHook() - ctx.Data["HookType"] = "slack" - case models.GOGS: - ctx.Data["HookType"] = "gogs" case models.DISCORD: ctx.Data["DiscordHook"] = w.GetDiscordHook() - ctx.Data["HookType"] = "discord" - default: - ctx.Data["HookType"] = "gitea" } ctx.Data["History"], err = w.History(1) @@ -544,6 +579,38 @@ func DiscordHooksEditPost(ctx *context.Context, form auth.NewDiscordHookForm) { ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID)) } +// DingtalkHooksEditPost response for editing discord hook +func DingtalkHooksEditPost(ctx *context.Context, form auth.NewDingtalkHookForm) { + 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.Handle(500, "UpdateEvent", err) + return + } else if err := models.UpdateWebhook(w); err != nil { + ctx.Handle(500, "UpdateWebhook", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success")) + ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID)) +} + // TestWebhook test if web hook is work fine func TestWebhook(ctx *context.Context) { hookID := ctx.ParamsInt64(":id") diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 5a76dddb66..ece2565683 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -396,11 +396,13 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/gogs/new", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost) m.Post("/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost) m.Post("/discord/new", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksNewPost) + m.Post("/dingtalk/new", bindIgnErr(auth.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost) m.Get("/:id", repo.WebHooksEdit) m.Post("/gitea/:id", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksEditPost) m.Post("/gogs/:id", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksEditPost) m.Post("/slack/:id", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksEditPost) m.Post("/discord/:id", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksEditPost) + m.Post("/dingtalk/:id", bindIgnErr(auth.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost) }) m.Route("/delete", "GET,POST", org.SettingsDelete) @@ -444,12 +446,14 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/gogs/new", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost) m.Post("/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost) m.Post("/discord/new", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksNewPost) + m.Post("/dingtalk/new", bindIgnErr(auth.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost) m.Get("/:id", repo.WebHooksEdit) m.Post("/:id/test", repo.TestWebhook) m.Post("/gitea/:id", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksEditPost) m.Post("/gogs/:id", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost) m.Post("/slack/:id", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksEditPost) m.Post("/discord/:id", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksEditPost) + m.Post("/dingtalk/:id", bindIgnErr(auth.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost) m.Group("/git", func() { m.Get("", repo.GitHooks) diff --git a/templates/org/settings/hook_new.tmpl b/templates/org/settings/hook_new.tmpl index 15c6943185..bedb2dc142 100644 --- a/templates/org/settings/hook_new.tmpl +++ b/templates/org/settings/hook_new.tmpl @@ -17,6 +17,8 @@ {{else if eq .HookType "discord"}} + {{else if eq .HookType "dingtalk"}} + {{end}} @@ -25,6 +27,7 @@ {{template "repo/settings/hook_gogs" .}} {{template "repo/settings/hook_slack" .}} {{template "repo/settings/hook_discord" .}} + {{template "repo/settings/hook_dingtalk" .}} {{template "repo/settings/hook_history" .}} diff --git a/templates/repo/settings/hook_dingtalk.tmpl b/templates/repo/settings/hook_dingtalk.tmpl new file mode 100644 index 0000000000..37271a7db5 --- /dev/null +++ b/templates/repo/settings/hook_dingtalk.tmpl @@ -0,0 +1,11 @@ +{{if eq .HookType "dingtalk"}} +

{{.i18n.Tr "repo.settings.add_dingtalk_hook_desc" "https://dingtalk.com" | Str2html}}

+
+ {{.CsrfTokenHtml}} +
+ + +
+ {{template "repo/settings/hook_settings" .}} +
+{{end}} diff --git a/templates/repo/settings/hook_list.tmpl b/templates/repo/settings/hook_list.tmpl index dce3439096..4e61ba7a07 100644 --- a/templates/repo/settings/hook_list.tmpl +++ b/templates/repo/settings/hook_list.tmpl @@ -17,6 +17,9 @@ Discord + + Dingtalk + diff --git a/templates/repo/settings/hook_new.tmpl b/templates/repo/settings/hook_new.tmpl index a40eb9e428..7e3cf3c8cf 100644 --- a/templates/repo/settings/hook_new.tmpl +++ b/templates/repo/settings/hook_new.tmpl @@ -15,6 +15,8 @@ {{else if eq .HookType "discord"}} + {{else if eq .HookType "dingtalk"}} + {{end}} @@ -23,6 +25,7 @@ {{template "repo/settings/hook_gogs" .}} {{template "repo/settings/hook_slack" .}} {{template "repo/settings/hook_discord" .}} + {{template "repo/settings/hook_dingtalk" .}} {{template "repo/settings/hook_history" .}} diff --git a/vendor/github.com/lunny/dingtalk_webhook/LICENSE b/vendor/github.com/lunny/dingtalk_webhook/LICENSE new file mode 100644 index 0000000000..a8d4b49dd0 --- /dev/null +++ b/vendor/github.com/lunny/dingtalk_webhook/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2016 The Gitea Authors +Copyright (c) 2015 The Gogs Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/lunny/dingtalk_webhook/README.md b/vendor/github.com/lunny/dingtalk_webhook/README.md new file mode 100644 index 0000000000..5625d36e23 --- /dev/null +++ b/vendor/github.com/lunny/dingtalk_webhook/README.md @@ -0,0 +1,18 @@ +# 非官方 Dingtalk webhook Golang SDK + +## 此工程仅封装了 Dingtalk 的 webhook 部分的请求 + +## 使用 + +首先在dingtalk中创建一个机器人,将accessToken拷贝出来,然后执行下面方法即可 + +```Go +webhook := dingtalk.Webhook(accessToken) +webhook.SendTextMsg("这是一个没有AT的文本消息", false) +``` + +## License + +This project is licensed under the MIT License. +See the [LICENSE](https://github.com/lunny/webhook_dingtalk/blob/master/LICENSE) file +for the full license text. \ No newline at end of file diff --git a/vendor/github.com/lunny/dingtalk_webhook/webhook.go b/vendor/github.com/lunny/dingtalk_webhook/webhook.go new file mode 100644 index 0000000000..5d8cbf6c08 --- /dev/null +++ b/vendor/github.com/lunny/dingtalk_webhook/webhook.go @@ -0,0 +1,361 @@ +// Copyright 2017 Lunny Xiao. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package dingtalk + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" +) + +/* +{ + "msgtype": "text", + "text": { + "content": "我就是我, 是不一样的烟火" + }, + "at": { + "atMobiles": [ + "156xxxx8827", + "189xxxx8325" + ], + "isAtAll": false + } +} + +{ + "msgtype": "link", + "link": { + "text": "这个即将发布的新版本,创始人陈航(花名“无招”)称它为“红树林”。 +而在此之前,每当面临重大升级,产品经理们都会取一个应景的代号,这一次,为什么是“红树林”?", + "title": "时代的火车向前开", + "picUrl": "", + "messageUrl": "https://mp.weixin.qq.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI" + } +} + +{ + "msgtype": "markdown", + "markdown": { + "title":"杭州天气", + "text": "#### 杭州天气 @156xxxx8827\n" + + "> 9度,西北风1级,空气良89,相对温度73%\n\n" + + "> ![screenshot](http://image.jpg)\n" + + "> ###### 10点20分发布 [天气](http://www.thinkpage.cn/) \n" + }, + "at": { + "atMobiles": [ + "156xxxx8827", + "189xxxx8325" + ], + "isAtAll": false + } +} + +{ + "actionCard": { + "title": "乔布斯 20 年前想打造一间苹果咖啡厅,而它正是 Apple Store 的前身", + "text": "![screenshot](@lADOpwk3K80C0M0FoA) + ### 乔布斯 20 年前想打造的苹果咖啡厅 + Apple Store 的设计正从原来满满的科技感走向生活化,而其生活化的走向其实可以追溯到 20 年前苹果一个建立咖啡馆的计划", + "hideAvatar": "0", + "btnOrientation": "0", + "singleTitle" : "阅读全文", + "singleURL" : "https://www.dingtalk.com/", + "btns": [ + { + "title": "内容不错", + "actionURL": "https://www.dingtalk.com/" + }, + { + "title": "不感兴趣", + "actionURL": "https://www.dingtalk.com/" + } + ] + }, + "msgtype": "actionCard" +} + +{ + "feedCard": { + "links": [ + { + "title": "时代的火车向前开", + "messageURL": "https://mp.weixin.qq.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI", + "picURL": "https://www.dingtalk.com/" + }, + { + "title": "时代的火车向前开2", + "messageURL": "https://mp.weixin.qq.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI", + "picURL": "https://www.dingtalk.com/" + } + ] + }, + "msgtype": "feedCard" +} +*/ + +type LinkMsg struct { + Title string `json:"title"` + MessageURL string `json:"messageURL"` + PicURL string `json:"picURL"` +} + +type ActionCard struct { + Text string `json:"text"` + Title string `json:"title"` + HideAvatar string `json:"hideAvatar"` + BtnOrientation string `json:"btnOrientation"` + SingleTitle string `json:"singleTitle"` + SingleURL string `json:"singleURL"` + Buttons []struct { + Title string `json:"title"` + ActionURL string `json:"actionURL"` + } `json:"btns"` +} + +// Payload struct +type Payload struct { + MsgType string `json:"msgtype"` + Text struct { + Content string `json:"content"` + } `json:"text"` + Link struct { + Text string `json:"text"` + Title string `json:"title"` + PicURL string `json:"picUrl"` + MessageURL string `json:"messageUrl"` + } `json:"link"` + Markdown struct { + Text string `json:"text"` + Title string `json:"title"` + } `json:"markdown"` + ActionCard ActionCard `json:"actionCard"` + FeedCard struct { + Links []LinkMsg `json:"links"` + } `json:"feedCard"` + At struct { + AtMobiles []string `json:"atMobiles"` + IsAtAll bool `json:"isAtAll"` + } `json:"at"` +} + +type Webhook struct { + accessToken string +} + +func NewWebhook(accessToken string) *Webhook { + return &Webhook{accessToken} +} + +type Response struct { + ErrorCode int `json:"errcode"` + ErrorMessage string `json:"errmsg"` +} + +// SendPayload 发送消息 +func (w *Webhook) SendPayload(payload *Payload) error { + bs, err := json.Marshal(payload) + if err != nil { + return err + } + + resp, err := http.Post("https://oapi.dingtalk.com/robot/send?access_token="+w.accessToken, "application/json", bytes.NewReader(bs)) + if err != nil { + return err + } + + bs, err = ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode != 200 { + return fmt.Errorf("%d: %s", resp.StatusCode, string(bs)) + } + + var result Response + err = json.Unmarshal(bs, &result) + if err != nil { + return err + } + if result.ErrorCode != 0 { + return fmt.Errorf("%d: %s", result.ErrorCode, result.ErrorMessage) + } + + return nil +} + +// SendTextMsg 发送文本消息 +func (w *Webhook) SendTextMsg(content string, isAtAll bool, mobiles ...string) error { + return w.SendPayload(&Payload{ + MsgType: "text", + Text: struct { + Content string `json:"content"` + }{ + Content: content, + }, + At: struct { + AtMobiles []string `json:"atMobiles"` + IsAtAll bool `json:"isAtAll"` + }{ + AtMobiles: mobiles, + IsAtAll: isAtAll, + }, + }) +} + +// SendLinkMsg 发送链接消息 +func (w *Webhook) SendLinkMsg(title, content, picURL, msgURL string) error { + return w.SendPayload(&Payload{ + MsgType: "link", + Link: struct { + Text string `json:"text"` + Title string `json:"title"` + PicURL string `json:"picUrl"` + MessageURL string `json:"messageUrl"` + }{ + Text: content, + Title: title, + PicURL: picURL, + MessageURL: msgURL, + }, + }) +} + +// SendMarkdownMsg 发送markdown消息,仅支持以下格式 +/* +标题 +# 一级标题 +## 二级标题 +### 三级标题 +#### 四级标题 +##### 五级标题 +###### 六级标题 + +引用 +> A man who stands for nothing will fall for anything. + +文字加粗、斜体 +**bold** +*italic* + +链接 +[this is a link](http://name.com) + +图片 +![](http://name.com/pic.jpg) + +无序列表 +- item1 +- item2 + +有序列表 +1. item1 +2. item2 +*/ +func (w *Webhook) SendMarkdownMsg(title, content string, isAtAll bool, mobiles ...string) error { + return w.SendPayload(&Payload{ + MsgType: "markdown", + Markdown: struct { + Text string `json:"text"` + Title string `json:"title"` + }{ + Text: content, + Title: title, + }, + At: struct { + AtMobiles []string `json:"atMobiles"` + IsAtAll bool `json:"isAtAll"` + }{ + AtMobiles: mobiles, + IsAtAll: isAtAll, + }, + }) +} + +// SendSingleActionCardMsg 发送整体跳转ActionCard类型消息 +func (w *Webhook) SendSingleActionCardMsg(title, content, linkTitle, linkURL string, hideAvatar, btnOrientation bool) error { + var strHideAvatar = "0" + if hideAvatar { + strHideAvatar = "1" + } + var strBtnOrientation = "0" + if btnOrientation { + strBtnOrientation = "1" + } + + return w.SendPayload(&Payload{ + MsgType: "actionCard", + ActionCard: ActionCard{ + Text: content, + Title: title, + HideAvatar: strHideAvatar, + BtnOrientation: strBtnOrientation, + SingleTitle: linkTitle, + SingleURL: linkURL, + }, + }) +} + +// SendActionCardMsg 独立跳转ActionCard类型 +func (w *Webhook) SendActionCardMsg(title, content string, linkTitles, linkURLs []string, hideAvatar, btnOrientation bool) error { + if len(linkTitles) == 0 || len(linkURLs) == 0 { + return errors.New("链接参数不能为空") + } + if len(linkTitles) != len(linkURLs) { + return errors.New("链接数量不匹配") + } + + var strHideAvatar = "0" + if hideAvatar { + strHideAvatar = "1" + } + var strBtnOrientation = "0" + if btnOrientation { + strBtnOrientation = "1" + } + + var btns []struct { + Title string `json:"title"` + ActionURL string `json:"actionURL"` + } + + for i := 0; i < len(linkTitles); i++ { + btns = append(btns, struct { + Title string `json:"title"` + ActionURL string `json:"actionURL"` + }{ + Title: linkTitles[i], + ActionURL: linkURLs[i], + }) + } + + return w.SendPayload(&Payload{ + MsgType: "actionCard", + ActionCard: ActionCard{ + Text: content, + Title: title, + HideAvatar: strHideAvatar, + BtnOrientation: strBtnOrientation, + Buttons: btns, + }, + }) +} + +// SendLinkCardMsg 发送链接消息 +func (w *Webhook) SendLinkCardMsg(msgs []LinkMsg) error { + return w.SendPayload(&Payload{ + MsgType: "feedCard", + FeedCard: struct { + Links []LinkMsg `json:"links"` + }{ + Links: msgs, + }, + }) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index d3cb55ebb6..8e91a66b88 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -647,6 +647,12 @@ "revision": "456514e2defec52e0cd37f90ccf17ec8b28295e2", "revisionTime": "2017-10-19T22:30:07Z" }, + { + "checksumSHA1": "gVEVVVLsFxLE+ADLuzkmzMxlmMA=", + "path": "github.com/lunny/dingtalk_webhook", + "revision": "e3534c89ef969912856dfa39e56b09e58c5f5daf", + "revisionTime": "2017-10-25T03:15:54Z" + }, { "checksumSHA1": "O3KUfEXQPfdQ+tCMpP2RAIRJJqY=", "path": "github.com/markbates/goth",