mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 16:14:57 +00:00
Add two new channel implementations for Facebook Fanpage (comment + Messenger auto-reply, first inbox DM) and Pancake/pages.fm (multi-platform inbox via Facebook, Zalo, Instagram, TikTok, WhatsApp, LINE). Key features: - Facebook: comment auto-reply, Messenger auto-reply, first inbox DM, HMAC-SHA256 webhook verification, multi-page webhook routing - Pancake: multi-platform inbox, outbound echo dedup with HTML normalization, race-condition-safe echo fingerprinting, platform-aware formatting - Bootstrap skip: pre-fill USER.md from channel metadata (Pancake) - SanitizeDisplayName across all channels (defense in depth) Code audit fixes: - Fix truncateForTikTok byte→rune slicing (UTF-8 corruption) - Fix empty message.ID shared dedup slot (silent message loss) - Fix DisplayName markdown injection in buildPrefilledUser - Consolidate duplicate ChannelMeta type (agent→bootstrap) - Compile-time interface assertions, alphabetical type constants - Per-message logs demoted to slog.Debug, errors.As for wrapped errors - UI: alphabetical channel ordering, complete config schemas - Remove deprecated WhatsApp bridge_url, fix nested error parsing
73 lines
1.8 KiB
Go
73 lines
1.8 KiB
Go
package pancake
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"mime"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
|
)
|
|
|
|
// handleMediaAttachments uploads media files from an OutboundMessage and returns attachment IDs.
|
|
// Returns an empty slice (not an error) when no media is attached.
|
|
// On upload failure the error is returned so the caller can decide to send text-only.
|
|
func (ch *Channel) handleMediaAttachments(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
|
if len(msg.Media) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
var ids []string
|
|
for _, att := range msg.Media {
|
|
if att.URL == "" {
|
|
continue
|
|
}
|
|
|
|
id, err := ch.uploadMediaFile(ctx, att.URL, att.ContentType)
|
|
if err != nil {
|
|
slog.Warn("pancake: skipping media attachment on upload error",
|
|
"page_id", ch.pageID, "url", att.URL, "err", err)
|
|
return ids, err
|
|
}
|
|
ids = append(ids, id)
|
|
}
|
|
|
|
return ids, nil
|
|
}
|
|
|
|
// uploadMediaFile opens a local file path and uploads it to the Pancake API.
|
|
// path must be absolute to prevent directory traversal via relative paths.
|
|
func (ch *Channel) uploadMediaFile(ctx context.Context, path string, contentType string) (string, error) {
|
|
cleanPath := filepath.Clean(path)
|
|
if !filepath.IsAbs(cleanPath) {
|
|
return "", fmt.Errorf("pancake: media path must be absolute, got: %s", path)
|
|
}
|
|
f, err := os.Open(cleanPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer f.Close()
|
|
|
|
ct := contentType
|
|
if ct == "" {
|
|
ct = mimeTypeFromPath(cleanPath)
|
|
}
|
|
|
|
return ch.apiClient.UploadMedia(ctx, filepath.Base(cleanPath), f, ct)
|
|
}
|
|
|
|
// mimeTypeFromPath guesses the MIME type from the file extension.
|
|
func mimeTypeFromPath(path string) string {
|
|
ext := filepath.Ext(path)
|
|
if ext == "" {
|
|
return "application/octet-stream"
|
|
}
|
|
mt := mime.TypeByExtension(ext)
|
|
if mt == "" {
|
|
return "application/octet-stream"
|
|
}
|
|
return mt
|
|
}
|