Files
chatgpt-dingtalk/pkg/dingbot/client.go
2023-09-07 23:01:14 +08:00

215 lines
5.2 KiB
Go

package dingbot
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
url2 "net/url"
"sync"
"time"
"github.com/eryajf/chatgpt-dingtalk/config"
)
// 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
}