sns及refreshaccesstoken测试通过

This commit is contained in:
suguo.yao 2021-11-18 18:56:31 +08:00
commit a519daf9f1
30 changed files with 2425 additions and 0 deletions

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
go.sum
.auth_file

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Hugo Zhu
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.

108
README.md Normal file
View File

@ -0,0 +1,108 @@
# DingTalk Open API golang SDK
![image](http://static.dingtalk.com/media/lALOAQ6nfSvM5Q_229_43.png)
Check out DingTalk Open API document at: https://ding-doc.dingtalk.com/
## Usage
Fetch the SDK
```
export GOPATH=`pwd`
go get myschools.me/suguo/godingtalk
```
### Example code to send a micro app message
```
package main
import (
"myschools.me/suguo/godingtalk"
"log"
"os"
)
func main() {
c := godingtalk.NewDingTalkClient(os.Getenv("corpid"), os.Getenv("corpsecret"))
c.RefreshAccessToken()
err := c.SendAppMessage(os.Args[1], os.Args[2], os.Args[3])
if err != nil {
log.Println(err)
}
}
```
## Guide
Step-by-step Guide to use this SDK
http://hugozhu.myalert.info/2016/05/02/66-use-dingtalk-golang-sdk-to-send-message-on-pi.html
## Tools
**ding_alert** : Command line tool to send app/text/oa ... messages
```
export GOPATH=`pwd`
go get myschools.me/suguo/godingtalk/demo/ding_alert
export corpid=<组织的corpid 通过 https://oa.dingtalk.com 获取>
export corpsecret=<组织的corpsecret 通过 https://oa.dingtalk.com 获取>
./bin/ding_alert
Usage of ./bin/ding_alert:
-agent string
agent Id (default "22194403")
-chat string
chat id (default "chat6a93bc1ee3b7d660d372b1b877a9de62")
-file string
file path for media message
-link string
link url (default "http://hugozhu.myalert.info/dingtalk")
-sender string
sender id (default "011217462940")
-text string
text for link message (default "This is link text")
-title string
title for link message (default "This is link title")
-touser string
touser id (default "0420506555")
-type string
message type (app, text, image, voice, link, oa) (default "app")
```
**github**: Deliver Github webhook events to DingTalk, which can be deployed on Google AppEngine.
more info at: http://hugozhu.myalert.info/2016/05/15/67-use-free-google-cloud-service-to-deliver-github-webhook-events-to-dingtalk.html
```
export GOPATH=`pwd`
go get myschools.me/suguo/godingtalk/demo/github/appengine
```
Modify `app.yaml`
```
cd src/myschools.me/suguo/godingtalk/demo/github/appengine
cat app.yaml
application: github-alert-<random_number>
version: 1
runtime: go
api_version: go1
env_variables:
CORP_ID: '< http://oa.dingtalk.com 获取>'
CORP_SECRET: '< http://oa.dingtalk.com 获取>'
GITHUB_WEBHOOK_SECRET: '< http://github.com/ 获取>'
SENDER_ID: '< http://open.dingtalk.com 调用api获取>'
CHAT_ID: '< http://open.dingtalk.com 调用api获取>'
handlers:
- url: /.*
script: _go_app
```

104
api_attendance.go Normal file
View File

@ -0,0 +1,104 @@
/*
* Author Kevin Zhu
*
* Direct questions, comments to <ipandtcp@gmail.com>
*/
package godingtalk
import (
"errors"
"time"
)
type Attendance struct {
GmtModifed int64 `json:"gmtModified"` //: 1492594486000,
IsLegal string `json:"isLegal"` //: "N",
BaseCheckTime int64 `json:"baseCheckTime"` //: 1492568460000,
ID int64 `json:"id"` //: 933202551,
UserAddress string `json:"userAddress"` //: "北京市朝阳区崔各庄镇阿里中心.望京A座阿里巴巴绿地中心",
UID string `json:"userId"` //: "manager7078",
CheckType string `json:"checkType"` //: "OnDuty",
TimeResult string `json:"timeResult"` //: "Normal",
DeviceID string `json:"deviceId"` // :"cb7ace07d52fe9be14f4d8bec5e1ba79"
CorpID string `json:"corpId"` //: "ding7536bfee6fb1fa5a35c2f4657eb6378f",
SourceType string `json:"sourceType"` //: "USER",
WorkDate int64 `json:"workDate"` //: 1492531200000,
PlanCheckTime int64 `json:"planCheckTime"` //: 1492568497000,
GmtCreate int64 `json:"gmtCreate"` //: 1492594486000,
LocaltionMethod string `json:"locationMethod"` //: "MAP",
LocationResult string `json:"locationResult"` //: "Outside",
UserLongitude float64 `json:"userLongitude"` //: 116.486888,
PlanID int `json:"planId"` //: 4550269081,
GroupID int `json:"groupId"` //: 121325603,
UserAccuracy int `json:"userAccuracy"` //: 65,
UserCheckTime int64 `json:"userCheckTime"` //: 1492568497000,
UserLatitude float64 `json:"userLatitude"` //: 39.999946,
ProcInstID string `json:"procInstId"` //: "cb992267-9b70"
ApproveID int `json:"approveId"` // string, `json:""`//关联的审批id
ClassId int `json:"classId"` //考勤班次id没有的话表示该次打卡不在排班内
UserSsid string `json:"userSsid"` //用户打卡wifi SSID
UserMacAddr string `json:"userMacAddr"` //用户打卡wifi Mac地址
BaseAddress string `json:"baseAddress"` //基准地址
BaseLongitude float32 `json:"baseLongitude"` // 基准经度
BaseLatitude float32 `json:"baseLatitude"` // 基准纬度
BaseAccuracy int `json:"baseAccuracy"` // 基准定位精度
BaseSsid string `json:"baseSsid"` //基准wifi ssid
BaseMacAddr string `json:"baseMacAddr"` //基准 Mac 地址
OutsideRemark string `json:"outsideRemark"` //打卡备注
}
type listAttendanceRecordResp struct {
OAPIResponse
Records []Attendance `json:"recordresult"`
}
// 获取所有的打卡记录该员工当天如果打卡10条那么10条都将返回
func (c *DingTalkClient) ListAttendanceRecord(ulist []string, dateFrom time.Time, dateTo time.Time) ([]Attendance, error) {
var resp listAttendanceRecordResp
if len(ulist) > 50 || len(ulist) < 1 {
return nil, errors.New("Users can't more than 50 or less than 1")
}
if !dateFrom.Before(dateTo) {
return nil, errors.New("FromDate must before ToDate")
}
if time.Duration(dateTo.UnixNano()-dateFrom.UnixNano()).Hours() > float64(7*24) {
return nil, errors.New("Can't more than 6 days at once")
}
request := map[string]interface{}{
"checkDateFrom": dateFrom.Format("2006-01-02 15:04:05"), // "yyyy-MM-dd hh:mm:ss",
"checkDateTo": dateTo.Format("2006-01-02 15:04:05"), // "yyyy-MM-dd hh:mm:ss",
"userIds": ulist, // 企业内的员工id列表最多不能超过50个
}
return resp.Records, c.httpRPC("/attendance/listRecord", nil, request, &resp)
}
type listAttendanceResultResp struct {
OAPIResponse
HasMore bool `json:"hasMore"`
Records []Attendance `json:"recordresult"`
}
// 即使员工在这期间打了多次,该接口也只会返回两条记录,包括上午的打卡结果和下午的打卡结果
// 用户如果为空则获取所有用户
func (c *DingTalkClient) ListAttendanceResult(ulist []string, dateFrom, dateTo time.Time, offset, lmt int64) (listAttendanceResultResp, error) {
var resp listAttendanceResultResp
if time.Duration(dateTo.UnixNano()-dateFrom.UnixNano()).Hours() > float64(7*24) {
return resp, errors.New("Can't more than 7 days at once")
}
if !dateFrom.Before(dateTo) {
return resp, errors.New("FromDate must before ToDate")
}
request := map[string]interface{}{
"workDateFrom": dateFrom.Format("2006-01-02 15:04:05"), // "yyyy-MM-dd hh:mm:ss",
"workDateTo": dateTo.Format("2006-01-02 15:04:05"), // "yyyy-MM-dd hh:mm:ss",
"userIdList": ulist, // ["员工UserId列表"], 必填与offset和limit配合使用不传表示分页获取全员的数据
"offset": offset, // 必填第一次传0如果还有多余数据下次传之前的offset加上limit的值
"limit": lmt, // 最多50
}
return resp, c.httpRPC("/attendance/list", nil, request, &resp)
}

28
api_attendance_test.go Normal file
View File

@ -0,0 +1,28 @@
package godingtalk
import (
"testing"
"time"
)
func TestListAttendanceRecord(t *testing.T) {
dataFrom, _ := time.Parse("2006-01-02", "2018-03-06")
dataTo, _ := time.Parse("2006-01-02", "2018-03-10")
records, err := c.ListAttendanceRecord([]string{"085354234826136236"}, dataFrom, dataTo)
if err != nil {
t.Error(err)
} else if len(records) > 0 {
t.Logf("%+v\n", records)
}
}
func TestListAttendanceResult(t *testing.T) {
dataFrom, _ := time.Parse("2006-01-02", "2018-03-06")
dataTo, _ := time.Parse("2006-01-02", "2018-03-10")
resp, err := c.ListAttendanceResult([]string{"085354234826136236"}, dataFrom, dataTo, 0, 2)
if err != nil {
t.Error(err)
} else if len(resp.Records) > 0 {
t.Logf("%+v\n", resp.Records[0])
}
}

59
api_calendar.go Normal file
View File

@ -0,0 +1,59 @@
package godingtalk
import "time"
type Event struct {
OAPIResponse
Id string
Location string
Summary string
Description string
Start struct {
DateTime string `json:"date_time"`
}
End struct {
DateTime string `json:"date_time"`
}
}
type ListEventsResponse struct {
OAPIResponse
Success bool `json:"success"`
Result struct {
Events []Event `json:"items"`
Summary string `json:"summary"`
NextPageToken string `json:"next_page_token"`
} `json:"result"`
}
type CalendarTime struct {
TimeZone string `json:"time_zone"`
Date string `json:"date_time"`
}
type CalendarRequest struct {
TimeMax CalendarTime `json:"time_max"`
TimeMin CalendarTime `json:"time_min"`
StaffId string `json:"user_id"`
}
func (c *DingTalkClient) ListEvents(staffid string, from time.Time, to time.Time) (events []Event, err error) {
location := time.Now().Location().String()
timeMin := CalendarTime{
TimeZone: location,
Date: from.Format("2006-01-02T15:04:05Z0700"),
}
timeMax := CalendarTime{
TimeZone: location,
Date: to.Format("2006-01-02T15:04:05Z0700"),
}
data := CalendarRequest{
TimeMax: timeMax,
TimeMin: timeMin,
StaffId: staffid,
}
var resp ListEventsResponse
err = c.httpRPC("topapi/calendar/list", nil, data, &resp)
events = resp.Result.Events
return events, err
}

49
api_callback.go Normal file
View File

@ -0,0 +1,49 @@
package godingtalk
type Callback struct {
OAPIResponse
Token string
AES_KEY string `json:"aes_key"`
URL string
Callbacks []string `json:"call_back_tag"`
}
//RegisterCallback is 注册事件回调接口
func (c *DingTalkClient) RegisterCallback(callbacks []string, token string, aes_key string, callbackURL string) error {
var data OAPIResponse
request := map[string]interface{}{
"call_back_tag": callbacks,
"token": token,
"aes_key": aes_key,
"url": callbackURL,
}
err := c.httpRPC("call_back/register_call_back", nil, request, &data)
return err
}
//UpdateCallback is 更新事件回调接口
func (c *DingTalkClient) UpdateCallback(callbacks []string, token string, aes_key string, callbackURL string) error {
var data OAPIResponse
request := map[string]interface{}{
"call_back_tag": callbacks,
"token": token,
"aes_key": aes_key,
"url": callbackURL,
}
err := c.httpRPC("call_back/update_call_back", nil, request, &data)
return err
}
//DeleteCallback is 删除事件回调接口
func (c *DingTalkClient) DeleteCallback() error {
var data OAPIResponse
err := c.httpRPC("call_back/delete_call_back", nil, nil, &data)
return err
}
//ListCallback is 查询事件回调接口
func (c *DingTalkClient) ListCallback() (Callback, error) {
var data Callback
err := c.httpRPC("call_back/get_call_back", nil, nil, &data)
return data, err
}

28
api_callback_test.go Normal file
View File

@ -0,0 +1,28 @@
package godingtalk
import (
"testing"
)
func TestRegisterCallback(t *testing.T) {
err := c.UpdateCallback([]string{"user_modify_org"}, "hello", "1234567890123456789012345678901234567890aes", "http://go.myalert.info:8888/dingtalk/callback/")
if err != nil {
t.Log(err)
}
err = c.DeleteCallback()
if err != nil {
t.Log(err)
}
err = c.RegisterCallback([]string{"user_add_org"}, "hello", "1234567890123456789012345678901234567890aes", "http://go.myalert.info:8888/dingtalk/callback/")
if err != nil {
t.Log(err)
}
}
func TestListCallback(t *testing.T) {
data, err := c.ListCallback()
if err != nil {
t.Log(err)
}
t.Log(data)
}

146
api_contact.go Normal file
View File

@ -0,0 +1,146 @@
package godingtalk
import (
"fmt"
"net/url"
)
type User struct {
OAPIResponse
Userid string
Name string
Mobile string
Tel string
Remark string
Order int
IsAdmin bool
IsBoss bool
IsLeader bool
Active bool
Department []int
Position string
Email string
OrgEmail string
Avatar string
Extattr interface{}
}
type UserList struct {
OAPIResponse
HasMore bool
Userlist []User
}
type Department struct {
OAPIResponse
Id int
Name string
ParentId int
Order int
DeptPerimits string
UserPerimits string
OuterDept bool
OuterPermitDepts string
OuterPermitUsers string
OrgDeptOwner string
DeptManagerUseridList string
}
type DepartmentList struct {
OAPIResponse
Departments []Department `json:"department"`
}
// DepartmentList is 获取部门列表
func (c *DingTalkClient) DepartmentList() (DepartmentList, error) {
var data DepartmentList
err := c.httpRPC("department/list", nil, nil, &data)
return data, err
}
//DepartmentDetail is 获取部门详情
func (c *DingTalkClient) DepartmentDetail(id int) (Department, error) {
var data Department
params := url.Values{}
params.Add("id", fmt.Sprintf("%d", id))
err := c.httpRPC("department/get", params, nil, &data)
return data, err
}
//UserList is 获取部门成员
func (c *DingTalkClient) UserList(departmentID, offset, size int) (UserList, error) {
var data UserList
if size > 100 {
return data, fmt.Errorf("size 最大100")
}
params := url.Values{}
params.Add("department_id", fmt.Sprintf("%d", departmentID))
params.Add("offset", fmt.Sprintf("%d", offset))
params.Add("size", fmt.Sprintf("%d", size))
err := c.httpRPC("user/list", params, nil, &data)
return data, err
}
//CreateChat is
func (c *DingTalkClient) CreateChat(name string, owner string, useridlist []string) (string, error) {
var data struct {
OAPIResponse
Chatid string
}
request := map[string]interface{}{
"name": name,
"owner": owner,
"useridlist": useridlist,
}
err := c.httpRPC("chat/create", nil, request, &data)
return data.Chatid, err
}
//UserInfoByCode 校验免登录码并换取用户身份
func (c *DingTalkClient) UserInfoByCode(code string) (User, error) {
var data User
params := url.Values{}
params.Add("code", code)
err := c.httpRPC("user/getuserinfo", params, nil, &data)
return data, err
}
//UserInfoByUserId 获取用户详情
func (c *DingTalkClient) UserInfoByUserId(userid string) (User, error) {
var data User
params := url.Values{}
params.Add("userid", userid)
err := c.httpRPC("user/get", params, nil, &data)
return data, err
}
//UseridByUnionId 通过UnionId获取玩家Userid
func (c *DingTalkClient) UseridByUnionId(unionid string) (string, error) {
var data struct {
OAPIResponse
UserID string `json:"userid"`
}
params := url.Values{}
params.Add("unionid", unionid)
err := c.httpRPC("user/getUseridByUnionid", params, nil, &data)
if err != nil {
return "", err
}
return data.UserID, err
}
//UseridByMobile 通过手机号获取Userid
func (c *DingTalkClient) UseridByMobile(mobile string) (string, error) {
var data struct {
OAPIResponse
UserID string `json:"userid"`
}
params := url.Values{}
params.Add("mobile", mobile)
err := c.httpRPC("user/get_by_mobile", params, nil, &data)
return data.UserID, err
}

34
api_encryption.go Normal file
View File

@ -0,0 +1,34 @@
package godingtalk
//DataMessage 服务端加密、解密消息
type DataMessage struct {
OAPIResponse
Data string
}
//Encrypt is 服务端加密
func (c *DingTalkClient) Encrypt(str string) (string, error) {
var data DataMessage
request := map[string]interface{}{
"data": str,
}
err := c.httpRPC("encryption/encrypt", nil, request, &data)
if err!=nil {
return "", err
}
return data.Data, nil
}
//Decrypt is 服务端解密
func (c *DingTalkClient) Decrypt(str string) (string, error) {
var data DataMessage
request := map[string]interface{}{
"data": str,
}
err := c.httpRPC("encryption/decrypt", nil, request, &data)
if err!=nil {
return "", err
}
return data.Data, nil
}

14
api_encryption_test.go Normal file
View File

@ -0,0 +1,14 @@
package godingtalk
import (
"testing"
)
func TestEncryption(t *testing.T) {
str, err := c.Encrypt("Hello")
if err!=nil {
t.Log(err)
} else {
t.Log(str)
}
}

38
api_file.go Normal file
View File

@ -0,0 +1,38 @@
package godingtalk
import (
"bytes"
"fmt"
"io"
"net/url"
)
/**
* https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.UeYQVr&treeId=172&articleId=104970&docType=1
* TODO: not completed yet
**/
//FileResponse is
type FileResponse struct {
OAPIResponse
Code int
Msg string
UploadID string `json:"uploadid"`
Writer io.Writer
}
func (f *FileResponse) getWriter() io.Writer {
return f.Writer
}
//CreateFile is to create a new file in Ding Space
func (c *DingTalkClient) CreateFile(size int64) (file FileResponse, err error) {
buf := bytes.Buffer{}
file = FileResponse{
Writer: &buf,
}
params := url.Values{}
params.Add("size", fmt.Sprintf("%d", size))
err = c.httpRPC("file/upload/create", params, nil, &file)
return file, err
}

10
api_file_test.go Normal file
View File

@ -0,0 +1,10 @@
package godingtalk
import (
"testing"
)
func TestCreateFile(t *testing.T) {
file, err := c.CreateFile(1024)
t.Log(file, err)
}

44
api_media.go Normal file
View File

@ -0,0 +1,44 @@
package godingtalk
import (
"io"
"net/url"
"time"
)
//MediaResponse is
type MediaResponse struct {
OAPIResponse
Type string
MediaID string `json:"media_id"`
Writer io.Writer
}
func (m *MediaResponse) getWriter() io.Writer {
return m.Writer
}
//UploadMedia is to upload media file to DingTalk
func (c *DingTalkClient) UploadMedia(mediaType string, filename string, reader io.Reader) (media MediaResponse, err error) {
upload := UploadFile{
FieldName: "media",
FileName: filename,
Reader: reader,
}
params := url.Values{}
params.Add("type", mediaType)
c.HTTPClient.Timeout = 120 * time.Second
err = c.httpRPC("media/upload", params, upload, &media)
return media, err
}
//DownloadMedia is to download a media file from DingTalk
func (c *DingTalkClient) DownloadMedia(mediaID string, write io.Writer) error {
var data MediaResponse
data.Writer = write
params := url.Values{}
params.Add("media_id", mediaID)
c.HTTPClient.Timeout = 120 * time.Second
err := c.httpRPC("media/get", params, nil, &data)
return err
}

259
api_message.go Normal file
View File

@ -0,0 +1,259 @@
package godingtalk
import (
"net/url"
"strconv"
)
//SendAppMessage is 发送企业会话消息
func (c *DingTalkClient) SendAppMessage(agentID string, touser string, msg string) error {
if agentID == "" {
agentID = c.AgentID
}
var data OAPIResponse
request := map[string]interface{}{
"touser": touser,
"agentid": agentID,
"msgtype": "text",
"text": map[string]interface{}{
"content": msg,
},
}
err := c.httpRPC("message/send", nil, request, &data)
return err
}
//SendAppOAMessage is 发送OA消息
func (c *DingTalkClient) SendAppOAMessage(agentID string, touser string, msg OAMessage) error {
if agentID == "" {
agentID = c.AgentID
}
var data OAPIResponse
request := map[string]interface{}{
"touser": touser,
"agentid": agentID,
"msgtype": "oa",
"oa": msg,
}
err := c.httpRPC("message/send", nil, request, &data)
return err
}
// ActionCardMessage
func (c *DingTalkClient) SendOverAllActionCardMessage(agentID string, touser string, msg OverAllActionCardMessage) error {
if agentID == "" {
agentID = c.AgentID
}
var data OAPIResponse
request := map[string]interface{}{
"touser": touser,
"agentid": agentID,
"msgtype": "action_card",
"action_card": msg,
}
err := c.httpRPC("message/send", nil, request, &data)
return err
}
func (c *DingTalkClient) SendIndependentActionCardMessage(agentID string, touser string, msg IndependentActionCardMessage) error {
if agentID == "" {
agentID = c.AgentID
}
var data OAPIResponse
request := map[string]interface{}{
"touser": touser,
"agentid": agentID,
"msgtype": "action_card",
"action_card": msg,
}
err := c.httpRPC("message/send", nil, request, &data)
return err
}
//SendAppLinkMessage is 发送企业会话链接消息
func (c *DingTalkClient) SendAppLinkMessage(agentID, touser string, title, text string, picUrl, url string) error {
if agentID == "" {
agentID = c.AgentID
}
var data OAPIResponse
request := map[string]interface{}{
"touser": touser,
"agentid": agentID,
"msgtype": "link",
"link": map[string]string{
"messageUrl": url,
"picUrl": picUrl,
"title": title,
"text": text,
},
}
err := c.httpRPC("message/send", nil, request, &data)
return err
}
//SendTextMessage is 发送普通文本消息
func (c *DingTalkClient) SendTextMessage(sender string, cid string, msg string) (data MessageResponse, err error) {
request := map[string]interface{}{
"chatid": cid,
"sender": sender,
"msgtype": "text",
"text": map[string]interface{}{
"content": msg,
},
}
err = c.httpRPC("chat/send", nil, request, &data)
return data, err
}
//SendImageMessage is 发送图片消息
func (c *DingTalkClient) SendImageMessage(sender string, cid string, mediaID string) (data MessageResponse, err error) {
request := map[string]interface{}{
"chatid": cid,
"sender": sender,
"msgtype": "image",
"image": map[string]string{
"media_id": mediaID,
},
}
err = c.httpRPC("chat/send", nil, request, &data)
return data, err
}
//SendVoiceMessage is 发送语音消息
func (c *DingTalkClient) SendVoiceMessage(sender string, cid string, mediaID string, duration string) (data MessageResponse, err error) {
request := map[string]interface{}{
"chatid": cid,
"sender": sender,
"msgtype": "voice",
"voice": map[string]string{
"media_id": mediaID,
"duration": duration,
},
}
err = c.httpRPC("chat/send", nil, request, &data)
return data, err
}
//SendFileMessage is 发送文件消息
func (c *DingTalkClient) SendFileMessage(sender string, cid string, mediaID string) (data MessageResponse, err error) {
request := map[string]interface{}{
"chatid": cid,
"sender": sender,
"msgtype": "file",
"file": map[string]string{
"media_id": mediaID,
},
}
err = c.httpRPC("chat/send", nil, request, &data)
return data, err
}
//SendLinkMessage is 发送链接消息
func (c *DingTalkClient) SendLinkMessage(sender string, cid string, mediaID string, url string, title string, text string) (data MessageResponse, err error) {
request := map[string]interface{}{
"chatid": cid,
"sender": sender,
"msgtype": "link",
"link": map[string]string{
"messageUrl": url,
"picUrl": mediaID,
"title": title,
"text": text,
},
}
err = c.httpRPC("chat/send", nil, request, &data)
return data, err
}
// OverAllActionCardMessage 整体跳转ActionCard
type OverAllActionCardMessage struct {
Title string `json:"title"`
MarkDown string `json:"markdown"`
SingleTitle string `json:"single_title"`
SingleUrl string `json:"single_url"`
}
// IndependentActionCardMessage 独立跳转ActionCard
type IndependentActionCardMessage struct {
Title string `json:"title"`
MarkDown string `json:"markdown"`
BtnOrientation string `json:"btn_orientation"`
BtnJsonList []ActionCardMessageBtnList `json:"btn_json_list"`
}
type ActionCardMessageBtnList struct {
Title string `json:"title,omitempty"`
ActionUrl string `json:"action_url,omitempty"`
}
func (m *IndependentActionCardMessage) AppendBtnItem(title string, action_url string) {
f := ActionCardMessageBtnList{Title: title, ActionUrl: action_url}
if m.BtnJsonList == nil {
m.BtnJsonList = []ActionCardMessageBtnList{}
}
m.BtnJsonList = append(m.BtnJsonList, f)
}
//OAMessage is the Message for OA
type OAMessage struct {
URL string `json:"message_url"`
PcURL string `json:"pc_message_url"`
Head struct {
BgColor string `json:"bgcolor,omitempty"`
Text string `json:"text,omitempty"`
} `json:"head,omitempty"`
Body struct {
Title string `json:"title,omitempty"`
Form []OAMessageForm `json:"form,omitempty"`
Rich OAMessageRich `json:"rich,omitempty"`
Content string `json:"content,omitempty"`
Image string `json:"image,omitempty"`
FileCount int `json:"file_count,omitempty"`
Author string `json:"author,omitempty"`
} `json:"body,omitempty"`
}
type OAMessageForm struct {
Key string `json:"key,omitempty"`
Value string `json:"value,omitempty"`
}
type OAMessageRich struct {
Num string `json:"num,omitempty"`
Unit string `json:"body,omitempty"`
}
func (m *OAMessage) AppendFormItem(key string, value string) {
f := OAMessageForm{Key: key, Value: value}
if m.Body.Form == nil {
m.Body.Form = []OAMessageForm{}
}
m.Body.Form = append(m.Body.Form, f)
}
//SendOAMessage is 发送OA消息
func (c *DingTalkClient) SendOAMessage(sender string, cid string, msg OAMessage) (data MessageResponse, err error) {
request := map[string]interface{}{
"chatid": cid,
"sender": sender,
"msgtype": "oa",
"oa": msg,
}
err = c.httpRPC("chat/send", nil, request, &data)
return data, err
}
//GetMessageReadList is 获取已读列表
func (c *DingTalkClient) GetMessageReadList(messageID string, cursor int, size int) (data MessageReadListResponse, err error) {
params := url.Values{}
params.Add("messageId", messageID)
params.Add("cursor", strconv.Itoa(cursor))
params.Add("size", strconv.Itoa(size))
err = c.httpRPC("chat/getReadList", params, nil, &data)
return data, err
}

75
api_robot.go Normal file
View File

@ -0,0 +1,75 @@
package godingtalk
import (
"net/url"
)
type RobotAtList struct {
AtMobiles []string `json:"atMobiles"`
IsAtAll bool `json:"isAtAll"`
}
type RobotOutgoingMessage struct {
MessageType string `json:"msgtype"`
Text struct {
Content string `json:"content,omitempty"`
} `json:"text,omitempty"`
MessageID string `json:"msgId"`
CreatedAt int64 `json:"createAt"`
ConversationID string `json:"conversationId"`
ConversationType string `json:"conversationType"`
ConversationTitle string `json:"conversationTitle"`
SenderID string `json:"senderId"`
SenderNick string `json:"senderNick"`
SenderCorpID string `json:"senderCorpId"`
SenderStaffID string `json:"senderStaffId"`
ChatbotUserID string `json:"chatbotUserId"`
AtUsers []struct {
DingTalkID string `json:"dingtalkId,omitempty"`
StaffID string `json:"staffId,omitempty"`
} `json:"atUsers,omitempty"`
}
//SendRobotTextMessage can send a text message to a group chat
func (c *DingTalkClient) SendRobotTextMessage(accessToken string, msg string) (data MessageResponse, err error) {
params := url.Values{}
params.Add("access_token", accessToken)
request := map[string]interface{}{
"msgtype": "text",
"text": map[string]interface{}{
"content": msg,
},
}
err = c.httpRPC("robot/send", params, request, &data)
return data, err
}
//SendRobotMarkdownMessage can send a text message to a group chat
func (c *DingTalkClient) SendRobotMarkdownMessage(accessToken string, title string, msg string) (data MessageResponse, err error) {
params := url.Values{}
params.Add("access_token", accessToken)
request := map[string]interface{}{
"msgtype": "markdown",
"markdown": map[string]interface{}{
"title": title,
"text": msg,
},
}
err = c.httpRPC("robot/send", params, request, &data)
return data, err
}
// SendRobotTextAtMessage can send a text message and at user to a group chat
func (c *DingTalkClient) SendRobotTextAtMessage(accessToken string, msg string, at *RobotAtList) (data OAPIResponse, err error) {
params := url.Values{}
params.Add("access_token", accessToken)
request := map[string]interface{}{
"msgtype": "text",
"text": map[string]interface{}{
"content": msg,
},
"at": at,
}
err = c.httpRPC("robot/send", params, request, &data)
return data, err
}

44
api_sns.go Normal file
View File

@ -0,0 +1,44 @@
//普通钉钉用户账号开放相关接口
package godingtalk
import (
"net/url"
"strconv"
"time"
)
type SnsUserInfoResponse struct {
OAPIResponse
CorpInfo []struct {
CorpName string `json:"corp_name"`
IsAuth bool `json:"is_auth"`
IsManager bool `json:"is_manager"`
RightsLevel int `json:"rights_level"`
} `json:"corp_info"`
UserInfo struct {
MaskedMobile string `json:"marskedMobile"`
Nick string `json:"nick"`
OpenID string `json:"openid"`
UnionID string `json:"unionid"`
DingID string `json:"dingId"`
} `json:"user_info"`
}
//获取用户授权的个人信息
func (c *DingTalkClient) SnsUserInfo(code string) (SnsUserInfoResponse, error) {
ts := strconv.FormatInt(time.Now().UnixNano()/1000000, 10)
params := url.Values{}
params.Add("accessKey", c.AppKey)
params.Add("timestamp", ts)
params.Add("signature", encodeSHA256(ts, c.AppSecret))
body := struct {
Code string `json:"tmp_auth_code"`
}{code}
var data SnsUserInfoResponse
err := c.httpRequest("sns/getuserinfo_bycode", params, body, &data)
return data, err
}

14
api_sns_test.go Normal file
View File

@ -0,0 +1,14 @@
package godingtalk
import (
"fmt"
"testing"
)
func TestSnsUserInfo(t *testing.T) {
userinfo, err := c.SnsUserInfo("f9f9ba22256136f29a7fb3dd5d26c24c")
if err != nil {
t.Fatal(err.Error())
}
fmt.Println(userinfo)
}

174
crypto.go Normal file
View File

@ -0,0 +1,174 @@
package godingtalk
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"errors"
"math/rand"
r "math/rand"
"sort"
"time"
)
const (
AES_ENCODE_KEY_LENGTH = 43
)
var DefaultDingtalkCrypto *Crypto
type Crypto struct {
Token string
AesKey string
SuiteKey string
block cipher.Block
bkey []byte
}
func encodeSHA256(body, secret string) string {
// 钉钉签名算法实现
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(body))
sum := h.Sum(nil)
return base64.StdEncoding.EncodeToString(sum)
}
/*
token 数据签名需要用到的tokenISV(服务提供商)推荐使用注册套件时填写的token普通企业可以随机填写
aesKey 数据加密密钥用于回调数据的加密长度固定为43个字符从a-z, A-Z, 0-9共62个字符中选取,您可以随机生成ISV(服务提供商)推荐使用注册套件时填写的EncodingAESKey
suiteKey 一般使用corpID
*/
func NewCrypto(token, aesKey, suiteKey string) (c *Crypto) {
c = &Crypto{
Token: token,
AesKey: aesKey,
SuiteKey: suiteKey,
}
if len(c.AesKey) != AES_ENCODE_KEY_LENGTH {
panic("不合法的aeskey")
}
var err error
c.bkey, err = base64.StdEncoding.DecodeString(aesKey + "=")
if err != nil {
panic(err.Error())
}
c.block, err = aes.NewCipher(c.bkey)
if err != nil {
panic(err.Error())
}
return c
}
/*
signature: 签名字符串
timeStamp: 时间戳
nonce: 随机字符串
secretStr: 密文
返回: 解密后的明文
*/
func (c *Crypto) DecryptMsg(signature, timeStamp, nonce, secretStr string) (string, error) {
if !c.VerifySignature(c.Token, timeStamp, nonce, secretStr, signature) {
return "", errors.New("签名不匹配")
}
decode, err := base64.StdEncoding.DecodeString(secretStr)
if err != nil {
return "", err
}
if len(decode) < aes.BlockSize {
return "", errors.New("密文太短啦")
}
blockMode := cipher.NewCBCDecrypter(c.block, c.bkey[:c.block.BlockSize()])
plantText := make([]byte, len(decode))
blockMode.CryptBlocks(plantText, decode)
plantText = PKCS7UnPadding(plantText)
size := binary.BigEndian.Uint32(plantText[16 : 16+4])
plantText = plantText[16+4:]
cropid := plantText[size:]
if string(cropid) != c.SuiteKey {
return "", errors.New("CropID不正确")
}
return string(plantText[:size]), nil
}
func PKCS7UnPadding(plantText []byte) []byte {
length := len(plantText)
unpadding := int(plantText[length-1])
return plantText[:(length - unpadding)]
}
/*
replyMsg: 明文字符串
timeStamp: 时间戳
nonce: 随机字符串
返回: 密文,签名字符串
*/
func (c *Crypto) EncryptMsg(replyMsg, timeStamp, nonce string) (string, string, error) {
//原生消息体长度
size := make([]byte, 4)
binary.BigEndian.PutUint32(size, uint32(len(replyMsg)))
replyMsg = c.RandomString(16) + string(size) + replyMsg + c.SuiteKey
plantText := PKCS7Padding([]byte(replyMsg), c.block.BlockSize())
if len(plantText)%aes.BlockSize != 0 {
return "", "", errors.New("消息体大小不为16的倍数")
}
blockMode := cipher.NewCBCEncrypter(c.block, c.bkey[:c.block.BlockSize()])
ciphertext := make([]byte, len(plantText))
blockMode.CryptBlocks(ciphertext, plantText)
outStr := base64.StdEncoding.EncodeToString(ciphertext)
sigStr := c.GenerateSignature(c.Token, timeStamp, nonce, string(outStr))
return string(outStr), sigStr, nil
}
func PKCS7Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
// 数据签名
func (c *Crypto) GenerateSignature(token, timeStamp, nonce, secretStr string) string {
// 先将参数值进行排序
params := make([]string, 0)
params = append(params, token)
params = append(params, secretStr)
params = append(params, timeStamp)
params = append(params, nonce)
sort.Strings(params)
return sha1Sign(params[0] + params[1] + params[2] + params[3])
}
// 校验数据签名
func (c *Crypto) VerifySignature(token, timeStamp, nonce, secretStr, sigture string) bool {
return c.GenerateSignature(token, timeStamp, nonce, secretStr) == sigture
}
func (c *Crypto) RandomString(n int, alphabets ...byte) string {
const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
var bytes = make([]byte, n)
var randby bool
if num, err := rand.Read(bytes); num != n || err != nil {
r.Seed(time.Now().UnixNano())
randby = true
}
for i, b := range bytes {
if len(alphabets) == 0 {
if randby {
bytes[i] = alphanum[r.Intn(len(alphanum))]
} else {
bytes[i] = alphanum[b%byte(len(alphanum))]
}
} else {
if randby {
bytes[i] = alphabets[r.Intn(len(alphabets))]
} else {
bytes[i] = alphabets[b%byte(len(alphabets))]
}
}
}
return string(bytes)
}

