feat(webhook): align chatwoot ingress routes

This commit is contained in:
2026-06-04 23:20:58 +08:00
parent 6aa62c6d7b
commit 9e3f561bed
12 changed files with 467 additions and 120 deletions
+35 -16
View File
@@ -4,17 +4,19 @@ package webhook
// Reference: Facebook webhook adapter pattern (facebook_webhook.go)
//
// URL patterns:
// /webhooks/twilio/sms/:inbox_id — inbound SMS/MMS
// /webhooks/twilio/status/:inbox_id — delivery status callbacks
// /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"
"strconv"
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"
@@ -37,18 +39,17 @@ func NewTwilioWebhookHandler(twilioWebhook *twiliochannel.WebhookHandler, db *go
// HandleTwilioInboundSMS processes an incoming Twilio SMS webhook Gin request.
func (h *TwilioWebhookHandler) HandleTwilioInboundSMS(c *gin.Context) {
inboxIDStr := c.Param("inbox_id")
inboxID, err := strconv.ParseUint(inboxIDStr, 10, 32)
if err != nil {
applogger.L().Warnf("Twilio webhook: invalid inbox_id %s", inboxIDStr)
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.lookupInbox(uint(inboxID))
inbox, err := h.lookupInboxByPhoneNumber(phoneNumber)
if err != nil {
applogger.L().Warnf("Twilio webhook: inbox lookup failed for id %d: %v", inboxID, err)
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
}
@@ -60,18 +61,17 @@ func (h *TwilioWebhookHandler) HandleTwilioInboundSMS(c *gin.Context) {
// HandleTwilioDeliveryStatus processes a Twilio delivery status callback.
func (h *TwilioWebhookHandler) HandleTwilioDeliveryStatus(c *gin.Context) {
inboxIDStr := c.Param("inbox_id")
inboxID, err := strconv.ParseUint(inboxIDStr, 10, 32)
if err != nil {
applogger.L().Warnf("Twilio status webhook: invalid inbox_id %s", inboxIDStr)
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.lookupInbox(uint(inboxID))
inbox, err := h.lookupInboxByPhoneNumber(phoneNumber)
if err != nil {
applogger.L().Warnf("Twilio status webhook: inbox lookup failed for id %d: %v", inboxID, err)
applogger.L().Warnf("Twilio status webhook: inbox lookup failed for phone_number %s: %v", phoneNumber, err)
c.Status(http.StatusOK)
return
}
@@ -87,4 +87,23 @@ func (h *TwilioWebhookHandler) lookupInbox(inboxID uint) (*model.Inbox, error) {
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
}