Files
gochat/internal/handler/api/v1/inbox_handler.go
T

476 lines
14 KiB
Go

package v1
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/service"
"github.com/gochat/gochat/pkg/response"
)
// InboxHandler handles inbox-related API endpoints.
// Reference: Chatwoot app/controllers/api/v1/inboxes_controller.rb
type InboxHandler struct {
svc *service.InboxService
}
// NewInboxHandler creates a new InboxHandler.
func NewInboxHandler(svc *service.InboxService) *InboxHandler {
return &InboxHandler{svc: svc}
}
// @Summary List inboxes for an account
// @Description Retrieves all inboxes for an account with pagination
// @Tags Inboxes
// @Accept json
// @Produce json
// @Param id path uint true "Account ID"
// @Param page query int false "Page number" default(1)
// @Param page_size query int false "Items per page" default(25)
// @Success 200 {object} []model.Inbox
// @Failure 400 {object} model.ErrorResponse
// @Failure 401 {object} model.ErrorResponse
// @Failure 500 {object} model.ErrorResponse
// @Security ApiKeyAuth
// @Router /api/v1/accounts/{id}/inboxes [get]
// List retrieves all inboxes for an account.
// GET /api/v1/accounts/:id/inboxes
func (h *InboxHandler) List(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
page := getPage(c)
perPage := getPageSize(c)
offset := (page - 1) * perPage
inboxes, _, svcErr := h.svc.ListByAccount(c.Request.Context(), accountID, offset, perPage)
if svcErr != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list inboxes"})
return
}
c.JSON(http.StatusOK, inboxListResponse(inboxes))
}
// @Summary Get a single inbox
// @Description Retrieves detailed information about a specific inbox within an account
// @Tags Inboxes
// @Accept json
// @Produce json
// @Param id path uint true "Account ID"
// @Param inbox_id path uint true "Inbox ID"
// @Success 200 {object} model.Inbox
// @Failure 400 {object} model.ErrorResponse
// @Failure 401 {object} model.ErrorResponse
// @Failure 404 {object} model.ErrorResponse
// @Failure 500 {object} model.ErrorResponse
// @Security ApiKeyAuth
// @Router /api/v1/accounts/{id}/inboxes/{inbox_id} [get]
// Get retrieves a single inbox.
// GET /api/v1/accounts/:id/inboxes/:inbox_id
func (h *InboxHandler) Get(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
inboxID, err := parseUintParam(c, "inbox_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"})
return
}
inbox, svcErr := h.svc.GetByAccountAndID(c.Request.Context(), accountID, inboxID)
if svcErr != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "inbox not found"})
return
}
c.JSON(http.StatusOK, serializeInbox(inbox))
}
// @Summary Create a new inbox
// @Description Creates a new inbox for an account
// @Tags Inboxes
// @Accept json
// @Produce json
// @Param id path uint true "Account ID"
// @Param body body service.CreateInboxRequest true "Inbox creation payload"
// @Success 201 {object} model.Inbox
// @Failure 400 {object} model.ErrorResponse
// @Failure 401 {object} model.ErrorResponse
// @Failure 500 {object} model.ErrorResponse
// @Security ApiKeyAuth
// @Router /api/v1/accounts/{id}/inboxes [post]
// Create creates a new inbox.
// POST /api/v1/accounts/:id/inboxes
func (h *InboxHandler) Create(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
var req service.CreateInboxRequest
if bindErr := c.ShouldBindJSON(&req); bindErr != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": bindErr.Error()})
return
}
inbox, svcErr := h.svc.Create(c.Request.Context(), accountID, req)
if svcErr != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create inbox"})
return
}
c.JSON(http.StatusOK, serializeInbox(inbox))
}
// @Summary Update an inbox
// @Description Updates an existing inbox's configuration
// @Tags Inboxes
// @Accept json
// @Produce json
// @Param id path uint true "Account ID"
// @Param inbox_id path uint true "Inbox ID"
// @Param body body service.UpdateInboxRequest true "Inbox update payload"
// @Success 200 {object} model.Inbox
// @Failure 400 {object} model.ErrorResponse
// @Failure 401 {object} model.ErrorResponse
// @Failure 404 {object} model.ErrorResponse
// @Failure 500 {object} model.ErrorResponse
// @Security ApiKeyAuth
// @Router /api/v1/accounts/{id}/inboxes/{inbox_id} [put]
// Update updates an inbox.
// PUT /api/v1/accounts/:id/inboxes/:inbox_id
// Reference: Chatwoot inboxes#update
func (h *InboxHandler) Update(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
inboxID, err := parseUintParam(c, "inbox_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"})
return
}
var req service.UpdateInboxRequest
if bindErr := c.ShouldBindJSON(&req); bindErr != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": bindErr.Error()})
return
}
inbox, svcErr := h.svc.Update(c.Request.Context(), accountID, inboxID, req)
if svcErr != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to update inbox"})
return
}
c.JSON(http.StatusOK, serializeInbox(inbox))
}
// @Summary Delete an inbox
// @Description Deletes an inbox from an account
// @Tags Inboxes
// @Accept json
// @Produce json
// @Param id path uint true "Account ID"
// @Param inbox_id path uint true "Inbox ID"
// @Success 200 {object} object
// @Failure 400 {object} model.ErrorResponse
// @Failure 401 {object} model.ErrorResponse
// @Failure 404 {object} model.ErrorResponse
// @Failure 500 {object} model.ErrorResponse
// @Security ApiKeyAuth
// @Router /api/v1/accounts/{id}/inboxes/{inbox_id} [delete]
// Delete deletes an inbox.
// DELETE /api/v1/accounts/:id/inboxes/:inbox_id
// Reference: Chatwoot inboxes#destroy
func (h *InboxHandler) Delete(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
inboxID, err := parseUintParam(c, "inbox_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"})
return
}
if svcErr := h.svc.DeleteByAccount(c.Request.Context(), accountID, inboxID); svcErr != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to delete inbox"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Your inbox deletion request will be processed in some time."})
}
// ========================================
// Member-action handlers (Chatwoot InboxesController member routes)
// ========================================
// SetAgentBot sets or removes an agent bot from an inbox.
// POST /api/v1/accounts/:id/inboxes/:inbox_id/set_agent_bot
// Reference: Chatwoot InboxesController#set_agent_bot
func (h *InboxHandler) SetAgentBot(c *gin.Context) {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
inboxID, err := parseUintParam(c, "inbox_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"})
return
}
var req service.SetAgentBotRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body: " + err.Error()})
return
}
if !h.svc.Ready() {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to set agent bot")
return
}
binding, svcErr := h.svc.SetAgentBot(c.Request.Context(), accountID, inboxID, req)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
if binding == nil {
// Agent bot was removed
c.JSON(http.StatusOK, gin.H{"message": "agent bot removed from inbox"})
return
}
c.JSON(http.StatusOK, binding)
}
// Health checks the health status of an inbox's channel connection.
// GET /api/v1/accounts/:id/inboxes/:inbox_id/health
// Reference: Chatwoot InboxesController#health
func (h *InboxHandler) Health(c *gin.Context) {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
inboxID, err := parseUintParam(c, "inbox_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"})
return
}
if !h.svc.Ready() {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to check inbox health")
return
}
result, svcErr := h.svc.Health(c.Request.Context(), accountID, inboxID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, result)
}
// SyncTemplates syncs message templates for an inbox's channel (WhatsApp only).
// POST /api/v1/accounts/:id/inboxes/:inbox_id/sync_templates
// Reference: Chatwoot InboxesController#sync_templates
func (h *InboxHandler) SyncTemplates(c *gin.Context) {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
inboxID, err := parseUintParam(c, "inbox_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"})
return
}
if !h.svc.Ready() {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to sync templates")
return
}
templates, svcErr := h.svc.SyncTemplates(c.Request.Context(), accountID, inboxID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, gin.H{"templates": templates})
}
// RegisterWebhook registers a webhook URL with the channel provider for an inbox.
// POST /api/v1/accounts/:id/inboxes/:inbox_id/register_webhook
// Reference: Chatwoot InboxesController#register_webhook
func (h *InboxHandler) RegisterWebhook(c *gin.Context) {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
inboxID, err := parseUintParam(c, "inbox_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"})
return
}
var req service.RegisterWebhookRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body: " + err.Error()})
return
}
if !h.svc.Ready() {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to register webhook")
return
}
svcErr := h.svc.RegisterWebhook(c.Request.Context(), accountID, inboxID, req)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, gin.H{"message": "webhook registered successfully"})
}
// GetAgentBot retrieves the currently active agent bot for an inbox.
// GET /api/v1/accounts/:id/inboxes/:inbox_id/agent_bot
// Reference: Chatwoot InboxesController#agent_bot
func (h *InboxHandler) GetAgentBot(c *gin.Context) {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
inboxID, err := parseUintParam(c, "inbox_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"})
return
}
if !h.svc.Ready() {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to get agent bot")
return
}
agentBot, svcErr := h.svc.GetAgentBot(c.Request.Context(), accountID, inboxID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
if agentBot == nil {
c.JSON(http.StatusOK, gin.H{"agent_bot": nil})
return
}
c.JSON(http.StatusOK, gin.H{"agent_bot": agentBot})
}
// DeleteAvatar removes the avatar URL from an inbox.
// DELETE /api/v1/accounts/:id/inboxes/:inbox_id/avatar
// Reference: Chatwoot InboxesController#destroy_avatar
func (h *InboxHandler) DeleteAvatar(c *gin.Context) {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
inboxID, err := parseUintParam(c, "inbox_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"})
return
}
if !h.svc.Ready() {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to delete inbox avatar")
return
}
_, svcErr := h.svc.DeleteAvatar(c.Request.Context(), accountID, inboxID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.Status(http.StatusOK)
}
// ListCampaigns retrieves all campaigns for a specific inbox.
// GET /api/v1/accounts/:id/inboxes/:inbox_id/campaigns
// Reference: Chatwoot InboxesController#campaigns
func (h *InboxHandler) ListCampaigns(c *gin.Context) {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
inboxID, err := parseUintParam(c, "inbox_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"})
return
}
if !h.svc.Ready() {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to list campaigns")
return
}
campaigns, svcErr := h.svc.ListCampaigns(c.Request.Context(), accountID, inboxID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, gin.H{"campaigns": campaigns})
}
// ResetSecret regenerates the HMAC token for an API-type inbox.
// POST /api/v1/accounts/:id/inboxes/:inbox_id/reset_secret
// Reference: Chatwoot inboxes_controller#reset_secret — only works for API inboxes
func (h *InboxHandler) ResetSecret(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
inboxID, err := parseUintParam(c, "inbox_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"})
return
}
inbox, svcErr := h.svc.ResetSecret(c.Request.Context(), accountID, inboxID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, serializeInbox(inbox))
}