├── .gitignore ├── LICENSE ├── README.md ├── api_callback.go ├── api_callback_test.go ├── api_contact.go ├── api_encryption.go ├── api_encryption_test.go ├── api_file.go ├── api_file_test.go ├── api_media.go ├── api_message.go ├── api_robot.go ├── api_sns.go ├── crypto.go ├── demo ├── ding_alert │ └── ding_alert.go ├── ding_server │ ├── public │ │ ├── javascripts │ │ │ ├── demo.js │ │ │ ├── demopc.js │ │ │ ├── logger.js │ │ │ └── zepto.min.js │ │ └── stylesheets │ │ │ └── style.css │ ├── server.go │ └── templates │ │ ├── index.html │ │ └── layout.html └── github │ ├── appengine │ ├── app.yaml │ └── main.go │ └── github.go ├── godingtalk.go ├── godingtalk_test.go ├── transport.go ├── util.go └── util_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Hugo Zhu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DingTalk Open API golang SDK 2 | 3 | ![image](http://static.dingtalk.com/media/lALOAQ6nfSvM5Q_229_43.png) 4 | 5 | Check out DingTalk Open API document at: http://open.dingtalk.com 6 | 7 | ## Usage 8 | 9 | Fetch the SDK 10 | ``` 11 | export GOPATH=`pwd` 12 | go get github.com/hugozhu/godingtalk 13 | ``` 14 | 15 | ### Example code to send a micro app message 16 | 17 | ``` 18 | package main 19 | 20 | import ( 21 | "github.com/hugozhu/godingtalk" 22 | "log" 23 | "os" 24 | ) 25 | 26 | func main() { 27 | c := godingtalk.NewDingTalkClient(os.Getenv("corpid"), os.Getenv("corpsecret")) 28 | c.RefreshAccessToken() 29 | err := c.SendAppMessage(os.Args[1], os.Args[2], os.Args[3]) 30 | if err != nil { 31 | log.Println(err) 32 | } 33 | } 34 | ``` 35 | 36 | 37 | ## Guide 38 | 39 | Step-by-step Guide to use this SDK 40 | 41 | http://hugozhu.myalert.info/2016/05/02/66-use-dingtalk-golang-sdk-to-send-message-on-pi.html 42 | 43 | ## Tools 44 | 45 | **ding_alert** : Command line tool to send app/text/oa ... messages 46 | 47 | ``` 48 | export GOPATH=`pwd` 49 | go get github.com/hugozhu/godingtalk/ding_alert 50 | 51 | export corpid=<组织的corpid 通过 https://oa.dingtalk.com 获取> 52 | export corpsecret=<组织的corpsecret 通过 https://oa.dingtalk.com 获取> 53 | 54 | ./bin/ding_alert 55 | Usage of ./bin/ding_alert: 56 | -agent string 57 | agent Id (default "22194403") 58 | -chat string 59 | chat id (default "chat6a93bc1ee3b7d660d372b1b877a9de62") 60 | -file string 61 | file path for media message 62 | -link string 63 | link url (default "http://hugozhu.myalert.info/dingtalk") 64 | -sender string 65 | sender id (default "011217462940") 66 | -text string 67 | text for link message (default "This is link text") 68 | -title string 69 | title for link message (default "This is link title") 70 | -touser string 71 | touser id (default "0420506555") 72 | -type string 73 | message type (app, text, image, voice, link, oa) (default "app") 74 | 75 | ``` 76 | 77 | **github**: Deliver Github webhook events to DingTalk, which can be deployed on Google AppEngine. 78 | 79 | more info at: http://hugozhu.myalert.info/2016/05/15/67-use-free-google-cloud-service-to-deliver-github-webhook-events-to-dingtalk.html 80 | 81 | ``` 82 | export GOPATH=`pwd` 83 | go get github.com/hugozhu/godingtalk/demo/github/appengine 84 | ``` 85 | 86 | Modify `app.yaml` 87 | 88 | ``` 89 | cd src/github.com/hugozhu/godingtalk/demo/github/appengine 90 | cat app.yaml 91 | application: github-alert- 92 | version: 1 93 | runtime: go 94 | api_version: go1 95 | env_variables: 96 | CORP_ID: '<从 http://oa.dingtalk.com 获取>' 97 | CORP_SECRET: '<从 http://oa.dingtalk.com 获取>' 98 | GITHUB_WEBHOOK_SECRET: '<从 http://github.com/ 获取>' 99 | SENDER_ID: '<从 http://open.dingtalk.com 调用api获取>' 100 | CHAT_ID: '<从 http://open.dingtalk.com 调用api获取>' 101 | handlers: 102 | - url: /.* 103 | script: _go_app 104 | 105 | ``` 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /api_callback.go: -------------------------------------------------------------------------------- 1 | package godingtalk 2 | 3 | type Callback struct { 4 | OAPIResponse 5 | Token string 6 | AES_KEY string `json:"aes_key"` 7 | URL string 8 | Callbacks []string `json:"call_back_tag"` 9 | } 10 | 11 | //RegisterCallback is 注册事件回调接口 12 | func (c *DingTalkClient) RegisterCallback(callbacks []string, token string, aes_key string, callbackURL string) error { 13 | var data OAPIResponse 14 | request := map[string]interface{}{ 15 | "call_back_tag": callbacks, 16 | "token": token, 17 | "aes_key": aes_key, 18 | "url": callbackURL, 19 | } 20 | err := c.httpRPC("call_back/register_call_back", nil, request, &data) 21 | return err 22 | } 23 | 24 | //UpdateCallback is 更新事件回调接口 25 | func (c *DingTalkClient) UpdateCallback(callbacks []string, token string, aes_key string, callbackURL string) error { 26 | var data OAPIResponse 27 | request := map[string]interface{}{ 28 | "call_back_tag": callbacks, 29 | "token": token, 30 | "aes_key": aes_key, 31 | "url": callbackURL, 32 | } 33 | err := c.httpRPC("call_back/update_call_back", nil, request, &data) 34 | return err 35 | } 36 | 37 | //DeleteCallback is 删除事件回调接口 38 | func (c *DingTalkClient) DeleteCallback() error { 39 | var data OAPIResponse 40 | err := c.httpRPC("call_back/delete_call_back", nil, nil, &data) 41 | return err 42 | } 43 | 44 | //ListCallback is 查询事件回调接口 45 | func (c *DingTalkClient) ListCallback() (Callback, error) { 46 | var data Callback 47 | err := c.httpRPC("call_back/get_call_back", nil, nil, &data) 48 | return data, err 49 | } 50 | -------------------------------------------------------------------------------- /api_callback_test.go: -------------------------------------------------------------------------------- 1 | package godingtalk 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestRegisterCallback(t *testing.T) { 8 | err := c.UpdateCallback([]string{"user_modify_org"}, "hello", "1234567890123456789012345678901234567890aes", "https://go.myalert.info/dingtalk/callback/") 9 | if err != nil { 10 | t.Error(err) 11 | } 12 | err = c.DeleteCallback() 13 | if err != nil { 14 | t.Error(err) 15 | } 16 | err = c.RegisterCallback([]string{"user_add_org"}, "hello", "1234567890123456789012345678901234567890aes", "https://go.myalert.info/dingtalk/callback/") 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | } 21 | 22 | func TestListCallback(t *testing.T) { 23 | data, err := c.ListCallback() 24 | if err != nil { 25 | t.Error(err) 26 | } 27 | t.Log(data) 28 | } 29 | -------------------------------------------------------------------------------- /api_contact.go: -------------------------------------------------------------------------------- 1 | package godingtalk 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | type User struct { 9 | OAPIResponse 10 | Userid string 11 | Name string 12 | Mobile string 13 | Tel string 14 | Remark string 15 | Order int 16 | IsAdmin bool 17 | IsBoss bool 18 | IsLeader bool 19 | IsSys bool `json:"is_sys"` 20 | SysLevel int `json:"sys_level"` 21 | Active bool 22 | Department []int 23 | Position string 24 | Email string 25 | Avatar string 26 | Extattr interface{} 27 | } 28 | 29 | type UserList struct { 30 | OAPIResponse 31 | HasMore bool 32 | Userlist []User 33 | } 34 | 35 | type Department struct { 36 | OAPIResponse 37 | Id int 38 | Name string 39 | ParentId int 40 | Order int 41 | DeptPerimits string 42 | UserPerimits string 43 | OuterDept bool 44 | OuterPermitDepts string 45 | OuterPermitUsers string 46 | OrgDeptOwner string 47 | DeptManagerUseridList string 48 | } 49 | 50 | type DepartmentList struct { 51 | OAPIResponse 52 | Departments []Department `json:"department"` 53 | } 54 | 55 | // DepartmentList is 获取部门列表 56 | func (c *DingTalkClient) DepartmentList() (DepartmentList, error) { 57 | var data DepartmentList 58 | err := c.httpRPC("department/list", nil, nil, &data) 59 | return data, err 60 | } 61 | 62 | //DepartmentDetail is 获取部门详情 63 | func (c *DingTalkClient) DepartmentDetail(id int) (Department, error) { 64 | var data Department 65 | params := url.Values{} 66 | params.Add("id", fmt.Sprintf("%d", id)) 67 | err :=c.httpRPC("department/get", params, nil, &data) 68 | return data, err 69 | } 70 | 71 | //UserList is 获取部门成员 72 | func (c *DingTalkClient) UserList(departmentID int) (UserList, error) { 73 | var data UserList 74 | params := url.Values{} 75 | params.Add("department_id", fmt.Sprintf("%d", departmentID)) 76 | err :=c.httpRPC("user/list", params, nil, &data) 77 | return data, err 78 | } 79 | 80 | //CreateChat is 81 | func (c *DingTalkClient) CreateChat(name string, owner string, useridlist []string) (string, error) { 82 | var data struct { 83 | OAPIResponse 84 | Chatid string 85 | } 86 | request := map[string]interface{} { 87 | "name":name, 88 | "owner":owner, 89 | "useridlist":useridlist, 90 | } 91 | err :=c.httpRPC("chat/create", nil, request, &data) 92 | return data.Chatid, err 93 | } 94 | 95 | //UserInfoByCode 校验免登录码并换取用户身份 96 | func (c *DingTalkClient) UserInfoByCode(code string) (User, error) { 97 | var data User 98 | params := url.Values{} 99 | params.Add("code", code) 100 | err :=c.httpRPC("user/getuserinfo", params, nil, &data) 101 | return data, err 102 | } 103 | 104 | //UseridByUnionId 通过UnionId获取玩家Userid 105 | func (c *DingTalkClient) UseridByUnionId(unionid string) (string, error) { 106 | var data struct { 107 | OAPIResponse 108 | UserID string `json:"userid"` 109 | } 110 | 111 | params := url.Values{} 112 | params.Add("unionid", unionid) 113 | err :=c.httpRPC("user/getUseridByUnionid", params, nil, &data) 114 | if err!=nil { 115 | return "",err 116 | } 117 | 118 | return data.UserID, err 119 | } -------------------------------------------------------------------------------- /api_encryption.go: -------------------------------------------------------------------------------- 1 | package godingtalk 2 | 3 | //DataMessage 服务端加密、解密消息 4 | type DataMessage struct { 5 | OAPIResponse 6 | Data string 7 | } 8 | 9 | 10 | //Encrypt is 服务端加密 11 | func (c *DingTalkClient) Encrypt(str string) (string, error) { 12 | var data DataMessage 13 | request := map[string]interface{}{ 14 | "data": str, 15 | } 16 | err := c.httpRPC("encryption/encrypt", nil, request, &data) 17 | if err!=nil { 18 | return "", err 19 | } 20 | return data.Data, nil 21 | } 22 | 23 | //Decrypt is 服务端解密 24 | func (c *DingTalkClient) Decrypt(str string) (string, error) { 25 | var data DataMessage 26 | request := map[string]interface{}{ 27 | "data": str, 28 | } 29 | err := c.httpRPC("encryption/decrypt", nil, request, &data) 30 | if err!=nil { 31 | return "", err 32 | } 33 | return data.Data, nil 34 | } -------------------------------------------------------------------------------- /api_encryption_test.go: -------------------------------------------------------------------------------- 1 | package godingtalk 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestEncryption(t *testing.T) { 8 | str, err := c.Encrypt("Hello") 9 | if err!=nil { 10 | t.Error(err) 11 | } else { 12 | t.Log(str) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /api_file.go: -------------------------------------------------------------------------------- 1 | package godingtalk 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/url" 8 | ) 9 | 10 | /** 11 | * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.UeYQVr&treeId=172&articleId=104970&docType=1 12 | * TODO: not completed yet 13 | **/ 14 | 15 | //FileResponse is 16 | type FileResponse struct { 17 | OAPIResponse 18 | Code int 19 | Msg string 20 | UploadID string `json:"uploadid"` 21 | Writer io.Writer 22 | } 23 | 24 | func (f *FileResponse) getWriter() io.Writer { 25 | return f.Writer 26 | } 27 | 28 | //CreateFile is to create a new file in Ding Space 29 | func (c *DingTalkClient) CreateFile(size int64) (file FileResponse, err error) { 30 | buf := bytes.Buffer{} 31 | file = FileResponse{ 32 | Writer: &buf, 33 | } 34 | params := url.Values{} 35 | params.Add("size", fmt.Sprintf("%d", size)) 36 | err = c.httpRPC("file/upload/create", params, nil, &file) 37 | return file, err 38 | } 39 | -------------------------------------------------------------------------------- /api_file_test.go: -------------------------------------------------------------------------------- 1 | package godingtalk 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCreateFile(t *testing.T) { 8 | file, err := c.CreateFile(1024) 9 | t.Log(file, err) 10 | } 11 | -------------------------------------------------------------------------------- /api_media.go: -------------------------------------------------------------------------------- 1 | package godingtalk 2 | 3 | import ( 4 | "io" 5 | "net/url" 6 | ) 7 | 8 | //MediaResponse is 9 | type MediaResponse struct { 10 | OAPIResponse 11 | Type string 12 | MediaID string `json:"media_id"` 13 | Writer io.Writer 14 | } 15 | 16 | func (m *MediaResponse) getWriter() io.Writer { 17 | return m.Writer 18 | } 19 | 20 | //UploadMedia is to upload media file to DingTalk 21 | func (c *DingTalkClient) UploadMedia(mediaType string, filename string, reader io.Reader) (media MediaResponse, err error) { 22 | upload := UploadFile{ 23 | FieldName: "media", 24 | FileName: filename, 25 | Reader: reader, 26 | } 27 | params := url.Values{} 28 | params.Add("type", mediaType) 29 | err = c.httpRPC("media/upload", params, upload, &media) 30 | return media, err 31 | } 32 | 33 | //DownloadMedia is to download a media file from DingTalk 34 | func (c *DingTalkClient) DownloadMedia(mediaID string, write io.Writer) error { 35 | var data MediaResponse 36 | data.Writer = write 37 | params := url.Values{} 38 | params.Add("media_id", mediaID) 39 | err := c.httpRPC("media/get", params, nil, &data) 40 | return err 41 | } 42 | -------------------------------------------------------------------------------- /api_message.go: -------------------------------------------------------------------------------- 1 | package godingtalk 2 | 3 | //SendAppMessage is 发送企业会话消息 4 | func (c *DingTalkClient) SendAppMessage(agentID string, touser string, msg string) error { 5 | if agentID == "" { 6 | agentID = c.AgentID 7 | } 8 | var data OAPIResponse 9 | request := map[string]interface{}{ 10 | "touser": touser, 11 | "agentid": agentID, 12 | "msgtype": "text", 13 | "text": map[string]interface{}{ 14 | "content": msg, 15 | }, 16 | } 17 | err := c.httpRPC("message/send", nil, request, &data) 18 | return err 19 | } 20 | 21 | //SendAppOAMessage is 发送OA消息 22 | func (c *DingTalkClient) SendAppOAMessage(agentID string, touser string, msg OAMessage) error { 23 | if agentID == "" { 24 | agentID = c.AgentID 25 | } 26 | var data OAPIResponse 27 | request := map[string]interface{}{ 28 | "touser": touser, 29 | "agentid": agentID, 30 | "msgtype": "oa", 31 | "oa": msg, 32 | } 33 | err := c.httpRPC("message/send", nil, request, &data) 34 | return err 35 | } 36 | 37 | //SendAppLinkMessage is 发送企业会话链接消息 38 | func (c *DingTalkClient) SendAppLinkMessage(agentID, touser string, title, text string, picUrl, url string) error { 39 | if agentID == "" { 40 | agentID = c.AgentID 41 | } 42 | var data OAPIResponse 43 | request := map[string]interface{}{ 44 | "touser": touser, 45 | "agentid": agentID, 46 | "msgtype": "link", 47 | "link": map[string]string{ 48 | "messageUrl": url, 49 | "picUrl": picUrl, 50 | "title": title, 51 | "text": text, 52 | }, 53 | } 54 | err := c.httpRPC("message/send", nil, request, &data) 55 | return err 56 | } 57 | 58 | //SendTextMessage is 发送普通文本消息 59 | func (c *DingTalkClient) SendTextMessage(sender string, cid string, msg string) error { 60 | var data OAPIResponse 61 | request := map[string]interface{}{ 62 | "chatid": cid, 63 | "sender": sender, 64 | "msgtype": "text", 65 | "text": map[string]interface{}{ 66 | "content": msg, 67 | }, 68 | } 69 | err := c.httpRPC("chat/send", nil, request, &data) 70 | return err 71 | } 72 | 73 | //SendImageMessage is 发送图片消息 74 | func (c *DingTalkClient) SendImageMessage(sender string, cid string, mediaID string) error { 75 | var data OAPIResponse 76 | request := map[string]interface{}{ 77 | "chatid": cid, 78 | "sender": sender, 79 | "msgtype": "image", 80 | "image": map[string]string{ 81 | "media_id": mediaID, 82 | }, 83 | } 84 | err := c.httpRPC("chat/send", nil, request, &data) 85 | return err 86 | } 87 | 88 | //SendVoiceMessage is 发送语音消息 89 | func (c *DingTalkClient) SendVoiceMessage(sender string, cid string, mediaID string, duration string) error { 90 | var data OAPIResponse 91 | request := map[string]interface{}{ 92 | "chatid": cid, 93 | "sender": sender, 94 | "msgtype": "voice", 95 | "voice": map[string]string{ 96 | "media_id": mediaID, 97 | "duration": duration, 98 | }, 99 | } 100 | err := c.httpRPC("chat/send", nil, request, &data) 101 | return err 102 | } 103 | 104 | //SendFileMessage is 发送文件消息 105 | func (c *DingTalkClient) SendFileMessage(sender string, cid string, mediaID string) error { 106 | var data OAPIResponse 107 | request := map[string]interface{}{ 108 | "chatid": cid, 109 | "sender": sender, 110 | "msgtype": "file", 111 | "file": map[string]string{ 112 | "media_id": mediaID, 113 | }, 114 | } 115 | err := c.httpRPC("chat/send", nil, request, &data) 116 | return err 117 | } 118 | 119 | //SendLinkMessage is 发送链接消息 120 | func (c *DingTalkClient) SendLinkMessage(sender string, cid string, mediaID string, url string, title string, text string) error { 121 | var data OAPIResponse 122 | request := map[string]interface{}{ 123 | "chatid": cid, 124 | "sender": sender, 125 | "msgtype": "link", 126 | "link": map[string]string{ 127 | "messageUrl": url, 128 | "picUrl": mediaID, 129 | "title": title, 130 | "text": text, 131 | }, 132 | } 133 | err := c.httpRPC("chat/send", nil, request, &data) 134 | return err 135 | } 136 | 137 | //OAMessage is the Message for OA 138 | type OAMessage struct { 139 | URL string `json:"message_url"` 140 | PcURL string `json:"pc_message_url"` 141 | Head struct { 142 | BgColor string `json:"bgcolor,omitempty"` 143 | Text string `json:"text,omitempty"` 144 | } `json:"head,omitempty"` 145 | Body struct { 146 | Title string `json:"title,omitempty"` 147 | Form []OAMessageForm `json:"form,omitempty"` 148 | Rich OAMessageRich `json:"rich,omitempty"` 149 | Content string `json:"content,omitempty"` 150 | Image string `json:"image,omitempty"` 151 | FileCount int `json:"file_count,omitempty"` 152 | Author string `json:"author,omitempty"` 153 | } `json:"body,omitempty"` 154 | } 155 | 156 | type OAMessageForm struct { 157 | Key string `json:"key,omitempty"` 158 | Value string `json:"value,omitempty"` 159 | } 160 | 161 | type OAMessageRich struct { 162 | Num string `json:"num,omitempty"` 163 | Unit string `json:"body,omitempty"` 164 | } 165 | 166 | func (m *OAMessage) AppendFormItem(key string, value string) { 167 | f := OAMessageForm{Key: key, Value: value} 168 | 169 | if m.Body.Form == nil { 170 | m.Body.Form = []OAMessageForm{} 171 | } 172 | 173 | m.Body.Form = append(m.Body.Form, f) 174 | } 175 | 176 | //SendOAMessage is 发送OA消息 177 | func (c *DingTalkClient) SendOAMessage(sender string, cid string, msg OAMessage) error { 178 | var data OAPIResponse 179 | request := map[string]interface{}{ 180 | "chatid": cid, 181 | "sender": sender, 182 | "msgtype": "oa", 183 | "oa": msg, 184 | } 185 | err := c.httpRPC("chat/send", nil, request, &data) 186 | return err 187 | } 188 | -------------------------------------------------------------------------------- /api_robot.go: -------------------------------------------------------------------------------- 1 | package godingtalk 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | //SendRobotTextMessage can send a text message to a group chat 8 | func (c *DingTalkClient) SendRobotTextMessage(accessToken string, msg string) error { 9 | var data OAPIResponse 10 | params := url.Values{} 11 | params.Add("access_token", accessToken) 12 | request := map[string]interface{}{ 13 | "msgtype": "text", 14 | "text": map[string]interface{}{ 15 | "content": msg, 16 | }, 17 | } 18 | err := c.httpRPC("robot/send", params, request, &data) 19 | return err 20 | } -------------------------------------------------------------------------------- /api_sns.go: -------------------------------------------------------------------------------- 1 | //普通钉钉用户账号开放相关接口 2 | package godingtalk 3 | 4 | import( 5 | "net/url" 6 | ) 7 | 8 | //获取钉钉开放应用ACCESS_TOKEN 9 | //TODO: 10 | // 根据和赤司(钉钉开发者)的沟通,ACCESS_TOKEN只有两个小时的有效期 11 | // 但是目前接口貌似没有返回过期时间相关的信息,因此所有相关的调用都需要强制刷新 12 | func (c *DingTalkClient) RefreshSnsAccessToken() error { 13 | var data AccessTokenResponse 14 | 15 | params := url.Values{} 16 | params.Add("appid", c.SnsAppID) 17 | params.Add("appsecret", c.SnsAppSecret) 18 | 19 | err := c.httpRPC("sns/gettoken", params, nil, &data) 20 | if err==nil { 21 | c.SnsAccessToken = data.AccessToken 22 | } 23 | return err 24 | } 25 | 26 | //获取用户授权的持久授权码返回信息 27 | type SnsPersistentCodeResponse struct { 28 | OAPIResponse 29 | UnionID string `json:"unionid"` 30 | OpenID string `json:"openid"` 31 | PersistentCode string `json:"persistent_code"` 32 | } 33 | 34 | //获取用户授权的持久授权码 35 | func (c *DingTalkClient) GetSnsPersistentCode(tmpAuthCode string) (string, string, string, error) { 36 | c.RefreshSnsAccessToken() 37 | 38 | params := url.Values{} 39 | params.Add("access_token", c.SnsAccessToken) 40 | 41 | request := map[string]interface{}{ 42 | "tmp_auth_code": tmpAuthCode, 43 | } 44 | 45 | var data SnsPersistentCodeResponse 46 | err := c.httpRequest("sns/get_persistent_code", params, request, &data) 47 | if err!=nil { 48 | return "","","",err 49 | } 50 | return data.UnionID, data.OpenID, data.PersistentCode, nil 51 | } 52 | 53 | 54 | type SnsTokenResponse struct { 55 | OAPIResponse 56 | Expires int `json:"expires_in"` 57 | SnsToken string `json:"sns_token"` 58 | } 59 | 60 | //获取用户授权的SNS_TOKEN 61 | func (c *DingTalkClient) GetSnsToken(openid, persistentCode string) (string, error) { 62 | c.RefreshSnsAccessToken() 63 | 64 | params := url.Values{} 65 | params.Add("access_token", c.SnsAccessToken) 66 | 67 | request := map[string]interface{}{ 68 | "openid": openid, 69 | "persistent_code": persistentCode, 70 | } 71 | 72 | var data SnsTokenResponse 73 | err := c.httpRequest("sns/get_sns_token", params, request, &data) 74 | if err!=nil { 75 | return "", err 76 | } 77 | return data.SnsToken, err 78 | } 79 | 80 | type SnsUserInfoResponse struct { 81 | OAPIResponse 82 | 83 | CorpInfo []struct{ 84 | CorpName string `json:"corp_name"` 85 | IsAuth bool `json:"is_auth"` 86 | IsManager bool `json:"is_manager"` 87 | RightsLevel int `json:"rights_level"` 88 | } `json:"corp_info"` 89 | 90 | UserInfo struct { 91 | MaskedMobile string `json:"marskedMobile"` 92 | Nick string `json:"nick"` 93 | OpenID string `json:"openid"` 94 | UnionID string `json:"unionid"` 95 | DingID string `json:"dingId"` 96 | } `json:"user_info"` 97 | } 98 | 99 | //获取用户授权的个人信息 100 | func (c *DingTalkClient) GetSnsUserInfo(snsToken string) (SnsUserInfoResponse, error) { 101 | c.RefreshSnsAccessToken() 102 | 103 | params := url.Values{} 104 | params.Add("sns_token", snsToken) 105 | 106 | var data SnsUserInfoResponse 107 | err := c.httpRequest("sns/getuserinfo", params, nil, &data) 108 | return data, err 109 | } 110 | -------------------------------------------------------------------------------- /crypto.go: -------------------------------------------------------------------------------- 1 | package godingtalk 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "encoding/base64" 8 | "encoding/binary" 9 | "errors" 10 | "math/rand" 11 | r "math/rand" 12 | "sort" 13 | "time" 14 | ) 15 | 16 | const ( 17 | AES_ENCODE_KEY_LENGTH = 43 18 | ) 19 | 20 | var DefaultDingtalkCrypto *Crypto 21 | 22 | type Crypto struct { 23 | Token string 24 | AesKey string 25 | SuiteKey string 26 | block cipher.Block 27 | bkey []byte 28 | } 29 | 30 | /* 31 | token 数据签名需要用到的token,ISV(服务提供商)推荐使用注册套件时填写的token,普通企业可以随机填写 32 | aesKey 数据加密密钥。用于回调数据的加密,长度固定为43个字符,从a-z, A-Z, 0-9共62个字符中选取,您可以随机生成,ISV(服务提供商)推荐使用注册套件时填写的EncodingAESKey 33 | suiteKey 一般使用corpID 34 | */ 35 | func NewCrypto(token, aesKey, suiteKey string) (c *Crypto) { 36 | c = &Crypto{ 37 | Token: token, 38 | AesKey: aesKey, 39 | SuiteKey: suiteKey, 40 | } 41 | if len(c.AesKey) != AES_ENCODE_KEY_LENGTH { 42 | panic("不合法的aeskey") 43 | } 44 | var err error 45 | c.bkey, err = base64.StdEncoding.DecodeString(aesKey + "=") 46 | if err != nil { 47 | panic(err.Error()) 48 | } 49 | c.block, err = aes.NewCipher(c.bkey) 50 | if err != nil { 51 | panic(err.Error()) 52 | } 53 | return c 54 | } 55 | 56 | /* 57 | signature: 签名字符串 58 | timeStamp: 时间戳 59 | nonce: 随机字符串 60 | secretStr: 密文 61 | 返回: 解密后的明文 62 | */ 63 | func (c *Crypto) DecryptMsg(signature, timeStamp, nonce, secretStr string) (string, error) { 64 | if !c.VerifySignature(c.Token, timeStamp, nonce, secretStr, signature) { 65 | return "", errors.New("签名不匹配") 66 | } 67 | decode, err := base64.StdEncoding.DecodeString(secretStr) 68 | if err != nil { 69 | return "", err 70 | } 71 | if len(decode) < aes.BlockSize { 72 | return "", errors.New("密文太短啦") 73 | } 74 | blockMode := cipher.NewCBCDecrypter(c.block, c.bkey[:c.block.BlockSize()]) 75 | plantText := make([]byte, len(decode)) 76 | blockMode.CryptBlocks(plantText, decode) 77 | plantText = PKCS7UnPadding(plantText) 78 | size := binary.BigEndian.Uint32(plantText[16 : 16+4]) 79 | plantText = plantText[16+4:] 80 | cropid := plantText[size:] 81 | if string(cropid) != c.SuiteKey { 82 | return "", errors.New("CropID不正确") 83 | } 84 | return string(plantText[:size]), nil 85 | } 86 | 87 | func PKCS7UnPadding(plantText []byte) []byte { 88 | length := len(plantText) 89 | unpadding := int(plantText[length-1]) 90 | return plantText[:(length - unpadding)] 91 | } 92 | 93 | /* 94 | replyMsg: 明文字符串 95 | timeStamp: 时间戳 96 | nonce: 随机字符串 97 | 返回: 密文,签名字符串 98 | */ 99 | func (c *Crypto) EncryptMsg(replyMsg, timeStamp, nonce string) (string, string, error) { 100 | //原生消息体长度 101 | size := make([]byte, 4) 102 | binary.BigEndian.PutUint32(size, uint32(len(replyMsg))) 103 | replyMsg = c.RandomString(16) + string(size) + replyMsg + c.SuiteKey 104 | plantText := PKCS7Padding([]byte(replyMsg), c.block.BlockSize()) 105 | if len(plantText)%aes.BlockSize != 0 { 106 | return "", "", errors.New("消息体大小不为16的倍数") 107 | } 108 | 109 | blockMode := cipher.NewCBCEncrypter(c.block, c.bkey[:c.block.BlockSize()]) 110 | ciphertext := make([]byte, len(plantText)) 111 | blockMode.CryptBlocks(ciphertext, plantText) 112 | outStr := base64.StdEncoding.EncodeToString(ciphertext) 113 | sigStr := c.GenerateSignature(c.Token, timeStamp, nonce, string(outStr)) 114 | return string(outStr), sigStr, nil 115 | } 116 | 117 | func PKCS7Padding(ciphertext []byte, blockSize int) []byte { 118 | padding := blockSize - len(ciphertext)%blockSize 119 | padtext := bytes.Repeat([]byte{byte(padding)}, padding) 120 | return append(ciphertext, padtext...) 121 | } 122 | 123 | // 数据签名 124 | func (c *Crypto) GenerateSignature(token, timeStamp, nonce, secretStr string) string { 125 | // 先将参数值进行排序 126 | params := make([]string, 0) 127 | params = append(params, token) 128 | params = append(params, secretStr) 129 | params = append(params, timeStamp) 130 | params = append(params, nonce) 131 | sort.Strings(params) 132 | return sha1Sign(params[0] + params[1] + params[2] + params[3]) 133 | } 134 | 135 | // 校验数据签名 136 | func (c *Crypto) VerifySignature(token, timeStamp, nonce, secretStr, sigture string) bool { 137 | return c.GenerateSignature(token, timeStamp, nonce, secretStr) == sigture 138 | } 139 | 140 | func (c *Crypto) RandomString(n int, alphabets ...byte) string { 141 | const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 142 | var bytes = make([]byte, n) 143 | var randby bool 144 | if num, err := rand.Read(bytes); num != n || err != nil { 145 | r.Seed(time.Now().UnixNano()) 146 | randby = true 147 | } 148 | for i, b := range bytes { 149 | if len(alphabets) == 0 { 150 | if randby { 151 | bytes[i] = alphanum[r.Intn(len(alphanum))] 152 | } else { 153 | bytes[i] = alphanum[b%byte(len(alphanum))] 154 | } 155 | } else { 156 | if randby { 157 | bytes[i] = alphabets[r.Intn(len(alphabets))] 158 | } else { 159 | bytes[i] = alphabets[b%byte(len(alphabets))] 160 | } 161 | } 162 | } 163 | return string(bytes) 164 | } 165 | -------------------------------------------------------------------------------- /demo/ding_alert/ding_alert.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | 10 | dingtalk "github.com/hugozhu/godingtalk" 11 | ) 12 | 13 | var msgType string 14 | var agentID string 15 | var senderID string 16 | var toUser string 17 | var chatID string 18 | var content string 19 | var link string 20 | var file string 21 | var title string 22 | var text string 23 | var robot bool 24 | var token string 25 | 26 | func init() { 27 | flag.BoolVar(&robot,"robot", false,"use robot api?") 28 | flag.StringVar(&token,"token", "" ,"robot access token or token to override env setting") 29 | flag.StringVar(&msgType, "type", "app", "message type (app, text, image, voice, link, oa)") 30 | flag.StringVar(&agentID, "agent", "22194403", "agent Id") 31 | flag.StringVar(&senderID, "sender", "011217462940", "sender id") 32 | flag.StringVar(&toUser, "touser", "0420506555", "touser id") 33 | flag.StringVar(&chatID, "chat", "chat6a93bc1ee3b7d660d372b1b877a9de62", "chat id") 34 | flag.StringVar(&link, "link", "http://hugozhu.myalert.info/dingtalk", "link url") 35 | flag.StringVar(&file, "file", "", "file path for media message") 36 | flag.StringVar(&title, "title", "This is link title", "title for link message") 37 | flag.StringVar(&text, "text", "This is link text", "text for link message") 38 | flag.Parse() 39 | } 40 | 41 | func usage() { 42 | flag.Usage() 43 | os.Exit(-1) 44 | } 45 | 46 | func fatalError(err error) { 47 | fmt.Println(err) 48 | os.Exit(-1) 49 | } 50 | 51 | func main() { 52 | c := dingtalk.NewDingTalkClient(os.Getenv("corpid"), os.Getenv("corpsecret")) 53 | c.RefreshAccessToken() 54 | var err error 55 | if len(os.Args) < 2 { 56 | usage() 57 | } 58 | content = os.Args[len(os.Args)-1] 59 | switch msgType { 60 | case "app": 61 | err = c.SendAppMessage(agentID, toUser, content) 62 | case "text": 63 | if (robot) { 64 | err = c.SendRobotTextMessage(token, content) 65 | } else { 66 | err = c.SendTextMessage(senderID, chatID, content) 67 | } 68 | case "image": 69 | if file == "" { 70 | panic("Image path is empty") 71 | } 72 | f, err := os.Open(file) 73 | defer f.Close() 74 | if err != nil { 75 | fatalError(err) 76 | } 77 | media, err := c.UploadMedia("image", filepath.Base(file), f) 78 | if err != nil { 79 | fatalError(err) 80 | } 81 | err = c.SendImageMessage(senderID, chatID, media.MediaID) 82 | if err != nil { 83 | fatalError(err) 84 | } 85 | case "voice": 86 | if file == "" { 87 | panic("Voice file path is empty") 88 | } 89 | f, err := os.Open(file) 90 | defer f.Close() 91 | if err != nil { 92 | fatalError(err) 93 | } 94 | media, err := c.UploadMedia("voice", filepath.Base(file), f) 95 | if err != nil { 96 | fatalError(err) 97 | } 98 | err = c.SendVoiceMessage(senderID, chatID, media.MediaID, "10") 99 | if err != nil { 100 | fatalError(err) 101 | } 102 | case "file": 103 | if file == "" { 104 | panic("File path is empty") 105 | } 106 | f, err := os.Open(file) 107 | defer f.Close() 108 | if err != nil { 109 | fatalError(err) 110 | } 111 | media, err := c.UploadMedia("file", filepath.Base(file), f) 112 | if err != nil { 113 | fatalError(err) 114 | } 115 | err = c.SendFileMessage(senderID, chatID, media.MediaID) 116 | if err != nil { 117 | fatalError(err) 118 | } 119 | case "link": 120 | if file == "" { 121 | panic("File path is empty") 122 | } 123 | f, err := os.Open(file) 124 | defer f.Close() 125 | if err != nil { 126 | fatalError(err) 127 | } 128 | media, err := c.UploadMedia("image", filepath.Base(file), f) 129 | if err != nil { 130 | fatalError(err) 131 | } 132 | err = c.SendLinkMessage(senderID, chatID, media.MediaID, link, title, text) 133 | if err != nil { 134 | fatalError(err) 135 | } 136 | case "oa": 137 | msg := dingtalk.OAMessage{} 138 | json.Unmarshal([]byte(content), &msg) 139 | err = c.SendOAMessage(senderID, chatID, msg) 140 | } 141 | if err != nil { 142 | fmt.Println(err) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /demo/ding_server/public/javascripts/demo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by liqiao on 8/10/15. 3 | */ 4 | 5 | logger.i('Here we go...'); 6 | 7 | logger.i(location.href); 8 | 9 | /** 10 | * _config comes from server-side template. see views/index.jade 11 | */ 12 | 13 | dd.config({ 14 | agentId: _config.agentId, 15 | corpId: _config.corpId, 16 | timeStamp: _config.timeStamp, 17 | nonceStr: _config.nonceStr, 18 | signature: _config.signature, 19 | jsApiList: [ 20 | 'runtime.info', 21 | 'device.notification.prompt', 22 | 'biz.chat.chooseConversationByCorpId', 23 | 'device.notification.confirm', 24 | 'device.notification.alert', 25 | 'device.notification.prompt', 26 | 'biz.chat.open', 27 | 'biz.util.open', 28 | 'biz.user.get', 29 | 'biz.contact.choose', 30 | 'biz.telephone.call', 31 | 'biz.ding.post'] 32 | }); 33 | 34 | dd.ready(function() { 35 | logger.i('dd.ready rocks!'); 36 | dd.runtime.info({ 37 | onSuccess: function(info) { 38 | logger.i('runtime info: ' + JSON.stringify(info)); 39 | }, 40 | onFail: function(err) { 41 | logger.e('fail: ' + JSON.stringify(err)); 42 | } 43 | }); 44 | 45 | dd.runtime.permission.requestAuthCode({ 46 | corpId: _config.corpId, //企业id 47 | onSuccess: function (info) { 48 | logger.i('authcode: ' + info.code); 49 | $.ajax({ 50 | url: '/get_user_info', 51 | type:"POST", 52 | data: {"code":info.code}, 53 | dataType:'json', 54 | timeout: 900, 55 | success: function (info, status, xhr) { 56 | if (info.errcode === 0) { 57 | logger.i('user id: ' + info.Userid); 58 | dd.userid = info.Userid; 59 | } 60 | else { 61 | logger.e('auth error: ' + JSON.stringify(info)); 62 | } 63 | }, 64 | error: function (xhr, errorType, error) { 65 | logger.e(errorType + ', ' + error); 66 | } 67 | }); 68 | }, 69 | onFail: function (err) { 70 | logger.e('requestAuthCode fail: ' + JSON.stringify(err)); 71 | } 72 | }); 73 | 74 | dd.biz.user.get({ 75 | onSuccess: function (info) { 76 | logger.e('userGet success: ' + JSON.stringify(info)); 77 | }, 78 | onFail: function (err) { 79 | logger.e('userGet fail: ' + JSON.stringify(err)); 80 | } 81 | }); 82 | $('.chooseonebtn').on('click', function() { 83 | dd.biz.chat.chooseConversationByCorpId({ 84 | corpId: _config.corpId, //企业id 85 | onSuccess: function (data) { 86 | var chatinfo = data; 87 | if(chatinfo){ 88 | console.log(chatinfo.chatId); 89 | logger.i("cid: "+chatinfo.chatId+"\nsender: "+dd.userid+"\n"+JSON.stringify(data)) 90 | dd.device.notification.prompt({ 91 | message: "发送消息", 92 | title: chatinfo.title, 93 | buttonLabels: ['发送', '取消'], 94 | onSuccess : function(result) { 95 | var text = result.value; 96 | if(text==''){ 97 | return false; 98 | } 99 | $.ajax({ 100 | url: '/send_message', 101 | type:"POST", 102 | data: {"cid":chatinfo.chatId,"sender":dd.userid,"content":text}, 103 | dataType:'json', 104 | timeout: 900, 105 | success: function (info, status, xhr) { 106 | logger.i('sendMsg: ' + JSON.stringify(info)); 107 | if(info.errcode==0){ 108 | logger.i('sendMsg: 发送成功'); 109 | /** 110 | * 跳转到对话界面 111 | */ 112 | // dd.biz.chat.open({ 113 | // cid:chatinfo.cid, 114 | // onSuccess : function(result) { 115 | // }, 116 | // onFail : function(err) {} 117 | // }); 118 | }else{ 119 | logger.e('sendMsg: 发送失败'+info.errmsg); 120 | } 121 | }, 122 | error: function (xhr, errorType, error) { 123 | logger.e(errorType + ', ' + error); 124 | } 125 | }); 126 | }, 127 | onFail : function(err) {} 128 | }); 129 | } 130 | }, 131 | onFail: function (err) { 132 | } 133 | }); 134 | }); 135 | 136 | $('.phonecall').on('click', function() { 137 | dd.biz.contact.choose({ 138 | startWithDepartmentId: 0, //-1表示打开的通讯录从自己所在部门开始展示, 0表示从企业最上层开始,(其他数字表示从该部门开始:暂时不支持) 139 | multiple: false, //是否多选: true多选 false单选; 默认true 140 | users: [], //默认选中的用户列表,userid;成功回调中应包含该信息 141 | corpId: _config.corpId, //企业id 142 | max: 10, //人数限制,当multiple为true才生效,可选范围1-1500 143 | onSuccess: function(data) { 144 | if(data&&data.length>0){ 145 | var selectUserId = data[0].emplId; 146 | if(selectUserId>0){ 147 | dd.biz.telephone.call({ 148 | users: [selectUserId], //用户列表,工号 149 | corpId: _config.corpId, //企业id 150 | onSuccess : function(info) { 151 | logger.i('biz.telephone.call: info' + JSON.stringify(info)); 152 | 153 | }, 154 | onFail : function(err) { 155 | logger.e('biz.telephone.call: error' + JSON.stringify(err)); 156 | } 157 | }) 158 | }else{ 159 | return false; 160 | } 161 | }else{ 162 | return false; 163 | } 164 | }, 165 | onFail : function(err) {} 166 | }); 167 | }); 168 | }); 169 | 170 | dd.error(function(err) { 171 | logger.e('dd error: ' + JSON.stringify(err)); 172 | }); 173 | -------------------------------------------------------------------------------- /demo/ding_server/public/javascripts/demopc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by liqiao on 8/10/15. 3 | */ 4 | 5 | logger.i('Here we go...'); 6 | 7 | logger.i(location.href); 8 | 9 | /** 10 | * _config comes from server-side template. see views/index.jade 11 | */ 12 | DingTalkPC.config({ 13 | agentId: _config.agentId, 14 | corpId: _config.corpId, 15 | timeStamp: _config.timeStamp, 16 | nonceStr: _config.nonceStr, 17 | signature: _config.signature, 18 | jsApiList: [ 19 | 'runtime.permission.requestAuthCode', 20 | 'device.notification.alert', 21 | 'device.notification.confirm', 22 | 'biz.contact.choose', 23 | 'device.notification.prompt', 24 | 'biz.ding.post' 25 | ] // 必填,需要使用的jsapi列表 26 | }); 27 | DingTalkPC.userid=0; 28 | DingTalkPC.ready(function(res){ 29 | logger.i('dd.ready rocks!'); 30 | 31 | DingTalkPC.runtime.permission.requestAuthCode({ 32 | corpId: _config.corpId, //企业ID 33 | onSuccess: function(info) { 34 | /*{ 35 | code: 'hYLK98jkf0m' //string authCode 36 | }*/ 37 | logger.i('authcode: ' + info.code); 38 | $.ajax({ 39 | url: '/sendMsg.php', 40 | type:"POST", 41 | data: {"event":"get_userinfo","code":info.code}, 42 | dataType:'json', 43 | timeout: 900, 44 | success: function (data, status, xhr) { 45 | var info = JSON.parse(data); 46 | if (info.errcode === 0) { 47 | logger.i('user id: ' + info.userid); 48 | DingTalkPC.userid = info.userid; 49 | } 50 | else { 51 | logger.e('auth error: ' + data); 52 | } 53 | }, 54 | error: function (xhr, errorType, error) { 55 | logger.e(errorType + ', ' + error); 56 | } 57 | }); 58 | }, 59 | onFail : function(err) { 60 | logger.e(JSON.stringify(err)); 61 | } 62 | 63 | }); 64 | $('.chooseonebtn').on('click', function() { 65 | 66 | DingTalkPC.biz.contact.choose({ 67 | multiple: false, //是否多选: true多选 false单选; 默认true 68 | users: [], //默认选中的用户列表,工号;成功回调中应包含该信息 69 | corpId: _config.corpId, //企业id 70 | max: 1, //人数限制,当multiple为true才生效,可选范围1-1500 71 | onSuccess: function(data) { 72 | if(data&&data.length>0){ 73 | var selectUserId = data[0].emplId; 74 | if(selectUserId>0){ 75 | DingTalkPC.device.notification.prompt({ 76 | message: "发送消息", 77 | title: data[0].name, 78 | buttonLabels: ['发送', '取消'], 79 | onSuccess : function(result) { 80 | var textContent = result.value; 81 | if(textContent==''){ 82 | return false; 83 | } 84 | DingTalkPC.biz.ding.post({ 85 | users : [selectUserId],//用户列表,工号 86 | corpId: _config.corpId, //加密的企业id 87 | type: 1, //钉类型 1:image 2:link 88 | alertType: 2, 89 | alertDate: {"format":"yyyy-MM-dd HH:mm","value":"2016-05-09 08:00"}, 90 | attachment: { 91 | images: [] //只取第一个image 92 | }, //附件信息 93 | text: textContent, //消息体 94 | onSuccess : function(info) { 95 | logger.i('DingTalkPC.biz.ding.post: info' + JSON.stringify(info)); 96 | }, 97 | onFail : function(err) { 98 | logger.e('DingTalkPC.biz.ding.post: info' + JSON.stringify(err)); 99 | } 100 | }) 101 | /* 102 | { 103 | buttonIndex: 0, //被点击按钮的索引值,Number类型,从0开始 104 | value: '' //输入的值 105 | } 106 | */ 107 | }, 108 | onFail : function(err) {} 109 | }); 110 | } 111 | } 112 | }, 113 | onFail : function(err) {} 114 | }); 115 | }); 116 | /*DingTalkPC.biz.util.uploadImage({ 117 | multiple: false, //是否多选,默认false 118 | max: 5, //最多可选个数 119 | onSuccess : function(result) { 120 | logger.i(result); 121 | }, 122 | onFail : function() {} 123 | });*/ 124 | /*DingTalkPC.device.notification.alert({ 125 | message: "亲爱的", 126 | title: "提示",//可传空 127 | buttonName: "收到", 128 | onSuccess : function() { 129 | }, 130 | onFail : function(err) {} 131 | });*/ 132 | }); 133 | 134 | 135 | -------------------------------------------------------------------------------- /demo/ding_server/public/javascripts/logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by liqiao on 8/14/15. 3 | */ 4 | 5 | var log = document.createElement('div'); 6 | log.setAttribute('id', 'log'); 7 | document.body.appendChild(log); 8 | 9 | var logger = { 10 | i: function(info) { 11 | add(info, 'i'); 12 | }, 13 | d: function(debug) { 14 | add(debug, 'd'); 15 | }, 16 | e: function(err) { 17 | add(err, 'e'); 18 | } 19 | }; 20 | 21 | function add(msg, level) { 22 | var row = document.createElement('div'); 23 | row.setAttribute('class', 'log-row log-' + level); 24 | row.innerHTML = "\n"+msg; 25 | document.querySelector('#log').appendChild(row); 26 | } -------------------------------------------------------------------------------- /demo/ding_server/public/javascripts/zepto.min.js: -------------------------------------------------------------------------------- 1 | (function(a){String.prototype.trim===a&&(String.prototype.trim=function(){return this.replace(/^\s+/,"").replace(/\s+$/,"")}),Array.prototype.reduce===a&&(Array.prototype.reduce=function(b){if(this===void 0||this===null)throw new TypeError;var c=Object(this),d=c.length>>>0,e=0,f;if(typeof b!="function")throw new TypeError;if(d==0&&arguments.length==1)throw new TypeError;if(arguments.length>=2)f=arguments[1];else do{if(e in c){f=c[e++];break}if(++e>=d)throw new TypeError}while(!0);while(e0?[].concat.apply([],a):a}function H(a){return a.replace(/::/g,"/").replace(/([A-Z]+)([A-Z][a-z])/g,"$1_$2").replace(/([a-z\d])([A-Z])/g,"$1_$2").replace(/_/g,"-").toLowerCase()}function I(a){return a in i?i[a]:i[a]=new RegExp("(^|\\s)"+a+"(\\s|$)")}function J(a,b){return typeof b=="number"&&!k[H(a)]?b+"px":b}function K(a){var b,c;return h[a]||(b=g.createElement(a),g.body.appendChild(b),c=j(b,"").getPropertyValue("display"),b.parentNode.removeChild(b),c=="none"&&(c="block"),h[a]=c),h[a]}function L(b,d){return d===a?c(b):c(b).filter(d)}function M(a,b,c,d){return A(b)?b.call(a,c,d):b}function N(a,b,d){var e=a%2?b:b.parentNode;e?e.insertBefore(d,a?a==1?e.firstChild:a==2?b:null:b.nextSibling):c(d).remove()}function O(a,b){b(a);for(var c in a.childNodes)O(a.childNodes[c],b)}var a,b,c,d,e=[],f=e.slice,g=window.document,h={},i={},j=g.defaultView.getComputedStyle,k={"column-count":1,columns:1,"font-weight":1,"line-height":1,opacity:1,"z-index":1,zoom:1},l=/^\s*<(\w+|!)[^>]*>/,m=[1,3,8,9,11],n=["after","prepend","before","append"],o=g.createElement("table"),p=g.createElement("tr"),q={tr:g.createElement("tbody"),tbody:o,thead:o,tfoot:o,td:p,th:p,"*":g.createElement("div")},r=/complete|loaded|interactive/,s=/^\.([\w-]+)$/,t=/^#([\w-]+)$/,u=/^[\w-]+$/,v={}.toString,w={},x,y,z=g.createElement("div");return w.matches=function(a,b){if(!a||a.nodeType!==1)return!1;var c=a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.matchesSelector;if(c)return c.call(a,b);var d,e=a.parentNode,f=!e;return f&&(e=z).appendChild(a),d=~w.qsa(e,b).indexOf(a),f&&z.removeChild(a),d},x=function(a){return a.replace(/-+(.)?/g,function(a,b){return b?b.toUpperCase():""})},y=function(a){return a.filter(function(b,c){return a.indexOf(b)==c})},w.fragment=function(b,d){d===a&&(d=l.test(b)&&RegExp.$1),d in q||(d="*");var e=q[d];return e.innerHTML=""+b,c.each(f.call(e.childNodes),function(){e.removeChild(this)})},w.Z=function(a,b){return a=a||[],a.__proto__=arguments.callee.prototype,a.selector=b||"",a},w.isZ=function(a){return a instanceof w.Z},w.init=function(b,d){if(!b)return w.Z();if(A(b))return c(g).ready(b);if(w.isZ(b))return b;var e;if(D(b))e=F(b);else if(C(b))e=[c.extend({},b)],b=null;else if(m.indexOf(b.nodeType)>=0||b===window)e=[b],b=null;else if(l.test(b))e=w.fragment(b.trim(),RegExp.$1),b=null;else{if(d!==a)return c(d).find(b);e=w.qsa(g,b)}return w.Z(e,b)},c=function(a,b){return w.init(a,b)},c.extend=function(c){return f.call(arguments,1).forEach(function(d){for(b in d)d[b]!==a&&(c[b]=d[b])}),c},w.qsa=function(a,b){var c;return a===g&&t.test(b)?(c=a.getElementById(RegExp.$1))?[c]:e:a.nodeType!==1&&a.nodeType!==9?e:f.call(s.test(b)?a.getElementsByClassName(RegExp.$1):u.test(b)?a.getElementsByTagName(b):a.querySelectorAll(b))},c.isFunction=A,c.isObject=B,c.isArray=D,c.isPlainObject=C,c.inArray=function(a,b,c){return e.indexOf.call(b,a,c)},c.trim=function(a){return a.trim()},c.uuid=0,c.map=function(a,b){var c,d=[],e,f;if(E(a))for(e=0;e0&&w.matches(this[0],a)},not:function(b){var d=[];if(A(b)&&b.call!==a)this.each(function(a){b.call(this,a)||d.push(this)});else{var e=typeof b=="string"?this.filter(b):E(b)&&A(b.item)?f.call(b):c(b);this.forEach(function(a){e.indexOf(a)<0&&d.push(a)})}return c(d)},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){var a=this[0];return a&&!B(a)?a:c(a)},last:function(){var a=this[this.length-1];return a&&!B(a)?a:c(a)},find:function(a){var b;return this.length==1?b=w.qsa(this[0],a):b=this.map(function(){return w.qsa(this,a)}),c(b)},closest:function(a,b){var d=this[0];while(d&&!w.matches(d,a))d=d!==b&&d!==g&&d.parentNode;return c(d)},parents:function(a){var b=[],d=this;while(d.length>0)d=c.map(d,function(a){if((a=a.parentNode)&&a!==g&&b.indexOf(a)<0)return b.push(a),a});return L(b,a)},parent:function(a){return L(y(this.pluck("parentNode")),a)},children:function(a){return L(this.map(function(){return f.call(this.children)}),a)},siblings:function(a){return L(this.map(function(a,b){return f.call(b.parentNode.children).filter(function(a){return a!==b})}),a)},empty:function(){return this.each(function(){this.innerHTML=""})},pluck:function(a){return this.map(function(){return this[a]})},show:function(){return this.each(function(){this.style.display=="none"&&(this.style.display=null),j(this,"").getPropertyValue("display")=="none"&&(this.style.display=K(this.nodeName))})},replaceWith:function(a){return this.before(a).remove()},wrap:function(a){return this.each(function(){c(this).wrapAll(c(a)[0].cloneNode(!1))})},wrapAll:function(a){return this[0]&&(c(this[0]).before(a=c(a)),a.append(this)),this},unwrap:function(){return this.parent().each(function(){c(this).replaceWith(c(this).children())}),this},clone:function(){return c(this.map(function(){return this.cloneNode(!0)}))},hide:function(){return this.css("display","none")},toggle:function(b){return(b===a?this.css("display")=="none":b)?this.show():this.hide()},prev:function(){return c(this.pluck("previousElementSibling"))},next:function(){return c(this.pluck("nextElementSibling"))},html:function(b){return b===a?this.length>0?this[0].innerHTML:null:this.each(function(a){var d=this.innerHTML;c(this).empty().append(M(this,b,a,d))})},text:function(b){return b===a?this.length>0?this[0].textContent:null:this.each(function(){this.textContent=b})},attr:function(c,d){var e;return typeof c=="string"&&d===a?this.length==0||this[0].nodeType!==1?a:c=="value"&&this[0].nodeName=="INPUT"?this.val():!(e=this[0].getAttribute(c))&&c in this[0]?this[0][c]:e:this.each(function(a){if(this.nodeType!==1)return;if(B(c))for(b in c)this.setAttribute(b,c[b]);else this.setAttribute(c,M(this,d,a,this.getAttribute(c)))})},removeAttr:function(a){return this.each(function(){this.nodeType===1&&this.removeAttribute(a)})},prop:function(b,c){return c===a?this[0]?this[0][b]:a:this.each(function(a){this[b]=M(this,c,a,this[b])})},data:function(b,c){var d=this.attr("data-"+H(b),c);return d!==null?d:a},val:function(b){return b===a?this.length>0?this[0].value:a:this.each(function(a){this.value=M(this,b,a,this.value)})},offset:function(){if(this.length==0)return null;var a=this[0].getBoundingClientRect();return{left:a.left+window.pageXOffset,top:a.top+window.pageYOffset,width:a.width,height:a.height}},css:function(c,d){if(d===a&&typeof c=="string")return this.length==0?a:this[0].style[x(c)]||j(this[0],"").getPropertyValue(c);var e="";for(b in c)typeof c[b]=="string"&&c[b]==""?this.each(function(){this.style.removeProperty(H(b))}):e+=H(b)+":"+J(b,c[b])+";";return typeof c=="string"&&(d==""?this.each(function(){this.style.removeProperty(H(c))}):e=H(c)+":"+J(c,d)),this.each(function(){this.style.cssText+=";"+e})},index:function(a){return a?this.indexOf(c(a)[0]):this.parent().children().indexOf(this[0])},hasClass:function(a){return this.length<1?!1:I(a).test(this[0].className)},addClass:function(a){return this.each(function(b){d=[];var e=this.className,f=M(this,a,b,e);f.split(/\s+/g).forEach(function(a){c(this).hasClass(a)||d.push(a)},this),d.length&&(this.className+=(e?" ":"")+d.join(" "))})},removeClass:function(b){return this.each(function(c){if(b===a)return this.className="";d=this.className,M(this,b,c,d).split(/\s+/g).forEach(function(a){d=d.replace(I(a)," ")}),this.className=d.trim()})},toggleClass:function(b,d){return this.each(function(e){var f=M(this,b,e,this.className);(d===a?!c(this).hasClass(f):d)?c(this).addClass(f):c(this).removeClass(f)})}},["width","height"].forEach(function(b){c.fn[b]=function(d){var e,f=b.replace(/./,function(a){return a[0].toUpperCase()});return d===a?this[0]==window?window["inner"+f]:this[0]==g?g.documentElement["offset"+f]:(e=this.offset())&&e[b]:this.each(function(a){var e=c(this);e.css(b,M(this,d,a,e[b]()))})}}),n.forEach(function(a,b){c.fn[a]=function(){var a=c.map(arguments,function(a){return B(a)?a:w.fragment(a)});if(a.length<1)return this;var d=this.length,e=d>1,f=b<2;return this.each(function(c,g){for(var h=0;h0&&this.bind(o,n),setTimeout(function(){m.css(i),e<=0&&setTimeout(function(){m.each(function(){n.call(this)})},0)},0),this},i=null}(Zepto),function($){function triggerAndReturn(a,b,c){var d=$.Event(b);return $(a).trigger(d,c),!d.defaultPrevented}function triggerGlobal(a,b,c,d){if(a.global)return triggerAndReturn(b||document,c,d)}function ajaxStart(a){a.global&&$.active++===0&&triggerGlobal(a,null,"ajaxStart")}function ajaxStop(a){a.global&&!--$.active&&triggerGlobal(a,null,"ajaxStop")}function ajaxBeforeSend(a,b){var c=b.context;if(b.beforeSend.call(c,a,b)===!1||triggerGlobal(b,c,"ajaxBeforeSend",[a,b])===!1)return!1;triggerGlobal(b,c,"ajaxSend",[a,b])}function ajaxSuccess(a,b,c){var d=c.context,e="success";c.success.call(d,a,e,b),triggerGlobal(c,d,"ajaxSuccess",[b,c,a]),ajaxComplete(e,b,c)}function ajaxError(a,b,c,d){var e=d.context;d.error.call(e,c,b,a),triggerGlobal(d,e,"ajaxError",[c,d,a]),ajaxComplete(b,c,d)}function ajaxComplete(a,b,c){var d=c.context;c.complete.call(d,b,a),triggerGlobal(c,d,"ajaxComplete",[b,c]),ajaxStop(c)}function empty(){}function mimeToDataType(a){return a&&(a==htmlType?"html":a==jsonType?"json":scriptTypeRE.test(a)?"script":xmlTypeRE.test(a)&&"xml")||"text"}function appendQuery(a,b){return(a+"&"+b).replace(/[&?]{1,2}/,"?")}function serializeData(a){isObject(a.data)&&(a.data=$.param(a.data)),a.data&&(!a.type||a.type.toUpperCase()=="GET")&&(a.url=appendQuery(a.url,a.data))}function serialize(a,b,c,d){var e=$.isArray(b);$.each(b,function(b,f){d&&(b=c?d:d+"["+(e?"":b)+"]"),!d&&e?a.add(f.name,f.value):(c?$.isArray(f):isObject(f))?serialize(a,f,c,b):a.add(b,f)})}var jsonpID=0,isObject=$.isObject,document=window.document,key,name,rscript=/)<[^<]*)*<\/script>/gi,scriptTypeRE=/^(?:text|application)\/javascript/i,xmlTypeRE=/^(?:text|application)\/xml/i,jsonType="application/json",htmlType="text/html",blankRE=/^\s*$/;$.active=0,$.ajaxJSONP=function(a){var b="jsonp"+ ++jsonpID,c=document.createElement("script"),d=function(){$(c).remove(),b in window&&(window[b]=empty),ajaxComplete("abort",e,a)},e={abort:d},f;return a.error&&(c.onerror=function(){e.abort(),a.error()}),window[b]=function(d){clearTimeout(f),$(c).remove(),delete window[b],ajaxSuccess(d,e,a)},serializeData(a),c.src=a.url.replace(/=\?/,"="+b),$("head").append(c),a.timeout>0&&(f=setTimeout(function(){e.abort(),ajaxComplete("timeout",e,a)},a.timeout)),e},$.ajaxSettings={type:"GET",beforeSend:empty,success:empty,error:empty,complete:empty,context:null,global:!0,xhr:function(){return new window.XMLHttpRequest},accepts:{script:"text/javascript, application/javascript",json:jsonType,xml:"application/xml, text/xml",html:htmlType,text:"text/plain"},crossDomain:!1,timeout:0},$.ajax=function(options){var settings=$.extend({},options||{});for(key in $.ajaxSettings)settings[key]===undefined&&(settings[key]=$.ajaxSettings[key]);ajaxStart(settings),settings.crossDomain||(settings.crossDomain=/^([\w-]+:)?\/\/([^\/]+)/.test(settings.url)&&RegExp.$2!=window.location.host);var dataType=settings.dataType,hasPlaceholder=/=\?/.test(settings.url);if(dataType=="jsonp"||hasPlaceholder)return hasPlaceholder||(settings.url=appendQuery(settings.url,"callback=?")),$.ajaxJSONP(settings);settings.url||(settings.url=window.location.toString()),serializeData(settings);var mime=settings.accepts[dataType],baseHeaders={},protocol=/^([\w-]+:)\/\//.test(settings.url)?RegExp.$1:window.location.protocol,xhr=$.ajaxSettings.xhr(),abortTimeout;settings.crossDomain||(baseHeaders["X-Requested-With"]="XMLHttpRequest"),mime&&(baseHeaders.Accept=mime,mime.indexOf(",")>-1&&(mime=mime.split(",",2)[0]),xhr.overrideMimeType&&xhr.overrideMimeType(mime));if(settings.contentType||settings.data&&settings.type.toUpperCase()!="GET")baseHeaders["Content-Type"]=settings.contentType||"application/x-www-form-urlencoded";settings.headers=$.extend(baseHeaders,settings.headers||{}),xhr.onreadystatechange=function(){if(xhr.readyState==4){clearTimeout(abortTimeout);var result,error=!1;if(xhr.status>=200&&xhr.status<300||xhr.status==304||xhr.status==0&&protocol=="file:"){dataType=dataType||mimeToDataType(xhr.getResponseHeader("content-type")),result=xhr.responseText;try{dataType=="script"?(1,eval)(result):dataType=="xml"?result=xhr.responseXML:dataType=="json"&&(result=blankRE.test(result)?null:JSON.parse(result))}catch(e){error=e}error?ajaxError(error,"parsererror",xhr,settings):ajaxSuccess(result,xhr,settings)}else ajaxError(null,"error",xhr,settings)}};var async="async"in settings?settings.async:!0;xhr.open(settings.type,settings.url,async);for(name in settings.headers)xhr.setRequestHeader(name,settings.headers[name]);return ajaxBeforeSend(xhr,settings)===!1?(xhr.abort(),!1):(settings.timeout>0&&(abortTimeout=setTimeout(function(){xhr.onreadystatechange=empty,xhr.abort(),ajaxError(null,"timeout",xhr,settings)},settings.timeout)),xhr.send(settings.data?settings.data:null),xhr)},$.get=function(a,b){return $.ajax({url:a,success:b})},$.post=function(a,b,c,d){return $.isFunction(b)&&(d=d||c,c=b,b=null),$.ajax({type:"POST",url:a,data:b,success:c,dataType:d})},$.getJSON=function(a,b){return $.ajax({url:a,success:b,dataType:"json"})},$.fn.load=function(a,b){if(!this.length)return this;var c=this,d=a.split(/\s/),e;return d.length>1&&(a=d[0],e=d[1]),$.get(a,function(a){c.html(e?$(document.createElement("div")).html(a.replace(rscript,"")).find(e).html():a),b&&b.call(c)}),this};var escape=encodeURIComponent;$.param=function(a,b){var c=[];return c.add=function(a,b){this.push(escape(a)+"="+escape(b))},serialize(c,a,b),c.join("&").replace("%20","+")}}(Zepto),function(a){a.fn.serializeArray=function(){var b=[],c;return a(Array.prototype.slice.call(this.get(0).elements)).each(function(){c=a(this);var d=c.attr("type");this.nodeName.toLowerCase()!="fieldset"&&!this.disabled&&d!="submit"&&d!="reset"&&d!="button"&&(d!="radio"&&d!="checkbox"||this.checked)&&b.push({name:c.attr("name"),value:c.val()})}),b},a.fn.serialize=function(){var a=[];return this.serializeArray().forEach(function(b){a.push(encodeURIComponent(b.name)+"="+encodeURIComponent(b.value))}),a.join("&")},a.fn.submit=function(b){if(b)this.bind("submit",b);else if(this.length){var c=a.Event("submit");this.eq(0).trigger(c),c.defaultPrevented||this.get(0).submit()}return this}}(Zepto),function(a){function d(a){return"tagName"in a?a:a.parentNode}function e(a,b,c,d){var e=Math.abs(a-b),f=Math.abs(c-d);return e>=f?a-b>0?"Left":"Right":c-d>0?"Up":"Down"}function h(){g=null,b.last&&(b.el.trigger("longTap"),b={})}function i(){g&&clearTimeout(g),g=null}var b={},c,f=750,g;a(document).ready(function(){var j,k;a(document.body).bind("touchstart",function(e){j=Date.now(),k=j-(b.last||j),b.el=a(d(e.touches[0].target)),c&&clearTimeout(c),b.x1=e.touches[0].pageX,b.y1=e.touches[0].pageY,k>0&&k<=250&&(b.isDoubleTap=!0),b.last=j,g=setTimeout(h,f)}).bind("touchmove",function(a){i(),b.x2=a.touches[0].pageX,b.y2=a.touches[0].pageY}).bind("touchend",function(a){i(),b.isDoubleTap?(b.el.trigger("doubleTap"),b={}):b.x2&&Math.abs(b.x1-b.x2)>30||b.y2&&Math.abs(b.y1-b.y2)>30?(b.el.trigger("swipe")&&b.el.trigger("swipe"+e(b.x1,b.x2,b.y1,b.y2)),b={}):"last"in b&&(b.el.trigger("tap"),c=setTimeout(function(){c=null,b.el.trigger("singleTap"),b={}},250))}).bind("touchcancel",function(){c&&clearTimeout(c),g&&clearTimeout(g),g=c=null,b={}})}),["swipe","swipeLeft","swipeRight","swipeUp","swipeDown","doubleTap","tap","singleTap","longTap"].forEach(function(b){a.fn[b]=function(a){return this.bind(b,a)}})}(Zepto); -------------------------------------------------------------------------------- /demo/ding_server/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | 10 | .log-i { 11 | color: green; 12 | } 13 | 14 | .log-e { 15 | color: red; 16 | } 17 | 18 | .tag { 19 | display: inline-block; 20 | width: 170px; 21 | } 22 | 23 | .api { 24 | display: inline-block; 25 | width: 300px; 26 | } 27 | 28 | .hidden { 29 | display: none; 30 | } 31 | 32 | .row button { 33 | width: 100%; 34 | height: 100px; 35 | } 36 | 37 | .log-row { 38 | margin: 20px 10px 20px 10px; 39 | font-size: 30px; 40 | word-break: break-all;word-wrap: break-word; 41 | } -------------------------------------------------------------------------------- /demo/ding_server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "html/template" 7 | "net/http" 8 | "os" 9 | "path" 10 | "time" 11 | 12 | "github.com/hugozhu/godingtalk" 13 | ) 14 | 15 | func GetTemplate(tpl string) *template.Template { 16 | t, _ := template.ParseFiles(tpl) 17 | return t 18 | } 19 | 20 | var client *godingtalk.DingTalkClient 21 | 22 | func getUserInfo(w http.ResponseWriter, r *http.Request) { 23 | code := r.FormValue("code") 24 | 25 | client.RefreshAccessToken() 26 | info, _ := client.UserInfoByCode(code) 27 | 28 | json.NewEncoder(w).Encode(info) 29 | } 30 | 31 | func sendMessage(w http.ResponseWriter, r *http.Request) { 32 | chatid := r.FormValue("cid") 33 | sender := r.FormValue("sender") 34 | content := r.FormValue("content") 35 | 36 | var resp struct { 37 | ErrCode int `json:"errcode"` 38 | ErrMsg string `json:"errmsg"` 39 | } 40 | 41 | client.RefreshAccessToken() 42 | err := client.SendTextMessage(sender, chatid, content) 43 | if err != nil { 44 | resp.ErrCode = -1 45 | resp.ErrMsg = err.Error() 46 | } 47 | 48 | json.NewEncoder(w).Encode(resp) 49 | } 50 | 51 | func serveTemplate(w http.ResponseWriter, r *http.Request) { 52 | lp := path.Join("templates", "layout.html") 53 | fp := path.Join("templates", "index.html") 54 | 55 | url := "http://" + r.Host + r.RequestURI 56 | timestamp := fmt.Sprintf("%d", time.Now().Unix()) 57 | 58 | client.RefreshAccessToken() 59 | configString := client.GetConfig("abcdabc", timestamp, url) 60 | 61 | data := make(map[string]interface{}) 62 | data["config"] = template.JS(configString) 63 | 64 | tmpl, err := template.ParseFiles(lp, fp) 65 | if err == nil { 66 | err = tmpl.ExecuteTemplate(w, "layout", data) 67 | } 68 | if err != nil { 69 | http.Error(w, err.Error(), http.StatusInternalServerError) 70 | } 71 | } 72 | 73 | func main() { 74 | corpId := os.Getenv("corpId") 75 | corpSecret := os.Getenv("corpSecret") 76 | client = godingtalk.NewDingTalkClient(corpId, corpSecret) 77 | client.AgentID = os.Getenv("agentID") 78 | 79 | fs := http.FileServer(http.Dir(path.Join(os.Getenv("root"), "public"))) 80 | http.Handle("/public/", http.StripPrefix("/public/", fs)) 81 | http.HandleFunc("/", serveTemplate) 82 | http.HandleFunc("/get_user_info", getUserInfo) 83 | http.HandleFunc("/send_message", sendMessage) 84 | 85 | http.ListenAndServe(":8000", nil) 86 | } 87 | -------------------------------------------------------------------------------- /demo/ding_server/templates/index.html: -------------------------------------------------------------------------------- 1 | {{define "title"}}Demo{{end}} -------------------------------------------------------------------------------- /demo/ding_server/templates/layout.html: -------------------------------------------------------------------------------- 1 | {{define "layout"}} 2 | 3 | 4 | {{template "title"}} 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 20 |

21 | Refresh 22 |

23 | 24 | 25 | 26 | 27 | {{end}} -------------------------------------------------------------------------------- /demo/github/appengine/app.yaml: -------------------------------------------------------------------------------- 1 | application: github-alert 2 | version: 1 3 | runtime: go 4 | api_version: go1 5 | 6 | env_variables: 7 | CORP_ID: 'dingaxxxx' 8 | CORP_SECRET: 'xxxxxxxxxxxx' 9 | GITHUB_WEBHOOK_SECRET: 'xxxxxxxxxxxx' 10 | SENDER_ID: '011217462940' 11 | CHAT_ID: 'chat6a93bc1ee3b7d660d372b1b877a9de62' 12 | 13 | handlers: 14 | - url: /.* 15 | script: _go_app -------------------------------------------------------------------------------- /demo/github/appengine/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/hugozhu/godingtalk/demo/github" 8 | ) 9 | 10 | func init() { 11 | http.HandleFunc("/github", github.Handle) 12 | } 13 | 14 | func main() { 15 | log.Print("Listening on port 8080") 16 | log.Fatal(http.ListenAndServe(":8080", nil)) 17 | } 18 | -------------------------------------------------------------------------------- /demo/github/github.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | "time" 11 | 12 | "appengine" 13 | "appengine/memcache" 14 | "appengine/urlfetch" 15 | 16 | "strings" 17 | 18 | "github.com/google/go-github/github" 19 | "github.com/hugozhu/godingtalk" 20 | 21 | "crypto/hmac" 22 | "crypto/sha1" 23 | "encoding/hex" 24 | ) 25 | 26 | //Handle 处理Github的Webhook Events 27 | func Handle(w http.ResponseWriter, r *http.Request) { 28 | var err error 29 | 30 | action := r.Header.Get("x-github-event") 31 | signature := r.Header.Get("x-hub-signature") 32 | // id := r.Header.Get("x-github-delivery") 33 | body, _ := ioutil.ReadAll(r.Body) 34 | defer r.Body.Close() 35 | 36 | if !verifySignature([]byte(os.Getenv("GITHUB_WEBHOOK_SECRET")), signature, body) { 37 | http.Error(w, fmt.Sprintf("%v", "Invalid or empty signature"), http.StatusBadRequest) 38 | return 39 | } 40 | 41 | var event github.WebHookPayload 42 | json.Unmarshal(body, &event) 43 | 44 | context := appengine.NewContext(r) 45 | c := godingtalk.NewDingTalkClient(os.Getenv("CORP_ID"), os.Getenv("CORP_SECRET")) 46 | c.HTTPClient = urlfetch.Client(context) 47 | c.HTTPClient.Transport = &urlfetch.Transport{ 48 | Context: context, 49 | AllowInvalidServerCertificate: true, 50 | } 51 | c.Cache = NewMemCache(context, ".access_token") 52 | refresh_token_error := c.RefreshAccessToken() 53 | 54 | msg := godingtalk.OAMessage{} 55 | msg.Head.Text = "Github" 56 | msg.Head.BgColor = "FF00AABB" 57 | switch action { 58 | case "push": 59 | msg.Body.Title = "[" + *event.Repo.Name + "] Push" 60 | msg.URL = *event.Compare 61 | for _, commit := range event.Commits { 62 | value := *commit.Message 63 | if len(commit.Added) > 0 { 64 | value = value + "\n Added: " + strings.Join(commit.Added, ", ") 65 | } 66 | if len(commit.Modified) > 0 { 67 | value = value + "\n Modified: " + strings.Join(commit.Modified, ", ") 68 | } 69 | if len(commit.Removed) > 0 { 70 | value = value + "\n Removed: " + strings.Join(commit.Removed, ", ") 71 | } 72 | msg.Body.Form = append(msg.Body.Form, godingtalk.OAMessageForm{ 73 | Key: "Commits: ", 74 | Value: value, 75 | }) 76 | } 77 | case "watch": 78 | msg.Body.Title = "[" + *event.Repo.Name + "] Watch Updated" 79 | msg.URL = *event.Repo.HTMLURL 80 | msg.Body.Form = []godingtalk.OAMessageForm{ 81 | { 82 | Key: "Watchers: ", 83 | Value: fmt.Sprintf("%d", *event.Repo.WatchersCount), 84 | }, 85 | { 86 | Key: "Forks: ", 87 | Value: fmt.Sprintf("%d", *event.Repo.ForksCount), 88 | }, 89 | } 90 | default: 91 | msg.Body.Title = "[" + *event.Repo.Name + "] " + action 92 | msg.URL = *event.Repo.HTMLURL 93 | } 94 | msg.Body.Author = *event.Sender.Login 95 | 96 | err = c.SendOAMessage(os.Getenv("SENDER_ID"), os.Getenv("CHAT_ID"), msg) 97 | if err != nil { 98 | http.Error(w, fmt.Sprintf("%v", err), http.StatusInternalServerError) 99 | } else if refresh_token_error != nil { 100 | http.Error(w, fmt.Sprintf("%v", refresh_token_error), http.StatusInternalServerError) 101 | } 102 | } 103 | 104 | func signBody(secret, body []byte) []byte { 105 | computed := hmac.New(sha1.New, secret) 106 | computed.Write(body) 107 | return []byte(computed.Sum(nil)) 108 | } 109 | 110 | func verifySignature(secret []byte, signature string, body []byte) bool { 111 | 112 | const signaturePrefix = "sha1=" 113 | const signatureLength = 45 // len(SignaturePrefix) + len(hex(sha1)) 114 | 115 | if len(signature) != signatureLength || !strings.HasPrefix(signature, signaturePrefix) { 116 | return false 117 | } 118 | 119 | actual := make([]byte, 20) 120 | hex.Decode(actual, []byte(signature[5:])) 121 | 122 | return hmac.Equal(signBody(secret, body), actual) 123 | } 124 | 125 | type MemCache struct { 126 | ctx appengine.Context 127 | key string 128 | } 129 | 130 | func NewMemCache(ctx appengine.Context, key string) *MemCache { 131 | return &MemCache{ 132 | ctx: ctx, 133 | key: key, 134 | } 135 | } 136 | 137 | func (c *MemCache) Set(data godingtalk.Expirable) error { 138 | bytes, err := json.Marshal(data) 139 | if err == nil { 140 | item := &memcache.Item{ 141 | Key: c.key, 142 | Value: bytes, 143 | Expiration: time.Duration(data.ExpiresIn()) * time.Second, 144 | } 145 | err = memcache.Set(c.ctx, item) 146 | } 147 | return err 148 | } 149 | 150 | func (c *MemCache) Get(data godingtalk.Expirable) error { 151 | item, err := memcache.Get(c.ctx, c.key) 152 | if err == nil { 153 | err = json.Unmarshal(item.Value, data) 154 | if err == nil { 155 | created := data.CreatedAt() 156 | expires := data.ExpiresIn() 157 | if err == nil && time.Now().Unix() > created+int64(expires-60) { 158 | err = errors.New("Data is already expired") 159 | } 160 | } 161 | } 162 | return err 163 | } 164 | -------------------------------------------------------------------------------- /godingtalk.go: -------------------------------------------------------------------------------- 1 | package godingtalk 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | "time" 10 | ) 11 | 12 | const ( 13 | //VERSION is SDK version 14 | VERSION = "0.1" 15 | 16 | //ROOT is the root url 17 | ROOT = "https://oapi.dingtalk.com/" 18 | ) 19 | 20 | //DingTalkClient is the Client to access DingTalk Open API 21 | type DingTalkClient struct { 22 | CorpID string 23 | CorpSecret string 24 | AgentID string 25 | AccessToken string 26 | HTTPClient *http.Client 27 | Cache Cache 28 | 29 | //社交相关的属性 30 | SnsAppID string 31 | SnsAppSecret string 32 | SnsAccessToken string 33 | } 34 | 35 | //Unmarshallable is 36 | type Unmarshallable interface { 37 | checkError() error 38 | getWriter() io.Writer 39 | } 40 | 41 | //OAPIResponse is 42 | type OAPIResponse struct { 43 | ErrCode int `json:"errcode"` 44 | ErrMsg string `json:"errmsg"` 45 | } 46 | 47 | func (data *OAPIResponse) checkError() (err error) { 48 | if data.ErrCode != 0 { 49 | err = fmt.Errorf("%d: %s", data.ErrCode, data.ErrMsg) 50 | } 51 | return err 52 | } 53 | 54 | func (data *OAPIResponse) getWriter() io.Writer { 55 | return nil 56 | } 57 | 58 | //AccessTokenResponse is 59 | type AccessTokenResponse struct { 60 | OAPIResponse 61 | AccessToken string `json:"access_token"` 62 | Expires int `json:"expires_in"` 63 | Created int64 64 | } 65 | 66 | //CreatedAt is when the access token is generated 67 | func (e *AccessTokenResponse) CreatedAt() int64 { 68 | return e.Created 69 | } 70 | 71 | //ExpiresIn is how soon the access token is expired 72 | func (e *AccessTokenResponse) ExpiresIn() int { 73 | return e.Expires 74 | } 75 | 76 | //JsAPITicketResponse is 77 | type JsAPITicketResponse struct { 78 | OAPIResponse 79 | Ticket string 80 | Expires int `json:"expires_in"` 81 | Created int64 82 | } 83 | 84 | //CreatedAt is when the ticket is generated 85 | func (e *JsAPITicketResponse) CreatedAt() int64 { 86 | return e.Created 87 | } 88 | 89 | //ExpiresIn is how soon the ticket is expired 90 | func (e *JsAPITicketResponse) ExpiresIn() int { 91 | return e.Expires 92 | } 93 | 94 | //NewDingTalkClient creates a DingTalkClient instance 95 | func NewDingTalkClient(corpID string, corpSecret string) *DingTalkClient { 96 | c := new(DingTalkClient) 97 | c.CorpID = corpID 98 | c.CorpSecret = corpSecret 99 | c.HTTPClient = &http.Client{ 100 | Timeout: 10 * time.Second, 101 | } 102 | c.Cache = NewFileCache(".auth_file") 103 | return c 104 | } 105 | 106 | //RefreshAccessToken is to get a valid access token 107 | func (c *DingTalkClient) RefreshAccessToken() error { 108 | var data AccessTokenResponse 109 | err := c.Cache.Get(&data) 110 | if err == nil { 111 | c.AccessToken = data.AccessToken 112 | return nil 113 | } 114 | 115 | params := url.Values{} 116 | params.Add("corpid", c.CorpID) 117 | params.Add("corpsecret", c.CorpSecret) 118 | err = c.httpRPC("gettoken", params, nil, &data) 119 | if err == nil { 120 | c.AccessToken = data.AccessToken 121 | data.Expires = data.Expires | 7200 122 | data.Created = time.Now().Unix() 123 | err = c.Cache.Set(&data) 124 | } 125 | return err 126 | } 127 | 128 | //GetJsAPITicket is to get a valid ticket for JS API 129 | func (c *DingTalkClient) GetJsAPITicket() (ticket string, err error) { 130 | var data JsAPITicketResponse 131 | cache := NewFileCache(".jsapi_ticket") 132 | err = cache.Get(&data) 133 | if err == nil { 134 | return data.Ticket, err 135 | } 136 | err = c.httpRPC("get_jsapi_ticket", nil, nil, &data) 137 | if err == nil { 138 | ticket = data.Ticket 139 | cache.Set(&data) 140 | } 141 | return ticket, err 142 | } 143 | 144 | //GetConfig is to return config in json 145 | func (c *DingTalkClient) GetConfig(nonceStr string, timestamp string, url string) string { 146 | ticket, _ := c.GetJsAPITicket() 147 | config := map[string]string{ 148 | "url": url, 149 | "nonceStr": nonceStr, 150 | "agentId": c.AgentID, 151 | "timeStamp": timestamp, 152 | "corpId": c.CorpID, 153 | "ticket": ticket, 154 | "signature": Sign(ticket, nonceStr, timestamp, url), 155 | } 156 | bytes, _ := json.Marshal(&config) 157 | return string(bytes) 158 | } 159 | 160 | //Sign is 签名 161 | func Sign(ticket string, nonceStr string, timeStamp string, url string) string { 162 | s := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%s&url=%s", ticket, nonceStr, timeStamp, url) 163 | return sha1Sign(s) 164 | } 165 | -------------------------------------------------------------------------------- /godingtalk_test.go: -------------------------------------------------------------------------------- 1 | package godingtalk 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | var c *DingTalkClient 9 | 10 | func init() { 11 | c = NewDingTalkClient(os.Getenv("corpid"), os.Getenv("corpsecret")) 12 | err := c.RefreshAccessToken() 13 | if err != nil { 14 | panic(err) 15 | } 16 | } 17 | 18 | func TestDepartmentApi(t *testing.T) { 19 | departments, err := c.DepartmentList() 20 | // t.Logf("%+v %+v", departments, err) 21 | if err != nil { 22 | t.Error(err) 23 | t.FailNow() 24 | } 25 | d, err := c.DepartmentDetail(departments.Departments[0].Id) 26 | if err != nil { 27 | t.Error(err) 28 | t.FailNow() 29 | } 30 | if d.Id != departments.Departments[0].Id { 31 | t.Error("DepartmentDetail error") 32 | } 33 | 34 | for _, department := range departments.Departments { 35 | t.Logf("dept: %v", department) 36 | list, err := c.UserList(department.Id) 37 | if err != nil { 38 | t.Error(err) 39 | } 40 | for _, user := range list.Userlist { 41 | t.Logf("\t\tuser: %v", user) 42 | } 43 | } 44 | } 45 | 46 | func TestJsAPITicket(t *testing.T) { 47 | ticket, err := c.GetJsAPITicket() 48 | if err != nil || ticket == "" { 49 | t.Error("JsAPITicket error", err) 50 | } 51 | } 52 | 53 | func TestCreateChat(t *testing.T) { 54 | // chatid, err := c.CreateChat("Test chat", "0420506555", []string{"0420506555"}) 55 | // if err!=nil { 56 | // t.Error(err) 57 | // } 58 | // t.Log("-----",chatid) 59 | } 60 | 61 | func TestSendAppMessageApi(t *testing.T) { 62 | err := c.SendAppMessage("22194403", "0420506555", "测试消息,请忽略") //@all 63 | if err != nil { 64 | t.Error(err) 65 | } 66 | } 67 | 68 | func TestTextMessage(t *testing.T) { 69 | err := c.SendTextMessage("011217462940", "chat6a93bc1ee3b7d660d372b1b877a9de62", "测试消息,请忽略") 70 | if err != nil { 71 | t.Error(err) 72 | } 73 | } 74 | 75 | func TestSendOAMessage(t *testing.T) { 76 | msg := OAMessage{} 77 | msg.URL = "http://www.google.com/" 78 | msg.Head.Text = "头部标题" 79 | msg.Head.BgColor = "FFBBBBBB" 80 | msg.Body.Title = "正文标题" 81 | msg.Body.Content = "test content" 82 | err := c.SendOAMessage("011217462940", "chat6a93bc1ee3b7d660d372b1b877a9de62", msg) 83 | if err != nil { 84 | t.Error(err) 85 | } 86 | } 87 | 88 | func TestDownloadAndUploadImage(t *testing.T) { 89 | f, err := os.Create("lADOHrf_oVxc.jpg") 90 | if err == nil { 91 | err = c.DownloadMedia("@lADOHrf_oVxc", f) 92 | } 93 | if err != nil { 94 | t.Error(err) 95 | } 96 | f.Close() 97 | 98 | f, _ = os.Open("lADOHrf_oVxc.jpg") 99 | defer f.Close() 100 | media, err := c.UploadMedia("image", "myfile.jpg", f) 101 | if media.MediaID == "" { 102 | t.Error("Upload File Failed") 103 | } 104 | t.Log("uploaded file mediaid:", media.MediaID) 105 | if err != nil { 106 | t.Error(err) 107 | } 108 | err = c.SendImageMessage("011217462940", "chat6a93bc1ee3b7d660d372b1b877a9de62", "@lADOHrf_oVxc") 109 | if err != nil { 110 | t.Error(err) 111 | } 112 | } 113 | 114 | func TestVoiceMessage(t *testing.T) { 115 | // f, _ := os.Open("/Users/hugozhu/Downloads/BlackBerry_test2_AMR-NB_Mono_12.2kbps_8000Hz.amr") 116 | // defer f.Close() 117 | // media, err := c.UploadMedia("voice", "sample.amr", f) 118 | // if media.MediaID == "" { 119 | // t.Error("Upload File Failed") 120 | // } 121 | // t.Log("uploaded file mediaid:", media.MediaID) 122 | // if err != nil { 123 | // t.Error(err) 124 | // } 125 | err := c.SendVoiceMessage("011217462940", "chat6a93bc1ee3b7d660d372b1b877a9de62", "@lATOHr53E84DALnDzml4wS0", "10") 126 | if err != nil { 127 | t.Error(err) 128 | } 129 | } 130 | 131 | 132 | func TestRobotMessage(t *testing.T) { 133 | err := c.SendRobotTextMessage("b7e4b04c66b5d53669affb0b92cf533b9eff9b2bc47f86ff9f4227a2ba73798e", "这是一条测试消息") 134 | if err != nil { 135 | t.Error(err) 136 | } 137 | } -------------------------------------------------------------------------------- /transport.go: -------------------------------------------------------------------------------- 1 | package godingtalk 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "io" 8 | "io/ioutil" 9 | "mime/multipart" 10 | "net/http" 11 | "net/url" 12 | ) 13 | 14 | const typeJSON = "application/json" 15 | 16 | //UploadFile is for uploading a single file to DingTalk 17 | type UploadFile struct { 18 | FieldName string 19 | FileName string 20 | Reader io.Reader 21 | } 22 | 23 | //DownloadFile is for downloading a single file from DingTalk 24 | type DownloadFile struct { 25 | MediaID string 26 | FileName string 27 | Reader io.Reader 28 | } 29 | 30 | func (c *DingTalkClient) httpRPC(path string, params url.Values, requestData interface{}, responseData Unmarshallable) error { 31 | if c.AccessToken != "" { 32 | if params == nil { 33 | params = url.Values{} 34 | } 35 | if params.Get("access_token") == "" { 36 | params.Set("access_token", c.AccessToken) 37 | } 38 | } 39 | return c.httpRequest(path, params, requestData, responseData) 40 | } 41 | 42 | func (c *DingTalkClient) httpRequest(path string, params url.Values, requestData interface{}, responseData Unmarshallable) error { 43 | client := c.HTTPClient 44 | var request *http.Request 45 | url2 := ROOT + path + "?" + params.Encode() 46 | if requestData != nil { 47 | switch requestData.(type) { 48 | case UploadFile: 49 | var b bytes.Buffer 50 | request, _ = http.NewRequest("POST", url2, &b) 51 | w := multipart.NewWriter(&b) 52 | 53 | uploadFile := requestData.(UploadFile) 54 | if uploadFile.Reader == nil { 55 | return errors.New("upload file is empty") 56 | } 57 | fw, err := w.CreateFormFile(uploadFile.FieldName, uploadFile.FileName) 58 | if err != nil { 59 | return err 60 | } 61 | if _, err = io.Copy(fw, uploadFile.Reader); err != nil { 62 | return err 63 | } 64 | if err = w.Close(); err != nil { 65 | return err 66 | } 67 | request.Header.Set("Content-Type", w.FormDataContentType()) 68 | default: 69 | d, _ := json.Marshal(requestData) 70 | // log.Printf("url: %s request: %s", url2, string(d)) 71 | request, _ = http.NewRequest("POST", url2, bytes.NewReader(d)) 72 | request.Header.Set("Content-Type", typeJSON) 73 | } 74 | } else { 75 | // log.Printf("url: %s", url2) 76 | request, _ = http.NewRequest("GET", url2, nil) 77 | } 78 | resp, err := client.Do(request) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | if resp.StatusCode != 200 { 84 | return errors.New("Server error: " + resp.Status) 85 | } 86 | 87 | defer resp.Body.Close() 88 | contentType := resp.Header.Get("Content-Type") 89 | //log.Printf("url: %s response content type: %s", url2, contentType) 90 | pos := len(typeJSON) 91 | if len(contentType) >= pos && contentType[0:pos] == typeJSON { 92 | content, err := ioutil.ReadAll(resp.Body) 93 | if err == nil { 94 | json.Unmarshal(content, responseData) 95 | return responseData.checkError() 96 | } 97 | } else { 98 | io.Copy(responseData.getWriter(), resp.Body) 99 | return responseData.checkError() 100 | } 101 | return err 102 | } 103 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package godingtalk 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "time" 10 | ) 11 | 12 | type Expirable interface { 13 | CreatedAt() int64 14 | ExpiresIn() int 15 | } 16 | 17 | type Cache interface { 18 | Set(data Expirable) error 19 | Get(data Expirable) error 20 | } 21 | 22 | type FileCache struct { 23 | Path string 24 | } 25 | 26 | func NewFileCache(path string) *FileCache { 27 | return &FileCache{ 28 | Path: path, 29 | } 30 | } 31 | 32 | func (c *FileCache) Set(data Expirable) error { 33 | bytes, err := json.Marshal(data) 34 | if err == nil { 35 | ioutil.WriteFile(c.Path, bytes, 0644) 36 | } 37 | return err 38 | } 39 | 40 | func (c *FileCache) Get(data Expirable) error { 41 | bytes, err := ioutil.ReadFile(c.Path) 42 | if err == nil { 43 | err = json.Unmarshal(bytes, data) 44 | if err == nil { 45 | created := data.CreatedAt() 46 | expires := data.ExpiresIn() 47 | if err == nil && time.Now().Unix() > created+int64(expires-60) { 48 | err = errors.New("Data is already expired") 49 | } 50 | } 51 | } 52 | return err 53 | } 54 | 55 | type InMemoryCache struct { 56 | data []byte 57 | } 58 | 59 | func NewInMemoryCache() *InMemoryCache { 60 | return &InMemoryCache{} 61 | } 62 | 63 | func (c *InMemoryCache) Set(data Expirable) error { 64 | bytes, err := json.Marshal(data) 65 | if err == nil { 66 | c.data = bytes 67 | } 68 | return err 69 | } 70 | 71 | func (c *InMemoryCache) Get(data Expirable) error { 72 | err := json.Unmarshal(c.data, data) 73 | if err == nil { 74 | created := data.CreatedAt() 75 | expires := data.ExpiresIn() 76 | if err == nil && time.Now().Unix() > created+int64(expires-60) { 77 | err = errors.New("Data is already expired") 78 | } 79 | } 80 | return err 81 | } 82 | 83 | func sha1Sign(s string) string { 84 | // The pattern for generating a hash is `sha1.New()`, 85 | // `sha1.Write(bytes)`, then `sha1.Sum([]byte{})`. 86 | // Here we start with a new hash. 87 | h := sha1.New() 88 | 89 | // `Write` expects bytes. If you have a string `s`, 90 | // use `[]byte(s)` to coerce it to bytes. 91 | h.Write([]byte(s)) 92 | 93 | // This gets the finalized hash result as a byte 94 | // slice. The argument to `Sum` can be used to append 95 | // to an existing byte slice: it usually isn't needed. 96 | bs := h.Sum(nil) 97 | 98 | // SHA1 values are often printed in hex, for example 99 | // in git commits. Use the `%x` format verb to convert 100 | // a hash results to a hex string. 101 | return fmt.Sprintf("%x", bs) 102 | } 103 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package godingtalk 2 | 3 | import "testing" 4 | import "time" 5 | 6 | type ExpiresData struct { 7 | Data string 8 | Expires int `json:"expires_in"` 9 | Created int64 `json:"created"` 10 | } 11 | 12 | func (e *ExpiresData) CreatedAt() int64 { 13 | return e.Created 14 | } 15 | 16 | func (e *ExpiresData) ExpiresIn() int { 17 | return e.Expires 18 | } 19 | 20 | func TestFileCache(t *testing.T) { 21 | cache := NewFileCache(".test_cache") 22 | data := ExpiresData{ 23 | Data: "Hello World!", 24 | Expires: 7200, 25 | Created: time.Now().Unix(), 26 | } 27 | cache.Set(&data) 28 | 29 | var data2 ExpiresData 30 | cache.Get(&data2) 31 | t.Logf("%+v %+v", data, data2) 32 | 33 | if data2.Created != data.Created { 34 | t.Errorf("FileCache error") 35 | } 36 | 37 | data = ExpiresData{ 38 | Data: "Hello World 2!", 39 | Expires: 0, 40 | Created: time.Now().Unix(), 41 | } 42 | cache.Set(&data) 43 | err := cache.Get(&data2) 44 | if err == nil { 45 | t.Error("FileCache error: err should not be nil") 46 | } 47 | t.Logf("%+v %+v", data, data2) 48 | } 49 | 50 | func TestInMemoryCache(t *testing.T) { 51 | cache := NewInMemoryCache() 52 | data := ExpiresData{ 53 | Data: "Hello World!", 54 | Expires: 7200, 55 | Created: time.Now().Unix(), 56 | } 57 | cache.Set(&data) 58 | 59 | var data2 ExpiresData 60 | cache.Get(&data2) 61 | t.Logf("%+v %+v", data, data2) 62 | 63 | if data2.Created != data.Created { 64 | t.Errorf("InMemoryCache error") 65 | } 66 | 67 | data = ExpiresData{ 68 | Data: "Hello World 2!", 69 | Expires: 0, 70 | Created: time.Now().Unix(), 71 | } 72 | cache.Set(&data) 73 | err := cache.Get(&data2) 74 | if err == nil { 75 | t.Error("InMemoryCache error: err should not be nil") 76 | } 77 | t.Logf("%+v %+v", data, data2) 78 | } 79 | --------------------------------------------------------------------------------