13
go.mod Normal file
View File

@ -0,0 +1,13 @@
module myschools.me/suguo/godingtalk
go 1.13
require (
github.com/google/go-github v17.0.0+incompatible
github.com/google/go-querystring v1.0.0 // indirect
github.com/ipandtcp/godingtalk v0.0.0-20180410032244-ca3d6ac197fb
golang.org/x/net v0.0.0-20190603091049-60506f45cf65
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
google.golang.org/api v0.10.0
google.golang.org/appengine v1.6.4
)

172
godingtalk.go Normal file
View File

@ -0,0 +1,172 @@
package godingtalk
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
const (
//VERSION is SDK version
VERSION = "0.3"
)
//DingTalkClient is the Client to access DingTalk Open API
type DingTalkClient struct {
AppKey string
AppSecret string
AgentID string
PartnerID string
AccessToken string
HTTPClient *http.Client
Cache Cache
}
//Unmarshallable is
type Unmarshallable interface {
checkError() error
getWriter() io.Writer
}
//OAPIResponse is
type OAPIResponse struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
func (data *OAPIResponse) checkError() (err error) {
if data.ErrCode != 0 {
err = fmt.Errorf("%d: %s", data.ErrCode, data.ErrMsg)
}
return err
}
func (data *OAPIResponse) getWriter() io.Writer {
return nil
}
//MessageResponse is
type MessageResponse struct {
OAPIResponse
MessageID string `json:"messageId"`
}
//MessageResponse is
type MessageReadListResponse struct {
OAPIResponse
NextCursor int64 `json:"next_cursor"`
ReadUserIdList []string `json:"readUserIdList"`
}
//AccessTokenResponse is
type AccessTokenResponse struct {
OAPIResponse
AccessToken string `json:"access_token"`
Expires int `json:"expires_in"`
Created int64
}
//CreatedAt is when the access token is generated
func (e *AccessTokenResponse) CreatedAt() int64 {
return e.Created
}
//ExpiresIn is how soon the access token is expired
func (e *AccessTokenResponse) ExpiresIn() int {
return e.Expires
}
//JsAPITicketResponse is
type JsAPITicketResponse struct {
OAPIResponse
Ticket string
Expires int `json:"expires_in"`
Created int64
}
//CreatedAt is when the ticket is generated
func (e *JsAPITicketResponse) CreatedAt() int64 {
return e.Created
}
//ExpiresIn is how soon the ticket is expired
func (e *JsAPITicketResponse) ExpiresIn() int {
return e.Expires
}
//NewDingTalkClient creates a DingTalkClient instance
func NewDingTalkClient(appkey, appsecret string) *DingTalkClient {
c := new(DingTalkClient)
c.AppKey = appkey
c.AppSecret = appsecret
c.HTTPClient = &http.Client{
Timeout: 30 * time.Second,
}
c.Cache = NewFileCache(".auth_file")
return c
}
//RefreshAccessToken is to get a valid access token
func (c *DingTalkClient) RefreshAccessToken() error {
var data AccessTokenResponse
err := c.Cache.Get(&data)
if err == nil {
c.AccessToken = data.AccessToken
return nil
}
params := url.Values{}
params.Add("appkey", c.AppKey)
params.Add("appsecret", c.AppSecret)
err = c.httpRPC("gettoken", params, nil, &data)
if err == nil {
c.AccessToken = data.AccessToken
data.Expires = data.Expires | 7200
data.Created = time.Now().Unix()
err = c.Cache.Set(&data)
}
return err
}
//GetJsAPITicket is to get a valid ticket for JS API
func (c *DingTalkClient) GetJsAPITicket() (ticket string, err error) {
var data JsAPITicketResponse
cache := NewFileCache(".jsapi_ticket")
err = cache.Get(&data)
if err == nil {
return data.Ticket, err
}
err = c.httpRPC("get_jsapi_ticket", nil, nil, &data)
if err == nil {
ticket = data.Ticket
cache.Set(&data)
}
return ticket, err
}
//GetConfig is to return config in json
func (c *DingTalkClient) GetConfig(nonceStr string, timestamp string, url string) string {
ticket, _ := c.GetJsAPITicket()
config := map[string]string{
"url": url,
"nonceStr": nonceStr,
"agentId": c.AgentID,
"timeStamp": timestamp,
"corpId": c.AppKey,
"ticket": ticket,
"signature": Sign(ticket, nonceStr, timestamp, url),
}
bytes, _ := json.Marshal(&config)
return string(bytes)
}
//Sign is 签名
func Sign(ticket string, nonceStr string, timeStamp string, url string) string {
s := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s&timestamp=%s&url=%s", ticket, nonceStr, timeStamp, url)
return sha1Sign(s)
}

