feat(whatsapp): ✨ Enhance webhook message handling
* Add support for new message types: audio, sticker, location, contacts, interactive, button, reaction, order, system, and unknown. * Implement logging for various webhook events for better visibility. * Update WebhookMessage struct to include new fields for enhanced message processing.
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/events"
|
||||
@@ -44,14 +45,53 @@ func (c *Client) HandleWebhook(r *http.Request) error {
|
||||
func (c *Client) processChange(change WebhookChange) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Process messages
|
||||
for _, msg := range change.Value.Messages {
|
||||
c.processMessage(ctx, msg, change.Value.Contacts)
|
||||
}
|
||||
// Handle different field types
|
||||
switch change.Field {
|
||||
case "messages":
|
||||
// Process messages
|
||||
for _, msg := range change.Value.Messages {
|
||||
c.processMessage(ctx, msg, change.Value.Contacts)
|
||||
}
|
||||
|
||||
// Process statuses
|
||||
for _, status := range change.Value.Statuses {
|
||||
c.processStatus(ctx, status)
|
||||
// Process statuses
|
||||
for _, status := range change.Value.Statuses {
|
||||
c.processStatus(ctx, status)
|
||||
}
|
||||
|
||||
case "message_template_status_update":
|
||||
// Log template status updates for visibility
|
||||
logging.Info("Message template status update received",
|
||||
"account_id", c.id,
|
||||
"phone_number_id", change.Value.Metadata.PhoneNumberID)
|
||||
|
||||
case "account_update":
|
||||
// Log account updates
|
||||
logging.Info("Account update received",
|
||||
"account_id", c.id,
|
||||
"phone_number_id", change.Value.Metadata.PhoneNumberID)
|
||||
|
||||
case "phone_number_quality_update":
|
||||
// Log quality updates
|
||||
logging.Info("Phone number quality update received",
|
||||
"account_id", c.id,
|
||||
"phone_number_id", change.Value.Metadata.PhoneNumberID)
|
||||
|
||||
case "phone_number_name_update":
|
||||
// Log name updates
|
||||
logging.Info("Phone number name update received",
|
||||
"account_id", c.id,
|
||||
"phone_number_id", change.Value.Metadata.PhoneNumberID)
|
||||
|
||||
case "account_alerts":
|
||||
// Log account alerts
|
||||
logging.Warn("Account alert received",
|
||||
"account_id", c.id,
|
||||
"phone_number_id", change.Value.Metadata.PhoneNumberID)
|
||||
|
||||
default:
|
||||
logging.Debug("Unknown webhook field type",
|
||||
"account_id", c.id,
|
||||
"field", change.Field)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,12 +170,114 @@ func (c *Client) processMessage(ctx context.Context, msg WebhookMessage, contact
|
||||
}
|
||||
}
|
||||
|
||||
case "audio":
|
||||
if msg.Audio != nil {
|
||||
messageType = "audio"
|
||||
mimeType = msg.Audio.MimeType
|
||||
|
||||
// Download and process media
|
||||
data, _, err := c.downloadMedia(ctx, msg.Audio.ID)
|
||||
if err != nil {
|
||||
logging.Error("Failed to download audio", "account_id", c.id, "media_id", msg.Audio.ID, "error", err)
|
||||
} else {
|
||||
filename, mediaURL = c.processMediaData(msg.ID, data, mimeType, &mediaBase64)
|
||||
}
|
||||
}
|
||||
|
||||
case "sticker":
|
||||
if msg.Sticker != nil {
|
||||
messageType = "sticker"
|
||||
mimeType = msg.Sticker.MimeType
|
||||
|
||||
// Download and process media
|
||||
data, _, err := c.downloadMedia(ctx, msg.Sticker.ID)
|
||||
if err != nil {
|
||||
logging.Error("Failed to download sticker", "account_id", c.id, "media_id", msg.Sticker.ID, "error", err)
|
||||
} else {
|
||||
filename, mediaURL = c.processMediaData(msg.ID, data, mimeType, &mediaBase64)
|
||||
}
|
||||
}
|
||||
|
||||
case "location":
|
||||
if msg.Location != nil {
|
||||
messageType = "location"
|
||||
// Format location as text
|
||||
text = fmt.Sprintf("Location: %s (%s) - %.6f, %.6f",
|
||||
msg.Location.Name, msg.Location.Address,
|
||||
msg.Location.Latitude, msg.Location.Longitude)
|
||||
}
|
||||
|
||||
case "contacts":
|
||||
if len(msg.Contacts) > 0 {
|
||||
messageType = "contacts"
|
||||
// Format contacts as text
|
||||
var contactNames []string
|
||||
for _, contact := range msg.Contacts {
|
||||
contactNames = append(contactNames, contact.Name.FormattedName)
|
||||
}
|
||||
text = fmt.Sprintf("Shared %d contact(s): %s", len(msg.Contacts), strings.Join(contactNames, ", "))
|
||||
}
|
||||
|
||||
case "interactive":
|
||||
if msg.Interactive != nil {
|
||||
messageType = "interactive"
|
||||
switch msg.Interactive.Type {
|
||||
case "button_reply":
|
||||
if msg.Interactive.ButtonReply != nil {
|
||||
text = msg.Interactive.ButtonReply.Title
|
||||
}
|
||||
case "list_reply":
|
||||
if msg.Interactive.ListReply != nil {
|
||||
text = msg.Interactive.ListReply.Title
|
||||
}
|
||||
case "nfm_reply":
|
||||
if msg.Interactive.NfmReply != nil {
|
||||
text = msg.Interactive.NfmReply.Body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case "button":
|
||||
if msg.Button != nil {
|
||||
messageType = "button"
|
||||
text = msg.Button.Text
|
||||
}
|
||||
|
||||
case "reaction":
|
||||
if msg.Reaction != nil {
|
||||
messageType = "reaction"
|
||||
text = msg.Reaction.Emoji
|
||||
}
|
||||
|
||||
case "order":
|
||||
if msg.Order != nil {
|
||||
messageType = "order"
|
||||
text = fmt.Sprintf("Order with %d item(s): %s", len(msg.Order.ProductItems), msg.Order.Text)
|
||||
}
|
||||
|
||||
case "system":
|
||||
if msg.System != nil {
|
||||
messageType = "system"
|
||||
text = msg.System.Body
|
||||
}
|
||||
|
||||
case "unknown":
|
||||
messageType = "unknown"
|
||||
logging.Warn("Received unknown message type", "account_id", c.id, "message_id", msg.ID)
|
||||
return
|
||||
|
||||
default:
|
||||
logging.Warn("Unsupported message type", "account_id", c.id, "type", msg.Type)
|
||||
return
|
||||
}
|
||||
|
||||
// Publish message received event
|
||||
logging.Debug("Publishing message received event",
|
||||
"account_id", c.id,
|
||||
"message_id", msg.ID,
|
||||
"from", msg.From,
|
||||
"type", messageType)
|
||||
|
||||
c.eventBus.Publish(events.MessageReceivedEvent(
|
||||
ctx,
|
||||
c.id,
|
||||
@@ -276,7 +418,10 @@ func getExtensionFromMimeType(mimeType string) string {
|
||||
"text/plain": ".txt",
|
||||
"application/json": ".json",
|
||||
"audio/mpeg": ".mp3",
|
||||
"audio/mp4": ".m4a",
|
||||
"audio/ogg": ".ogg",
|
||||
"audio/amr": ".amr",
|
||||
"audio/opus": ".opus",
|
||||
}
|
||||
|
||||
if ext, ok := extensions[mimeType]; ok {
|
||||
|
||||
@@ -116,15 +116,26 @@ type WebhookProfile struct {
|
||||
|
||||
// WebhookMessage represents a message in the webhook
|
||||
type WebhookMessage struct {
|
||||
From string `json:"from"` // Sender phone number
|
||||
ID string `json:"id"` // Message ID
|
||||
Timestamp string `json:"timestamp"` // Unix timestamp as string
|
||||
Type string `json:"type"` // "text", "image", "video", "document", etc.
|
||||
Text *WebhookText `json:"text,omitempty"`
|
||||
Image *WebhookMediaMessage `json:"image,omitempty"`
|
||||
Video *WebhookMediaMessage `json:"video,omitempty"`
|
||||
Document *WebhookDocumentMessage `json:"document,omitempty"`
|
||||
Context *WebhookContext `json:"context,omitempty"` // Reply context
|
||||
From string `json:"from"` // Sender phone number
|
||||
ID string `json:"id"` // Message ID
|
||||
Timestamp string `json:"timestamp"` // Unix timestamp as string
|
||||
Type string `json:"type"` // "text", "image", "video", "document", "audio", "sticker", "location", "contacts", "interactive", "button", "order", "system", "unknown", "reaction"
|
||||
Text *WebhookText `json:"text,omitempty"`
|
||||
Image *WebhookMediaMessage `json:"image,omitempty"`
|
||||
Video *WebhookMediaMessage `json:"video,omitempty"`
|
||||
Document *WebhookDocumentMessage `json:"document,omitempty"`
|
||||
Audio *WebhookMediaMessage `json:"audio,omitempty"`
|
||||
Sticker *WebhookMediaMessage `json:"sticker,omitempty"`
|
||||
Location *WebhookLocation `json:"location,omitempty"`
|
||||
Contacts []WebhookContactCard `json:"contacts,omitempty"`
|
||||
Interactive *WebhookInteractive `json:"interactive,omitempty"`
|
||||
Button *WebhookButton `json:"button,omitempty"`
|
||||
Reaction *WebhookReaction `json:"reaction,omitempty"`
|
||||
Order *WebhookOrder `json:"order,omitempty"`
|
||||
System *WebhookSystem `json:"system,omitempty"`
|
||||
Context *WebhookContext `json:"context,omitempty"` // Reply context
|
||||
Identity *WebhookIdentity `json:"identity,omitempty"`
|
||||
Referral *WebhookReferral `json:"referral,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookText represents a text message
|
||||
@@ -156,6 +167,156 @@ type WebhookContext struct {
|
||||
MessageID string `json:"message_id,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookLocation represents a location message
|
||||
type WebhookLocation struct {
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Address string `json:"address,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookContactCard represents a contact card
|
||||
type WebhookContactCard struct {
|
||||
Addresses []WebhookContactAddress `json:"addresses,omitempty"`
|
||||
Birthday string `json:"birthday,omitempty"`
|
||||
Emails []WebhookContactEmail `json:"emails,omitempty"`
|
||||
Name WebhookContactName `json:"name"`
|
||||
Org WebhookContactOrg `json:"org,omitempty"`
|
||||
Phones []WebhookContactPhone `json:"phones,omitempty"`
|
||||
URLs []WebhookContactURL `json:"urls,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookContactAddress represents a contact address
|
||||
type WebhookContactAddress struct {
|
||||
City string `json:"city,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
CountryCode string `json:"country_code,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Street string `json:"street,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Zip string `json:"zip,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookContactEmail represents a contact email
|
||||
type WebhookContactEmail struct {
|
||||
Email string `json:"email,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookContactName represents a contact name
|
||||
type WebhookContactName struct {
|
||||
FormattedName string `json:"formatted_name"`
|
||||
FirstName string `json:"first_name,omitempty"`
|
||||
LastName string `json:"last_name,omitempty"`
|
||||
MiddleName string `json:"middle_name,omitempty"`
|
||||
Suffix string `json:"suffix,omitempty"`
|
||||
Prefix string `json:"prefix,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookContactOrg represents a contact organization
|
||||
type WebhookContactOrg struct {
|
||||
Company string `json:"company,omitempty"`
|
||||
Department string `json:"department,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookContactPhone represents a contact phone
|
||||
type WebhookContactPhone struct {
|
||||
Phone string `json:"phone,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
WaID string `json:"wa_id,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookContactURL represents a contact URL
|
||||
type WebhookContactURL struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookInteractive represents an interactive message response
|
||||
type WebhookInteractive struct {
|
||||
Type string `json:"type"` // "button_reply", "list_reply"
|
||||
ButtonReply *WebhookButtonReply `json:"button_reply,omitempty"`
|
||||
ListReply *WebhookListReply `json:"list_reply,omitempty"`
|
||||
NfmReply *WebhookNfmReply `json:"nfm_reply,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookButtonReply represents a button reply
|
||||
type WebhookButtonReply struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// WebhookListReply represents a list reply
|
||||
type WebhookListReply struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookNfmReply represents a native flow message reply
|
||||
type WebhookNfmReply struct {
|
||||
ResponseJSON string `json:"response_json"`
|
||||
Body string `json:"body"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// WebhookButton represents a quick reply button
|
||||
type WebhookButton struct {
|
||||
Text string `json:"text"`
|
||||
Payload string `json:"payload"`
|
||||
}
|
||||
|
||||
// WebhookReaction represents a reaction to a message
|
||||
type WebhookReaction struct {
|
||||
MessageID string `json:"message_id"`
|
||||
Emoji string `json:"emoji"`
|
||||
}
|
||||
|
||||
// WebhookOrder represents an order
|
||||
type WebhookOrder struct {
|
||||
CatalogID string `json:"catalog_id"`
|
||||
ProductItems []WebhookProductItem `json:"product_items"`
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookProductItem represents a product in an order
|
||||
type WebhookProductItem struct {
|
||||
ProductRetailerID string `json:"product_retailer_id"`
|
||||
Quantity int `json:"quantity"`
|
||||
ItemPrice float64 `json:"item_price"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
// WebhookSystem represents a system message
|
||||
type WebhookSystem struct {
|
||||
Body string `json:"body,omitempty"`
|
||||
Type string `json:"type,omitempty"` // "customer_changed_number", "customer_identity_changed", etc.
|
||||
Identity string `json:"identity,omitempty"`
|
||||
NewWaID string `json:"new_wa_id,omitempty"`
|
||||
WaID string `json:"wa_id,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookIdentity represents identity information
|
||||
type WebhookIdentity struct {
|
||||
Acknowledged bool `json:"acknowledged"`
|
||||
CreatedTimestamp string `json:"created_timestamp"`
|
||||
Hash string `json:"hash"`
|
||||
}
|
||||
|
||||
// WebhookReferral represents referral information
|
||||
type WebhookReferral struct {
|
||||
SourceURL string `json:"source_url"`
|
||||
SourceID string `json:"source_id,omitempty"`
|
||||
SourceType string `json:"source_type"`
|
||||
Headline string `json:"headline,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
MediaType string `json:"media_type,omitempty"`
|
||||
ImageURL string `json:"image_url,omitempty"`
|
||||
VideoURL string `json:"video_url,omitempty"`
|
||||
ThumbnailURL string `json:"thumbnail_url,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookStatus represents a message status update
|
||||
type WebhookStatus struct {
|
||||
ID string `json:"id"` // Message ID
|
||||
@@ -199,26 +360,26 @@ type TokenDebugResponse struct {
|
||||
|
||||
// TokenDebugData contains token validation information
|
||||
type TokenDebugData struct {
|
||||
AppID string `json:"app_id"`
|
||||
Type string `json:"type"`
|
||||
Application string `json:"application"`
|
||||
DataAccessExpiresAt int64 `json:"data_access_expires_at"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
IsValid bool `json:"is_valid"`
|
||||
IssuedAt int64 `json:"issued_at,omitempty"`
|
||||
Scopes []string `json:"scopes"`
|
||||
UserID string `json:"user_id"`
|
||||
AppID string `json:"app_id"`
|
||||
Type string `json:"type"`
|
||||
Application string `json:"application"`
|
||||
DataAccessExpiresAt int64 `json:"data_access_expires_at"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
IsValid bool `json:"is_valid"`
|
||||
IssuedAt int64 `json:"issued_at,omitempty"`
|
||||
Scopes []string `json:"scopes"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
// PhoneNumberDetails represents phone number information from the API
|
||||
type PhoneNumberDetails struct {
|
||||
ID string `json:"id"`
|
||||
VerifiedName string `json:"verified_name"`
|
||||
CodeVerificationStatus string `json:"code_verification_status"`
|
||||
DisplayPhoneNumber string `json:"display_phone_number"`
|
||||
QualityRating string `json:"quality_rating"`
|
||||
PlatformType string `json:"platform_type"`
|
||||
Throughput ThroughputInfo `json:"throughput"`
|
||||
ID string `json:"id"`
|
||||
VerifiedName string `json:"verified_name"`
|
||||
CodeVerificationStatus string `json:"code_verification_status"`
|
||||
DisplayPhoneNumber string `json:"display_phone_number"`
|
||||
QualityRating string `json:"quality_rating"`
|
||||
PlatformType string `json:"platform_type"`
|
||||
Throughput ThroughputInfo `json:"throughput"`
|
||||
}
|
||||
|
||||
// ThroughputInfo contains throughput information
|
||||
@@ -228,8 +389,8 @@ type ThroughputInfo struct {
|
||||
|
||||
// BusinessAccountDetails represents business account information
|
||||
type BusinessAccountDetails struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
TimezoneID string `json:"timezone_id"`
|
||||
MessageTemplateNamespace string `json:"message_template_namespace,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
TimezoneID string `json:"timezone_id"`
|
||||
MessageTemplateNamespace string `json:"message_template_namespace,omitempty"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user