yao/integrations/dingtalk/bot.go
Max 33efd3e890 Add Feishu and DingTalk integration support in robot lifecycle</message>
<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.
2026-03-02 07:36:34 +08:00

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
}