186
godingtalk_test.go Normal file
View File

@ -0,0 +1,186 @@
package godingtalk
import (
"os"
"testing"
"time"
)
var c *DingTalkClient
func init() {
c = NewDingTalkClient("dinguipztmzpv8sog933", "kUYbKHxNixdMhTmW6IrdTE-yVnWfQLs1C7RQIAsrlwz8BYlVmceFs-3JRBmU32rQ")
err := c.RefreshAccessToken()
if err != nil {
panic(err)
}
}
func TestInitWithAppKey(t *testing.T) {
c1 := NewDingTalkClient("dinguipztmzpv8sog933", "kUYbKHxNixdMhTmW6IrdTE-yVnWfQLs1C7RQIAsrlwz8BYlVmceFs-3JRBmU32rQ")
err := c1.RefreshAccessToken()
if err != nil {
panic(err)
}
_, err = c1.SendRobotTextMessage(c1.AccessToken, "Message sent successfully with appkey and appsecret")
if err != nil {
t.Error(err)
}
}
func TestCalendarListApi(t *testing.T) {
from := time.Now().AddDate(0, 0, -1)
to := time.Now().AddDate(0, 0, 1)
_, err := c.ListEvents("0420506555", from, to)
if err != nil {
panic(err)
}
//for _, event := range events {
// t.Logf("%v %v %v %v", event.Start, event.End, event.Summary, event.Description)
//}
}
func TestDepartmentApi(t *testing.T) {
departments, err := c.DepartmentList()
// t.Logf("%+v %+v", departments, err)
if err != nil {
t.Error(err)
t.FailNow()
}
d, err := c.DepartmentDetail(departments.Departments[0].Id)
if err != nil {
t.Error(err)
t.FailNow()
}
if d.Id != departments.Departments[0].Id {
t.Error("DepartmentDetail error")
}
for _, department := range departments.Departments {
t.Logf("dept: %v", department)
list, err := c.UserList(department.Id, 0, 100)
if err != nil {
t.Error(err)
}
for _, user := range list.Userlist {
t.Logf("\t\tuser: %v", user)
}
}
}
func TestJsAPITicket(t *testing.T) {
ticket, err := c.GetJsAPITicket()
if err != nil || ticket == "" {
t.Error("JsAPITicket error", err)
}
}
func TestCreateChat(t *testing.T) {
// chatid, err := c.CreateChat("Test chat", "0420506555", []string{"0420506555"})
// if err!=nil {
// t.Error(err)
// }
// t.Log("-----",chatid)
}
func TestSendAppMessageApi(t *testing.T) {
err := c.SendAppMessage("22194403", "0420506555", "测试消息,请忽略") //@all
if err != nil {
t.Error(err)
}
}
func TestTextMessage(t *testing.T) {
data, err := c.SendTextMessage("011217462940", "chat6a93bc1ee3b7d660d372b1b877a9de62", "测试消息,来自双十一,请忽略")
if err != nil {
t.Error(err)
} else {
if data.MessageID == "" {
t.Error("Message id is empty")
}
}
data2, _ := c.GetMessageReadList(data.MessageID, 0, 10)
if len(data2.ReadUserIdList) == 0 {
t.Error("Message Read List should not be empty")
}
}
func TestSendOAMessage(t *testing.T) {
msg := OAMessage{}
msg.URL = "http://www.google.com/"
msg.Head.Text = "头部标题"
msg.Head.BgColor = "FFBBBBBB"
msg.Body.Title = "正文标题"
msg.Body.Content = "test content"
_, err := c.SendOAMessage("011217462940", "chat6a93bc1ee3b7d660d372b1b877a9de62", msg)
if err != nil {
t.Error(err)
}
}
func _TestDownloadAndUploadImage(t *testing.T) {
f, err := os.Create("lADOHrf_oVxc.jpg")
if err == nil {
err = c.DownloadMedia("@lADOHrf_oVxc", f)
}
if err != nil {
t.Error(err)
}
f.Close()
f, _ = os.Open("lADOHrf_oVxc.jpg")
defer f.Close()
media, err := c.UploadMedia("image", "myfile.jpg", f)
if media.MediaID == "" {
t.Error("Upload File Failed")
}
t.Log("uploaded file mediaid:", media.MediaID)
if err != nil {
t.Error(err)
}
_, err = c.SendImageMessage("011217462940", "chat6a93bc1ee3b7d660d372b1b877a9de62", "@lADOHrf_oVxc")
if err != nil {
t.Error(err)
}
}
func TestVoiceMessage(t *testing.T) {
// f, _ := os.Open("/Users/hugozhu/Downloads/BlackBerry_test2_AMR-NB_Mono_12.2kbps_8000Hz.amr")
// defer f.Close()
// media, err := c.UploadMedia("voice", "sample.amr", f)
// if media.MediaID == "" {
// t.Error("Upload File Failed")
// }
// t.Log("uploaded file mediaid:", media.MediaID)
// if err != nil {
// t.Error(err)
// }
_, err := c.SendVoiceMessage("011217462940", "chat6a93bc1ee3b7d660d372b1b877a9de62", "@lATOHr53E84DALnDzml4wS0", "10")
if err != nil {
t.Error(err)
}
}
func TestRobotMessage(t *testing.T) {
_, err := c.SendRobotTextMessage(os.Getenv("token"), "这是一条测试消息")
if err != nil {
t.Error(err)
}
_, err = c.SendRobotMarkdownMessage(os.Getenv("token"), "测试标题", "# 杭州天气\n这是一条测试消息\n"+
"> 9度西北风1级空气良89**相对温度**73%\n\n"+
"> ![screenshot](https://gw.alicdn.com/tfs/TB1ut3xxbsrBKNjSZFpXXcXhFXa-846-786.png)\n"+
"> ###### 10点20分发布 [天气](http://www.thinkpage.cn/) \n")
if err != nil {
t.Error(err)
}
}
func TestRobotAtMessage(t *testing.T) {
_, err := c.SendRobotTextAtMessage(os.Getenv("token"), "这是一条测试消息", &RobotAtList{
IsAtAll: true,
})
if err != nil {
t.Error(err)
}
}

