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, total, 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, gin.H{ "inboxes": inboxes, "meta": gin.H{"count": total, "page": page, "page_size": perPage}, }) } // @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, 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.StatusCreated, 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, 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{"id": inboxID, "deleted": true}) } // ======================================== // 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 } inbox, svcErr := h.svc.DeleteAvatar(c.Request.Context(), accountID, inboxID) if svcErr != nil { handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, inbox) } // 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, inbox) }