Add dingtalk webhook (#2777)

* add dingtalk webhook type

* add vendor

* some fixes

* fix name check

* fix name check & improvment
release/v1.4
Lunny Xiao 7 years ago committed by Lauris BH
parent 420fc8efc2
commit 10b54df2b2

@ -332,6 +332,7 @@ const (
SLACK
GITEA
DISCORD
DINGTALK
)
var hookTaskTypes = map[string]HookTaskType{
@ -339,6 +340,7 @@ var hookTaskTypes = map[string]HookTaskType{
"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

@ -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
}

@ -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)
}
// .___
// | | ______ ________ __ ____
// | |/ ___// ___/ | \_/ __ \

@ -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)
}

@ -978,6 +978,7 @@ settings.slack_token = Token
settings.slack_domain = Domain
settings.slack_channel = Channel
settings.add_discord_hook_desc = Add <a href="%s">Discord</a> integration to your repository.
settings.add_dingtalk_hook_desc = Add <a href="%s">Dingtalk</a> 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

@ -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")

@ -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)

@ -17,6 +17,8 @@
<img class="img-13" src="{{AppSubUrl}}/img/slack.png">
{{else if eq .HookType "discord"}}
<img class="img-13" src="{{AppSubUrl}}/img/discord.png">
{{else if eq .HookType "dingtalk"}}
<img class="img-13" src="{{AppSubUrl}}/img/dingtalk.png">
{{end}}
</div>
</h4>
@ -25,6 +27,7 @@
{{template "repo/settings/hook_gogs" .}}
{{template "repo/settings/hook_slack" .}}
{{template "repo/settings/hook_discord" .}}
{{template "repo/settings/hook_dingtalk" .}}
</div>
{{template "repo/settings/hook_history" .}}

@ -0,0 +1,11 @@
{{if eq .HookType "dingtalk"}}
<p>{{.i18n.Tr "repo.settings.add_dingtalk_hook_desc" "https://dingtalk.com" | Str2html}}</p>
<form class="ui form" action="{{.BaseLink}}/settings/hooks/dingtalk/{{if .PageIsSettingsHooksNew}}new{{else}}{{.Webhook.ID}}{{end}}" method="post">
{{.CsrfTokenHtml}}
<div class="required field {{if .Err_PayloadURL}}error{{end}}">
<label for="payload_url">{{.i18n.Tr "repo.settings.payload_url"}}</label>
<input id="payload_url" name="payload_url" type="url" value="{{.Webhook.URL}}" autofocus required>
</div>
{{template "repo/settings/hook_settings" .}}
</form>
{{end}}

@ -17,6 +17,9 @@
<a class="item" href="{{.BaseLink}}/settings/hooks/discord/new">
<img class="img-10" src="{{AppSubUrl}}/img/discord.png">Discord
</a>
<a class="item" href="{{.BaseLink}}/settings/hooks/dingtalk/new">
<img class="img-10" src="{{AppSubUrl}}/img/dingtalk.ico">Dingtalk
</a>
</div>
</div>
</div>

@ -15,6 +15,8 @@
<img class="img-13" src="{{AppSubUrl}}/img/slack.png">
{{else if eq .HookType "discord"}}
<img class="img-13" src="{{AppSubUrl}}/img/discord.png">
{{else if eq .HookType "dingtalk"}}
<img class="img-13" src="{{AppSubUrl}}/img/dingtalk.ico">
{{end}}
</div>
</h4>
@ -23,6 +25,7 @@
{{template "repo/settings/hook_gogs" .}}
{{template "repo/settings/hook_slack" .}}
{{template "repo/settings/hook_discord" .}}
{{template "repo/settings/hook_dingtalk" .}}
</div>
{{template "repo/settings/hook_history" .}}

@ -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.

@ -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.

@ -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,
},
})
}

@ -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",

Loading…
Cancel
Save