187
top_api_approval.go Normal file
View File

@ -0,0 +1,187 @@
/*
* Author Kevin Zhu
*
* Direct questions, comments to <ipandtcp@gmail.com>
*/
package godingtalk
import (
"encoding/json"
"errors"
"net/url"
"strconv"
"strings"
"time"
)
const (
topAPICreateProcInstMethod = "dingtalk.smartwork.bpms.processinstance.create"
topAPIGetProcInstMethod = "dingtalk.smartwork.bpms.processinstance.get"
topAPIListProcInstMethod = "dingtalk.smartwork.bpms.processinstance.list"
)
type TopAPICreateProcInst struct {
// 审批模板code
ProcessCode string `json:"process_code"`
// 发起人UID
OriginatorUID string `json:"originator_user_id"`
// 发起人所在部门
DeptID int `json:"dept_id"`
// 审批人列表
Approvers []string `json:"approvers"`
// 抄送人列表
CCList []string `json:"cc_list"`
//抄送时间,分为START,FINISH,START_FINISH
CCPosition string `json:"cc_position"`
// 审批单内容, Name为审批模板中的列名 value 为该列的值
FormCompntValues []ProcInstCompntValues `json:"form_component_values"`
}
type ProcInst struct {
ProcInstID string `json:"process_instance_id"`
Title string `json:"title"`
CreateTime string `json:"create_time"`
FinishTime string `json:"finish_time"`
OriginatorUID string `json:"originator_userid"`
Status string `json:"status"`
ApproverUIDS []string `json:"approver_userids"`
CCUIDS []string `json:"cc_userids"`
Result string `json:"result"`
BusinessID string `json:"business_id"`
FormCompntValues []ProcInstCompntValues `json:"form_component_values"` // 表单详情列表
Tasks []_ProcInstTasks `json:"tasks"` // 任务列表
OperationRecords []_ProcInstOperationRecords `json:"operation_records"` // 操作记录列表
OriginatorDeptID string `json:"originator_dept_id"`
OriginatorDeptName string `json:"originator_dept_name"`
}
type ProcInstCompntValues struct {
Name string `json:"name"`
Value string `json:"value"`
ExtValue string `json:"ext_value"`
}
type _ProcInstOperationRecords struct {
UID string `json:"userid"`
Date string `json:"date"`
Type string `json:"operation_type"`
Result string `json:"operation_result"`
Remark string `json:"remark"`
}
type _ProcInstTasks struct {
UID string `json:"userid"`
Status string `json:"task_status"`
Result string `json:"task_result"`
CreateTime string `json:"create_time"`
FinishTime string `json:"finish_time"`
}
type topAPICreateProcInstResp struct {
topAPIErrResponse
OK struct {
Errcode int `json:"ding_open_errcode"`
ErrMsg string `json:"error_msg"`
IsSuccess bool `jons:"is_success"`
ProcInstID string `json:"process_instance_id"`
} `json:"result"`
RequestID string `json:"request_id"`
}
// 发起审批
func (c *DingTalkClient) TopAPICreateProcInst(data TopAPICreateProcInst) (string, error) {
var resp topAPICreateProcInstResp
values, err := json.Marshal(data.FormCompntValues)
if err != nil {
return "", err
}
form := url.Values{}
form.Add("method", topAPICreateProcInstMethod)
form.Add("cc_list", strings.Join(data.CCList, ","))
form.Add("dept_id", strconv.Itoa(data.DeptID))
form.Add("approvers", strings.Join(data.Approvers, ","))
form.Add("cc_position", data.CCPosition)
form.Add("process_code", data.ProcessCode)
form.Add("originator_user_id", data.OriginatorUID)
form.Add("form_component_values", string(values))
if c.AgentID != "" {
form.Add("agent_id", c.AgentID)
}
return resp.OK.ProcInstID, c.topAPIRequest(form, &resp)
}
type topAPIGetProcInstResp struct {
Ok struct {
ErrCode int `json:"ding_open_errcode"`
ErrMsg string `json:"error_msg"`
Success bool `json:"success"`
ProcInst ProcInst `json:"process_instance"`
} `json:"result"`
RequestID string `json:"request_id"`
topAPIErrResponse
}
// 根据审批实例id获取单条审批实例详情
func (c *DingTalkClient) TopAPIGetProcInst(pid string) (ProcInst, error) {
var resp topAPIGetProcInstResp
reqForm := url.Values{}
reqForm.Add("process_instance_id", pid)
reqForm.Add("method", topAPIGetProcInstMethod)
err := c.topAPIRequest(reqForm, &resp)
if err != nil {
return resp.Ok.ProcInst, err
}
resp.Ok.ProcInst.ProcInstID = pid
return resp.Ok.ProcInst, err
}
type ListProcInst struct {
ApproverUIDS []string `json:"approver_userid_list"`
CCUIDS []string `json:"cc_userid_list"`
FormCompntValues []ProcInstCompntValues `json:"form_component_values"`
ProcInstID string `json:"process_instance_id"`
Title string `json:"title"`
CreateTime string `json:"create_time"`
FinishTime string `json:"finish_time"`
OriginatorUID string `json:"originator_userid"`
Status string `json:"status"`
BusinessID string `json:"business_id"`
OriginatorDeptID string `json:"originator_dept_id"`
ProcInstResult string `json:"process_instance_result"` // "agree",
}
type TopAPIListProcInstResp struct {
topAPIErrResponse
OK struct {
ErrCode int `json:"ding_open_errcode"`
ErrMsg string `json:"error_msg"`
Success bool `json:"success"`
Result struct {
List []ListProcInst `json:"list"`
NextCursor int `json:"next_cursor"`
} `json:"result"`
} `json:"result"`
RequestID string `json:"request_id"`
}
// 获取审批实例列表
// Note: processCode 官方不会检查错误请保证processCode正确
func (c *DingTalkClient) TopAPIListProcInst(processCode string, startTime, endTime time.Time, size, cursor int, useridList []string) (TopAPIListProcInstResp, error) {
var resp TopAPIListProcInstResp
if size > 10 {
return resp, errors.New("Max size is 10")
}
reqForm := url.Values{}
reqForm.Add("process_code", processCode)
reqForm.Add("start_time", strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10))
reqForm.Add("end_time", strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10))
reqForm.Add("size", strconv.Itoa(size))
reqForm.Add("cursor", strconv.Itoa(cursor))
reqForm.Add("userid_list", strings.Join(useridList, ","))
reqForm.Add("method", topAPIListProcInstMethod)
return resp, c.topAPIRequest(reqForm, &resp)
}

