Files
Plateau Nguyen fb8afd41bf feat(channels): add Facebook Messenger and Pancake channel integrations (#731)
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
2026-04-09 23:15:08 +07:00

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
}