<message> - Introduce Feishu and DingTalk adapters in the robot lifecycle for enhanced integration capabilities. - Update the integration dispatcher to include new adapters for handling events from Feishu and DingTalk. - Modify the configuration structure to support settings for both Feishu and DingTalk integrations. - Enhance the integration parsing logic to recognize and process configurations for Feishu and DingTalk.
133 lines
3.2 KiB
Go
133 lines
3.2 KiB
Go
package dingtalk
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
apiBase = "https://api.dingtalk.com"
|
|
oauthBase = "https://api.dingtalk.com/v1.0/oauth2/accessToken"
|
|
)
|
|
|
|
// Bot represents a DingTalk bot instance.
|
|
type Bot struct {
|
|
clientID string
|
|
clientSecret string
|
|
httpClient *http.Client
|
|
|
|
accessToken string
|
|
tokenExpires time.Time
|
|
}
|
|
|
|
// NewBot creates a Bot bound to DingTalk app credentials.
|
|
func NewBot(clientID, clientSecret string) *Bot {
|
|
return &Bot{
|
|
clientID: clientID,
|
|
clientSecret: clientSecret,
|
|
httpClient: &http.Client{Timeout: 30 * time.Second},
|
|
}
|
|
}
|
|
|
|
// ClientID returns the client ID.
|
|
func (b *Bot) ClientID() string { return b.clientID }
|
|
|
|
// ClientSecret returns the client secret.
|
|
func (b *Bot) ClientSecret() string { return b.clientSecret }
|
|
|
|
// GetAccessToken returns a valid access token, refreshing if necessary.
|
|
func (b *Bot) GetAccessToken(ctx context.Context) (string, error) {
|
|
if b.accessToken != "" && time.Now().Before(b.tokenExpires) {
|
|
return b.accessToken, nil
|
|
}
|
|
|
|
body, _ := json.Marshal(map[string]string{
|
|
"appKey": b.clientID,
|
|
"appSecret": b.clientSecret,
|
|
})
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", oauthBase, bytes.NewReader(body))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := b.httpClient.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("dingtalk get token: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", fmt.Errorf("read token response: %w", err)
|
|
}
|
|
|
|
var result struct {
|
|
AccessToken string `json:"accessToken"`
|
|
ExpireIn int `json:"expireIn"`
|
|
}
|
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
|
return "", fmt.Errorf("unmarshal token: %w", err)
|
|
}
|
|
if result.AccessToken == "" {
|
|
return "", fmt.Errorf("dingtalk token empty, body=%s", string(respBody))
|
|
}
|
|
|
|
b.accessToken = result.AccessToken
|
|
b.tokenExpires = time.Now().Add(time.Duration(result.ExpireIn-60) * time.Second)
|
|
return b.accessToken, nil
|
|
}
|
|
|
|
// GetBotInfo verifies the bot credentials by fetching the access token.
|
|
func (b *Bot) GetBotInfo(ctx context.Context) error {
|
|
_, err := b.GetAccessToken(ctx)
|
|
return err
|
|
}
|
|
|
|
// apiRequest makes an authenticated API call to DingTalk.
|
|
func (b *Bot) apiRequest(ctx context.Context, method, path string, body interface{}) ([]byte, error) {
|
|
token, err := b.GetAccessToken(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var bodyReader io.Reader
|
|
if body != nil {
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bodyReader = bytes.NewReader(data)
|
|
}
|
|
|
|
url := apiBase + path
|
|
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("x-acs-dingtalk-access-token", token)
|
|
|
|
resp, err := b.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dingtalk api: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read api response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return nil, fmt.Errorf("dingtalk api error: status=%d body=%s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
return respBody, nil
|
|
}
|