46
top_api_approval_test.go Normal file
View File

@ -0,0 +1,46 @@
package godingtalk
import (
"encoding/json"
"testing"
"time"
)
func TestTopAPIProcInst(t *testing.T) {
var compntValues []ProcInstCompntValues
compntValues = append(compntValues, ProcInstCompntValues{Name: "单行输入框", Value: "单行输入框输入的内容"})
detailCompntValues := [][]ProcInstCompntValues{[]ProcInstCompntValues{ProcInstCompntValues{Name: "明细内单行输入框", Value: "明细内单行输入框的内容"}}}
detailValues, _ := json.Marshal(detailCompntValues)
compntValues = append(compntValues, ProcInstCompntValues{Name: "明细1", Value: string(detailValues)})
procInstData := TopAPICreateProcInst{
Approvers: []string{"085354234826136236"},
CCList: []string{"085354234826136236"},
CCPosition: "START",
DeptID: 4207088,
OriginatorUID: "085354234826136236",
ProcessCode: "PROC-FF6YHQ9WQ2-RWDT8XCUTV0U5IAT7JBM1-8MD0TNEJ-6",
FormCompntValues: compntValues,
}
procInstID, err := c.TopAPICreateProcInst(procInstData)
if err != nil {
t.Log(err)
} else {
t.Logf("%+v\n", procInstID)
}
procInst, err := c.TopAPIGetProcInst(procInstID)
if err != nil {
t.Log(err)
} else {
t.Logf("%+v\n", procInst)
}
listResp, err := c.TopAPIListProcInst("PROC-FF6YHQ9WQ2-RWDT8XCUTV0U5IAT7JBM1-8MD0TNEJ-6", time.Now().AddDate(0, 0, -10), time.Now(), 10, 0, nil)
if err != nil {
t.Log(err)
} else {
t.Logf("%+v\n", listResp)
}
}

