-
-
Notifications
You must be signed in to change notification settings - Fork 385
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: 支持上传图片到钉钉平台,在图片生成流程中使用钉钉的图片 CDN 能力 (#225)
- Loading branch information
Showing
13 changed files
with
434 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Oops, something went wrong.