mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 04:16:06 +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
105 lines
3.1 KiB
Go
105 lines
3.1 KiB
Go
package facebook
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
commentMaxChars = 8000
|
|
messengerMaxChars = 2000
|
|
)
|
|
|
|
var (
|
|
// Two-pass bold: **text** first, then *text*, to avoid stray asterisk artifacts.
|
|
reBoldDouble = regexp.MustCompile(`\*\*([^*]+)\*\*`)
|
|
reBoldSingle = regexp.MustCompile(`\*([^*]+)\*`)
|
|
// markdownLink converts [text](url) → "text (url)".
|
|
reLink = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
|
|
// HTML tags.
|
|
reHTML = regexp.MustCompile(`<[^>]+>`)
|
|
// Heading markers (# … at line start).
|
|
reHeader = regexp.MustCompile(`(?m)^#{1,6}\s+`)
|
|
// Inline `code` backticks.
|
|
reCode = regexp.MustCompile("`([^`]+)`")
|
|
)
|
|
|
|
// FormatForComment sanitizes agent output for posting as a Facebook comment.
|
|
// Facebook comments do not support markdown or HTML.
|
|
func FormatForComment(text string) string {
|
|
text = reHeader.ReplaceAllString(text, "")
|
|
text = reBoldDouble.ReplaceAllString(text, "$1") // ** first
|
|
text = reBoldSingle.ReplaceAllString(text, "$1") // * second
|
|
text = reLink.ReplaceAllString(text, "$1 ($2)")
|
|
text = reCode.ReplaceAllString(text, "$1")
|
|
text = reHTML.ReplaceAllString(text, "")
|
|
text = strings.TrimSpace(text)
|
|
return truncateRunes(text, commentMaxChars)
|
|
}
|
|
|
|
// FormatForMessenger sanitizes agent output for Messenger messages.
|
|
func FormatForMessenger(text string) string {
|
|
text = reHeader.ReplaceAllString(text, "")
|
|
text = reBoldDouble.ReplaceAllString(text, "$1")
|
|
text = reBoldSingle.ReplaceAllString(text, "$1")
|
|
text = reLink.ReplaceAllString(text, "$1 ($2)")
|
|
text = reCode.ReplaceAllString(text, "$1")
|
|
text = reHTML.ReplaceAllString(text, "")
|
|
return strings.TrimSpace(text)
|
|
}
|
|
|
|
// splitMessage splits text into chunks of at most maxChars runes,
|
|
// preferring paragraph or sentence boundaries to avoid mid-word cuts.
|
|
// Uses plain-text splitting (not channels.ChunkMarkdown) because Facebook
|
|
// strips all markdown — fenced code block repair is unnecessary.
|
|
func splitMessage(text string, maxChars int) []string {
|
|
runes := []rune(text) // convert once
|
|
if len(runes) <= maxChars {
|
|
return []string{text}
|
|
}
|
|
|
|
var parts []string
|
|
for len(runes) > maxChars {
|
|
chunk := string(runes[:maxChars])
|
|
|
|
// Try paragraph boundary.
|
|
if idx := strings.LastIndex(chunk, "\n\n"); idx > maxChars/2 {
|
|
parts = append(parts, strings.TrimSpace(chunk[:idx]))
|
|
runes = []rune(strings.TrimSpace(string(runes[idx+2:])))
|
|
continue
|
|
}
|
|
|
|
// Try sentence boundary.
|
|
split := false
|
|
for _, sep := range []string{". ", "! ", "? ", "\n"} {
|
|
if idx := strings.LastIndex(chunk, sep); idx > maxChars/2 {
|
|
parts = append(parts, strings.TrimSpace(chunk[:idx+1]))
|
|
runes = []rune(strings.TrimSpace(string(runes[idx+len(sep):])))
|
|
split = true
|
|
break
|
|
}
|
|
}
|
|
if split {
|
|
continue
|
|
}
|
|
|
|
// Hard cut at maxChars.
|
|
parts = append(parts, strings.TrimSpace(chunk))
|
|
runes = []rune(strings.TrimSpace(string(runes[maxChars:])))
|
|
}
|
|
|
|
if remaining := strings.TrimSpace(string(runes)); remaining != "" {
|
|
parts = append(parts, remaining)
|
|
}
|
|
return parts
|
|
}
|
|
|
|
// truncateRunes truncates s to at most n Unicode code points.
|
|
func truncateRunes(s string, n int) string {
|
|
runes := []rune(s)
|
|
if len(runes) <= n {
|
|
return s
|
|
}
|
|
return string(runes[:n])
|
|
}
|