124
top_api_message.go Normal file
View File

@ -0,0 +1,124 @@
/*
* Author Kevin Zhu
*
* Direct questions, comments to <ipandtcp@gmail.com>
*/
package godingtalk
import (
"encoding/json"
"errors"
"net/url"
"strconv"
"strings"
)
const (
topAPIMsgAsyncSendMethod = "dingtalk.corp.message.corpconversation.asyncsend"
topAPIMsgGetResultMethod = "dingtalk.corp.message.corpconversation.getsendresult"
topAPIMsgGetprogressMethod = "dingtalk.corp.message.corpconversation.getsendprogress"
)
type topAPIMsgSendResponse struct {
topAPIErrResponse
OK struct {
ErrCode int `json:"ding_open_errcode"`
ErrMsg string `json:"error_msg"`
Success bool `json:"success"`
TaskID int `json:"task_id"`
} `json:"result"`
}
// mgType 消息类型text;iamge;voice;file;link;oa;markdown;action_card
// userList 接收推送的UID 列表
// deptList 接收推送的部门ID列表
// toAll 是否发送给所有用户
// msgContent 消息内容
// If success return task_id, or is error is not nil when errored
func (c *DingTalkClient) TopAPIMsgSend(msgType string, userList []string, deptList []int, toAll bool, msgContent interface{}) (int, error) {
var resp topAPIMsgSendResponse
if len(userList) > 20 || len(deptList) > 20 {
return 0, errors.New("Can't more than 20 users or departments at once")
}
mcontent, err := json.Marshal(msgContent)
if err != nil {
return 0, err
}
toAllStr := "false"
if toAll {
toAllStr = "true"
}
form := url.Values{
"method": {topAPIMsgAsyncSendMethod},
"agent_id": {c.AgentID},
"userid_list": {strings.Join(userList, ",")},
"to_all_user": {toAllStr},
"msgtype": {msgType},
"msgcontent": {string(mcontent)},
}
if len(deptList) > 0 {
var deptListStr string
for _, dept := range deptList {
deptListStr = strconv.Itoa(dept) + ","
}
deptListStr = string([]uint8(deptListStr)[0 : len(deptListStr)-1])
form.Set("dept_id_list", deptListStr)
}
return resp.OK.TaskID, c.topAPIRequest(form, &resp)
}
type TopAPIMsgGetSendResult struct {
topAPIErrResponse
OK struct {
ErrCode int `json:"ding_open_errcode"`
ErrMsg string `json:"error_msg"`
Success bool `json:"success"`
SendResult struct {
InvalidUserIDList []string `json:"invalid_user_id_list"`
ForbiddenUserIDList []string `json:"forbidden_user_id_list"`
FaildedUserIDList []string `json:"failed_user_id_list"`
ReadUserIDLIst []string `json:"read_user_id_list"`
UnreadUserIDList []string `json:"unread_user_id_list"`
InvalidDeptIDList []int `json:"invalid_dept_id_list"`
} `json:"send_result"`
} `json:"result"`
}
func (c *DingTalkClient) TopAPIMsgGetSendResult(taskID int) (TopAPIMsgGetSendResult, error) {
var resp TopAPIMsgGetSendResult
form := url.Values{
"method": {topAPIMsgGetResultMethod},
"agent_id": {c.AgentID},
"task_id": {strconv.Itoa(taskID)},
}
return resp, c.topAPIRequest(form, &resp)
}
type TopAPIMsgGetSendProgress struct {
topAPIErrResponse
OK struct {
ErrCode int `json:"ding_open_errcode"`
ErrMsg string `json:"error_msg"`
Success bool `json:"success"`
Progress struct {
Percent int `json:"progress_in_percent"`
Status int `json:"status"`
} `json:"progress"`
} `json:"result"`
}
func (c *DingTalkClient) TopAPIMsgGetSendProgress(taskID int) (TopAPIMsgGetSendProgress, error) {
var resp TopAPIMsgGetSendProgress
form := url.Values{
"method": {topAPIMsgGetprogressMethod},
"agent_id": {c.AgentID},
"task_id": {strconv.Itoa(taskID)},
}
return resp, c.topAPIRequest(form, &resp)
}

