7 Commits
Author SHA1 Message Date
Plateau Nguyenandviettranx 8dc101e425 feat(pancake): add Shopee platform support (#975)
* feat(pancake): add Shopee platform support

- Fix Accept header for JSON response negotiation (Shopee GETs)
- Platform-prefix-aware pageID parsing (spo_ convID format)
- 500-char message limit for Shopee DMs
- Rune-safe truncation with slog.Warn on truncation

Phase 3 (integration smoke) pending: requires real credentials.

* fix(pancake): address PR review feedback

- Add RegisterPlatformPrefix() for extensibility (issue #1)
- Rename setCommonHeaders → setAcceptJSONHeader (issue #2)
- Add testdata fixture with page_id for priority testing (issue #3)

* chore: retrigger CI

* fix(permissions): classify sessions.compact in RBAC policy

Add MethodSessionsCompact to isWriteMethod to fix drift coverage test.
Method was added in #958 but missing from permissions classification.

* fix(tests): use strings.Contains to avoid redeclaration

Remove duplicate contains helper in tts_gemini_live_test.go that
conflicts with mcp_grant_revoke_test.go in same package.

* refactor(pancake): guard platformPrefixes with RWMutex for concurrent safety

RegisterPlatformPrefix is exported for future use (Lazada, Tokopedia) but
webhook handler reads the map concurrently. Protect with sync.RWMutex so
callers can register prefixes at any time without data races.

- Add platformPrefixesMu sync.RWMutex
- Wrap RegisterPlatformPrefix with write lock
- Extract isKnownPlatformPrefix helper with read lock
- Document that RegisterPlatformPrefix is currently unused (extension point)

---------

Co-authored-by: viettranx <viettranx@gmail.com>
2026-04-24 08:37:40 +07:00
Plateau Nguyenandviettranx 5cca9d7ac6 refactor(pancake): simplify private-reply to stateless DM (#951)
* feat(pancake): private-reply funnel — 2 modes, scope filter, DB dedup, metrics

Refactor the pancake channel's one-time DM feature from the 5-day-old minimal
`first_inbox` toggle into a complete comment → DM funnel. Breaking rename
(first_inbox → private_reply) matches Pancake + Facebook Graph terminology.

Channel logic
- Two modes: `after_reply` (default — public reply then DM) and `standalone`
  (DM-only; bypasses keyword filter, publishes synthetic outbound via
  `ch.Bus().PublishOutbound` so the LLM pipeline is skipped for template DMs).
- Post-level scope filter (`private_reply_options.allow_post_ids` /
  `deny_post_ids`, deny beats allow).
- Template vars `{{commenter_name}}` / `{{post_title}}` with pre-sanitized
  literal-replace (strips `{{`/`}}` from values — no nested substitution).
- Locale-aware default text via `i18n.T(locale, MsgPancakePrivateReplyDefault)`
  with en/vi/zh catalogs and English fallback.

DB dedup (replaces in-memory sync.Map)
- New `store.PancakePrivateReplyStore` (PG + SQLite impls), tenant-scoped via
  `store.TenantIDFromContext`, fail-closed on missing tenant.
- Atomic `TryClaim` / `Unclaim` (INSERT ... ON CONFLICT DO UPDATE WHERE stale)
  eliminates the concurrent-comment TOCTOU that would have fired duplicate
  DMs — claim first, release on API failure so the next comment can retry.
- Configurable TTL (`private_reply_ttl_days`, default 7).
- Dual-DB: migration 000056 on PG; schema v25 on SQLite.
  Bumps RequiredSchemaVersion 55 → 56.

Wiring
- `pancake.FactoryWithStores(ppReplyStore)` follows the existing
  `FactoryWithStores*` pattern (telegram, discord, whatsapp) and replaces
  `pancake.Factory` in `cmd/gateway.go`.
- `Channel.Send` injects `store.WithTenantID(ctx, ch.TenantID())` before
  dispatch so the outbound path sees a non-nil tenant (regression guard:
  without this the new store fails-closed and drops DMs in production — unit
  fakes masked this by bypassing the tenant check).
- Routing metadata whitelist extended: `private_reply_mode`,
  `private_reply_only`, `post_id`, `display_name` survive inbound → outbound.

Observability
- New `internal/metrics` package with `pancake_private_reply_total{
  page_id, result, reason}` counter and `/metrics` endpoint mounted on the
  gateway.
- Endpoint gated behind Bearer auth when `gateway.token` is configured; open
  (with warning) when unset for local dev scraping. Operators should set a
  token or bind `GOCLAW_HOST=127.0.0.1` to avoid leaking page identifiers.

UI
- 6 additive Pancake fields in `channel-schemas.ts` with `showWhen` gating on
  `features.private_reply` (itself gated on platform = fb/ig).
- 10 schema tests; full web build passes.
- i18n entries in en/vi/zh `channels.json` + 2 new Go keys
  (`MsgPancakePrivateReplyWindowExpired`, `MsgPancakePrivateReplyDefault`).

Tests
- 18 unit tests in pancake package (render/filter/mode switch/scope/dedup/
  metrics/tenant-ctx regression); 6 store-level tests; 5 E2E tests against
  live PG covering happy path, dedup-across-restart, TTL expiry via SQL,
  scope filter, FB 7-day policy error → no claim retained.
- All green on both PG and SQLite build tags with race detector.

* refactor(pancake): simplify private-reply to stateless DM

Roll back the dedup funnel from PR 951 — no table, no in-memory state,
no metrics, no modes, no scope filter. Keep only feature flag + message
template.

Rationale: each platform owning its own dedup table is an anti-pattern
that inflates multi-tenant data footprint. Use the platform itself
(Facebook Graph /comment/private_replies is per-comment idempotent)
combined with the existing webhook comment_id dedup in
comment_handler.go. Zero GoClaw state required.

Removed:
- DB table pancake_private_reply_sent (PG migration 000056, SQLite v25)
- Store interface + PG + SQLite implementations + tests
- internal/metrics package + /metrics endpoint + prometheus dep
- Two modes (after_reply/standalone) + standalone fast-path
- Scope filter (allow/deny post IDs) + PrivateReplyOptions struct
- TTL config (private_reply_ttl_days)
- Locale-aware i18n default (MsgPancakePrivateReplyDefault,
  MsgPancakePrivateReplyWindowExpired)
- FactoryWithStores wiring + tenant ctx injection in Send()
- SetHTTPClientForTest dead code

Kept:
- features.private_reply flag
- private_reply_message template with {{commenter_name}} and
  {{post_title}} vars (literal-replace, injection-safe)
- After-reply flow: comment -> public reply -> DM
- Hard-coded English fallback when message is empty

Schema: RequiredSchemaVersion 56 -> 55; SQLite SchemaVersion 25 -> 24;
schema.sql DDL block removed.

Net delta: -2661 lines across 44 files. Build (PG + sqliteonly) +
vet + race tests clean.

* test(agent): bump none-mode prompt size budget to 3100

vault_read wiring (#948) added ~95 chars to read_file tool summary,
pushing none-mode prompt from <3000 to 3075 chars. Bump budget to
3100 (~775 tokens) to match the intentional addition.

* test(integration): ensure data_migrations table exists in reset helper

The reset helper runs before RunPendingHooks, but RunPendingHooks is
what normally creates data_migrations. On a fresh CI database the
DELETE fails with 'relation does not exist'. Create the table
defensively so reset works regardless of execution order.

* chore(sqlite): remove accidental trailing blank line in schema.sql

---------

Co-authored-by: viettranx <viettranx@gmail.com>
2026-04-24 07:41:38 +07:00
viettranx 9f68f3ee0d feat(pancake): auto-react allow/deny scope filter + UI toggle
Extends #919 with per-channel scope control for Facebook auto-react.

- AutoReactOptions pointer struct (allow/deny post & user IDs)
- filterAutoReact + containsString helpers; deny overrides allow
- UI surfaces features.auto_react toggle + 4 tags fields
- Nil options = react-all (backward compatible)
2026-04-18 17:55:11 +07:00
Plateau Nguyen 8873548102 feat(pancake): add comment auto-react (like) on Facebook (#919)
Opt-in feature (features.auto_react: true) that automatically likes
a user's Facebook comment via Pancake /likes endpoint when the comment
webhook is received, as an engagement signal.

- Add AutoReact bool to Features config struct
- Add ReactComment() to APIClient (multipart POST to /likes, auth via api_key)
- Add reactCommentAsync() to comment handler; fires before keyword filter,
  independent of comment_reply feature
- Bound with 10-slot semaphore + 5s stopCtx timeout per goroutine
- Guard: only fires on platform=facebook with non-empty convID + msgID
- Startup warning when auto_react=true without webhook_secret (F9)
- Update feature gate: exit only when BOTH auto_react AND comment_reply disabled
- Add 8 tests (ReactComment contract, error body, invalid IDs, handler variants)
2026-04-16 10:14:43 +07:00
Plateau Nguyen 85e6f216b5 feat(channels): Pancake comment reply fix + platform select + UI improvements (#904)
* feat(channels): make Pancake platform a required select with 11 options

- Add mandatory platform select field (11 options) to Pancake channel schema
- Add config required validation on channel instance form (create-only)
- Add i18n fieldOptions/fieldConfig for platform in en/vi/zh locales
- Add TDD tests for Pancake config schema (channel-schemas.test.ts)
- Add backend test TestFactoryExplicitPlatformPreserved
- Add slog.Debug for auto-detect path in pancake.go
- Update Platform field comment in types.go for clarity
- Update plan status to completed; add changelog entry

* feat(channels): hide comment_reply for non-social Pancake platforms

Extend showWhen to accept string | string[] values. Apply it to
features.comment_reply so the field only appears for platforms that
support public posts/comments (facebook, instagram, threads, tiktok,
youtube). E-commerce platforms (shopee, lazada, tokopedia) and
messaging-only platforms (line, google, chat_plugin) no longer show
the irrelevant Comment Reply toggle.

Also guard depValue before String() coercion to avoid the "undefined"
literal matching hazard.

* feat(channels): Pancake comment reply fix + platform select + UI improvements

- Fix comment reply: pass message_id (reply_to_comment_id) to Pancake API
  ReplyComment now requires messageID param; guard added for empty ID
- Add SendMessageRequest.MessageID field (omitempty) for reply_comment action
- Platform select: required field with 11 options, showWhen gate for comment_reply
- Webhook Page ID moved to Advanced collapsible section in channel form
  (auto-expands when existing value is configured)
- routing_metadata.go: centralize routing metadata key constants
- config-flatten.ts: flatten/unflatten nested config for form state

Closes: Pancake comment reply returns 'Missing required field: message_id'
2026-04-15 22:26:13 +07:00
viettranx 45b9240b3a fix(pancake): correct test assertion and remove unused config field
- Fix TestPrivateReply_ReturnsError: use HTTP 400 instead of 200 since
  doRequest only returns error for status >= 400
- Remove unused MaxThreadDepth config field from CommentReplyOptions
2026-04-12 17:13:53 +07:00
Plateau Nguyen 18c09be21b feat(pancake): comment auto-reply + first-inbox DM (#841)
* feat(pancake): add comment auto-reply + first-inbox inbox implementation

- Route COMMENT webhook events with feature gate and self-reply prevention
- Add PostFetcher with sync.Map cache + singleflight for comment context
- Implement ReplyComment() and PrivateReply() API client methods
- Split Send() into sendCommentReply() / sendInboxReply() flows
- Add sendFirstInbox() for one-time inbox DM with configurable greeting
- New config: Features.FirstInbox, CommentReplyOptions, FirstInboxMessage, PostContextCacheTTL
- 72 tests passing with race detection; both PG and SQLite builds clean

* fix(pancake): address PR 841 review issues

- Add slog.Debug when post context fetch fails in buildCommentContent
- Add comment to GetPosts explaining why it bypasses doRequest
- Add 72h TTL eviction for firstInboxSent in runDedupCleaner
- Add 30s timeout to sendCommentReply to bound API hang risk
- Replace custom containsStr helper with strings.Contains in tests
2026-04-12 17:12:57 +07:00