package v1 // TwilioChannelHandler handles Twilio SMS channel-specific configuration CRUD. // Reference: Chatwoot app/controllers/api/v1/accounts/channels/twilio_controller.rb // // Twilio SMS API: https://www.twilio.com/docs/sms // // gochat maps Twilio SMS channel operations to: // - POST /api/v1/accounts/:id/channels/twilio_channel → create Twilio SMS inbox // - GET /api/v1/accounts/:id/channels/twilio_channel/:tw_id → get Twilio SMS channel // - PATCH /api/v1/accounts/:id/channels/twilio_channel/:tw_id → update Twilio SMS channel // - DELETE /api/v1/accounts/:id/channels/twilio_channel/:tw_id → delete Twilio SMS channel // - GET /api/v1/accounts/:id/channels/twilio_channel → list Twilio SMS channels import ( "net/http" "strconv" "github.com/gin-gonic/gin" twiliochannel "github.com/gochat/gochat/internal/channel/twilio" channelmodel "github.com/gochat/gochat/internal/model/channel" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" applogger "github.com/gochat/gochat/pkg/logger" ) // TwilioChannelHandler handles Twilio SMS channel management. // Follows FacebookChannelHandler pattern: uses top-level ChannelTwilioSMSService for CRUD, // internal TwilioProvider for webhook setup via OnCreate. type TwilioChannelHandler struct { twChannelSvc *service.ChannelTwilioSMSService twProvider *twiliochannel.TwilioProvider inboxSvc *service.InboxService twRepo *repository.ChannelTwilioSMSRepo } // NewTwilioChannelHandler creates a new Twilio SMS channel handler. func NewTwilioChannelHandler( twChannelSvc *service.ChannelTwilioSMSService, twProvider *twiliochannel.TwilioProvider, inboxSvc *service.InboxService, twRepo *repository.ChannelTwilioSMSRepo, ) *TwilioChannelHandler { return &TwilioChannelHandler{ twChannelSvc: twChannelSvc, twProvider: twProvider, inboxSvc: inboxSvc, twRepo: twRepo, } } // CreateTwilioSMSChannel creates a new Twilio SMS channel and its associated inbox. // POST /api/v1/accounts/:id/channels/twilio_channel func (h *TwilioChannelHandler) CreateTwilioSMSChannel(c *gin.Context) { accountIDStr := c.Param("id") accountID, err := strconv.ParseUint(accountIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"}) return } var req struct { AccountSID string `json:"account_sid" validate:"required"` AuthToken string `json:"auth_token" validate:"required"` PhoneNumber string `json:"phone_number" validate:"required"` MessagingServiceSID string `json:"messaging_service_sid"` InboxName string `json:"inbox_name"` } if err := c.ShouldBindJSON(&req); err != nil { applogger.L().Errorf("Failed to bind Twilio SMS create request: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx := c.Request.Context() // Create inbox first inboxReq := service.CreateInboxRequest{ Name: req.InboxName, ChannelType: "twilio_sms", } if req.InboxName == "" { inboxReq.Name = req.PhoneNumber } inbox, err := h.inboxSvc.Create(ctx, uint(accountID), inboxReq) if err != nil { applogger.L().Errorf("Failed to create inbox for Twilio SMS channel: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create inbox"}) return } // Create Twilio SMS channel record ch := &channelmodel.ChannelTwilioSMS{ AccountSID: req.AccountSID, PhoneNumber: req.PhoneNumber, MessagingServiceSID: req.MessagingServiceSID, AccountID: uint(accountID), InboxID: inbox.ID, } if err := h.twChannelSvc.Create(ctx, ch); err != nil { applogger.L().Errorf("Failed to create Twilio SMS channel: %v", err) // Rollback inbox if delErr := h.inboxSvc.DeleteByAccount(ctx, uint(accountID), inbox.ID); delErr != nil { applogger.L().Warnf("Failed to rollback inbox after Twilio SMS channel creation failure: %v", delErr) } c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create Twilio SMS channel"}) return } // Invoke provider OnCreate for webhook setup if onCreateErr := h.twProvider.OnCreate(ctx, inbox, ch); onCreateErr != nil { applogger.L().Warnf("Twilio SMS OnCreate webhook setup failed (non-blocking): %v", onCreateErr) } c.JSON(http.StatusCreated, gin.H{ "id": ch.ID, "account_id": ch.AccountID, "inbox_id": ch.InboxID, "account_sid": ch.AccountSID, "phone_number": ch.PhoneNumber, "messaging_service_sid": ch.MessagingServiceSID, }) } // GetTwilioSMSChannel retrieves a Twilio SMS channel by ID. // GET /api/v1/accounts/:id/channels/twilio_channel/:tw_id func (h *TwilioChannelHandler) GetTwilioSMSChannel(c *gin.Context) { accountIDStr := c.Param("id") accountID, err := strconv.ParseUint(accountIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"}) return } twIDStr := c.Param("tw_id") twID, err := strconv.ParseUint(twIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tw_id"}) return } ctx := c.Request.Context() ch, err := h.twChannelSvc.GetByID(ctx, uint(twID)) if err != nil { applogger.L().Errorf("Failed to get Twilio SMS channel: %v", err) c.JSON(http.StatusNotFound, gin.H{"error": "Twilio SMS channel not found"}) return } // Verify account ownership if ch.AccountID != uint(accountID) { c.JSON(http.StatusForbidden, gin.H{"error": "Twilio SMS channel does not belong to this account"}) return } c.JSON(http.StatusOK, gin.H{ "id": ch.ID, "account_id": ch.AccountID, "inbox_id": ch.InboxID, "account_sid": ch.AccountSID, "phone_number": ch.PhoneNumber, "messaging_service_sid": ch.MessagingServiceSID, }) } // ListTwilioSMSChannels lists all Twilio SMS channels for an account. // GET /api/v1/accounts/:id/channels/twilio_channel func (h *TwilioChannelHandler) ListTwilioSMSChannels(c *gin.Context) { accountIDStr := c.Param("id") accountID, err := strconv.ParseUint(accountIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"}) return } ctx := c.Request.Context() channels, err := h.twChannelSvc.ListByAccount(ctx, uint(accountID)) if err != nil { applogger.L().Errorf("Failed to list Twilio SMS channels: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list Twilio SMS channels"}) return } result := make([]gin.H, 0, len(channels)) for _, ch := range channels { result = append(result, gin.H{ "id": ch.ID, "account_id": ch.AccountID, "inbox_id": ch.InboxID, "account_sid": ch.AccountSID, "phone_number": ch.PhoneNumber, "messaging_service_sid": ch.MessagingServiceSID, }) } c.JSON(http.StatusOK, gin.H{"channels": result}) } // DeleteTwilioSMSChannel deletes a Twilio SMS channel and its associated inbox. // DELETE /api/v1/accounts/:id/channels/twilio_channel/:tw_id func (h *TwilioChannelHandler) DeleteTwilioSMSChannel(c *gin.Context) { accountIDStr := c.Param("id") accountID, err := strconv.ParseUint(accountIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"}) return } twIDStr := c.Param("tw_id") twID, err := strconv.ParseUint(twIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tw_id"}) return } ctx := c.Request.Context() ch, err := h.twChannelSvc.GetByID(ctx, uint(twID)) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Twilio SMS channel not found"}) return } // Verify account ownership if ch.AccountID != uint(accountID) { c.JSON(http.StatusForbidden, gin.H{"error": "Twilio SMS channel does not belong to this account"}) return } // Invoke provider OnDestroy for webhook cleanup if onDestroyErr := h.twProvider.OnDestroy(ctx, inboxFromID(ch.InboxID), ch); onDestroyErr != nil { applogger.L().Warnf("Twilio SMS OnDestroy webhook cleanup failed (non-blocking): %v", onDestroyErr) } // Delete the channel record if err := h.twChannelSvc.Delete(ctx, uint(twID)); err != nil { applogger.L().Errorf("Failed to delete Twilio SMS channel: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete Twilio SMS channel"}) return } // Delete the associated inbox if ch.InboxID > 0 { if delErr := h.inboxSvc.DeleteByAccount(ctx, uint(accountID), ch.InboxID); delErr != nil { applogger.L().Warnf("Failed to delete inbox for Twilio SMS channel: %v", delErr) } } c.JSON(http.StatusOK, gin.H{"message": "Twilio SMS channel deleted successfully"}) } // UpdateTwilioSMSChannel updates a Twilio SMS channel configuration. // PATCH /api/v1/accounts/:id/channels/twilio_channel/:tw_id func (h *TwilioChannelHandler) UpdateTwilioSMSChannel(c *gin.Context) { accountIDStr := c.Param("id") accountID, err := strconv.ParseUint(accountIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"}) return } twIDStr := c.Param("tw_id") twID, err := strconv.ParseUint(twIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tw_id"}) return } ctx := c.Request.Context() ch, err := h.twChannelSvc.GetByID(ctx, uint(twID)) if err != nil { applogger.L().Errorf("Failed to get Twilio SMS channel for update: %v", err) c.JSON(http.StatusNotFound, gin.H{"error": "Twilio SMS channel not found"}) return } // Verify account ownership if ch.AccountID != uint(accountID) { c.JSON(http.StatusForbidden, gin.H{"error": "Twilio SMS channel does not belong to this account"}) return } var req struct { PhoneNumber *string `json:"phone_number"` MessagingServiceSID *string `json:"messaging_service_sid"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()}) return } if req.PhoneNumber != nil { ch.PhoneNumber = *req.PhoneNumber } if req.MessagingServiceSID != nil { ch.MessagingServiceSID = *req.MessagingServiceSID } if err := h.twChannelSvc.Update(ctx, ch); err != nil { applogger.L().Errorf("Failed to update Twilio SMS channel: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update Twilio SMS channel"}) return } c.JSON(http.StatusOK, gin.H{ "id": ch.ID, "account_id": ch.AccountID, "inbox_id": ch.InboxID, "account_sid": ch.AccountSID, "phone_number": ch.PhoneNumber, "messaging_service_sid": ch.MessagingServiceSID, }) }