35
top_api_message_test.go Normal file
View File

@ -0,0 +1,35 @@
package godingtalk
import (
"testing"
)
func TestTopAPImsg(t *testing.T) {
c.AgentID = "161271936"
msg := OAMessage{}
msg.URL = "http://www.google.com/"
msg.Head.Text = "头部标题"
msg.Head.BgColor = "FFBBBBBB"
msg.Body.Title = "正文标题"
msg.Body.Content = "test content"
taskID, err := c.TopAPIMsgSend("oa", []string{"085354234826136236"}, nil, false, msg)
if err != nil {
t.Error(err)
} else {
t.Logf("%d\n", taskID)
}
sendProgress, err := c.TopAPIMsgGetSendProgress(taskID)
if err != nil {
t.Error(err)
} else {
t.Logf("%v\n", sendProgress.OK.Progress)
}
sendResult, err := c.TopAPIMsgGetSendResult(taskID)
if err != nil {
t.Error(err)
} else {
t.Logf("%v\n", sendResult.OK.SendResult)
}
}

88
top_api_request.go Normal file
View File

@ -0,0 +1,88 @@
/*
* Author Kevin Zhu
*
* Direct questions, comments to <ipandtcp@gmail.com>
*/
package godingtalk
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
)
const (
topAPIRootURL = "https://eco.taobao.com/router/rest"
formDataType = "application/x-www-form-urlencoded;charset=utf-8"
)
type TopAPIResponse interface {
checkError() error
}
type topAPIErrResponse struct {
ERR struct {
Code int `json:"code"`
Msg string `json:"msg"`
SubCode string `json:"sub_code"`
SubMsg string `json:"sub_msg"`
RequestID string `json:"request_id"`
} `json:"error_response"`
}
func (data *topAPIErrResponse) checkError() (err error) {
if data.ERR.Code != 0 || len(data.ERR.SubCode) != 0 {
err = fmt.Errorf("%#v", data.ERR)
}
return err
}
func (c *DingTalkClient) topAPIRequest(requestForm url.Values, respData TopAPIResponse) error {
requestForm.Set("v", "2.0")
requestForm.Set("format", "json")
requestForm.Set("simplify", "true")
err := c.RefreshAccessToken()
if err != nil {
return err
}
requestForm.Set("session", c.AccessToken)
if requestForm.Get("timestamp") == "" {
requestForm.Set("timestamp", time.Now().Format("2006-01-02 15:04:05"))
}
if c.PartnerID != "" {
requestForm.Set("partner_id", c.PartnerID)
}
v := bytes.NewBuffer([]byte(requestForm.Encode()))
req, _ := http.NewRequest("POST", topAPIRootURL, v)
req.Header.Set("Content-Type", formDataType)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
if resp.StatusCode != 200 {
return errors.New("Server error: " + resp.Status)
}
defer resp.Body.Close()
buf, err := ioutil.ReadAll(resp.Body)
if err == nil {
err := json.Unmarshal(buf, &respData)
if err != nil {
return err
}
return respData.checkError()
}
return err
}

