Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
This commit is contained in:
@@ -0,0 +1,520 @@
|
||||
package facebook
|
||||
|
||||
// WebhookHandler parses and validates Facebook/Instagram webhook payloads.
|
||||
// Reference: Chatwoot's Facebook webhook handling:
|
||||
// - app/controllers/api/v1/accounts/channels/facebook_pages_controller.rb (CRUD + webhook)
|
||||
// - app/services/facebook/incoming_message_service.rb (message parsing)
|
||||
// - Facebook webhook verification: GET with hub.mode=subscribe, hub.verify_token, hub.challenge
|
||||
// - Facebook webhook events: POST with object=page, entry[].messaging[]
|
||||
// - Instagram webhook events: POST with object=instagram, entry[].messaging[]
|
||||
//
|
||||
// NOTE: HTTP routing is handled by the unified channel.WebhookHandler in webhook.go.
|
||||
// This file provides channel-specific payload parsing and event classification
|
||||
// that the FacebookProvider/InstagramProvider delegates to.
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gochat/gochat/internal/channel"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
applogger "github.com/gochat/gochat/pkg/logger"
|
||||
)
|
||||
|
||||
// WebhookEventType classifies the type of webhook event received.
|
||||
type WebhookEventType string
|
||||
|
||||
const (
|
||||
// Facebook Messenger events
|
||||
EventFBMessage WebhookEventType = "fb_message" // text/attachment message
|
||||
EventFBMessageEcho WebhookEventType = "fb_message_echo" // echo of agent's outgoing message
|
||||
EventFBPostback WebhookEventType = "fb_postback" // postback button click
|
||||
EventFBReferral WebhookEventType = "fb_referral" // m.me referral
|
||||
EventFBDelivery WebhookEventType = "fb_delivery" // delivery receipt
|
||||
EventFBRead WebhookEventType = "fb_read" // read receipt
|
||||
EventFBOptin WebhookEventType = "fb_optin" // opt-in event
|
||||
EventFBPassThread WebhookEventType = "fb_pass_thread" // thread control passed
|
||||
EventFBTakeThread WebhookEventType = "fb_take_thread" // thread control taken
|
||||
EventFBRequestThread WebhookEventType = "fb_request_thread" // thread control requested
|
||||
|
||||
// Instagram DM events
|
||||
EventIGMessage WebhookEventType = "ig_message" // Instagram DM text/attachment
|
||||
EventIGMessageEcho WebhookEventType = "ig_message_echo" // echo of agent's outgoing DM
|
||||
EventIGPostback WebhookEventType = "ig_postback" // Instagram postback
|
||||
EventIGDelivery WebhookEventType = "ig_delivery" // Instagram delivery receipt
|
||||
EventIGRead WebhookEventType = "ig_read" // Instagram read receipt
|
||||
|
||||
// Instagram Comment events
|
||||
EventIGComment WebhookEventType = "ig_comment" // new comment on IG media post
|
||||
EventIGCommentReply WebhookEventType = "ig_comment_reply" // reply to an IG comment
|
||||
EventIGCommentDeleted WebhookEventType = "ig_comment_deleted" // IG comment deleted
|
||||
)
|
||||
|
||||
// ParsedWebhookEvent is the result of parsing a raw FB/IG webhook payload.
|
||||
type ParsedWebhookEvent struct {
|
||||
EventType WebhookEventType
|
||||
Object string // "page" (FB) or "instagram" (IG)
|
||||
PageID string // FB Page ID or IG Business Account ID
|
||||
SenderID string // PSID (FB) or IGSID (IG)
|
||||
RecipientID string // Page-scoped recipient ID
|
||||
Message *FBMessageData // message content (for message events)
|
||||
Postback *FBPostback // postback payload (for postback events)
|
||||
Referral *FBReferral // referral data (for referral events)
|
||||
Delivery *FBDelivery // delivery receipt data
|
||||
Read *FBRead // read receipt data
|
||||
Comment *IGCommentChangeValue // comment data (for IG comment events)
|
||||
Timestamp int64 // event timestamp in milliseconds
|
||||
RawPayload []byte // original raw JSON
|
||||
}
|
||||
|
||||
// FBPostback represents a Facebook postback event payload.
|
||||
type FBPostback struct {
|
||||
Title string `json:"title"`
|
||||
Data string `json:"payload"`
|
||||
Referral *FBReferral `json:"referral,omitempty"`
|
||||
}
|
||||
|
||||
// FBReferral represents a Facebook referral (m.me link) event payload.
|
||||
type FBReferral struct {
|
||||
Ref string `json:"ref"`
|
||||
Source string `json:"source"`
|
||||
Type string `json:"type"`
|
||||
AdID string `json:"ad_id,omitempty"`
|
||||
}
|
||||
|
||||
// FBDelivery represents a Facebook message delivery receipt.
|
||||
type FBDelivery struct {
|
||||
Mids []string `json:"mids"`
|
||||
Watermark int64 `json:"watermark"`
|
||||
}
|
||||
|
||||
// FBRead represents a Facebook message read receipt.
|
||||
type FBRead struct {
|
||||
Watermark int64 `json:"watermark"`
|
||||
}
|
||||
|
||||
// WebhookParser handles parsing and classification of FB/IG webhook payloads.
|
||||
type WebhookParser struct{}
|
||||
|
||||
// NewWebhookParser creates a new webhook parser.
|
||||
func NewWebhookParser() *WebhookParser {
|
||||
return &WebhookParser{}
|
||||
}
|
||||
|
||||
// ParseWebhookPayload parses a raw FB/IG webhook JSON payload into structured events.
|
||||
// Returns one or more ParsedWebhookEvent for each messaging entry in the payload.
|
||||
//
|
||||
// Reference: Chatwoot IncomingMessageService.perform — iterates over entry[].messaging[]
|
||||
// and classifies each event type before processing.
|
||||
func (wp *WebhookParser) ParseWebhookPayload(rawPayload []byte) ([]*ParsedWebhookEvent, error) {
|
||||
var event FBWebhookEvent
|
||||
if err := json.Unmarshal(rawPayload, &event); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse webhook payload: %w", err)
|
||||
}
|
||||
|
||||
var results []*ParsedWebhookEvent
|
||||
object := event.Object // "page" (FB) or "instagram" (IG)
|
||||
|
||||
for _, entry := range event.Entry {
|
||||
pageID := entry.ID
|
||||
entryTime := entry.Time
|
||||
|
||||
// Process messaging events (FB Messenger + Instagram DMs share the same structure)
|
||||
for _, msgEvent := range entry.Messaging {
|
||||
parsed := wp.classifyMessagingEvent(object, pageID, msgEvent, entryTime, rawPayload)
|
||||
results = append(results, parsed)
|
||||
}
|
||||
|
||||
// Process standalone changes (for future: feed stories, mentions, etc.)
|
||||
for _, change := range entry.Changes {
|
||||
parsed := wp.classifyChangeEvent(object, pageID, change, entryTime, rawPayload)
|
||||
if parsed != nil {
|
||||
results = append(results, parsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
applogger.L().Debug("Webhook payload had no processable events",
|
||||
"object", object)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// classifyMessagingEvent determines the event type from a messaging entry.
|
||||
func (wp *WebhookParser) classifyMessagingEvent(object string, pageID string, msgEvent FBMessagingEvent, timestamp int64, rawPayload []byte) *ParsedWebhookEvent {
|
||||
senderID := msgEvent.Sender.ID
|
||||
recipientID := msgEvent.Recipient.ID
|
||||
eventTimestamp := msgEvent.Timestamp
|
||||
if eventTimestamp == 0 {
|
||||
eventTimestamp = timestamp
|
||||
}
|
||||
|
||||
parsed := &ParsedWebhookEvent{
|
||||
Object: object,
|
||||
PageID: pageID,
|
||||
SenderID: senderID,
|
||||
RecipientID: recipientID,
|
||||
Timestamp: eventTimestamp,
|
||||
RawPayload: rawPayload,
|
||||
}
|
||||
|
||||
// Determine if this is from FB or IG
|
||||
isInstagram := object == "instagram"
|
||||
|
||||
// Classify based on what fields are populated
|
||||
if msgEvent.Message != nil {
|
||||
parsed.Message = msgEvent.Message
|
||||
if msgEvent.Message.IsEcho {
|
||||
parsed.EventType = instagramOrFB(isInstagram, EventIGMessageEcho, EventFBMessageEcho)
|
||||
} else {
|
||||
parsed.EventType = instagramOrFB(isInstagram, EventIGMessage, EventFBMessage)
|
||||
}
|
||||
} else if msgEvent.Postback != nil {
|
||||
parsed.Postback = &FBPostback{
|
||||
Title: msgEvent.Postback.Title,
|
||||
Data: msgEvent.Postback.Payload,
|
||||
}
|
||||
if msgEvent.Postback.Referral != nil {
|
||||
parsed.Postback.Referral = &FBReferral{
|
||||
Ref: msgEvent.Postback.Referral.Ref,
|
||||
Source: msgEvent.Postback.Referral.Source,
|
||||
Type: msgEvent.Postback.Referral.Type,
|
||||
AdID: msgEvent.Postback.Referral.ADID,
|
||||
}
|
||||
}
|
||||
parsed.EventType = instagramOrFB(isInstagram, EventIGPostback, EventFBPostback)
|
||||
} else if msgEvent.Delivery != nil {
|
||||
parsed.Delivery = &FBDelivery{
|
||||
Mids: msgEvent.Delivery.Mids,
|
||||
Watermark: msgEvent.Delivery.Watermark,
|
||||
}
|
||||
parsed.EventType = instagramOrFB(isInstagram, EventIGDelivery, EventFBDelivery)
|
||||
} else if msgEvent.Read != nil {
|
||||
parsed.Read = &FBRead{
|
||||
Watermark: msgEvent.Read.Watermark,
|
||||
}
|
||||
parsed.EventType = instagramOrFB(isInstagram, EventIGRead, EventFBRead)
|
||||
} else if msgEvent.Optin != nil {
|
||||
parsed.EventType = EventFBOptin
|
||||
} else if msgEvent.PassThreadControl != nil {
|
||||
parsed.EventType = EventFBPassThread
|
||||
} else if msgEvent.TakeThreadControl != nil {
|
||||
parsed.EventType = EventFBTakeThread
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
// classifyChangeEvent processes standalone change events (e.g. feed stories).
|
||||
// Not yet fully implemented — placeholder for future expansion.
|
||||
func (wp *WebhookParser) classifyChangeEvent(object string, pageID string, change FBChangeEvent, timestamp int64, rawPayload []byte) *ParsedWebhookEvent {
|
||||
isInstagram := object == "instagram"
|
||||
|
||||
// Instagram comment webhook: object="instagram", field="comments"
|
||||
if isInstagram && change.Field == "comments" {
|
||||
commentData, err := parseIGCommentChangeValue(change.Value)
|
||||
if err != nil {
|
||||
applogger.L().Warn("Failed to parse Instagram comment change value",
|
||||
"error", err, "field", change.Field)
|
||||
return nil
|
||||
}
|
||||
|
||||
eventType := EventIGComment
|
||||
if commentData.ParentID != "" {
|
||||
eventType = EventIGCommentReply
|
||||
}
|
||||
if commentData.IsDeleted {
|
||||
eventType = EventIGCommentDeleted
|
||||
}
|
||||
|
||||
parsedTimestamp := timestamp
|
||||
if commentData.Timestamp != "" {
|
||||
// IG comment timestamps are ISO 8601 strings, convert to unix ms
|
||||
if ts, err := time.Parse(time.RFC3339, commentData.Timestamp); err == nil {
|
||||
parsedTimestamp = ts.UnixMilli()
|
||||
}
|
||||
}
|
||||
|
||||
parsed := &ParsedWebhookEvent{
|
||||
EventType: eventType,
|
||||
Object: object,
|
||||
PageID: pageID,
|
||||
SenderID: commentData.From.ID,
|
||||
Timestamp: parsedTimestamp,
|
||||
Comment: commentData,
|
||||
RawPayload: rawPayload,
|
||||
}
|
||||
|
||||
applogger.L().Debug("Parsed Instagram comment event",
|
||||
"event_type", eventType,
|
||||
"comment_id", commentData.CommentID,
|
||||
"media_id", commentData.MediaID,
|
||||
"from_id", commentData.From.ID,
|
||||
"text_length", len(commentData.Text))
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
applogger.L().Debug("Skipping unhandled change event",
|
||||
"object", object, "field", change.Field)
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseIGCommentChangeValue deserializes the map[string]interface{} value
|
||||
// from a FBChangeEvent with field="comments" into an IGCommentChangeValue struct.
|
||||
func parseIGCommentChangeValue(rawValue map[string]interface{}) (*IGCommentChangeValue, error) {
|
||||
// Re-marshal and unmarshal to get typed struct from raw map
|
||||
jsonBytes, err := json.Marshal(rawValue)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to re-marshal comment value: %w", err)
|
||||
}
|
||||
|
||||
var commentData IGCommentChangeValue
|
||||
if err := json.Unmarshal(jsonBytes, &commentData); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal comment value: %w", err)
|
||||
}
|
||||
|
||||
return &commentData, nil
|
||||
}
|
||||
|
||||
// instagramOrFB returns the Instagram or Facebook event type based on the object type.
|
||||
func instagramOrFB(isInstagram bool, igType WebhookEventType, fbType WebhookEventType) WebhookEventType {
|
||||
if isInstagram {
|
||||
return igType
|
||||
}
|
||||
return fbType
|
||||
}
|
||||
|
||||
// ValidateWebhookSignature validates the X-Hub-Signature-256 header against the raw payload.
|
||||
// Reference: Facebook requires SHA256 HMAC verification using the App Secret.
|
||||
// Chatwoot performs this in the webhook controller before processing.
|
||||
//
|
||||
// The signature header format is: sha256=<hex_digest>
|
||||
func ValidateWebhookSignature(appSecret string, signatureHeader string, rawPayload []byte) bool {
|
||||
if signatureHeader == "" {
|
||||
applogger.L().Warn("Facebook webhook: missing X-Hub-Signature-256 header")
|
||||
return false
|
||||
}
|
||||
|
||||
// Facebook sends signature as "sha256=<hex_digest>"
|
||||
// Split on "=" to extract the hex digest portion
|
||||
eqIdx := -1
|
||||
for i := 0; i < len(signatureHeader); i++ {
|
||||
if signatureHeader[i] == '=' {
|
||||
eqIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if eqIdx < 0 || signatureHeader[:eqIdx] != "sha256" {
|
||||
applogger.L().Warn("Facebook webhook: invalid signature format",
|
||||
"header", signatureHeader)
|
||||
return false
|
||||
}
|
||||
|
||||
expectedSig := signatureHeader[eqIdx+1:]
|
||||
|
||||
// Compute HMAC-SHA256 using app secret as key
|
||||
mac := hmac.New(sha256.New, []byte(appSecret))
|
||||
mac.Write(rawPayload)
|
||||
actualSig := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
// Timing-safe comparison — prevents timing attacks on signature verification
|
||||
if !hmac.Equal([]byte(expectedSig), []byte(actualSig)) {
|
||||
applogger.L().Warn("Facebook webhook: signature mismatch",
|
||||
"expected_length", len(expectedSig), "actual_length", len(actualSig))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// VerifyWebhookChallenge handles the FB webhook verification GET request.
|
||||
// Reference: Facebook requires responding with hub.challenge when hub.mode=subscribe
|
||||
// and hub.verify_token matches the configured token.
|
||||
//
|
||||
// This is called from the unified channel.WebhookHandler.HandleWebhookVerification.
|
||||
func VerifyWebhookChallenge(queryParams map[string]string, verifyToken string) (string, bool) {
|
||||
mode := queryParams["hub.mode"]
|
||||
token := queryParams["hub.verify_token"]
|
||||
challenge := queryParams["hub.challenge"]
|
||||
|
||||
if mode != "subscribe" {
|
||||
applogger.L().Debug("Facebook webhook verification: hub.mode is not subscribe",
|
||||
"mode", mode)
|
||||
return "", false
|
||||
}
|
||||
|
||||
if token != verifyToken {
|
||||
applogger.L().Warn("Facebook webhook verification: verify_token mismatch")
|
||||
return "", false
|
||||
}
|
||||
|
||||
applogger.L().Info("Facebook webhook verification successful")
|
||||
return challenge, true
|
||||
}
|
||||
|
||||
// IsEchoMessage checks if the event is an echo of an agent's outgoing message.
|
||||
// Echo messages should not create new conversations — they confirm delivery.
|
||||
// Reference: Chatwoot filters out is_echo messages in IncomingMessageService.
|
||||
func IsEchoMessage(event *ParsedWebhookEvent) bool {
|
||||
return event.EventType == EventFBMessageEcho || event.EventType == EventIGMessageEcho
|
||||
}
|
||||
|
||||
// IsDeliveryOrReadReceipt checks if the event is a delivery or read receipt.
|
||||
// Receipts should update message status, not create new messages.
|
||||
// Reference: Chatwoot handles delivery/read receipts separately.
|
||||
func IsDeliveryOrReadReceipt(event *ParsedWebhookEvent) bool {
|
||||
return event.EventType == EventFBDelivery || event.EventType == EventFBRead ||
|
||||
event.EventType == EventIGDelivery || event.EventType == EventIGRead
|
||||
}
|
||||
|
||||
// ShouldCreateMessage determines if the event should result in a new message creation.
|
||||
// Echo messages and receipts are excluded from message creation.
|
||||
func ShouldCreateMessage(event *ParsedWebhookEvent) bool {
|
||||
return !IsEchoMessage(event) && !IsDeliveryOrReadReceipt(event) &&
|
||||
!IsThreadControlEvent(event)
|
||||
}
|
||||
|
||||
// IsThreadControlEvent checks if the event is a thread control handoff.
|
||||
func IsThreadControlEvent(event *ParsedWebhookEvent) bool {
|
||||
return event.EventType == EventFBPassThread || event.EventType == EventFBTakeThread ||
|
||||
event.EventType == EventFBRequestThread
|
||||
}
|
||||
|
||||
// ExtractIncomingMessageFromEvent converts a ParsedWebhookEvent into a channel.IncomingMessage.
|
||||
// This is the main entry point called by FacebookProvider.ProcessIncoming and InstagramProvider.ProcessIncoming.
|
||||
func ExtractIncomingMessageFromEvent(event *ParsedWebhookEvent, inbox *model.Inbox, channelType channel.ChannelType) (*channel.IncomingMessage, error) {
|
||||
if !ShouldCreateMessage(event) {
|
||||
// Delivery/read receipts and echo messages are handled separately
|
||||
return nil, fmt.Errorf("event type %s does not create incoming messages", event.EventType)
|
||||
}
|
||||
|
||||
msg := &channel.IncomingMessage{
|
||||
ChannelType: channelType,
|
||||
SourceID: extractSourceID(event),
|
||||
ConversationID: event.PageID,
|
||||
SenderID: event.SenderID,
|
||||
SenderType: channel.SenderContact,
|
||||
InboxID: inbox.ID,
|
||||
AccountID: inbox.AccountID,
|
||||
ReceivedAt: millisToTime(event.Timestamp),
|
||||
}
|
||||
|
||||
// Fill content based on event type
|
||||
switch event.EventType {
|
||||
case EventFBMessage, EventIGMessage:
|
||||
if event.Message != nil {
|
||||
msg.Content = event.Message.Text
|
||||
msg.ContentType = determineContentType(event.Message)
|
||||
msg.Attachments = convertAttachments(event.Message.Attachments, channelType)
|
||||
if event.Message.ReplyTo != nil {
|
||||
msg.ReplyToID = event.Message.ReplyTo.Mid
|
||||
}
|
||||
}
|
||||
case EventFBPostback, EventIGPostback:
|
||||
if event.Postback != nil {
|
||||
msg.Content = event.Postback.Data
|
||||
msg.ContentType = channel.ContentText
|
||||
msg.Extra = map[string]interface{}{
|
||||
"postback_title": event.Postback.Title,
|
||||
}
|
||||
if event.Postback.Referral != nil {
|
||||
msg.Extra["referral_ref"] = event.Postback.Referral.Ref
|
||||
msg.Extra["referral_source"] = event.Postback.Referral.Source
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add sender/channel metadata
|
||||
msg.SenderExtra = map[string]interface{}{
|
||||
"sender_id": event.SenderID,
|
||||
"recipient_id": event.RecipientID,
|
||||
}
|
||||
msg.ConversationExtra = map[string]interface{}{
|
||||
"page_id": event.PageID,
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// extractSourceID extracts the external message ID from an event.
|
||||
func extractSourceID(event *ParsedWebhookEvent) string {
|
||||
if event.Message != nil && event.Message.Mid != "" {
|
||||
return event.Message.Mid
|
||||
}
|
||||
// For postbacks, generate a synthetic source ID
|
||||
return fmt.Sprintf("%s_%s_%d", event.PageID, event.SenderID, event.Timestamp)
|
||||
}
|
||||
|
||||
// determineContentType determines the message content type from FB message data.
|
||||
func determineContentType(msgData *FBMessageData) channel.ContentType {
|
||||
if len(msgData.Attachments) > 0 {
|
||||
att := msgData.Attachments[0]
|
||||
switch att.Type {
|
||||
case "image":
|
||||
return channel.ContentImage
|
||||
case "video":
|
||||
return channel.ContentVideo
|
||||
case "audio":
|
||||
return channel.ContentAudio
|
||||
case "file":
|
||||
return channel.ContentFile
|
||||
case "location":
|
||||
return channel.ContentLocation
|
||||
case "fallback":
|
||||
return channel.ContentFile
|
||||
default:
|
||||
return channel.ContentFile
|
||||
}
|
||||
}
|
||||
if msgData.Text != "" {
|
||||
return channel.ContentText
|
||||
}
|
||||
return channel.ContentText
|
||||
}
|
||||
|
||||
// convertAttachments converts FB attachments to gochat Attachment structs.
|
||||
func convertAttachments(fbAttachments []FBAttachment, channelType channel.ChannelType) []channel.Attachment {
|
||||
var attachments []channel.Attachment
|
||||
for _, att := range fbAttachments {
|
||||
attachment := channel.Attachment{
|
||||
URL: att.Payload.URL,
|
||||
ContentType: mimeTypeFromFBType(att.Type),
|
||||
Extra: map[string]interface{}{
|
||||
"fb_attachment_type": att.Type,
|
||||
"channel_type": string(channelType),
|
||||
},
|
||||
}
|
||||
if att.Payload.Name != "" {
|
||||
attachment.Filename = att.Payload.Name
|
||||
}
|
||||
// Add location data if applicable
|
||||
if att.Type == "location" && att.Payload.Coordinates != nil {
|
||||
attachment.Extra["latitude"] = att.Payload.Coordinates.Lat
|
||||
attachment.Extra["longitude"] = att.Payload.Coordinates.Long
|
||||
}
|
||||
attachments = append(attachments, attachment)
|
||||
}
|
||||
return attachments
|
||||
}
|
||||
|
||||
// millisToTime converts Unix milliseconds to time.Time.
|
||||
func millisToTime(millis int64) time.Time {
|
||||
if millis == 0 {
|
||||
return time.Now()
|
||||
}
|
||||
return time.UnixMilli(millis)
|
||||
}
|
||||
|
||||
// IsIGCommentEvent checks if the parsed event is an Instagram comment event.
|
||||
func IsIGCommentEvent(event *ParsedWebhookEvent) bool {
|
||||
return event.EventType == EventIGComment ||
|
||||
event.EventType == EventIGCommentReply ||
|
||||
event.EventType == EventIGCommentDeleted
|
||||
}
|
||||
Reference in New Issue
Block a user