Files
gochat/internal/handler/api/v1/twilio_channel_handler.go
T
2026-06-04 15:44:48 +08:00

294 lines
9.6 KiB
Go

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"
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 LINEChannelHandler pattern: uses top-level ChannelTwilioSMSService for CRUD,
// generic InboxService for inbox lifecycle.
type TwilioChannelHandler struct {
twChannelSvc *service.ChannelTwilioSMSService
inboxSvc *service.InboxService
twRepo *repository.ChannelTwilioSMSRepo
}
// NewTwilioChannelHandler creates a new Twilio SMS channel handler.
func NewTwilioChannelHandler(
twChannelSvc *service.ChannelTwilioSMSService,
inboxSvc *service.InboxService,
twRepo *repository.ChannelTwilioSMSRepo,
) *TwilioChannelHandler {
return &TwilioChannelHandler{
twChannelSvc: twChannelSvc,
inboxSvc: inboxSvc,
twRepo: twRepo,
}
}
// CreateTwilioSMSChannelRequest is the DTO for creating a Twilio SMS channel.
type CreateTwilioSMSChannelRequest 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"`
}
// Create adds a new Twilio SMS channel and creates the associated inbox.
// POST /api/v1/accounts/:id/channels/twilio_channel
func (h *TwilioChannelHandler) Create(c *gin.Context) {
accountIDStr := c.Param("id")
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
if err != nil {
applogger.L().Errorf("Invalid account_id: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
return
}
var req CreateTwilioSMSChannelRequest
if err := c.ShouldBindJSON(&req); err != nil {
applogger.L().Errorf("Failed to bind Twilio SMS channel create request: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
ctx := c.Request.Context()
// Create the channel record first (without InboxID)
ch := &channelmodel.ChannelTwilioSMS{
AccountSID: req.AccountSID,
PhoneNumber: req.PhoneNumber,
MessagingServiceSID: req.MessagingServiceSID,
AccountID: uint(accountID),
}
if err := h.twChannelSvc.Create(ctx, ch); err != nil {
applogger.L().Errorf("Failed to create Twilio SMS channel: %v", err)
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create Twilio SMS channel"})
return
}
// Create inbox for the Twilio SMS channel
inboxName := req.InboxName
if inboxName == "" {
inboxName = req.PhoneNumber
}
inboxReq := service.CreateInboxRequest{
Name: inboxName,
ChannelType: "twilio_sms",
Enabled: true,
}
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)
// Rollback channel creation
if delErr := h.twChannelSvc.Delete(ctx, ch.ID); delErr != nil {
applogger.L().Warnf("Failed to rollback Twilio SMS channel after inbox creation failure: %v", delErr)
}
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create inbox"})
return
}
// Update channel with InboxID
ch.InboxID = inbox.ID
if err := h.twChannelSvc.Update(ctx, ch); err != nil {
applogger.L().Errorf("Failed to update Twilio SMS channel with inbox_id: %v", err)
}
c.JSON(http.StatusCreated, gin.H{
"channel": ch,
"inbox": inbox,
})
}
// Get retrieves a Twilio SMS channel by ID.
// GET /api/v1/accounts/:id/channels/twilio_channel/:tw_id
func (h *TwilioChannelHandler) Get(c *gin.Context) {
twIDStr := c.Param("tw_id")
twID, err := strconv.ParseUint(twIDStr, 10, 64)
if err != nil {
applogger.L().Errorf("Invalid tw_id: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tw_id"})
return
}
ch, err := h.twChannelSvc.GetByID(c.Request.Context(), 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
}
c.JSON(http.StatusOK, gin.H{"channel": ch})
}
// UpdateTwilioSMSChannelRequest is the DTO for updating a Twilio SMS channel.
type UpdateTwilioSMSChannelRequest struct {
PhoneNumber *string `json:"phone_number"`
MessagingServiceSID *string `json:"messaging_service_sid"`
InboxName *string `json:"inbox_name"`
}
// Update updates a Twilio SMS channel configuration.
// PATCH /api/v1/accounts/:id/channels/twilio_channel/:tw_id
func (h *TwilioChannelHandler) Update(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 UpdateTwilioSMSChannelRequest
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.StatusUnprocessableEntity, gin.H{"error": "failed to update Twilio SMS channel"})
return
}
// Update inbox name if provided
if req.InboxName != nil && ch.InboxID > 0 {
updateReq := service.UpdateInboxRequest{
Name: *req.InboxName,
}
if _, inboxErr := h.inboxSvc.Update(ctx, uint(accountID), ch.InboxID, updateReq); inboxErr != nil {
applogger.L().Warnf("Failed to update Twilio SMS inbox name: %v", inboxErr)
}
}
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,
"message": "Twilio SMS channel updated successfully",
})
}
// Delete removes a Twilio SMS channel and its associated inbox.
// DELETE /api/v1/accounts/:id/channels/twilio_channel/:tw_id
func (h *TwilioChannelHandler) Delete(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
}
// 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.StatusUnprocessableEntity, 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"})
}
// List lists all Twilio SMS channels for an account.
// GET /api/v1/accounts/:id/channels/twilio_channel
func (h *TwilioChannelHandler) List(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.StatusUnprocessableEntity, gin.H{"error": "failed to list Twilio SMS channels"})
return
}
c.JSON(http.StatusOK, gin.H{"channels": channels})
}