553 lines
20 KiB
Go
553 lines
20 KiB
Go
package webhook
|
|
|
|
// FacebookWebhookHandler processes incoming Facebook/Instagram webhook HTTP requests via Gin.
|
|
//
|
|
// Reference: Chatwoot routes Facebook webhooks at:
|
|
// post '/webhooks/facebook/:page_id' => 'channels/facebook#process_message'
|
|
// FB webhook verification (GET) at the same URL
|
|
//
|
|
// The handler:
|
|
// 1. GET: Webhook verification — responds with hub.challenge when hub.mode=subscribe
|
|
// and hub.verify_token matches the stored token (FB requirement for initial setup)
|
|
// 2. POST: Incoming message/event processing — reads the JSON payload, validates
|
|
// the X-Hub-Signature-256 header, parses events, and delegates to the
|
|
// Facebook/Instagram provider pipeline
|
|
//
|
|
// Facebook webhook requirements:
|
|
// - GET verification: must echo hub.challenge back when hub.verify_token matches
|
|
// - POST events: must validate X-Hub-Signature-256 (SHA256 HMAC with app_secret)
|
|
// - Must respond within 20 seconds
|
|
// - Must return 200 OK even on processing errors (FB retries on non-200)
|
|
//
|
|
// Instagram DMs use the same webhook endpoint format (Meta Business Suite):
|
|
// - object="instagram" for IG events vs object="page" for FB Messenger
|
|
// - Same X-Hub-Signature-256 verification
|
|
// - Same hub.mode/hub.verify_token/hub.challenge verification flow
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/gochat/gochat/internal/channel"
|
|
fbchannel "github.com/gochat/gochat/internal/channel/facebook"
|
|
"github.com/gochat/gochat/internal/model"
|
|
channelmodel "github.com/gochat/gochat/internal/model/channel"
|
|
"github.com/gochat/gochat/internal/worker"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// FacebookWebhookHandler processes Facebook/Instagram webhook requests via Gin.
|
|
type FacebookWebhookHandler struct {
|
|
fbProvider *fbchannel.FacebookProvider
|
|
igProvider *fbchannel.InstagramProvider
|
|
webhookParser *fbchannel.WebhookParser
|
|
db *gorm.DB
|
|
persister *IncomingPersister
|
|
}
|
|
|
|
// NewFacebookWebhookHandler creates a Facebook/Instagram webhook handler for Gin integration.
|
|
func NewFacebookWebhookHandler(
|
|
fbProvider *fbchannel.FacebookProvider,
|
|
igProvider *fbchannel.InstagramProvider,
|
|
db *gorm.DB,
|
|
dispatcher ...*channel.Dispatcher,
|
|
) *FacebookWebhookHandler {
|
|
return &FacebookWebhookHandler{
|
|
fbProvider: fbProvider,
|
|
igProvider: igProvider,
|
|
webhookParser: fbchannel.NewWebhookParser(),
|
|
db: db,
|
|
persister: NewIncomingPersister(db, dispatcher...),
|
|
}
|
|
}
|
|
|
|
func (h *FacebookWebhookHandler) WithWorkerPool(wp *worker.WorkerPool) *FacebookWebhookHandler {
|
|
if h != nil && h.persister != nil {
|
|
h.persister.SetWorkerPool(wp)
|
|
}
|
|
return h
|
|
}
|
|
|
|
// HandleFacebookVerification handles the GET webhook verification request from Facebook.
|
|
// URL pattern: /webhooks/facebook/:inbox_id
|
|
// Method: GET
|
|
// Query params: hub.mode=subscribe, hub.verify_token=<token>, hub.challenge=<challenge>
|
|
//
|
|
// Reference: https://developers.facebook.com/docs/graph-api/webhooks/getting-started#verification-requests
|
|
// Facebook sends a GET request with hub.mode=subscribe when verifying a new webhook subscription.
|
|
// The server must respond with the hub.challenge value if hub.verify_token matches.
|
|
func (h *FacebookWebhookHandler) HandleFacebookVerification(c *gin.Context) {
|
|
pageID := c.Param("page_id")
|
|
inboxID, _ := parseOptionalUintParam(c.Param("inbox_id"))
|
|
|
|
// Extract verification parameters
|
|
queryParams := map[string]string{
|
|
"hub.mode": c.Query("hub.mode"),
|
|
"hub.verify_token": c.Query("hub.verify_token"),
|
|
"hub.challenge": c.Query("hub.challenge"),
|
|
}
|
|
|
|
// Look up the inbox to find the verify token stored in ChannelConfig
|
|
inbox, err := h.lookupFacebookInbox(pageID, inboxID)
|
|
if err != nil {
|
|
applogger.L().Warnf("Facebook webhook verification: inbox lookup failed for page_id=%s inbox_id=%d: %v", pageID, inboxID, err)
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "inbox not found"})
|
|
return
|
|
}
|
|
|
|
// Resolve the verify token from channel config
|
|
verifyToken := h.resolveVerifyToken(inbox)
|
|
if verifyToken == "" {
|
|
applogger.L().Warnf("Facebook webhook verification: no verify_token for inbox %d", inbox.ID)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "verify token not configured"})
|
|
return
|
|
}
|
|
|
|
// Delegate to the channel-level verification logic
|
|
challenge, ok := fbchannel.VerifyWebhookChallenge(queryParams, verifyToken)
|
|
if !ok {
|
|
applogger.L().Warnf("Facebook webhook verification: token mismatch for inbox %d", inbox.ID)
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "verification failed"})
|
|
return
|
|
}
|
|
|
|
applogger.L().Infof("Facebook webhook verified for inbox %d", inbox.ID)
|
|
|
|
// Facebook expects the challenge value as the plain response body
|
|
c.String(http.StatusOK, challenge)
|
|
}
|
|
|
|
// HandleFacebookWebhook processes an incoming Facebook/Instagram webhook POST request.
|
|
// URL pattern: /webhooks/facebook/:inbox_id
|
|
// Method: POST
|
|
// Content-Type: application/json
|
|
// Header: X-Hub-Signature-256 for signature verification
|
|
//
|
|
// Reference: Chatwoot processes Facebook webhook events via IncomingMessageService
|
|
// FB sends webhook events as JSON POST to the configured callback URL
|
|
//
|
|
// Flow:
|
|
// 1. Read raw body from request
|
|
// 2. Validate X-Hub-Signature-256 HMAC signature
|
|
// 3. Parse the payload to determine object type (page=FB, instagram=IG)
|
|
// 4. Delegate to appropriate provider's ProcessIncoming method
|
|
// 5. Return 200 OK immediately (Facebook retries on non-200)
|
|
func (h *FacebookWebhookHandler) HandleFacebookWebhook(c *gin.Context) {
|
|
pageID := c.Param("page_id")
|
|
inboxID, _ := parseOptionalUintParam(c.Param("inbox_id"))
|
|
|
|
// Read request body
|
|
body, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
applogger.L().Errorf("Facebook webhook: failed to read body for page_id=%s inbox_id=%d: %v", pageID, inboxID, err)
|
|
c.JSON(http.StatusOK, gin.H{"status": "ignored"})
|
|
return
|
|
}
|
|
defer c.Request.Body.Close()
|
|
|
|
// Validate webhook signature (X-Hub-Signature-256)
|
|
signatureHeader := c.GetHeader("X-Hub-Signature-256")
|
|
inbox, err := h.lookupFacebookInbox(pageID, inboxID)
|
|
if err != nil {
|
|
applogger.L().Warnf("Facebook webhook: inbox lookup failed for page_id=%s inbox_id=%d: %v", pageID, inboxID, err)
|
|
c.JSON(http.StatusOK, gin.H{"status": "ignored"})
|
|
return
|
|
}
|
|
|
|
// Resolve app_secret from channel config for signature validation
|
|
appSecret := h.resolveAppSecret(inbox)
|
|
if appSecret != "" && signatureHeader != "" {
|
|
if !fbchannel.ValidateWebhookSignature(appSecret, signatureHeader, body) {
|
|
applogger.L().Warnf("Facebook webhook: signature validation failed for inbox %d", inbox.ID)
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "invalid signature"})
|
|
return
|
|
}
|
|
}
|
|
|
|
// Parse the payload to determine if it's Facebook or Instagram
|
|
// object="page" → Facebook Messenger, object="instagram" → Instagram DMs
|
|
parsedEvents, err := h.webhookParser.ParseWebhookPayload(body)
|
|
if err != nil {
|
|
applogger.L().Errorf("Facebook webhook: failed to parse payload for inbox %d: %v", inbox.ID, err)
|
|
c.JSON(http.StatusOK, gin.H{"status": "ignored"})
|
|
return
|
|
}
|
|
|
|
applogger.L().Infof("Facebook webhook: received %d events for inbox %d", len(parsedEvents), inbox.ID)
|
|
|
|
// Process each parsed event
|
|
for _, event := range parsedEvents {
|
|
// Skip echo messages (sent by the page/business itself)
|
|
if fbchannel.IsEchoMessage(event) {
|
|
mid := ""
|
|
if event.Message != nil {
|
|
mid = event.Message.Mid
|
|
}
|
|
applogger.L().Debug("Facebook webhook: skipping echo message",
|
|
"mid", mid,
|
|
"inbox_id", inbox.ID,
|
|
)
|
|
continue
|
|
}
|
|
|
|
// Skip delivery/read receipts — no actionable message content
|
|
if fbchannel.IsDeliveryOrReadReceipt(event) {
|
|
applogger.L().Debug("Facebook webhook: skipping delivery/read receipt",
|
|
"event_type", event.EventType,
|
|
"inbox_id", inbox.ID,
|
|
)
|
|
h.persistFacebookReceipt(c.Request.Context(), inbox, event)
|
|
continue
|
|
}
|
|
|
|
// Skip thread control events (handled by TakeThreadControl in listener)
|
|
if fbchannel.IsThreadControlEvent(event) {
|
|
applogger.L().Debug("Facebook webhook: skipping thread control event",
|
|
"event_type", event.EventType,
|
|
"inbox_id", inbox.ID,
|
|
)
|
|
continue
|
|
}
|
|
|
|
// Handle Instagram comment events separately from DM messages
|
|
// Comment events use the Changes array (field="comments") and require
|
|
// ProcessCommentIncoming to transform them into IncomingMessage objects.
|
|
if fbchannel.IsIGCommentEvent(event) {
|
|
applogger.L().Info("Facebook webhook: processing Instagram comment event",
|
|
"event_type", event.EventType,
|
|
"inbox_id", inbox.ID,
|
|
)
|
|
|
|
commentMsg, err := h.igProvider.ProcessCommentIncoming(c.Request.Context(), inbox, event.Comment, event.EventType)
|
|
if err != nil {
|
|
applogger.L().Errorf("Facebook webhook: IG comment processing failed: %v", err)
|
|
continue
|
|
}
|
|
if commentMsg != nil {
|
|
if _, persistErr := h.persister.PersistIncoming(c.Request.Context(), inbox, commentMsg); persistErr != nil {
|
|
applogger.L().Errorf("Facebook webhook: IG comment persist failed (source_id=%s): %v", commentMsg.SourceID, persistErr)
|
|
continue
|
|
}
|
|
applogger.L().Infof("Facebook webhook: IG comment persisted (inbox_id=%d, source_id=%s, type=%s)",
|
|
commentMsg.InboxID, commentMsg.SourceID, event.EventType)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Only process events that should create messages
|
|
if !fbchannel.ShouldCreateMessage(event) {
|
|
applogger.L().Debug("Facebook webhook: skipping non-message event",
|
|
"event_type", event.EventType,
|
|
"inbox_id", inbox.ID,
|
|
)
|
|
continue
|
|
}
|
|
|
|
// Delegate to the appropriate provider based on object type
|
|
var incomingMsg *channel.IncomingMessage
|
|
switch event.Object {
|
|
case "page":
|
|
// Facebook Messenger
|
|
incomingMsg, err = fbchannel.ExtractIncomingMessageFromEvent(event, inbox, channel.ChannelFacebook)
|
|
if err != nil {
|
|
applogger.L().Errorf("Facebook webhook: extract message failed (FB): %v", err)
|
|
continue
|
|
}
|
|
case "instagram":
|
|
// Instagram DMs
|
|
incomingMsg, err = fbchannel.ExtractIncomingMessageFromEvent(event, inbox, channel.ChannelInstagram)
|
|
if err != nil {
|
|
applogger.L().Errorf("Facebook webhook: extract message failed (IG): %v", err)
|
|
continue
|
|
}
|
|
default:
|
|
applogger.L().Warnf("Facebook webhook: unknown object type %s for inbox %d", event.Object, inbox.ID)
|
|
continue
|
|
}
|
|
|
|
if incomingMsg != nil {
|
|
if _, persistErr := h.persister.PersistIncoming(c.Request.Context(), inbox, incomingMsg); persistErr != nil {
|
|
applogger.L().Errorf("Facebook webhook: persist message failed (source_id=%s): %v", incomingMsg.SourceID, persistErr)
|
|
continue
|
|
}
|
|
applogger.L().Infof("Facebook webhook: message persisted (inbox_id=%d, source_id=%s, type=%s)",
|
|
incomingMsg.InboxID, incomingMsg.SourceID, event.EventType)
|
|
}
|
|
}
|
|
|
|
// Always return 200 OK to Facebook — it retries if not 200
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
|
}
|
|
|
|
// HandleInstagramVerification handles the Chatwoot-compatible no-param Instagram webhook verification route.
|
|
func (h *FacebookWebhookHandler) HandleInstagramVerification(c *gin.Context) {
|
|
token := c.Query("hub.verify_token")
|
|
challenge := c.Query("hub.challenge")
|
|
if h.validInstagramVerifyToken(token) {
|
|
applogger.L().Info("Instagram webhook verified")
|
|
c.String(http.StatusOK, challenge)
|
|
return
|
|
}
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Error; wrong verify token"})
|
|
}
|
|
|
|
// HandleInstagramWebhook processes the Chatwoot-compatible no-param Instagram event route.
|
|
func (h *FacebookWebhookHandler) HandleInstagramWebhook(c *gin.Context) {
|
|
body, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
applogger.L().Errorf("Instagram webhook: failed to read body: %v", err)
|
|
c.JSON(http.StatusOK, gin.H{"status": "ignored"})
|
|
return
|
|
}
|
|
defer c.Request.Body.Close()
|
|
|
|
parsedEvents, err := h.webhookParser.ParseWebhookPayload(body)
|
|
if err != nil {
|
|
applogger.L().Errorf("Instagram webhook: failed to parse payload: %v", err)
|
|
c.JSON(http.StatusOK, gin.H{"status": "ignored"})
|
|
return
|
|
}
|
|
|
|
if len(parsedEvents) == 0 {
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
|
return
|
|
}
|
|
if parsedEvents[0].Object != "instagram" {
|
|
applogger.L().Warnf("Message is not received from the instagram webhook event: %s", parsedEvents[0].Object)
|
|
c.Status(http.StatusUnprocessableEntity)
|
|
return
|
|
}
|
|
|
|
if err := h.verifyInstagramSignature(c, body, parsedEvents); err != nil {
|
|
applogger.L().Warnf("Instagram webhook: signature verification failed: %v", err)
|
|
c.Status(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
for _, event := range parsedEvents {
|
|
inbox, err := h.lookupInstagramInboxForEvent(event)
|
|
if err != nil {
|
|
applogger.L().Warnf("Instagram webhook: inbox lookup failed for page_id=%s sender=%s recipient=%s: %v", event.PageID, event.SenderID, event.RecipientID, err)
|
|
continue
|
|
}
|
|
h.processInstagramEvent(c, inbox, event)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
|
}
|
|
|
|
// ===========================
|
|
// Helper methods
|
|
// ===========================
|
|
|
|
// lookupInbox finds the Inbox by ID via GORM.
|
|
func (h *FacebookWebhookHandler) lookupInbox(inboxID uint) (*model.Inbox, error) {
|
|
var inbox model.Inbox
|
|
if err := h.db.Where("id = ?", inboxID).First(&inbox).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &inbox, nil
|
|
}
|
|
|
|
func (h *FacebookWebhookHandler) lookupFacebookInbox(pageID string, inboxID uint) (*model.Inbox, error) {
|
|
if inboxID != 0 {
|
|
return h.lookupInbox(inboxID)
|
|
}
|
|
return h.lookupInboxByFacebookPageID(pageID)
|
|
}
|
|
|
|
func (h *FacebookWebhookHandler) lookupInboxByFacebookPageID(pageID string) (*model.Inbox, error) {
|
|
var fb channelmodel.ChannelFacebook
|
|
if err := h.db.Where("page_id = ?", pageID).First(&fb).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return h.lookupInbox(fb.InboxID)
|
|
}
|
|
|
|
func (h *FacebookWebhookHandler) lookupInstagramInboxForEvent(event *fbchannel.ParsedWebhookEvent) (*model.Inbox, error) {
|
|
instagramID := event.RecipientID
|
|
if fbchannel.IsEchoMessage(event) && event.SenderID != "" {
|
|
instagramID = event.SenderID
|
|
}
|
|
if instagramID == "" {
|
|
instagramID = event.PageID
|
|
}
|
|
|
|
var ig channelmodel.ChannelInstagram
|
|
if err := h.db.Where("instagram_account_id = ? OR instagram_business_account_id = ?", instagramID, instagramID).First(&ig).Error; err == nil {
|
|
return h.lookupInbox(ig.InboxID)
|
|
}
|
|
|
|
var fb channelmodel.ChannelFacebook
|
|
if err := h.db.Where("instagram_business_account_id = ?", instagramID).First(&fb).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return h.lookupInbox(fb.InboxID)
|
|
}
|
|
|
|
func (h *FacebookWebhookHandler) validInstagramVerifyToken(token string) bool {
|
|
if token == "" {
|
|
return false
|
|
}
|
|
return token == os.Getenv("IG_VERIFY_TOKEN") || token == os.Getenv("INSTAGRAM_VERIFY_TOKEN")
|
|
}
|
|
|
|
func (h *FacebookWebhookHandler) verifyInstagramSignature(c *gin.Context, body []byte, events []*fbchannel.ParsedWebhookEvent) error {
|
|
signatureHeader := c.GetHeader("X-Hub-Signature-256")
|
|
if signatureHeader == "" {
|
|
return strconv.ErrSyntax
|
|
}
|
|
|
|
for _, secret := range h.instagramAppSecrets(events) {
|
|
if fbchannel.ValidateWebhookSignature(secret, signatureHeader, body) {
|
|
return nil
|
|
}
|
|
}
|
|
return strconv.ErrSyntax
|
|
}
|
|
|
|
func (h *FacebookWebhookHandler) instagramAppSecrets(events []*fbchannel.ParsedWebhookEvent) []string {
|
|
seen := map[string]bool{}
|
|
secrets := make([]string, 0, 4)
|
|
add := func(secret string) {
|
|
if secret != "" && !seen[secret] {
|
|
seen[secret] = true
|
|
secrets = append(secrets, secret)
|
|
}
|
|
}
|
|
|
|
add(os.Getenv("INSTAGRAM_APP_SECRET"))
|
|
add(os.Getenv("FB_APP_SECRET"))
|
|
for _, event := range events {
|
|
if inbox, err := h.lookupInstagramInboxForEvent(event); err == nil {
|
|
add(h.resolveAppSecret(inbox))
|
|
}
|
|
}
|
|
return secrets
|
|
}
|
|
|
|
func (h *FacebookWebhookHandler) processInstagramEvent(c *gin.Context, inbox *model.Inbox, event *fbchannel.ParsedWebhookEvent) {
|
|
if fbchannel.IsDeliveryOrReadReceipt(event) {
|
|
h.persistFacebookReceipt(c.Request.Context(), inbox, event)
|
|
return
|
|
}
|
|
if fbchannel.IsEchoMessage(event) || fbchannel.IsThreadControlEvent(event) {
|
|
return
|
|
}
|
|
|
|
if fbchannel.IsIGCommentEvent(event) {
|
|
if h.igProvider == nil {
|
|
applogger.L().Warn("Instagram webhook: comment event ignored because Instagram provider is not configured")
|
|
return
|
|
}
|
|
commentMsg, err := h.igProvider.ProcessCommentIncoming(c.Request.Context(), inbox, event.Comment, event.EventType)
|
|
if err != nil {
|
|
applogger.L().Errorf("Instagram webhook: comment processing failed: %v", err)
|
|
}
|
|
if commentMsg != nil {
|
|
if _, persistErr := h.persister.PersistIncoming(c.Request.Context(), inbox, commentMsg); persistErr != nil {
|
|
applogger.L().Errorf("Instagram webhook: comment persist failed (source_id=%s): %v", commentMsg.SourceID, persistErr)
|
|
return
|
|
}
|
|
applogger.L().Infof("Instagram webhook: comment persisted (inbox_id=%d, source_id=%s, type=%s)", commentMsg.InboxID, commentMsg.SourceID, event.EventType)
|
|
}
|
|
return
|
|
}
|
|
|
|
if !fbchannel.ShouldCreateMessage(event) {
|
|
return
|
|
}
|
|
incomingMsg, err := fbchannel.ExtractIncomingMessageFromEvent(event, inbox, channel.ChannelInstagram)
|
|
if err != nil {
|
|
applogger.L().Errorf("Instagram webhook: extract message failed: %v", err)
|
|
return
|
|
}
|
|
if incomingMsg != nil {
|
|
if _, persistErr := h.persister.PersistIncoming(c.Request.Context(), inbox, incomingMsg); persistErr != nil {
|
|
applogger.L().Errorf("Instagram webhook: persist message failed (source_id=%s): %v", incomingMsg.SourceID, persistErr)
|
|
return
|
|
}
|
|
applogger.L().Infof("Instagram webhook: message persisted (inbox_id=%d, source_id=%s, type=%s)", incomingMsg.InboxID, incomingMsg.SourceID, event.EventType)
|
|
}
|
|
}
|
|
|
|
func (h *FacebookWebhookHandler) persistFacebookReceipt(ctx context.Context, inbox *model.Inbox, event *fbchannel.ParsedWebhookEvent) {
|
|
if h.persister == nil {
|
|
return
|
|
}
|
|
switch event.EventType {
|
|
case fbchannel.EventFBDelivery, fbchannel.EventIGDelivery:
|
|
if event.Delivery == nil {
|
|
return
|
|
}
|
|
occurredAt := millisToTime(event.Delivery.Watermark)
|
|
for _, mid := range event.Delivery.Mids {
|
|
if err := h.persister.UpdateMessageStatus(ctx, inbox, mid, model.MessageStatusDelivered, occurredAt); err != nil {
|
|
applogger.L().Errorf("Facebook webhook: delivery status persistence failed mid=%s inbox=%d: %v", mid, inbox.ID, err)
|
|
}
|
|
}
|
|
case fbchannel.EventFBRead, fbchannel.EventIGRead:
|
|
if event.Read == nil {
|
|
return
|
|
}
|
|
if err := h.persister.UpdateContactConversationMessagesStatus(ctx, inbox, event.SenderID, model.MessageStatusRead, millisToTime(event.Read.Watermark)); err != nil {
|
|
applogger.L().Errorf("Facebook webhook: read status persistence failed sender=%s inbox=%d: %v", event.SenderID, inbox.ID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func millisToTime(value int64) *time.Time {
|
|
if value <= 0 {
|
|
return nil
|
|
}
|
|
t := time.UnixMilli(value)
|
|
return &t
|
|
}
|
|
|
|
func parseOptionalUintParam(value string) (uint, error) {
|
|
if value == "" {
|
|
return 0, nil
|
|
}
|
|
parsed, err := strconv.ParseUint(value, 10, 32)
|
|
return uint(parsed), err
|
|
}
|
|
|
|
// resolveVerifyToken extracts the webhook verify token from the inbox ChannelConfig JSON.
|
|
func (h *FacebookWebhookHandler) resolveVerifyToken(inbox *model.Inbox) string {
|
|
config := h.parseChannelConfig(inbox)
|
|
if token, ok := config["webhook_verify_token"].(string); ok {
|
|
return token
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// resolveAppSecret extracts the Facebook app secret from the inbox ChannelConfig JSON.
|
|
func (h *FacebookWebhookHandler) resolveAppSecret(inbox *model.Inbox) string {
|
|
config := h.parseChannelConfig(inbox)
|
|
if secret, ok := config["app_secret"].(string); ok {
|
|
return secret
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// parseChannelConfig parses the JSON-encoded ChannelConfig string into a map.
|
|
func (h *FacebookWebhookHandler) parseChannelConfig(inbox *model.Inbox) map[string]interface{} {
|
|
if inbox.ChannelConfig == "" {
|
|
return map[string]interface{}{}
|
|
}
|
|
var config map[string]interface{}
|
|
if err := json.Unmarshal([]byte(inbox.ChannelConfig), &config); err != nil {
|
|
applogger.L().Warnf("Failed to parse ChannelConfig for inbox %d: %v", inbox.ID, err)
|
|
return map[string]interface{}{}
|
|
}
|
|
return config
|
|
}
|