package v1 import ( "net/http" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/service" "github.com/gochat/gochat/pkg/pagination" "github.com/gochat/gochat/pkg/response" ) // AgentBotHandler handles AgentBot CRUD + ResetToken + ResetSecret + DeleteAvatar endpoints. // Reference: Chatwoot app/controllers/api/v1/accounts/agent_bots_controller.rb type AgentBotHandler struct { svc *service.AgentBotService } // NewAgentBotHandler creates a new AgentBot handler with service injection. func NewAgentBotHandler(svc *service.AgentBotService) *AgentBotHandler { return &AgentBotHandler{svc: svc} } // --- Account-scoped AgentBot endpoints --- // Reference: Chatwoot routes — namespace :agent_bots under :account // List retrieves all agent bots accessible to an account (global + account-scoped), paginated. // GET /api/v1/accounts/:account_id/agent_bots func (h *AgentBotHandler) List(c *gin.Context) { accountID, err := parseUintParam(c, "account_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } bots, svcErr := h.svc.ListAccessibleAll(c.Request.Context(), accountID) if svcErr != nil { handleServiceError(c, svcErr) return } payload := make([]gin.H, 0, len(bots)) for i := range bots { payload = append(payload, serializeAccountAgentBot(&bots[i], accountID)) } c.JSON(http.StatusOK, payload) } // Get retrieves a single agent bot by ID. // GET /api/v1/accounts/:account_id/agent_bots/:id func (h *AgentBotHandler) Get(c *gin.Context) { accountID, err := parseUintParam(c, "account_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } agentBotID, err := parseUintParam(c, "agent_bot_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid agent bot ID") return } bot, svcErr := h.svc.GetAccessible(c.Request.Context(), accountID, agentBotID) if svcErr != nil { handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, serializeAccountAgentBot(bot, accountID)) } // Create creates a new agent bot scoped to an account. // POST /api/v1/accounts/:account_id/agent_bots func (h *AgentBotHandler) Create(c *gin.Context) { accountID, err := parseUintParam(c, "account_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } var req service.CreateAgentBotRequest if err := c.ShouldBind(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } // Set account scope from URL param req.AccountID = &accountID bot, svcErr := h.svc.Create(c.Request.Context(), req) if svcErr != nil { handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, serializeAccountAgentBot(bot, accountID, true)) } // Update modifies an existing agent bot. // PUT /api/v1/accounts/:account_id/agent_bots/:id func (h *AgentBotHandler) Update(c *gin.Context) { accountID, err := parseUintParam(c, "account_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } agentBotID, err := parseUintParam(c, "agent_bot_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid agent bot ID") return } var req service.UpdateAgentBotRequest if err := c.ShouldBind(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } bot, svcErr := h.svc.UpdateByAccount(c.Request.Context(), accountID, agentBotID, req) if svcErr != nil { handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, serializeAccountAgentBot(bot, accountID)) } // Delete removes an agent bot by ID. // DELETE /api/v1/accounts/:account_id/agent_bots/:id func (h *AgentBotHandler) Delete(c *gin.Context) { accountID, err := parseUintParam(c, "account_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } agentBotID, err := parseUintParam(c, "agent_bot_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid agent bot ID") return } if svcErr := h.svc.DeleteByAccount(c.Request.Context(), accountID, agentBotID); svcErr != nil { handleServiceError(c, svcErr) return } c.Status(http.StatusOK) } // ResetToken generates a new access token for the bot. // POST /api/v1/accounts/:account_id/agent_bots/:id/reset_token // Reference: Chatwoot AgentBotsController#reset_access_token func (h *AgentBotHandler) ResetToken(c *gin.Context) { accountID, err := parseUintParam(c, "account_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } agentBotID, err := parseUintParam(c, "agent_bot_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid agent bot ID") return } bot, svcErr := h.svc.ResetTokenByAccount(c.Request.Context(), accountID, agentBotID) if svcErr != nil { handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, serializeAccountAgentBot(bot, accountID, true)) } // ResetSecret generates a new webhook signing secret for the bot. // POST /api/v1/accounts/:account_id/agent_bots/:id/reset_secret // Reference: Chatwoot AgentBotsController#reset_secret func (h *AgentBotHandler) ResetSecret(c *gin.Context) { accountID, err := parseUintParam(c, "account_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } agentBotID, err := parseUintParam(c, "agent_bot_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid agent bot ID") return } bot, svcErr := h.svc.ResetSecretByAccount(c.Request.Context(), accountID, agentBotID) if svcErr != nil { handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, serializeAccountAgentBot(bot, accountID, true)) } // DeleteAvatar removes the bot's avatar URL. // POST /api/v1/accounts/:account_id/agent_bots/:id/delete_avatar // Reference: Chatwoot AgentBotsController#destroy_avatar func (h *AgentBotHandler) DeleteAvatar(c *gin.Context) { accountID, err := parseUintParam(c, "account_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } agentBotID, err := parseUintParam(c, "agent_bot_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid agent bot ID") return } bot, svcErr := h.svc.DeleteAvatarByAccount(c.Request.Context(), accountID, agentBotID) if svcErr != nil { handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, serializeAccountAgentBot(bot, accountID)) } func serializeAccountAgentBot(bot *model.AgentBot, accountID uint, reveal ...bool) gin.H { if bot == nil { return gin.H{} } systemBot := bot.AccountID == nil payload := gin.H{ "id": bot.ID, "name": bot.Name, "description": bot.Description, "thumbnail": bot.AvatarURL, "bot_type": bot.BotType, "bot_config": bot.Config, "account_id": bot.AccountID, "system_bot": systemBot, } if !systemBot { payload["outgoing_url"] = bot.OutgoingURL } showSecrets := len(reveal) > 0 && reveal[0] if showSecrets && bot.AccountID != nil && *bot.AccountID == accountID && bot.AccessToken != "" { payload["access_token"] = bot.AccessToken } if showSecrets && bot.AccountID != nil && *bot.AccountID == accountID && bot.Secret != "" { payload["secret"] = bot.Secret } return payload } // --- Platform (super admin) AgentBot endpoints --- // Reference: Chatwoot platform-level agent_bots management (super_admin only) // PlatformList lists all agent bots (no account scope filter). // GET /platform/api/v1/agent_bots func (h *AgentBotHandler) PlatformList(c *gin.Context) { page := pagination.Parse(c) // For platform-level, show all bots; use accountID=0 as a sentinel // that includes global (account_id IS NULL) bots + all account bots // The FindAccessible query uses "account_id IS NULL OR account_id = ?" // with accountID=0 won't match any real account, but IS NULL still matches global bots // For true platform listing, we need a different approach — just list everything // Use a very high accountID so FindAccessible only returns global bots, // then we also need account-scoped ones. Let's use the repo directly. // Actually, let's just call List with page 0 (no account filter) // Service doesn't have a ListAll method, so let's use FindAccessible with a large sentinel // Better: we should have the platform view call ListAccessible with a special sentinel // For now, let's paginate over all bots accessible as if no account constraint // Use account_id = 0 to only get global bots in FindAccessible... not ideal. // Simplest: pass account_id as max uint to skip the = clause in FindAccessible sentinelID := uint(0) // FindAccessible with accountID=0 returns only global bots (account_id IS NULL) // because "account_id = 0" won't match any real bot. bots, total, svcErr := h.svc.ListAccessible(c.Request.Context(), sentinelID, page.Offset, page.PerPage) if svcErr != nil { handleServiceError(c, svcErr) return } // Convert total from int64 response.OKWithMeta(c, bots, page.Page, page.PerPage, total) } // PlatformCreate creates a global (platform-level) agent bot with no account scope. // POST /platform/api/v1/agent_bots func (h *AgentBotHandler) PlatformCreate(c *gin.Context) { var req service.CreateAgentBotRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } // Platform-level bot: no account scope (AccountID stays nil) req.AccountID = nil bot, svcErr := h.svc.Create(c.Request.Context(), req) if svcErr != nil { handleServiceError(c, svcErr) return } response.Created(c, bot) } // PlatformGet retrieves a single agent bot (super admin context, no account filter). // GET /platform/api/v1/agent_bots/:id func (h *AgentBotHandler) PlatformGet(c *gin.Context) { agentBotID, err := parseUintParam(c, "agent_bot_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid agent bot ID") return } bot, svcErr := h.svc.Get(c.Request.Context(), agentBotID) if svcErr != nil { handleServiceError(c, svcErr) return } response.OK(c, bot) } // PlatformUpdate modifies an agent bot (super admin context). // PUT /platform/api/v1/agent_bots/:id func (h *AgentBotHandler) PlatformUpdate(c *gin.Context) { agentBotID, err := parseUintParam(c, "agent_bot_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid agent bot ID") return } var req service.UpdateAgentBotRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } bot, svcErr := h.svc.Update(c.Request.Context(), agentBotID, req) if svcErr != nil { handleServiceError(c, svcErr) return } response.OK(c, bot) } // PlatformDelete removes an agent bot (super admin context). // DELETE /platform/api/v1/agent_bots/:id func (h *AgentBotHandler) PlatformDelete(c *gin.Context) { agentBotID, err := parseUintParam(c, "agent_bot_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid agent bot ID") return } if svcErr := h.svc.Delete(c.Request.Context(), agentBotID); svcErr != nil { handleServiceError(c, svcErr) return } response.NoContent(c) } // PlatformUpdateAvatar updates the avatar URL for an agent bot (super admin context). // PUT /platform/api/v1/agent_bots/:id/avatar // Reference: Chatwoot AgentBotsController#update_avatar (platform-level) func (h *AgentBotHandler) PlatformUpdateAvatar(c *gin.Context) { agentBotID, err := parseUintParam(c, "agent_bot_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid agent bot ID") return } var req service.AgentBotUpdateAvatarRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } bot, svcErr := h.svc.UpdateAvatar(c.Request.Context(), agentBotID, req) if svcErr != nil { handleServiceError(c, svcErr) return } response.OK(c, bot) } // PlatformResetConfig resets the configuration for an agent bot (super admin context). // POST /platform/api/v1/agent_bots/:id/reset // Reference: Chatwoot AgentBotsController#reset_config (platform-level) func (h *AgentBotHandler) PlatformResetConfig(c *gin.Context) { agentBotID, err := parseUintParam(c, "agent_bot_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid agent bot ID") return } bot, svcErr := h.svc.ResetConfig(c.Request.Context(), agentBotID) if svcErr != nil { handleServiceError(c, svcErr) return } response.OK(c, bot) } // ListAccessible retrieves agent bots accessible to an account (global + account-scoped), paginated. // GET /api/v1/accounts/:account_id/agent_bots/accessible func (h *AgentBotHandler) ListAccessible(c *gin.Context) { accountID, err := parseUintParam(c, "account_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } page := pagination.Parse(c) bots, total, svcErr := h.svc.ListAccessible(c.Request.Context(), accountID, page.Offset, page.PerPage) if svcErr != nil { handleServiceError(c, svcErr) return } response.OKWithMeta(c, bots, page.Page, page.PerPage, total) } // UpdateAvatar sets a new avatar URL for the agent bot. // PUT /api/v1/accounts/:account_id/agent_bots/:id/avatar // Reference: Chatwoot agent_bots_controller#avatar — handles avatar upload/URL update. func (h *AgentBotHandler) UpdateAvatar(c *gin.Context) { agentBotID, err := parseUintParam(c, "agent_bot_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid agent bot ID") return } var req service.AgentBotUpdateAvatarRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error()) return } bot, svcErr := h.svc.UpdateAvatar(c.Request.Context(), agentBotID, req) if svcErr != nil { handleServiceError(c, svcErr) return } response.OK(c, bot) }