352 lines
10 KiB
Go
352 lines
10 KiB
Go
package dingtalk
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
|
dingtalkcard_1_0 "github.com/alibabacloud-go/dingtalk/card_1_0"
|
|
dingtalkoauth2_1_0 "github.com/alibabacloud-go/dingtalk/v2/oauth2_1_0"
|
|
"github.com/alibabacloud-go/tea/tea"
|
|
|
|
"github.com/chaitin/panda-wiki/log"
|
|
"github.com/chaitin/panda-wiki/store/cache"
|
|
)
|
|
|
|
const (
|
|
callbackPath = "/share/pro/v1/openapi/dingtalk/callback"
|
|
userInfoUrl = "https://api.dingtalk.com/v1.0/contact/users/me"
|
|
DepartmentListUrl = "https://oapi.dingtalk.com/department/list"
|
|
// https://open.dingtalk.com/document/isvapp/queries-the-complete-information-of-a-department-user
|
|
UserListUrl = "https://oapi.dingtalk.com/topapi/v2/user/list"
|
|
)
|
|
|
|
type Client struct {
|
|
ctx context.Context
|
|
logger *log.Logger
|
|
httpClient *http.Client
|
|
clientID string
|
|
clientSecret string
|
|
oauthClient *dingtalkoauth2_1_0.Client
|
|
cardClient *dingtalkcard_1_0.Client
|
|
dingTalkAuthURL string
|
|
cache *cache.Cache
|
|
}
|
|
|
|
// UserInfo 用于解析获取用户信息的接口返回
|
|
type UserInfo struct {
|
|
Nick string `json:"nick"`
|
|
UnionID string `json:"unionId"`
|
|
OpenID string `json:"openId"`
|
|
AvatarURL string `json:"avatarUrl"`
|
|
StateCode string `json:"stateCode"`
|
|
}
|
|
|
|
// DepartmentListRsp 用于解析组织信息接口返回
|
|
type DepartmentListRsp struct {
|
|
Errcode int `json:"errcode"`
|
|
Department []struct {
|
|
CreateDeptGroup bool `json:"createDeptGroup"`
|
|
Name string `json:"name"`
|
|
Id int `json:"id"`
|
|
AutoAddUser bool `json:"autoAddUser"`
|
|
Parentid int `json:"parentid,omitempty"`
|
|
} `json:"department"`
|
|
Errmsg string `json:"errmsg"`
|
|
}
|
|
|
|
type GetUserListResp struct {
|
|
Errcode int `json:"errcode"`
|
|
Result struct {
|
|
HasMore bool `json:"has_more"`
|
|
List []UserDetail `json:"list"`
|
|
} `json:"result"`
|
|
Errmsg string `json:"errmsg"`
|
|
}
|
|
|
|
type UserDetail struct {
|
|
Active bool `json:"active"`
|
|
Admin bool `json:"admin"`
|
|
Avatar string `json:"avatar"`
|
|
Boss bool `json:"boss"`
|
|
DeptIdList []int `json:"dept_id_list"`
|
|
DeptOrder int64 `json:"dept_order"`
|
|
Email string `json:"email"`
|
|
ExclusiveAccount bool `json:"exclusive_account"`
|
|
HideMobile bool `json:"hide_mobile"`
|
|
JobNumber string `json:"job_number"`
|
|
Leader bool `json:"leader"`
|
|
Mobile string `json:"mobile"`
|
|
Name string `json:"name"`
|
|
Remark string `json:"remark"`
|
|
StateCode string `json:"state_code"`
|
|
Telephone string `json:"telephone"`
|
|
Title string `json:"title"`
|
|
Unionid string `json:"unionid"`
|
|
Userid string `json:"userid"`
|
|
WorkPlace string `json:"work_place"`
|
|
}
|
|
|
|
func NewDingTalkClient(ctx context.Context, logger *log.Logger, clientId, clientSecret string, cache *cache.Cache) (*Client, error) {
|
|
config := &openapi.Config{}
|
|
config.Protocol = tea.String("https")
|
|
config.RegionId = tea.String("central")
|
|
oauthClient, err := dingtalkoauth2_1_0.NewClient(config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create oauth client: %w", err)
|
|
}
|
|
cardClient, err := dingtalkcard_1_0.NewClient(config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create card client: %w", err)
|
|
}
|
|
return &Client{
|
|
ctx: ctx,
|
|
logger: logger.WithModule("pkg.dingtalk"),
|
|
httpClient: &http.Client{},
|
|
clientID: clientId,
|
|
clientSecret: clientSecret,
|
|
oauthClient: oauthClient,
|
|
cardClient: cardClient,
|
|
dingTalkAuthURL: "https://login.dingtalk.com/oauth2/auth",
|
|
cache: cache,
|
|
}, nil
|
|
}
|
|
|
|
// GenerateAuthURL 生成钉钉授权URL
|
|
func (c *Client) GenerateAuthURL(baseUrl string, state string) string {
|
|
redirectURI, err := url.JoinPath(baseUrl, callbackPath)
|
|
if err != nil {
|
|
c.logger.Error("failed to join path", log.Error(err))
|
|
return ""
|
|
}
|
|
|
|
params := url.Values{}
|
|
params.Add("response_type", "code")
|
|
params.Add("client_id", c.clientID)
|
|
params.Add("redirect_uri", redirectURI)
|
|
params.Add("scope", "openid")
|
|
params.Add("state", state)
|
|
params.Add("prompt", "consent")
|
|
|
|
return fmt.Sprintf("%s?%s", c.dingTalkAuthURL, params.Encode())
|
|
}
|
|
|
|
func (c *Client) GetAccessTokenByCode(code string) (string, error) {
|
|
request := &dingtalkoauth2_1_0.GetUserTokenRequest{
|
|
ClientId: tea.String(c.clientID),
|
|
ClientSecret: tea.String(c.clientSecret),
|
|
Code: tea.String(code),
|
|
GrantType: tea.String("authorization_code"),
|
|
}
|
|
response, err := c.oauthClient.GetUserToken(request)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get user access token: %w", err)
|
|
}
|
|
accessToken := tea.StringValue(response.Body.AccessToken)
|
|
return accessToken, nil
|
|
}
|
|
|
|
func (c *Client) GetAccessToken() (string, error) {
|
|
ctx := context.Background()
|
|
cacheKey := fmt.Sprintf("dingtalk-access-token:%s", c.clientID)
|
|
cachedData, err := c.cache.Get(ctx, cacheKey).Result()
|
|
if err == nil && cachedData != "" {
|
|
return cachedData, nil
|
|
}
|
|
|
|
request := &dingtalkoauth2_1_0.GetAccessTokenRequest{
|
|
AppKey: tea.String(c.clientID),
|
|
AppSecret: tea.String(c.clientSecret),
|
|
}
|
|
response, tryErr := func() (_resp *dingtalkoauth2_1_0.GetAccessTokenResponse, _e error) {
|
|
defer func() {
|
|
if r := tea.Recover(recover()); r != nil {
|
|
_e = r
|
|
}
|
|
}()
|
|
_resp, _err := c.oauthClient.GetAccessToken(request)
|
|
if _err != nil {
|
|
return nil, _err
|
|
}
|
|
|
|
return _resp, nil
|
|
}()
|
|
if tryErr != nil {
|
|
return "", tryErr
|
|
}
|
|
accessToken := *response.Body.AccessToken
|
|
c.logger.Debug("get access token", log.String("access_token", accessToken), log.Int("expire_in", int(*response.Body.ExpireIn)))
|
|
|
|
if err := c.cache.Set(ctx, cacheKey, accessToken, time.Duration(*response.Body.ExpireIn-300)*time.Second).Err(); err != nil {
|
|
c.logger.Warn("failed to set cache", log.Error(err))
|
|
}
|
|
|
|
return accessToken, nil
|
|
}
|
|
|
|
func (c *Client) GetUserInfoByCode(code string) (*UserInfo, error) {
|
|
req, err := http.NewRequest("GET", userInfoUrl, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create GET request: %w", err)
|
|
}
|
|
|
|
accessToken, err := c.GetAccessTokenByCode(code)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set request headers
|
|
req.Header.Set("x-acs-dingtalk-access-token", accessToken)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to send GET request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("DingTalk API returned non-200 status: %s, response: %s", resp.Status, string(body))
|
|
}
|
|
|
|
var userInfo UserInfo
|
|
if err := json.Unmarshal(body, &userInfo); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal JSON response: %w", err)
|
|
}
|
|
|
|
return &userInfo, nil
|
|
}
|
|
|
|
func (c *Client) GetDepartmentList() (*DepartmentListRsp, error) {
|
|
accessToken, err := c.GetAccessToken()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
params := url.Values{}
|
|
params.Add("access_token", accessToken)
|
|
requestURL := fmt.Sprintf("%s?%s", DepartmentListUrl, params.Encode())
|
|
|
|
req, err := http.NewRequest(http.MethodGet, requestURL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("DingTalk API returned non-200 status: %s, response: %s", resp.Status, string(body))
|
|
}
|
|
|
|
c.logger.Debug("DepartmentListUrl:", log.String("body", string(body)))
|
|
var departmentListRsp DepartmentListRsp
|
|
if err := json.Unmarshal(body, &departmentListRsp); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal JSON response: %w", err)
|
|
}
|
|
|
|
if departmentListRsp.Errcode != 0 {
|
|
return nil, fmt.Errorf("DingTalk API error: errcode=%d errmsg=%s", departmentListRsp.Errcode, departmentListRsp.Errmsg)
|
|
}
|
|
|
|
return &departmentListRsp, nil
|
|
}
|
|
|
|
func (c *Client) GetAllUserList(deptID int) ([]UserDetail, error) {
|
|
depth := 0
|
|
const maxDepth = 10
|
|
|
|
userList := make([]UserDetail, 0)
|
|
for depth < maxDepth {
|
|
resp, err := c.GetUserList(deptID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(resp.Result.List) > 0 {
|
|
userList = append(userList, resp.Result.List...)
|
|
}
|
|
if !resp.Result.HasMore {
|
|
break
|
|
}
|
|
depth++
|
|
}
|
|
return userList, nil
|
|
}
|
|
|
|
func (c *Client) GetUserList(deptID int) (*GetUserListResp, error) {
|
|
accessToken, err := c.GetAccessToken()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
params := url.Values{}
|
|
params.Add("access_token", accessToken)
|
|
requestURL := fmt.Sprintf("%s?%s", UserListUrl, params.Encode())
|
|
|
|
bodyMap := map[string]interface{}{
|
|
"dept_id": deptID,
|
|
"size": 100,
|
|
"cursor": 0,
|
|
}
|
|
|
|
jsonData, err := json.Marshal(bodyMap)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("DingTalk API returned non-200 status: %s, response: %s", resp.Status, string(body))
|
|
}
|
|
|
|
c.logger.Debug("GetUserList:", log.String("body", string(body)))
|
|
var getUserListResp GetUserListResp
|
|
if err := json.Unmarshal(body, &getUserListResp); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal JSON response: %w", err)
|
|
}
|
|
|
|
if getUserListResp.Errcode != 0 {
|
|
return nil, fmt.Errorf("DingTalk GetUserList error: errcode=%d errcode=%s", getUserListResp.Errcode, getUserListResp.Errmsg)
|
|
}
|
|
|
|
return &getUserListResp, nil
|
|
}
|