sns及refreshaccesstoken测试通过
This commit is contained in:
commit
a519daf9f1
|
|
@ -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
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
# DingTalk Open API golang SDK
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package godingtalk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateFile(t *testing.T) {
|
||||||
|
file, err := c.CreateFile(1024)
|
||||||
|
t.Log(file, err)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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 数据签名需要用到的token,ISV(服务提供商)推荐使用注册套件时填写的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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -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×tamp=%s&url=%s", ticket, nonceStr, timeStamp, url)
|
||||||
|
return sha1Sign(s)
|
||||||
|
}
|
||||||
|
|
@ -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"+
|
||||||
|
"> \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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue