yao/integrations/discord/file.go
Max 5333302055 Add Discord integration support in robot lifecycle
- Introduce Discord adapter in the robot lifecycle to enable integration with Discord events.
- Update the integration dispatcher to recognize and handle events from Discord.
- Modify the configuration structure to include settings for Discord integration.
- Enhance the integration parsing logic to support Discord configurations.
2026-03-02 07:53:49 +08:00

122 lines
3.1 KiB
Go

package discord
import (
"bytes"
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/textproto"
"strings"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/attachment"
)
const defaultUploader = "__yao.attachment"
// FileResult holds attachment wrapper and metadata.
type FileResult struct {
Wrapper string
MimeType string
FileName string
}
// DownloadAndStoreURL downloads a file from URL and stores it through
// the attachment manager. Uses the URL as fingerprint for dedup.
func DownloadAndStoreURL(ctx context.Context, url, contentType, 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(url, groups)
if manager.Exists(ctx, probeID) {
wrapper := fmt.Sprintf("%s://%s", defaultUploader, probeID)
return &FileResult{Wrapper: wrapper, MimeType: contentType, FileName: fileName}, nil
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("download: %w", err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read body: %w", err)
}
if contentType == "" {
contentType = resp.Header.Get("Content-Type")
if contentType == "" {
contentType = "application/octet-stream"
}
}
if fileName == "" {
fileName = "file"
}
header := &attachment.FileHeader{
FileHeader: &multipart.FileHeader{
Filename: fileName,
Size: int64(len(data)),
Header: make(textproto.MIMEHeader),
},
}
header.Header.Set("Content-Type", contentType)
header.Header.Set("Content-Fingerprint", url)
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: contentType, FileName: fileName}, nil
}
// ResolveMedia downloads and stores all media items in a ConvertedMessage.
func ResolveMedia(ctx context.Context, cm *ConvertedMessage, groups []string) {
if cm == nil {
return
}
for i := range cm.MediaItems {
mi := &cm.MediaItems[i]
if mi.URL == "" {
continue
}
result, err := DownloadAndStoreURL(ctx, mi.URL, mi.ContentType, mi.FileName, groups)
if err != nil {
log.Error("discord ResolveMedia: %s %s: %v", mi.Type, mi.URL, err)
continue
}
mi.Wrapper = result.Wrapper
if result.MimeType != "" {
mi.ContentType = result.MimeType
}
}
}
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[:])
}