client.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. package dingbot
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "mime/multipart"
  9. "net/http"
  10. url2 "net/url"
  11. "sync"
  12. "time"
  13. "github.com/eryajf/chatgpt-dingtalk/config"
  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. if err != nil {
  126. return nil, err
  127. }
  128. if err = writer.WriteField("type", mediaType); err != nil {
  129. return nil, err
  130. }
  131. err = writer.Close()
  132. if err != nil {
  133. return nil, err
  134. }
  135. // Create a new HTTP request to upload the media file
  136. url := fmt.Sprintf("https://oapi.dingtalk.com/media/upload?access_token=%s", url2.QueryEscape(accessToken))
  137. req, err := http.NewRequest("POST", url, body)
  138. if err != nil {
  139. return nil, err
  140. }
  141. req.Header.Set("Content-Type", writer.FormDataContentType())
  142. // Send the HTTP request and parse the response
  143. client := &http.Client{
  144. Timeout: time.Second * 60,
  145. }
  146. res, err := client.Do(req)
  147. if err != nil {
  148. return nil, err
  149. }
  150. defer res.Body.Close()
  151. // Parse the response body as JSON and extract the media ID
  152. media := &MediaUploadResult{}
  153. bodyBytes, err := io.ReadAll(res.Body)
  154. if err != nil {
  155. return nil, err
  156. }
  157. if err = json.Unmarshal(bodyBytes, media); err != nil {
  158. return nil, err
  159. }
  160. if media.ErrorCode != 0 {
  161. return nil, errors.New(media.ErrorMessage)
  162. }
  163. return media, nil
  164. }
  165. func (c *DingTalkClient) getAccessTokenFromDingTalk() (*OAuthTokenResult, error) {
  166. // OpenAPI doc: https://open.dingtalk.com/document/orgapp/obtain-orgapp-token
  167. apiUrl := "https://oapi.dingtalk.com/gettoken"
  168. queryParams := url2.Values{}
  169. queryParams.Add("appkey", c.Credential.ClientID)
  170. queryParams.Add("appsecret", c.Credential.ClientSecret)
  171. // Create a new HTTP request to get the AccessToken
  172. req, err := http.NewRequest("GET", apiUrl+"?"+queryParams.Encode(), nil)
  173. if err != nil {
  174. return nil, err
  175. }
  176. // Send the HTTP request and parse the response body as JSON
  177. client := http.Client{
  178. Timeout: time.Second * 60,
  179. }
  180. res, err := client.Do(req)
  181. if err != nil {
  182. return nil, err
  183. }
  184. defer res.Body.Close()
  185. body, err := io.ReadAll(res.Body)
  186. if err != nil {
  187. return nil, err
  188. }
  189. tokenResult := &OAuthTokenResult{}
  190. err = json.Unmarshal(body, tokenResult)
  191. if err != nil {
  192. return nil, err
  193. }
  194. if tokenResult.ErrorCode != 0 {
  195. return nil, errors.New(tokenResult.ErrorMessage)
  196. }
  197. return tokenResult, nil
  198. }