client.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. package dingbot
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "github.com/eryajf/chatgpt-dingtalk/config"
  8. "io"
  9. "mime/multipart"
  10. "net/http"
  11. url2 "net/url"
  12. "sync"
  13. "time"
  14. )
  15. // OpenAPI doc: https://open.dingtalk.com/document/isvapp/upload-media-files
  16. const (
  17. MediaTypeImage string = "image"
  18. MediaTypeVoice string = "voice"
  19. MediaTypeVideo string = "video"
  20. MediaTypeFile string = "file"
  21. )
  22. const (
  23. MimeTypeImagePng string = "image/png"
  24. )
  25. type MediaUploadResult struct {
  26. ErrorCode int64 `json:"errcode"`
  27. ErrorMessage string `json:"errmsg"`
  28. MediaID string `json:"media_id"`
  29. CreatedAt int64 `json:"created_at"`
  30. Type string `json:"type"`
  31. }
  32. type OAuthTokenResult struct {
  33. ErrorCode int `json:"errcode"`
  34. ErrorMessage string `json:"errmsg"`
  35. AccessToken string `json:"access_token"`
  36. ExpiresIn int `json:"expires_in"`
  37. }
  38. type DingTalkClientInterface interface {
  39. GetAccessToken() (string, error)
  40. UploadMedia(content []byte, filename, mediaType, mimeType string) (*MediaUploadResult, error)
  41. }
  42. type DingTalkClientManagerInterface interface {
  43. GetClientByOAuthClientID(clientId string) DingTalkClientInterface
  44. }
  45. type DingTalkClient struct {
  46. Credential config.Credential
  47. AccessToken string
  48. expireAt int64
  49. mutex sync.Mutex
  50. }
  51. type DingTalkClientManager struct {
  52. Credentials []config.Credential
  53. Clients map[string]*DingTalkClient
  54. mutex sync.Mutex
  55. }
  56. func NewDingTalkClient(credential config.Credential) *DingTalkClient {
  57. return &DingTalkClient{
  58. Credential: credential,
  59. }
  60. }
  61. func NewDingTalkClientManager(conf *config.Configuration) *DingTalkClientManager {
  62. clients := make(map[string]*DingTalkClient)
  63. if conf != nil && conf.Credentials != nil {
  64. for _, credential := range conf.Credentials {
  65. clients[credential.ClientID] = NewDingTalkClient(credential)
  66. }
  67. }
  68. return &DingTalkClientManager{
  69. Credentials: conf.Credentials,
  70. Clients: clients,
  71. }
  72. }
  73. func (m *DingTalkClientManager) GetClientByOAuthClientID(clientId string) DingTalkClientInterface {
  74. m.mutex.Lock()
  75. defer m.mutex.Unlock()
  76. if client, ok := m.Clients[clientId]; ok {
  77. return client
  78. }
  79. return nil
  80. }
  81. func (c *DingTalkClient) GetAccessToken() (string, error) {
  82. accessToken := ""
  83. {
  84. // 先查询缓存
  85. c.mutex.Lock()
  86. now := time.Now().Unix()
  87. if c.expireAt > 0 && c.AccessToken != "" && (now+60) < c.expireAt {
  88. // 预留一分钟有效期避免在Token过期的临界点调用接口出现401错误
  89. accessToken = c.AccessToken
  90. }
  91. c.mutex.Unlock()
  92. }
  93. if accessToken != "" {
  94. return accessToken, nil
  95. }
  96. tokenResult, err := c.getAccessTokenFromDingTalk()
  97. if err != nil {
  98. return "", err
  99. }
  100. {
  101. // 更新缓存
  102. c.mutex.Lock()
  103. c.AccessToken = tokenResult.AccessToken
  104. c.expireAt = time.Now().Unix() + int64(tokenResult.ExpiresIn)
  105. c.mutex.Unlock()
  106. }
  107. return tokenResult.AccessToken, nil
  108. }
  109. func (c *DingTalkClient) UploadMedia(content []byte, filename, mediaType, mimeType string) (*MediaUploadResult, error) {
  110. // OpenAPI doc: https://open.dingtalk.com/document/isvapp/upload-media-files
  111. accessToken, err := c.GetAccessToken()
  112. if err != nil {
  113. return nil, err
  114. }
  115. if len(accessToken) == 0 {
  116. return nil, errors.New("empty access token")
  117. }
  118. body := &bytes.Buffer{}
  119. writer := multipart.NewWriter(body)
  120. part, err := writer.CreateFormFile("media", filename)
  121. if err != nil {
  122. return nil, err
  123. }
  124. _, err = part.Write(content)
  125. writer.WriteField("type", mediaType)
  126. err = writer.Close()
  127. if err != nil {
  128. return nil, err
  129. }
  130. // Create a new HTTP request to upload the media file
  131. url := fmt.Sprintf("https://oapi.dingtalk.com/media/upload?access_token=%s", url2.QueryEscape(accessToken))
  132. req, err := http.NewRequest("POST", url, body)
  133. if err != nil {
  134. return nil, err
  135. }
  136. req.Header.Set("Content-Type", writer.FormDataContentType())
  137. // Send the HTTP request and parse the response
  138. client := &http.Client{
  139. Timeout: time.Second * 60,
  140. }
  141. res, err := client.Do(req)
  142. if err != nil {
  143. return nil, err
  144. }
  145. defer res.Body.Close()
  146. // Parse the response body as JSON and extract the media ID
  147. media := &MediaUploadResult{}
  148. bodyBytes, err := io.ReadAll(res.Body)
  149. json.Unmarshal(bodyBytes, media)
  150. if err != nil {
  151. return nil, err
  152. }
  153. if media.ErrorCode != 0 {
  154. return nil, errors.New(media.ErrorMessage)
  155. }
  156. return media, nil
  157. }
  158. func (c *DingTalkClient) getAccessTokenFromDingTalk() (*OAuthTokenResult, error) {
  159. // OpenAPI doc: https://open.dingtalk.com/document/orgapp/obtain-orgapp-token
  160. apiUrl := "https://oapi.dingtalk.com/gettoken"
  161. queryParams := url2.Values{}
  162. queryParams.Add("appkey", c.Credential.ClientID)
  163. queryParams.Add("appsecret", c.Credential.ClientSecret)
  164. // Create a new HTTP request to get the AccessToken
  165. req, err := http.NewRequest("GET", apiUrl+"?"+queryParams.Encode(), nil)
  166. if err != nil {
  167. return nil, err
  168. }
  169. // Send the HTTP request and parse the response body as JSON
  170. client := http.Client{
  171. Timeout: time.Second * 60,
  172. }
  173. res, err := client.Do(req)
  174. if err != nil {
  175. return nil, err
  176. }
  177. defer res.Body.Close()
  178. body, err := io.ReadAll(res.Body)
  179. if err != nil {
  180. return nil, err
  181. }
  182. tokenResult := &OAuthTokenResult{}
  183. err = json.Unmarshal(body, tokenResult)
  184. if err != nil {
  185. return nil, err
  186. }
  187. if tokenResult.ErrorCode != 0 {
  188. return nil, errors.New(tokenResult.ErrorMessage)
  189. }
  190. return tokenResult, nil
  191. }