yao/integrations/feishu/file.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

169 lines
4.7 KiB
Go

package feishu
import (
"bytes"
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"mime/multipart"
"net/textproto"
"strings"
larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/attachment"
)
const defaultUploader = "__yao.attachment"
// FileResult holds the attachment wrapper and metadata for a stored file.
type FileResult struct {
Wrapper string
MimeType string
FileName string
}
// DownloadAndStoreImage downloads a Feishu image by image_key and stores it
// through the attachment manager. Uses image_key as the fingerprint for dedup.
func (b *Bot) DownloadAndStoreImage(ctx context.Context, messageID, imageKey string, groups []string) (*FileResult, error) {
manager, exists := attachment.Managers[defaultUploader]
if !exists {
return nil, fmt.Errorf("attachment manager %s not found", defaultUploader)
}
probeID := fingerprintKey(imageKey, groups)
if manager.Exists(ctx, probeID) {
wrapper := fmt.Sprintf("%s://%s", defaultUploader, probeID)
return &FileResult{Wrapper: wrapper, MimeType: "image/jpeg", FileName: imageKey + ".jpg"}, nil
}
req := larkim.NewGetMessageResourceReqBuilder().
MessageId(messageID).
FileKey(imageKey).
Type("image").
Build()
resp, err := b.client.Im.MessageResource.Get(ctx, req)
if err != nil {
return nil, fmt.Errorf("feishu get image resource: %w", err)
}
if !resp.Success() {
return nil, fmt.Errorf("feishu get image resource: code=%d", resp.Code)
}
data, err := io.ReadAll(resp.File)
if err != nil {
return nil, fmt.Errorf("read image body: %w", err)
}
return storeData(ctx, manager, imageKey, "image/jpeg", imageKey+".jpg", data, groups)
}
// DownloadAndStoreFile downloads a Feishu file by file_key and stores it.
func (b *Bot) DownloadAndStoreFile(ctx context.Context, messageID, fileKey, mimeType, fileName string, groups []string) (*FileResult, error) {
manager, exists := attachment.Managers[defaultUploader]
if !exists {
return nil, fmt.Errorf("attachment manager %s not found", defaultUploader)
}
probeID := fingerprintKey(fileKey, groups)
if manager.Exists(ctx, probeID) {
wrapper := fmt.Sprintf("%s://%s", defaultUploader, probeID)
return &FileResult{Wrapper: wrapper, MimeType: mimeType, FileName: fileName}, nil
}
req := larkim.NewGetMessageResourceReqBuilder().
MessageId(messageID).
FileKey(fileKey).
Type("file").
Build()
resp, err := b.client.Im.MessageResource.Get(ctx, req)
if err != nil {
return nil, fmt.Errorf("feishu get file resource: %w", err)
}
if !resp.Success() {
return nil, fmt.Errorf("feishu get file resource: code=%d", resp.Code)
}
data, err := io.ReadAll(resp.File)
if err != nil {
return nil, fmt.Errorf("read file body: %w", err)
}
if mimeType == "" {
mimeType = "application/octet-stream"
}
if fileName == "" {
fileName = resp.FileName
if fileName == "" {
fileName = fileKey
}
}
return storeData(ctx, manager, fileKey, mimeType, fileName, data, groups)
}
// ResolveMedia downloads and stores all media items in a ConvertedMessage.
func (b *Bot) ResolveMedia(ctx context.Context, cm *ConvertedMessage, groups []string) {
if cm == nil {
return
}
for i := range cm.MediaItems {
mi := &cm.MediaItems[i]
var result *FileResult
var err error
switch mi.Type {
case MediaImage:
result, err = b.DownloadAndStoreImage(ctx, cm.MessageID, mi.Key, groups)
default:
result, err = b.DownloadAndStoreFile(ctx, cm.MessageID, mi.Key, mi.MimeType, mi.FileName, groups)
}
if err != nil {
log.Error("feishu ResolveMedia: %s %s: %v", mi.Type, mi.Key, err)
continue
}
mi.Wrapper = result.Wrapper
if result.MimeType != "" {
mi.MimeType = result.MimeType
}
}
}
func storeData(ctx context.Context, manager *attachment.Manager, fingerprint, mimeType, fileName string, data []byte, groups []string) (*FileResult, error) {
header := &attachment.FileHeader{
FileHeader: &multipart.FileHeader{
Filename: fileName,
Size: int64(len(data)),
Header: make(textproto.MIMEHeader),
},
}
header.Header.Set("Content-Type", mimeType)
header.Header.Set("Content-Fingerprint", fingerprint)
option := attachment.UploadOption{
OriginalFilename: fileName,
Groups: groups,
}
uploaded, err := manager.Upload(ctx, header, bytes.NewReader(data), option)
if err != nil {
return nil, fmt.Errorf("attachment upload: %w", err)
}
wrapper := fmt.Sprintf("%s://%s", defaultUploader, uploaded.ID)
return &FileResult{Wrapper: wrapper, MimeType: mimeType, FileName: fileName}, nil
}
func fingerprintKey(key string, groups []string) string {
parts := make([]string, 0, len(groups)+1)
parts = append(parts, groups...)
parts = append(parts, key)
storagePath := strings.Join(parts, "/")
hash := md5.Sum([]byte(storagePath))
return hex.EncodeToString(hash[:])
}