mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-18 14:13:34 +00:00
fix(zalo): download CDN media to temp file and handle []byte credentials (#130)
Zalo CDN photo URLs are auth-restricted and expire quickly. Download images to local temp files before passing to the agent, falling back to the raw URL on failure. Also adds PhotoURL field support. Credentials Update() now handles []byte type in the switch case, preventing incorrect encryption when credentials arrive as raw bytes.
This commit is contained in:
1 parent
cd2e407b29
commit
6a51e8d7c4
2 files changed
+82
-10
No files matched your search
@@ -13,6 +13,7 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -231,14 +232,32 @@ func (c *Channel) handleImageMessage(msg *zaloMessage) {
|
||||
content = "[image]"
|
||||
}
|
||||
|
||||
// Download photo from Zalo CDN to local temp file (CDN URLs are auth-restricted/expiring)
|
||||
var media []string
|
||||
if msg.Photo != "" {
|
||||
media = []string{msg.Photo}
|
||||
var photoURL string
|
||||
switch {
|
||||
case msg.PhotoURL != "":
|
||||
photoURL = msg.PhotoURL
|
||||
case msg.Photo != "":
|
||||
photoURL = msg.Photo
|
||||
}
|
||||
|
||||
slog.Debug("zalo image message received",
|
||||
if photoURL != "" {
|
||||
localPath, err := c.downloadMedia(photoURL)
|
||||
if err != nil {
|
||||
slog.Warn("zalo photo download failed, passing URL as fallback",
|
||||
"photo_url", photoURL, "error", err)
|
||||
media = []string{photoURL}
|
||||
} else {
|
||||
media = []string{localPath}
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("zalo image message received",
|
||||
"sender_id", senderID,
|
||||
"chat_id", chatID,
|
||||
"photo_url", photoURL,
|
||||
"has_media", len(media) > 0,
|
||||
)
|
||||
|
||||
metadata := map[string]string{
|
||||
@@ -316,6 +335,55 @@ func (c *Channel) sendPairingReply(senderID, chatID string) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Media download ---
|
||||
|
||||
const maxMediaBytes = 10 * 1024 * 1024 // 10MB
|
||||
|
||||
// downloadMedia fetches a photo from a Zalo CDN URL and saves it as a local temp file.
|
||||
// Zalo CDN URLs are auth-restricted and expire, so we must download immediately.
|
||||
func (c *Channel) downloadMedia(url string) (string, error) {
|
||||
resp, err := c.client.Get(url)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fetch: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("http %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Detect extension from Content-Type
|
||||
ext := ".jpg"
|
||||
ct := resp.Header.Get("Content-Type")
|
||||
switch {
|
||||
case strings.Contains(ct, "png"):
|
||||
ext = ".png"
|
||||
case strings.Contains(ct, "gif"):
|
||||
ext = ".gif"
|
||||
case strings.Contains(ct, "webp"):
|
||||
ext = ".webp"
|
||||
}
|
||||
|
||||
f, err := os.CreateTemp("", "goclaw_zalo_*"+ext)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create temp: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
n, err := io.Copy(f, io.LimitReader(resp.Body, maxMediaBytes))
|
||||
if err != nil {
|
||||
os.Remove(f.Name())
|
||||
return "", fmt.Errorf("write: %w", err)
|
||||
}
|
||||
if n == 0 {
|
||||
os.Remove(f.Name())
|
||||
return "", fmt.Errorf("empty response")
|
||||
}
|
||||
|
||||
slog.Debug("zalo media downloaded", "path", f.Name(), "size", n)
|
||||
return f.Name(), nil
|
||||
}
|
||||
|
||||
// --- Chunked text sending ---
|
||||
|
||||
func (c *Channel) sendChunkedText(chatID, text string) error {
|
||||
@@ -358,6 +426,7 @@ type zaloMessage struct {
|
||||
MessageID string `json:"message_id"`
|
||||
Text string `json:"text"`
|
||||
Photo string `json:"photo"`
|
||||
PhotoURL string `json:"photo_url"`
|
||||
Caption string `json:"caption"`
|
||||
From zaloFrom `json:"from"`
|
||||
Chat zaloChat `json:"chat"`
|
||||
|
||||
@@ -147,23 +147,26 @@ func (s *PGChannelInstanceStore) scanInstances(rows *sql.Rows) ([]store.ChannelI
|
||||
func (s *PGChannelInstanceStore) Update(ctx context.Context, id uuid.UUID, updates map[string]any) error {
|
||||
// Encrypt credentials if present
|
||||
if credsVal, ok := updates["credentials"]; ok && credsVal != nil {
|
||||
var credsStr string
|
||||
var credsBytes []byte
|
||||
switch v := credsVal.(type) {
|
||||
case []byte:
|
||||
credsBytes = v
|
||||
case string:
|
||||
credsStr = v
|
||||
credsBytes = []byte(v)
|
||||
default:
|
||||
// Object/map from JSON — marshal to string for encryption
|
||||
// Object/map from JSON — marshal to []byte
|
||||
if b, err := json.Marshal(v); err == nil {
|
||||
credsStr = string(b)
|
||||
credsBytes = b
|
||||
}
|
||||
}
|
||||
if credsStr != "" && s.encKey != "" {
|
||||
encrypted, err := crypto.Encrypt(credsStr, s.encKey)
|
||||
if len(credsBytes) > 0 && s.encKey != "" {
|
||||
encrypted, err := crypto.Encrypt(string(credsBytes), s.encKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt credentials: %w", err)
|
||||
}
|
||||
updates["credentials"] = []byte(encrypted)
|
||||
credsBytes = []byte(encrypted)
|
||||
}
|
||||
updates["credentials"] = credsBytes
|
||||
}
|
||||
updates["updated_at"] = time.Now()
|
||||
return execMapUpdate(ctx, s.db, "channel_instances", id, updates)
|
||||
|
||||
Reference in new issue
Block a user