121 lines
4.4 KiB
Go
121 lines
4.4 KiB
Go
package webhook
|
|
|
|
// TwilioWebhookHandler processes incoming Twilio SMS webhook HTTP requests via Gin.
|
|
// Reference: Facebook webhook adapter pattern (facebook_webhook.go)
|
|
//
|
|
// URL patterns:
|
|
// /webhooks/sms/:phone_number — Chatwoot-compatible inbound SMS/MMS
|
|
// /webhooks/twilio/sms/:phone_number — legacy inbound SMS/MMS
|
|
// /webhooks/twilio/status/:phone_number — delivery status callbacks
|
|
//
|
|
// Method: POST (Twilio sends form-encoded data, not JSON)
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
|
|
twiliochannel "github.com/gochat/gochat/internal/channel/twilio"
|
|
"github.com/gochat/gochat/internal/model"
|
|
channelmodel "github.com/gochat/gochat/internal/model/channel"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// TwilioWebhookHandler processes Twilio SMS webhook requests via Gin.
|
|
type TwilioWebhookHandler struct {
|
|
twilioWebhook *twiliochannel.WebhookHandler
|
|
db *gorm.DB
|
|
persister *IncomingPersister
|
|
}
|
|
|
|
// NewTwilioWebhookHandler creates a Twilio SMS webhook handler for Gin integration.
|
|
func NewTwilioWebhookHandler(twilioWebhook *twiliochannel.WebhookHandler, db *gorm.DB) *TwilioWebhookHandler {
|
|
return &TwilioWebhookHandler{
|
|
twilioWebhook: twilioWebhook,
|
|
db: db,
|
|
persister: NewIncomingPersister(db),
|
|
}
|
|
}
|
|
|
|
// HandleTwilioInboundSMS processes an incoming Twilio SMS webhook Gin request.
|
|
func (h *TwilioWebhookHandler) HandleTwilioInboundSMS(c *gin.Context) {
|
|
phoneNumber := c.Param("phone_number")
|
|
if phoneNumber == "" {
|
|
applogger.L().Warn("Twilio webhook: missing phone_number in path")
|
|
c.Data(http.StatusOK, "application/xml", []byte("<Response></Response>"))
|
|
return
|
|
}
|
|
|
|
// Lookup inbox from database
|
|
inbox, err := h.lookupInboxByPhoneNumber(phoneNumber)
|
|
if err != nil {
|
|
applogger.L().Warnf("Twilio webhook: inbox lookup failed for phone_number %s: %v", phoneNumber, err)
|
|
c.Data(http.StatusOK, "application/xml", []byte("<Response></Response>"))
|
|
return
|
|
}
|
|
|
|
incomingMsg, err := h.twilioWebhook.ProcessInboundSMS(c.Request, inbox)
|
|
if err != nil {
|
|
applogger.L().Errorf("Twilio webhook: process inbound SMS failed for inbox %d: %v", inbox.ID, err)
|
|
c.Data(http.StatusOK, "application/xml", []byte("<Response></Response>"))
|
|
return
|
|
}
|
|
if incomingMsg != nil {
|
|
if _, persistErr := h.persister.PersistIncoming(c.Request.Context(), inbox, incomingMsg); persistErr != nil {
|
|
applogger.L().Errorf("Twilio webhook: persist inbound SMS failed for inbox %d source_id=%s: %v", inbox.ID, incomingMsg.SourceID, persistErr)
|
|
}
|
|
}
|
|
c.Data(http.StatusOK, "application/xml", []byte("<Response></Response>"))
|
|
}
|
|
|
|
// HandleTwilioDeliveryStatus processes a Twilio delivery status callback.
|
|
func (h *TwilioWebhookHandler) HandleTwilioDeliveryStatus(c *gin.Context) {
|
|
phoneNumber := c.Param("phone_number")
|
|
if phoneNumber == "" {
|
|
applogger.L().Warn("Twilio status webhook: missing phone_number in path")
|
|
c.Status(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
// Lookup inbox from database
|
|
inbox, err := h.lookupInboxByPhoneNumber(phoneNumber)
|
|
if err != nil {
|
|
applogger.L().Warnf("Twilio status webhook: inbox lookup failed for phone_number %s: %v", phoneNumber, err)
|
|
c.Status(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
// Dispatch to the channel-level delivery status handler
|
|
h.twilioWebhook.HandleDeliveryStatus(c.Writer, c.Request, inbox)
|
|
}
|
|
|
|
// lookupInbox fetches an Inbox record from the database.
|
|
func (h *TwilioWebhookHandler) 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
|
|
}
|
|
|
|
// lookupInboxByPhoneNumber fetches an Inbox through the Twilio SMS channel record.
|
|
// Chatwoot exposes /webhooks/sms/:phone_number and routes by the phone number.
|
|
func (h *TwilioWebhookHandler) lookupInboxByPhoneNumber(phoneNumber string) (*model.Inbox, error) {
|
|
if h.db == nil {
|
|
return nil, fmt.Errorf("twilio webhook database is not configured")
|
|
}
|
|
|
|
var channel channelmodel.ChannelTwilioSMS
|
|
if err := h.db.Where("phone_number = ?", phoneNumber).First(&channel).Error; err != nil {
|
|
return nil, fmt.Errorf("twilio sms channel not found for phone_number=%s: %w", phoneNumber, err)
|
|
}
|
|
|
|
var inbox model.Inbox
|
|
if err := h.db.Where("id = ? AND channel_type IN ?", channel.InboxID, []string{"twilio_sms", "sms"}).First(&inbox).Error; err != nil {
|
|
return nil, fmt.Errorf("twilio inbox not found for channel inbox_id=%d: %w", channel.InboxID, err)
|
|
}
|
|
return &inbox, nil
|
|
}
|