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
This commit is contained in:
Plateau Nguyen authored and GitHub committed 2026-04-09 23:15:08 +07:00
1 parent 7205e6bb3b
commit fb8afd41bf
44 files changed
+4214 -39

No files matched your search

+4
View File
@@ -20,6 +20,8 @@ import (
"github.com/nextlevelbuilder/goclaw/internal/eventbus"
kg "github.com/nextlevelbuilder/goclaw/internal/knowledgegraph"
"github.com/nextlevelbuilder/goclaw/internal/channels/discord"
"github.com/nextlevelbuilder/goclaw/internal/channels/facebook"
"github.com/nextlevelbuilder/goclaw/internal/channels/pancake"
"github.com/nextlevelbuilder/goclaw/internal/channels/feishu"
slackchannel "github.com/nextlevelbuilder/goclaw/internal/channels/slack"
"github.com/nextlevelbuilder/goclaw/internal/channels/telegram"
@@ -417,6 +419,8 @@ func runGateway() {
instanceLoader.RegisterFactory(channels.TypeZaloPersonal, zalopersonal.FactoryWithPendingStore(pgStores.PendingMessages))
instanceLoader.RegisterFactory(channels.TypeWhatsApp, whatsapp.FactoryWithDB(pgStores.DB, pgStores.PendingMessages, "pgx"))
instanceLoader.RegisterFactory(channels.TypeSlack, slackchannel.FactoryWithPendingStore(pgStores.PendingMessages))
instanceLoader.RegisterFactory(channels.TypeFacebook, facebook.Factory)
instanceLoader.RegisterFactory(channels.TypePancake, pancake.Factory)
if err := instanceLoader.LoadAll(context.Background()); err != nil {
slog.Error("failed to load channel instances from DB", "error", err)
}
+2 -2
View File
@@ -30,8 +30,8 @@ func buildEnsureUserProfile(as store.AgentStore) agent.EnsureUserProfileFunc {
// isNew=true seeds all files; isNew=false only seeds if user has zero files
// (avoids re-seeding BOOTSTRAP.md after auto-cleanup on server restart).
func buildSeedUserFiles(as store.AgentStore) agent.SeedUserFilesFunc {
return func(ctx context.Context, agentID uuid.UUID, userID, agentType string, isNew bool) error {
_, err := bootstrap.SeedUserFiles(ctx, as, agentID, userID, agentType, !isNew)
return func(ctx context.Context, agentID uuid.UUID, userID, agentType string, isNew bool, channelMeta *bootstrap.ChannelMeta) error {
_, err := bootstrap.SeedUserFiles(ctx, as, agentID, userID, agentType, !isNew, channelMeta)
return err
}
}
+12
View File
@@ -91,6 +91,18 @@ func extractSessionMetadata(msg bus.InboundMessage, peerKind string) map[string]
return meta
}
// buildPancakeSessionLabel returns "Pancake:{senderName}:{pageName}" with non-empty parts only.
func buildPancakeSessionLabel(senderName, pageName string) string {
label := "Pancake"
if senderName != "" {
label += ":" + senderName
}
if pageName != "" {
label += ":" + pageName
}
return label
}
// buildAnnounceOutMeta builds outbound metadata for announce messages so that
// Send() can route replies to the correct forum topic or DM thread.
func buildAnnounceOutMeta(localKey string) map[string]string {
+6
View File
@@ -108,6 +108,12 @@ func processNormalMessage(
}
}
// Set session label for Pancake channels: "Pancake:{SenderName}:{PageName}"
if msg.Metadata["pancake_mode"] != "" {
label := buildPancakeSessionLabel(msg.Metadata["display_name"], msg.Metadata["page_name"])
deps.SessStore.SetLabel(ctx, sessionKey, label)
}
// Auto-collect channel contacts for the contact selector.
// Skip internal senders (system:*, notification:*, teammate:*, ticker:*, session_send_tool).
if deps.ContactCollector != nil && msg.SenderID != "" && !bus.IsInternalSender(msg.SenderID) {
+1
View File
@@ -157,6 +157,7 @@ func wireExtras(
ContextFileLoader: contextFileLoader,
BootstrapCleanup: buildBootstrapCleanup(stores.Agents),
CacheInvalidate: buildCacheInvalidate(contextFileInterceptor),
DefaultTimezone: appCfg.Cron.DefaultTimezone,
InjectionAction: injectionAction,
MaxMessageChars: appCfg.Gateway.MaxMessageChars,
CompactionCfg: appCfg.Agents.Defaults.Compaction,