feat(messages): queue delivery statuses

This commit is contained in:
2026-06-05 19:05:45 +08:00
parent 1a4b4a2d29
commit dec8854ec9
11 changed files with 711 additions and 57 deletions
+59 -18
View File
@@ -13,11 +13,13 @@ package webhook
import (
"fmt"
"net/http"
"net/url"
"github.com/gochat/gochat/internal/channel"
twiliochannel "github.com/gochat/gochat/internal/channel/twilio"
"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"
"github.com/gin-gonic/gin"
@@ -31,6 +33,13 @@ type TwilioWebhookHandler struct {
persister *IncomingPersister
}
func (h *TwilioWebhookHandler) WithWorkerPool(wp *worker.WorkerPool) *TwilioWebhookHandler {
if h != nil && h.persister != nil {
h.persister.SetWorkerPool(wp)
}
return h
}
// NewTwilioWebhookHandler creates a Twilio SMS webhook handler for Gin integration.
func NewTwilioWebhookHandler(twilioWebhook *twiliochannel.WebhookHandler, db *gorm.DB, dispatcher ...*channel.Dispatcher) *TwilioWebhookHandler {
return &TwilioWebhookHandler{
@@ -74,33 +83,65 @@ func (h *TwilioWebhookHandler) HandleTwilioInboundSMS(c *gin.Context) {
// 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
}
if err := c.Request.ParseForm(); err != nil {
applogger.L().Errorf("Twilio status webhook: parse form failed for inbox %d: %v", inbox.ID, err)
c.Status(http.StatusOK)
applogger.L().Errorf("Twilio status webhook: parse form failed: %v", err)
c.Status(http.StatusNoContent)
return
}
var inbox *model.Inbox
var err error
if phoneNumber != "" {
inbox, err = h.lookupInboxByPhoneNumber(phoneNumber)
} else {
inbox, err = h.lookupDeliveryStatusInbox(c.Request.Form)
}
if err != nil {
applogger.L().Warnf("Twilio status webhook: inbox lookup failed: %v", err)
c.Status(http.StatusNoContent)
return
}
messageSID := c.Request.FormValue("MessageSid")
messageStatus := c.Request.FormValue("MessageStatus")
if mapped, ok := mapTwilioMessageStatus(messageStatus); ok {
if err := h.persister.UpdateMessageStatus(c.Request.Context(), inbox, messageSID, mapped, nil); err != nil {
if err := h.persister.UpdateMessageStatusWithError(c.Request.Context(), inbox, messageSID, mapped, nil, twilioExternalError(c.Request.FormValue("ErrorCode"), c.Request.FormValue("ErrorMessage"), messageStatus)); err != nil {
applogger.L().Errorf("Twilio status webhook: status persistence failed for inbox %d sid=%s status=%s: %v", inbox.ID, messageSID, messageStatus, err)
}
}
c.Status(http.StatusOK)
c.Status(http.StatusNoContent)
}
func twilioExternalError(errorCode, errorMessage, status string) string {
if errorCode == "" || (status != "failed" && status != "undelivered") {
return ""
}
if errorMessage != "" {
return fmt.Sprintf("%s - %s", errorCode, errorMessage)
}
return fmt.Sprintf("Twilio delivery failed with error code %s", errorCode)
}
func (h *TwilioWebhookHandler) lookupDeliveryStatusInbox(params url.Values) (*model.Inbox, error) {
if h.db == nil {
return nil, fmt.Errorf("twilio webhook database is not configured")
}
var twilioChannel channelmodel.ChannelTwilioSMS
query := h.db
if sid := params.Get("MessagingServiceSid"); sid != "" {
query = query.Where(&channelmodel.ChannelTwilioSMS{MessagingServiceSID: sid})
} else if accountSID, from := params.Get("AccountSid"), params.Get("From"); accountSID != "" && from != "" {
query = query.Where(&channelmodel.ChannelTwilioSMS{AccountSID: accountSID, PhoneNumber: from})
} else {
return nil, fmt.Errorf("delivery status missing MessagingServiceSid or AccountSid/From")
}
if err := query.First(&twilioChannel).Error; err != nil {
return nil, err
}
var inbox model.Inbox
if err := h.db.Where("id = ? AND channel_type IN ?", twilioChannel.InboxID, []string{"twilio_sms", "sms"}).First(&inbox).Error; err != nil {
return nil, err
}
return &inbox, nil
}
func mapTwilioMessageStatus(status string) (model.MessageStatus, bool) {