|
@@ -0,0 +1,213 @@
|
|
|
+package dingbot
|
|
|
+
|
|
|
+import (
|
|
|
+ "bytes"
|
|
|
+ "encoding/json"
|
|
|
+ "errors"
|
|
|
+ "fmt"
|
|
|
+ "github.com/eryajf/chatgpt-dingtalk/config"
|
|
|
+ "io"
|
|
|
+ "mime/multipart"
|
|
|
+ "net/http"
|
|
|
+ url2 "net/url"
|
|
|
+ "sync"
|
|
|
+ "time"
|
|
|
+)
|
|
|
+
|
|
|
+// OpenAPI doc: https://open.dingtalk.com/document/isvapp/upload-media-files
|
|
|
+const (
|
|
|
+ MediaTypeImage string = "image"
|
|
|
+ MediaTypeVoice string = "voice"
|
|
|
+ MediaTypeVideo string = "video"
|
|
|
+ MediaTypeFile string = "file"
|
|
|
+)
|
|
|
+const (
|
|
|
+ MimeTypeImagePng string = "image/png"
|
|
|
+)
|
|
|
+
|
|
|
+type MediaUploadResult struct {
|
|
|
+ ErrorCode int64 `json:"errcode"`
|
|
|
+ ErrorMessage string `json:"errmsg"`
|
|
|
+ MediaID string `json:"media_id"`
|
|
|
+ CreatedAt int64 `json:"created_at"`
|
|
|
+ Type string `json:"type"`
|
|
|
+}
|
|
|
+
|
|
|
+type OAuthTokenResult struct {
|
|
|
+ ErrorCode int `json:"errcode"`
|
|
|
+ ErrorMessage string `json:"errmsg"`
|
|
|
+ AccessToken string `json:"access_token"`
|
|
|
+ ExpiresIn int `json:"expires_in"`
|
|
|
+}
|
|
|
+
|
|
|
+type DingTalkClientInterface interface {
|
|
|
+ GetAccessToken() (string, error)
|
|
|
+ UploadMedia(content []byte, filename, mediaType, mimeType string) (*MediaUploadResult, error)
|
|
|
+}
|
|
|
+
|
|
|
+type DingTalkClientManagerInterface interface {
|
|
|
+ GetClientByOAuthClientID(clientId string) DingTalkClientInterface
|
|
|
+}
|
|
|
+
|
|
|
+type DingTalkClient struct {
|
|
|
+ Credential config.Credential
|
|
|
+ AccessToken string
|
|
|
+ expireAt int64
|
|
|
+ mutex sync.Mutex
|
|
|
+}
|
|
|
+
|
|
|
+type DingTalkClientManager struct {
|
|
|
+ Credentials []config.Credential
|
|
|
+ Clients map[string]*DingTalkClient
|
|
|
+ mutex sync.Mutex
|
|
|
+}
|
|
|
+
|
|
|
+func NewDingTalkClient(credential config.Credential) *DingTalkClient {
|
|
|
+ return &DingTalkClient{
|
|
|
+ Credential: credential,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func NewDingTalkClientManager(conf *config.Configuration) *DingTalkClientManager {
|
|
|
+ clients := make(map[string]*DingTalkClient)
|
|
|
+
|
|
|
+ if conf != nil && conf.Credentials != nil {
|
|
|
+ for _, credential := range conf.Credentials {
|
|
|
+ clients[credential.ClientID] = NewDingTalkClient(credential)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return &DingTalkClientManager{
|
|
|
+ Credentials: conf.Credentials,
|
|
|
+ Clients: clients,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (m *DingTalkClientManager) GetClientByOAuthClientID(clientId string) DingTalkClientInterface {
|
|
|
+ m.mutex.Lock()
|
|
|
+ defer m.mutex.Unlock()
|
|
|
+ if client, ok := m.Clients[clientId]; ok {
|
|
|
+ return client
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func (c *DingTalkClient) GetAccessToken() (string, error) {
|
|
|
+ accessToken := ""
|
|
|
+ {
|
|
|
+ // 先查询缓存
|
|
|
+ c.mutex.Lock()
|
|
|
+ now := time.Now().Unix()
|
|
|
+ if c.expireAt > 0 && c.AccessToken != "" && (now+60) < c.expireAt {
|
|
|
+ // 预留一分钟有效期避免在Token过期的临界点调用接口出现401错误
|
|
|
+ accessToken = c.AccessToken
|
|
|
+ }
|
|
|
+ c.mutex.Unlock()
|
|
|
+ }
|
|
|
+ if accessToken != "" {
|
|
|
+ return accessToken, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ tokenResult, err := c.getAccessTokenFromDingTalk()
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ // 更新缓存
|
|
|
+ c.mutex.Lock()
|
|
|
+ c.AccessToken = tokenResult.AccessToken
|
|
|
+ c.expireAt = time.Now().Unix() + int64(tokenResult.ExpiresIn)
|
|
|
+ c.mutex.Unlock()
|
|
|
+ }
|
|
|
+ return tokenResult.AccessToken, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (c *DingTalkClient) UploadMedia(content []byte, filename, mediaType, mimeType string) (*MediaUploadResult, error) {
|
|
|
+ // OpenAPI doc: https://open.dingtalk.com/document/isvapp/upload-media-files
|
|
|
+ accessToken, err := c.GetAccessToken()
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ if len(accessToken) == 0 {
|
|
|
+ return nil, errors.New("empty access token")
|
|
|
+ }
|
|
|
+ body := &bytes.Buffer{}
|
|
|
+ writer := multipart.NewWriter(body)
|
|
|
+ part, err := writer.CreateFormFile("media", filename)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ _, err = part.Write(content)
|
|
|
+ writer.WriteField("type", mediaType)
|
|
|
+ err = writer.Close()
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ // Create a new HTTP request to upload the media file
|
|
|
+ url := fmt.Sprintf("https://oapi.dingtalk.com/media/upload?access_token=%s", url2.QueryEscape(accessToken))
|
|
|
+ req, err := http.NewRequest("POST", url, body)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
|
+
|
|
|
+ // Send the HTTP request and parse the response
|
|
|
+ client := &http.Client{
|
|
|
+ Timeout: time.Second * 60,
|
|
|
+ }
|
|
|
+ res, err := client.Do(req)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ defer res.Body.Close()
|
|
|
+
|
|
|
+ // Parse the response body as JSON and extract the media ID
|
|
|
+ media := &MediaUploadResult{}
|
|
|
+ bodyBytes, err := io.ReadAll(res.Body)
|
|
|
+ json.Unmarshal(bodyBytes, media)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ if media.ErrorCode != 0 {
|
|
|
+ return nil, errors.New(media.ErrorMessage)
|
|
|
+ }
|
|
|
+ return media, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (c *DingTalkClient) getAccessTokenFromDingTalk() (*OAuthTokenResult, error) {
|
|
|
+ // OpenAPI doc: https://open.dingtalk.com/document/orgapp/obtain-orgapp-token
|
|
|
+ apiUrl := "https://oapi.dingtalk.com/gettoken"
|
|
|
+ queryParams := url2.Values{}
|
|
|
+ queryParams.Add("appkey", c.Credential.ClientID)
|
|
|
+ queryParams.Add("appsecret", c.Credential.ClientSecret)
|
|
|
+
|
|
|
+ // Create a new HTTP request to get the AccessToken
|
|
|
+ req, err := http.NewRequest("GET", apiUrl+"?"+queryParams.Encode(), nil)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ // Send the HTTP request and parse the response body as JSON
|
|
|
+ client := http.Client{
|
|
|
+ Timeout: time.Second * 60,
|
|
|
+ }
|
|
|
+ res, err := client.Do(req)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ defer res.Body.Close()
|
|
|
+ body, err := io.ReadAll(res.Body)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ tokenResult := &OAuthTokenResult{}
|
|
|
+ err = json.Unmarshal(body, tokenResult)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ if tokenResult.ErrorCode != 0 {
|
|
|
+ return nil, errors.New(tokenResult.ErrorMessage)
|
|
|
+ }
|
|
|
+ return tokenResult, nil
|
|
|
+}
|