119
transport.go Normal file
View File

@ -0,0 +1,119 @@
package godingtalk
import (
"bytes"
"encoding/json"
"errors"
"io"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"net/url"
"os"
)
const typeJSON = "application/json"
//UploadFile is for uploading a single file to DingTalk
type UploadFile struct {
FieldName string
FileName string
Reader io.Reader
}
//DownloadFile is for downloading a single file from DingTalk
type DownloadFile struct {
MediaID string
FileName string
Reader io.Reader
}
func (c *DingTalkClient) httpRPC(path string, params url.Values, requestData interface{}, responseData Unmarshallable) error {
if c.AccessToken != "" {
if params == nil {
params = url.Values{}
}
if params.Get("access_token") == "" {
params.Set("access_token", c.AccessToken)
}
}
return c.httpRequest(path, params, requestData, responseData)
}
func (c *DingTalkClient) httpRequest(path string, params url.Values, requestData interface{}, responseData Unmarshallable) error {
client := c.HTTPClient
var request *http.Request
ROOT := os.Getenv("oapi_server")
if ROOT == "" {
ROOT = "oapi.dingtalk.com"
}
DEBUG := os.Getenv("debug") != ""
url2 := "https://" + ROOT + "/" + path + "?" + params.Encode()
// log.Println(url2)
if requestData != nil {
switch requestData.(type) {
case UploadFile:
var b bytes.Buffer
w := multipart.NewWriter(&b)
uploadFile := requestData.(UploadFile)
if uploadFile.Reader == nil {
return errors.New("upload file is empty")
}
fw, err := w.CreateFormFile(uploadFile.FieldName, uploadFile.FileName)
if err != nil {
return err
}
if _, err = io.Copy(fw, uploadFile.Reader); err != nil {
return err
}
if err = w.Close(); err != nil {
return err
}
request, _ = http.NewRequest("POST", url2, &b)
request.Header.Set("Content-Type", w.FormDataContentType())
default:
d, _ := json.Marshal(requestData)
if DEBUG {
log.Printf("url: %s request: %s", url2, string(d))
}
request, _ = http.NewRequest("POST", url2, bytes.NewReader(d))
request.Header.Set("Content-Type", typeJSON)
}
} else {
if DEBUG {
log.Printf("url: %s", url2)
}
request, _ = http.NewRequest("GET", url2, nil)
}
resp, err := client.Do(request)
if err != nil {
return err
}
if resp.StatusCode != 200 {
return errors.New("Server error: " + resp.Status)
}
defer resp.Body.Close()
contentType := resp.Header.Get("Content-Type")
if DEBUG {
log.Printf("url: %s response content type: %s", url2, contentType)
}
pos := len(typeJSON)
if len(contentType) >= pos && contentType[0:pos] == typeJSON {
content, err := ioutil.ReadAll(resp.Body)
if DEBUG {
log.Println(string(content))
}
if err == nil {
json.Unmarshal(content, responseData)
return responseData.checkError()
}
} else {
io.Copy(responseData.getWriter(), resp.Body)
return responseData.checkError()
}
return err
}

102
util.go Normal file
View File

@ -0,0 +1,102 @@
package godingtalk
import (
"crypto/sha1"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"time"
)
type Expirable interface {
CreatedAt() int64
ExpiresIn() int
}
type Cache interface {
Set(data Expirable) error
Get(data Expirable) error
}
type FileCache struct {
Path string
}
func NewFileCache(path string) *FileCache {
return &FileCache{
Path: path,
}
}
func (c *FileCache) Set(data Expirable) error {
bytes, err := json.Marshal(data)
if err == nil {
ioutil.WriteFile(c.Path, bytes, 0644)
}
return err
}
func (c *FileCache) Get(data Expirable) error {
bytes, err := ioutil.ReadFile(c.Path)
if err == nil {
err = json.Unmarshal(bytes, data)
if err == nil {
created := data.CreatedAt()
expires := data.ExpiresIn()
if err == nil && time.Now().Unix() > created+int64(expires-60) {
err = errors.New("Data is already expired")
}
}
}
return err
}
type InMemoryCache struct {
data []byte
}
func NewInMemoryCache() *InMemoryCache {
return &InMemoryCache{}
}
func (c *InMemoryCache) Set(data Expirable) error {
bytes, err := json.Marshal(data)
if err == nil {
c.data = bytes
}
return err
}
func (c *InMemoryCache) Get(data Expirable) error {
err := json.Unmarshal(c.data, data)
if err == nil {
created := data.CreatedAt()
expires := data.ExpiresIn()
if err == nil && time.Now().Unix() > created+int64(expires-60) {
err = errors.New("Data is already expired")
}
}
return err
}
func sha1Sign(s string) string {
// The pattern for generating a hash is `sha1.New()`,
// `sha1.Write(bytes)`, then `sha1.Sum([]byte{})`.
// Here we start with a new hash.
h := sha1.New()
// `Write` expects bytes. If you have a string `s`,
// use `[]byte(s)` to coerce it to bytes.
h.Write([]byte(s))
// This gets the finalized hash result as a byte
// slice. The argument to `Sum` can be used to append
// to an existing byte slice: it usually isn't needed.
bs := h.Sum(nil)
// SHA1 values are often printed in hex, for example
// in git commits. Use the `%x` format verb to convert
// a hash results to a hex string.
return fmt.Sprintf("%x", bs)
}

78
util_test.go Normal file
View File

@ -0,0 +1,78 @@
package godingtalk
import "testing"
import "time"
type ExpiresData struct {
Data string
Expires int `json:"expires_in"`
Created int64 `json:"created"`
}
func (e *ExpiresData) CreatedAt() int64 {
return e.Created
}
func (e *ExpiresData) ExpiresIn() int {
return e.Expires
}
func TestFileCache(t *testing.T) {
cache := NewFileCache(".test_cache")
data := ExpiresData{
Data: "Hello World!",
Expires: 7200,
Created: time.Now().Unix(),
}
cache.Set(&data)
var data2 ExpiresData
cache.Get(&data2)
t.Logf("%+v %+v", data, data2)
if data2.Created != data.Created {
t.Errorf("FileCache error")
}
data = ExpiresData{
Data: "Hello World 2!",
Expires: 0,
Created: time.Now().Unix(),
}
cache.Set(&data)
err := cache.Get(&data2)
if err == nil {
t.Error("FileCache error: err should not be nil")
}
t.Logf("%+v %+v", data, data2)
}
func TestInMemoryCache(t *testing.T) {
cache := NewInMemoryCache()
data := ExpiresData{
Data: "Hello World!",
Expires: 7200,
Created: time.Now().Unix(),
}
cache.Set(&data)
var data2 ExpiresData
cache.Get(&data2)
t.Logf("%+v %+v", data, data2)
if data2.Created != data.Created {
t.Errorf("InMemoryCache error")
}
data = ExpiresData{
Data: "Hello World 2!",
Expires: 0,
Created: time.Now().Unix(),
}
cache.Set(&data)
err := cache.Get(&data2)
if err == nil {
t.Error("InMemoryCache error: err should not be nil")
}
t.Logf("%+v %+v", data, data2)
}