package v1 import ( "encoding/json" "errors" "io" "net/http" "strconv" "strings" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "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 auditSvc *service.AuditService } // NewInboxHandler creates a new InboxHandler. func NewInboxHandler(svc *service.InboxService) *InboxHandler { return &InboxHandler{svc: svc} } func (h *InboxHandler) WithAuditService(auditSvc *service.AuditService) *InboxHandler { h.auditSvc = auditSvc return h } // @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 := bindCreateInboxRequest(c, &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 { if renderInboxLimitExceeded(c, svcErr) { return } c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create inbox"}) return } recordAuditMutation(c, h.auditSvc, auditMutation{ AccountID: accountID, AuditableType: "Inbox", AuditableID: inbox.ID, Action: "create", AuditedChanges: serializeInbox(inbox), }) 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 := bindUpdateInboxRequest(c, &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 } recordAuditMutation(c, h.auditSvc, auditMutation{ AccountID: accountID, AuditableType: "Inbox", AuditableID: inbox.ID, Action: "update", AuditedChanges: serializeInbox(inbox), }) 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."}) } func renderInboxLimitExceeded(c *gin.Context, err error) bool { if !service.IsInboxLimitExceeded(err) { return false } c.JSON(http.StatusPaymentRequired, gin.H{"error": service.InboxLimitExceededMessage}) return true } func bindCreateInboxRequest(c *gin.Context, req *service.CreateInboxRequest) error { if strings.Contains(c.ContentType(), "json") { return bindCreateInboxJSON(c, req) } values, err := inboxFormValues(c) if err != nil { return err } applyCreateInboxForm(req, values) return nil } func bindUpdateInboxRequest(c *gin.Context, req *service.UpdateInboxRequest) error { if strings.Contains(c.ContentType(), "json") { return bindUpdateInboxJSON(c, req) } values, err := inboxFormValues(c) if err != nil { return err } applyUpdateInboxForm(req, values) return nil } func inboxFormValues(c *gin.Context) (map[string][]string, error) { values := map[string][]string{} if strings.Contains(c.ContentType(), "multipart/form-data") { if err := c.Request.ParseMultipartForm(32 << 20); err != nil { return nil, err } if c.Request.MultipartForm != nil { for key, list := range c.Request.MultipartForm.Value { values[key] = append(values[key], list...) } } return values, nil } else if err := c.Request.ParseForm(); err != nil { return nil, err } for key, list := range c.Request.PostForm { values[key] = append(values[key], list...) } for key, list := range c.Request.Form { if _, ok := values[key]; !ok { values[key] = append(values[key], list...) } } return values, nil } func bindCreateInboxJSON(c *gin.Context, req *service.CreateInboxRequest) error { body, err := io.ReadAll(c.Request.Body) if err != nil { return err } if err := json.Unmarshal(body, req); err != nil { return err } var raw map[string]any if err := json.Unmarshal(body, &raw); err != nil { return err } hoistInboxChannelJSONFields(&req.Channel, raw) return nil } func bindUpdateInboxJSON(c *gin.Context, req *service.UpdateInboxRequest) error { body, err := io.ReadAll(c.Request.Body) if err != nil { return err } if err := json.Unmarshal(body, req); err != nil { return err } var raw map[string]any if err := json.Unmarshal(body, &raw); err != nil { return err } hoistInboxChannelJSONFields(&req.Channel, raw) return nil } func hoistInboxChannelJSONFields(channel *map[string]any, raw map[string]any) { for key, value := range raw { if !isInboxChannelConfigKey(key) || value == nil { continue } if *channel == nil { *channel = map[string]any{} } if _, exists := (*channel)[key]; !exists { (*channel)[key] = value } } } func isInboxChannelConfigKey(key string) bool { switch key { case "website_url", "widget_color", "welcome_title", "welcome_tagline", "reply_time", "pre_chat_form_enabled", "pre_chat_form_options", "continuity_via_email", "hmac_mandatory", "allowed_domains", "selected_feature_flags", "webhook_url", "additional_attributes", "email", "forward_to_email", "imap_login", "imap_password", "imap_address", "imap_port", "imap_enabled", "imap_enable_ssl", "imap_authentication", "smtp_login", "smtp_password", "smtp_address", "smtp_port", "smtp_enabled", "smtp_domain", "smtp_enable_ssl_tls", "smtp_enable_starttls_auto", "smtp_openssl_verify_mode", "smtp_authentication", "provider", "provider_config", "phone_number", "message_templates", "account_sid", "auth_token", "api_key_sid", "api_key_secret", "messaging_service_sid", "medium", "content_templates", "twiml_app_sid", "voice_enabled", "line_channel_id", "line_channel_secret", "line_channel_token", "bot_token": return true default: return false } } func applyCreateInboxForm(req *service.CreateInboxRequest, values map[string][]string) { req.Channel = map[string]any{} for key, list := range values { value := inboxFormValue(list) if strings.HasPrefix(key, "channel[") { applyInboxNestedValue(req.Channel, key, list) continue } if strings.HasPrefix(key, "csat_config[") { if req.CsatConfig == nil { req.CsatConfig = map[string]any{} } applyInboxNestedValue(req.CsatConfig, key, list) continue } switch key { case "name": req.Name = value case "channel_type": req.ChannelType = value case "enabled": req.Enabled = inboxBool(value) case "enable_auto_assignment": req.EnableAutoAssignment = inboxBool(value) case "greeting_enabled": req.GreetingEnabled = inboxBoolPtr(value) case "greeting_message": req.GreetingMessage = inboxStringPtr(value) case "enable_email_collect": req.EnableEmailCollect = inboxBoolPtr(value) case "csat_survey_enabled": req.CsatSurveyEnabled = inboxBoolPtr(value) case "working_hours_enabled": req.WorkingHoursEnabled = inboxBoolPtr(value) case "out_of_office_message": req.OutOfOfficeMessage = inboxStringPtr(value) case "timezone": req.Timezone = inboxStringPtr(value) case "allow_messages_after_resolved": req.AllowMessagesAfterResolved = inboxBoolPtr(value) case "lock_to_single_conversation": req.LockToSingleConversation = inboxBoolPtr(value) case "portal_id": req.PortalID = inboxUintPtr(value) case "sender_name_type": req.SenderNameType = inboxStringPtr(value) case "business_name": req.BusinessName = inboxStringPtr(value) case "csat_config": req.CsatConfig = inboxJSONObject(value) case "working_hours": req.WorkingHours = inboxWorkingHours(value) } } } func applyUpdateInboxForm(req *service.UpdateInboxRequest, values map[string][]string) { req.Channel = map[string]any{} for key, list := range values { value := inboxFormValue(list) if strings.HasPrefix(key, "channel[") { applyInboxNestedValue(req.Channel, key, list) continue } if strings.HasPrefix(key, "csat_config[") { if req.CsatConfig == nil { req.CsatConfig = map[string]any{} } applyInboxNestedValue(req.CsatConfig, key, list) continue } switch key { case "name": req.Name = value case "enabled": req.Enabled = inboxBoolPtr(value) case "enable_auto_assignment": req.EnableAutoAssignment = inboxBoolPtr(value) case "greeting_enabled": req.GreetingEnabled = inboxBoolPtr(value) case "greeting_message": req.GreetingMessage = inboxStringPtr(value) case "enable_email_collect": req.EnableEmailCollect = inboxBoolPtr(value) case "csat_survey_enabled": req.CsatSurveyEnabled = inboxBoolPtr(value) case "working_hours_enabled": req.WorkingHoursEnabled = inboxBoolPtr(value) case "out_of_office_message": req.OutOfOfficeMessage = inboxStringPtr(value) case "timezone": req.Timezone = inboxStringPtr(value) case "allow_messages_after_resolved": req.AllowMessagesAfterResolved = inboxBoolPtr(value) case "lock_to_single_conversation": req.LockToSingleConversation = inboxBoolPtr(value) case "portal_id": req.PortalID = inboxUintPtr(value) case "sender_name_type": req.SenderNameType = inboxStringPtr(value) case "business_name": req.BusinessName = inboxStringPtr(value) case "csat_config": req.CsatConfig = inboxJSONObject(value) case "working_hours": req.WorkingHours = inboxWorkingHours(value) } } } func applyInboxNestedValue(target map[string]any, key string, values []string) { inner := strings.TrimSuffix(strings.TrimPrefix(key[strings.Index(key, "[")+1:], ""), "]") parts := strings.Split(inner, "][") if len(parts) == 0 || parts[0] == "" { return } if len(parts) == 1 || (len(parts) == 2 && parts[1] == "") { if len(parts) == 2 && parts[1] == "" { target[parts[0]] = append([]string(nil), values...) return } target[parts[0]] = inboxFormValue(values) return } nested, _ := target[parts[0]].(map[string]any) if nested == nil { nested = map[string]any{} target[parts[0]] = nested } if len(parts) == 3 && parts[2] == "" { nested[parts[1]] = append([]string(nil), values...) return } nested[parts[1]] = inboxFormValue(values) } func inboxFormValue(values []string) string { if len(values) == 0 || values[0] == "null" { return "" } return values[0] } func inboxBool(value string) bool { parsed, _ := strconv.ParseBool(value) return parsed } func inboxBoolPtr(value string) *bool { parsed := inboxBool(value) return &parsed } func inboxStringPtr(value string) *string { return &value } func inboxUintPtr(value string) *uint { if value == "" { return nil } parsed, err := strconv.ParseUint(value, 10, 64) if err != nil { return nil } uintValue := uint(parsed) return &uintValue } func inboxJSONObject(value string) map[string]any { if value == "" { return nil } var data map[string]any if err := json.Unmarshal([]byte(value), &data); err != nil { return nil } return data } func inboxWorkingHours(value string) []repository.WorkingHourUpdateParam { if value == "" { return nil } var data []repository.WorkingHourUpdateParam if err := json.Unmarshal([]byte(value), &data); err != nil { return nil } return data } // ======================================== // 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 } req := service.SetAgentBotRequest{} if c.Request.Body != nil && c.Request.ContentLength != 0 { if err := c.ShouldBindJSON(&req); err != nil { if c.Request.ContentLength < 0 && errors.Is(err, io.EOF) { // Chunked empty body behaves like omitted params in Chatwoot and disconnects the bot. } else { 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 } _, svcErr := h.svc.SetAgentBot(c.Request.Context(), accountID, inboxID, req) if svcErr != nil { handleServiceError(c, svcErr) return } c.Status(http.StatusOK) } // 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 { if errors.Is(svcErr, service.ErrInboxHealthWhatsAppCloudOnly) { c.JSON(http.StatusBadRequest, gin.H{"error": service.InboxHealthWhatsAppCloudOnlyMessage}) return } 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 } svcErr := h.svc.SyncTemplates(c.Request.Context(), accountID, inboxID) if svcErr != nil { if errors.Is(svcErr, service.ErrInboxTemplateSyncWhatsAppOnly) { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": service.InboxTemplateSyncWhatsAppOnlyMessage}) return } handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, gin.H{"message": service.InboxTemplateSyncInitiatedMessage}) } // 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 c.Request.Body != nil && c.Request.ContentLength != 0 { 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 { if errors.Is(svcErr, service.ErrInboxHealthWhatsAppCloudOnly) { c.JSON(http.StatusBadRequest, gin.H{"error": service.InboxHealthWhatsAppCloudOnlyMessage}) return } 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": gin.H{}}) return } c.JSON(http.StatusOK, gin.H{"agent_bot": serializeAgentBot(agentBot)}) } func serializeAgentBot(bot *model.AgentBot) gin.H { if bot == nil { return gin.H{} } return gin.H{ "id": bot.ID, "name": bot.Name, "description": bot.Description, "thumbnail": bot.AvatarURL, "outgoing_url": bot.OutgoingURL, "bot_type": bot.BotType, "bot_config": bot.Config, "account_id": bot.AccountID, "access_token": bot.AccessToken, } } // 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)) }