mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 22:15:47 +00:00
Pairing requests and message handlers were silently using MasterTenantID instead of the correct tenant because context.Context had no tenant set. Root cause: instance_loader.go passed bare ctx (no tenant) to startChannelWithTimeout; channel handlers also missing store.WithTenantID at entry points. Channels fixed: - instance_loader.go: pass instCtx (with tenant) to startChannelWithTimeout - Telegram, Bitrix24, Feishu: add store.WithTenantID at handler entry - Facebook: add ctx param to handleMessagingEvent/handleCommentEvent + webhook_handler - Pancake: add ctx param to handleMessagingEvent/handleCommentEvent + webhook_handler Also fix consolidation pruner: PruneExpired SQL had tenant_id filter that caused zero rows to ever be deleted (context had no tenant). Remove tenant filter — pruning is a global maintenance operation. Add regression tests: - internal/channels/pancake/tenant_context_test.go (6 tests) - internal/channels/facebook/tenant_context_test.go (4 tests) - internal/consolidation/prune_expired_test.go (5 tests) Co-authored-by: Bruno Clermont <bruno.clermont@gmail.com>
108 lines
3.1 KiB
Go
108 lines
3.1 KiB
Go
package facebook
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net/http"
|
|
"sync"
|
|
)
|
|
|
|
// webhookRouter routes incoming Facebook webhook events to the correct channel instance by page_id.
|
|
// A single HTTP handler is shared across all facebook channel instances on the same server.
|
|
//
|
|
// Multi-Meta-App note: all page instances registered here are expected to share the same Meta App
|
|
// (and thus the same app_secret). If instances with different secrets are registered, ServeHTTP
|
|
// tries all known secrets and accepts the payload if any matches.
|
|
type webhookRouter struct {
|
|
mu sync.RWMutex
|
|
instances map[string]*Channel // pageID → channel
|
|
routeHandled bool // true after first webhookRoute() call
|
|
}
|
|
|
|
var globalRouter = &webhookRouter{
|
|
instances: make(map[string]*Channel),
|
|
}
|
|
|
|
func (r *webhookRouter) register(ch *Channel) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.instances[ch.pageID] = ch
|
|
}
|
|
|
|
func (r *webhookRouter) unregister(pageID string) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
delete(r.instances, pageID)
|
|
}
|
|
|
|
// webhookRoute returns the path+handler for the first call; ("", nil) for subsequent calls.
|
|
func (r *webhookRouter) webhookRoute() (string, http.Handler) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if !r.routeHandled {
|
|
r.routeHandled = true
|
|
return webhookPath, r
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
// ServeHTTP is the shared handler for all Facebook page webhooks.
|
|
// Routes each entry to the matching channel instance by page_id.
|
|
//
|
|
// Multi-Meta-App support: all registered page secrets are collected and tried in order.
|
|
// A payload is accepted if its signature matches any known app_secret. In the common
|
|
// case (single Meta App) there is exactly one secret and behavior is unchanged.
|
|
func (r *webhookRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
r.mu.RLock()
|
|
var primarySecret, verifyToken string
|
|
var extraSecrets []string
|
|
seenSecrets := make(map[string]bool)
|
|
for _, ch := range r.instances {
|
|
s := ch.webhookH.appSecret
|
|
if primarySecret == "" {
|
|
primarySecret = s
|
|
verifyToken = ch.webhookH.verifyToken
|
|
seenSecrets[s] = true
|
|
} else if !seenSecrets[s] {
|
|
extraSecrets = append(extraSecrets, s)
|
|
seenSecrets[s] = true
|
|
}
|
|
}
|
|
r.mu.RUnlock()
|
|
|
|
if primarySecret == "" {
|
|
// No instances registered yet.
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
if len(extraSecrets) > 0 {
|
|
slog.Warn("security.facebook_multi_meta_app",
|
|
"extra_app_count", len(extraSecrets),
|
|
"note", "multiple Meta App secrets registered; payloads verified against all known secrets")
|
|
}
|
|
|
|
routingWH := &WebhookHandler{
|
|
appSecret: primarySecret,
|
|
verifyToken: verifyToken,
|
|
extraSecrets: extraSecrets,
|
|
}
|
|
routingWH.onComment = func(ctx context.Context, entry WebhookEntry, change ChangeValue) {
|
|
r.mu.RLock()
|
|
target := r.instances[entry.ID]
|
|
r.mu.RUnlock()
|
|
if target != nil {
|
|
target.handleCommentEvent(ctx, entry, change)
|
|
}
|
|
}
|
|
routingWH.onMessage = func(ctx context.Context, entry WebhookEntry, event MessagingEvent) {
|
|
r.mu.RLock()
|
|
target := r.instances[entry.ID]
|
|
r.mu.RUnlock()
|
|
if target != nil {
|
|
target.handleMessagingEvent(ctx, entry, event)
|
|
}
|
|
}
|
|
routingWH.ServeHTTP(w, req)
|
|
}
|