dingtalk.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. package dingtalk
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "github.com/mindoc-org/mindoc/utils/auth2"
  9. "net/http"
  10. "net/url"
  11. "time"
  12. )
  13. const (
  14. AppName = "dingtalk"
  15. callbackState = "mindoc"
  16. )
  17. type BasicResponse struct {
  18. Message string `json:"errmsg"`
  19. Code int `json:"errcode"`
  20. }
  21. func (r *BasicResponse) Error() string {
  22. return fmt.Sprintf("errcode=%d, errmsg=%s", r.Code, r.Message)
  23. }
  24. func (r *BasicResponse) AsError() error {
  25. if r == nil {
  26. return nil
  27. }
  28. if r.Code != 0 || r.Message != "ok" {
  29. return r
  30. }
  31. return nil
  32. }
  33. type AccessToken struct {
  34. // 文档: https://open.dingtalk.com/document/orgapp/obtain-orgapp-token
  35. *BasicResponse
  36. AccessToken string `json:"access_token"`
  37. ExpireIn int `json:"expires_in"`
  38. createTime time.Time
  39. }
  40. func (a AccessToken) GetToken() string {
  41. return a.AccessToken
  42. }
  43. func (a AccessToken) GetExpireIn() time.Duration {
  44. return time.Duration(a.ExpireIn) * time.Second
  45. }
  46. func (a AccessToken) GetExpireTime() time.Time {
  47. return a.createTime.Add(a.GetExpireIn())
  48. }
  49. type UserAccessToken struct {
  50. // 文档: https://open.dingtalk.com/document/orgapp/obtain-user-token
  51. *BasicResponse // 此接口未返回错误代码信息,仅仅能检查HTTP状态码
  52. ExpireIn int `json:"expireIn"`
  53. AccessToken string `json:"accessToken"`
  54. RefreshToken string `json:"refreshToken"`
  55. CorpId string `json:"corpId"`
  56. }
  57. type UserInfo struct {
  58. // 文档: https://open.dingtalk.com/document/orgapp/dingtalk-retrieve-user-information
  59. *BasicResponse
  60. NickName string `json:"nick"`
  61. Avatar string `json:"avatarUrl"`
  62. Mobile string `json:"mobile"`
  63. OpenId string `json:"openId"`
  64. UnionId string `json:"unionId"`
  65. Email string `json:"email"`
  66. StateCode string `json:"stateCode"`
  67. }
  68. type UserIdByUnion struct {
  69. // 文档: https://open.dingtalk.com/document/isvapp/query-a-user-by-the-union-id
  70. *BasicResponse
  71. RequestId string `json:"request_id"`
  72. Result struct {
  73. ContactType int `json:"contact_type"`
  74. UserId string `json:"userid"`
  75. } `json:"result"`
  76. }
  77. func NewClient(appSecret string, appKey string) auth2.Client {
  78. return NewDingtalkClient(appSecret, appKey)
  79. }
  80. func NewDingtalkClient(appSecret string, appKey string) *DingtalkClient {
  81. return &DingtalkClient{AppSecret: appSecret, AppKey: appKey}
  82. }
  83. type DingtalkClient struct {
  84. AppSecret string
  85. AppKey string
  86. token auth2.IAccessToken
  87. }
  88. func (d *DingtalkClient) GetAccessToken(ctx context.Context) (auth2.IAccessToken, error) {
  89. if d.token != nil {
  90. return d.token, nil
  91. }
  92. endpoint := fmt.Sprintf("https://oapi.dingtalk.com/gettoken?appkey=%s&appsecret=%s", d.AppKey, d.AppSecret)
  93. req, _ := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
  94. var token AccessToken
  95. if err := auth2.Request(req, &token); err != nil {
  96. return nil, err
  97. }
  98. token.createTime = time.Now()
  99. return token, nil
  100. }
  101. func (d *DingtalkClient) SetAccessToken(token auth2.IAccessToken) {
  102. d.token = token
  103. }
  104. func (d *DingtalkClient) BuildURL(callback string, _ bool) string {
  105. v := url.Values{}
  106. v.Set("redirect_uri", callback)
  107. v.Set("response_type", "code")
  108. v.Set("client_id", d.AppKey)
  109. v.Set("scope", "openid")
  110. v.Set("state", callbackState)
  111. v.Set("prompt", "consent")
  112. return "https://login.dingtalk.com/oauth2/auth?" + v.Encode()
  113. }
  114. func (d *DingtalkClient) ValidateCallback(state string) error {
  115. if state != callbackState {
  116. return errors.New("auth2.state.wrong")
  117. }
  118. return nil
  119. }
  120. func (d *DingtalkClient) getUserAccessToken(ctx context.Context, code string) (UserAccessToken, error) {
  121. val := map[string]string{
  122. "clientId": d.AppKey,
  123. "clientSecret": d.AppSecret,
  124. "code": code,
  125. "grantType": "authorization_code",
  126. }
  127. jv, _ := json.Marshal(val)
  128. endpoint := "https://api.dingtalk.com/v1.0/oauth2/userAccessToken"
  129. req, _ := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(jv))
  130. req.Header.Set("Content-Type", "application/json")
  131. var token UserAccessToken
  132. if err := auth2.Request(req, &token); err != nil {
  133. return token, err
  134. }
  135. return token, nil
  136. }
  137. func (d *DingtalkClient) getUserInfo(ctx context.Context, userToken UserAccessToken, unionId string) (UserInfo, error) {
  138. var user UserInfo
  139. endpoint := fmt.Sprintf("https://api.dingtalk.com/v1.0/contact/users/%s", unionId)
  140. req, _ := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
  141. req.Header.Set("x-acs-dingtalk-access-token", userToken.AccessToken)
  142. req.Header.Set("Content-Type", "application/json")
  143. if err := auth2.Request(req, &user); err != nil {
  144. return user, err
  145. }
  146. return user, nil
  147. }
  148. func (d *DingtalkClient) getUserIdByUnion(ctx context.Context, union string) (UserIdByUnion, error) {
  149. var userId UserIdByUnion
  150. token, err := d.GetAccessToken(ctx)
  151. if err != nil {
  152. return userId, err
  153. }
  154. endpoint := fmt.Sprintf("https://oapi.dingtalk.com/topapi/user/getbyunionid?access_token=%s", token.GetToken())
  155. b, _ := json.Marshal(map[string]string{
  156. "unionid": union,
  157. })
  158. req, _ := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(b))
  159. req.Header.Set("Content-Type", "application/json")
  160. if err := auth2.Request(req, &userId); err != nil {
  161. return userId, err
  162. }
  163. return userId, nil
  164. }
  165. func (d *DingtalkClient) GetUserInfo(ctx context.Context, code string) (auth2.UserInfo, error) {
  166. var info auth2.UserInfo
  167. userToken, err := d.getUserAccessToken(ctx, code)
  168. if err != nil {
  169. return info, err
  170. }
  171. userInfo, err := d.getUserInfo(ctx, userToken, "me")
  172. if err != nil {
  173. return info, err
  174. }
  175. userId, err := d.getUserIdByUnion(ctx, userInfo.UnionId)
  176. if err != nil {
  177. return info, err
  178. }
  179. if userId.Result.ContactType > 0 {
  180. return info, errors.New("auth2.user.outer")
  181. }
  182. info.UserId = userId.Result.UserId
  183. info.Mail = userInfo.Email
  184. info.Mobile = userInfo.Mobile
  185. info.Name = userInfo.NickName
  186. info.Avatar = userInfo.Avatar
  187. return info, nil
  188. }