package router import ( "time" "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" _ "github.com/gochat/gochat/docs" // swaggo: trigger docs init() to register SwaggerInfo "github.com/gochat/gochat/internal/auth" "github.com/gochat/gochat/internal/config" v1 "github.com/gochat/gochat/internal/handler/api/v1" "github.com/gochat/gochat/internal/handler/webhook" "github.com/gochat/gochat/internal/handler/widget" ws "github.com/gochat/gochat/internal/handler/ws" "github.com/gochat/gochat/internal/middleware" wspkg "github.com/gochat/gochat/internal/ws" "gorm.io/gorm" ) // startTime records when the application process launched, used for uptime in /health. var startTime = time.Now() // Handlers holds all instantiated handler structs for route registration. // Passed from bootstrap to avoid global state and keep dependency wiring explicit. type Handlers struct { Auth *v1.AuthHandler MFA *v1.MFAHandler SAML *v1.SAMLHandler Account *v1.AccountHandler Contact *v1.ContactHandler Conversation *v1.ConversationHandler Inbox *v1.InboxHandler InboxMember *v1.InboxMemberHandler Message *v1.MessageHandler Profile *v1.ProfileHandler Notification *v1.NotificationHandler PlatformApp *v1.PlatformAppHandler Team *v1.TeamHandler CaptainAssistant *v1.CaptainAssistantHandler CaptainDocument *v1.CaptainDocumentHandler CaptainScenario *v1.CaptainScenarioHandler CaptainCustomTool *v1.CaptainCustomToolHandler CaptainTask *v1.CaptainTaskHandler CaptainPreference *v1.CaptainPreferenceHandler CaptainTaskExtended *v1.CaptainTaskExtendedHandler CaptainAssistantResponse *v1.CaptainAssistantResponseHandler CaptainBulkAction *v1.CaptainBulkActionHandler BulkAction *v1.BulkActionHandler Copilot *v1.CopilotHandler WebWidget *v1.WebWidgetHandler WebWidgetTheme *v1.WebWidgetThemeHandler WebWidgetPreChat *v1.WebWidgetPreChatHandler WebWidgetOffline *v1.WebWidgetOfflineHandler Analytics *v1.AnalyticsHandler LiveReport *v1.LiveReportHandler DashboardApp *v1.DashboardAppHandler Portal *v1.PortalHandler Category *v1.CategoryHandler Article *v1.ArticleHandler Folder *v1.FolderHandler PortalMember *v1.PortalMemberHandler AutomationRule *v1.AutomationRuleHandler Macro *v1.MacroHandler CsatSurvey *v1.CsatSurveyHandler CannedResponse *v1.CannedResponseHandler PushSubscription *v1.PushSubscriptionHandler NotificationSetting *v1.NotificationSettingHandler NotificationSubscription *v1.NotificationSubscriptionHandler WebhookSubscription *v1.WebhookSubscriptionHandler TelegramWebhook *webhook.TelegramWebhookHandler FacebookWebhook *webhook.FacebookWebhookHandler WhatsAppWebhook *webhook.WhatsAppWebhookHandler TikTokWebhook *webhook.TikTokWebhookHandler LineWebhook *webhook.LineWebhookHandler TwilioWebhook *webhook.TwilioWebhookHandler ShopifyWebhook *webhook.ShopifyWebhookHandler AssignmentPolicy *v1.AssignmentPolicyHandler Label *v1.LabelHandler Search *v1.SearchHandler Campaign *v1.CampaignHandler Widget *widget.WidgetHandler // M13: SSO/SAML enterprise authentication handlers AccountSamlSettings *v1.AccountSamlSettingsHandler SSOSession *v1.SSOSessionHandler // M13: LDAP/OIDC enterprise authentication handlers LDAP *v1.LDAPHandler OIDC *v1.OIDCHandler SSOMiddleware *auth.SSOMiddleware InstagramChannel *v1.InstagramChannelHandler FacebookChannel *v1.FacebookChannelHandler TwitterChannel *v1.TwitterChannelHandler MicrosoftChannel *v1.MicrosoftChannelHandler GoogleChannel *v1.GoogleChannelHandler // New channel handlers: TikTok, LINE, Twilio SMS, Email TikTokChannel *v1.TikTokChannelHandler LINEChannel *v1.LINEChannelHandler TwilioSMSChannel *v1.TwilioChannelHandler EmailChannel *v1.EmailChannelHandler EmailWebhook *webhook.EmailWebhookHandler // M12: AgentBot handlers (platform-level + account-level bots) AgentBot *v1.AgentBotHandler InstallationConfig *v1.InstallationConfigHandler WidgetTest *v1.WidgetTestHandler AgentBotInbox *v1.AgentBotInboxHandler // P9: AgentBot rule engine + trigger config handlers BotRule *v1.BotRuleHandler BotTriggerConfig *v1.BotTriggerConfigHandler // M12: SSE streaming + conversation insight handlers SSEStream *v1.SSEStreamHandler ConversationInsight *v1.ConversationInsightHandler // M4 G3: ContactInbox handler (filter endpoint at account scope) ContactInboxFilter *v1.ContactInboxHandler // Conversation participant + draft message handlers ConversationParticipant *v1.ConversationParticipantHandler DraftMessage *v1.DraftMessageHandler // G4: Company module handler (CRUD + search + nested contacts/conversations/notes) Company *v1.CompanyHandler // Custom attributes + custom filters (attribute definitions, attribute values, saved filters) CustomAttributeDefinition *v1.CustomAttributeDefinitionHandler CustomAttributeValue *v1.CustomAttributeValueHandler CustomFilter *v1.CustomFilterHandler // M11: SLA Policy handler (CRUD + applied SLA metrics/download + inbox associations) SlaPolicy *v1.SlaPolicyHandler // M11: Assignment Policy V2 handler (enhanced with Type field + inbox join model) AssignmentPolicyV2 *v1.AssignmentPolicyV2Handler // Enterprise: AuditLog, CustomRole, AgentCapacityPolicy, CsatMetrics Audit *v1.AuditHandler CustomRole *v1.CustomRoleHandler AgentCapacity *v1.AgentCapacityHandler CsatMetrics *v1.CsatMetricsHandler // G16: Third-party integration handlers (IntegrationHook CRUD + Slack/Shopify/Linear/Notion) IntegrationHook *v1.IntegrationHookHandler SlackIntegration *v1.SlackIntegrationHandler ShopifyIntegration *v1.ShopifyIntegrationHandler LinearIntegration *v1.LinearIntegrationHandler NotionIntegration *v1.NotionIntegrationHandler PlatformUserSSO *v1.PlatformUserSSOHandler // Platform API AccessToken-authenticated handlers (distinct from SuperAdmin routes) PlatformUser *v1.PlatformUserHandler PlatformAccount *v1.PlatformAccountHandler PlatformAgentBot *v1.PlatformAgentBotHandler PlatformAccountUser *v1.PlatformAccountUserHandler // Upload handler (account-level file upload + widget direct uploads) Upload *v1.UploadHandler // Lane B: AssignableAgent (find agents available for assignment to conversations) AssignableAgent *v1.AssignableAgentHandler AgentBulk *v1.AgentBulkHandler Agent *v1.AgentHandler // Lane C: CSAT template (singular per inbox) + Inbox limits InboxCsatTemplate *v1.InboxCsatTemplateHandler InboxLimit *v1.InboxLimitHandler WorkingHour *v1.WorkingHourHandler // Delivery status per message per recipient DeliveryStatus *v1.DeliveryStatusHandler // ReportingEvent handler (P11 — raw analytics events) ReportingEvent *v1.ReportingEventHandler // WhatsAppCall handler (WhatsApp voice call tracking) WhatsAppCall *v1.WhatsAppCallHandler // Banner handler (Platform CRUD + account read-only) Banner *v1.BannerHandler // EmailChannelMigration handler (account-scoped create-only) EmailChannelMigration *v1.EmailChannelMigrationHandler // SummaryReport handler (read-only reporting resource — agent/team/inbox/label summaries) SummaryReport *v1.SummaryReportHandler } // RegisterRoutes sets up all HTTP routes on the Gin engine. // Reference: Chatwoot config/routes.rb — the central routing definition // that maps 327+ routes across API v1, platform, widget, and webhook namespaces. func RegisterRoutes( engine *gin.Engine, jwtService *auth.JWTService, refreshStore *auth.RefreshTokenStore, webhookRegistry *auth.WebhookTokenRegistry, handlers *Handlers, hub *ws.Hub, wsAuthenticator *wspkg.WSAuthenticator, jwtCfg *config.JWTConfig, corsCfg middleware.CORSConfig, db *gorm.DB, ) { // Health check endpoint (ref: Chatwoot health_check route) engine.GET("/health", healthCheck) // Swagger UI — interactive API documentation engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) // Auth routes — PUBLIC, no AuthRequired middleware v1.RegisterAuthRoutes(engine.Group("/api/v1"), handlers.Auth) v1.RegisterChatwootAuthRoutes(engine.Group("/auth"), handlers.Auth) // SAML routes — PUBLIC, no AuthRequired middleware (SAML flow is external) v1.RegisterSAMLRoutes(engine.Group("/api/v1"), handlers.SAML) // LDAP routes — PUBLIC, no AuthRequired middleware (LDAP bind is external) v1.RegisterLDAPRoutes(engine.Group("/api/v1"), handlers.LDAP) // OIDC routes — mixed: public auth flow + admin config routes (OIDC flow is external) v1.RegisterOIDCRoutes(engine.Group("/api/v1"), handlers.OIDC, middleware.AuthMiddleware(jwtCfg)) // MFA routes — require authentication for enable/verify/disable mfaPublic := engine.Group("/api/v1") mfaPublic.Use(middleware.AuthMiddleware(jwtCfg)) v1.RegisterMFARoutes(mfaPublic, handlers.MFA) // API v1 routes — authenticated, account-scoped apiV1 := engine.Group("/api/v1") apiV1.Use(middleware.AuthMiddleware(jwtCfg)) registerV1Routes(apiV1, handlers) // Platform API routes — super admin only (ref: Chatwoot namespace :platform_app) // Chatwoot uses a single /platform/api/v1 prefix with AccessTokenable concern in controller layer // for auth differentiation. We merge SuperAdmin and AccessToken routes into one group. platform := engine.Group("/platform/api/v1") platform.Use(middleware.AuthMiddleware(jwtCfg)) // SuperAdmin-only routes use additional middleware; AccessToken routes are open to platform app tokens // Register both sets of routes in the same group (Gin does not allow duplicate prefix groups) registerPlatformRoutes(platform, handlers) registerPlatformTokenRoutes(platform, handlers) // Widget API routes — public + CORS for embed (ref: Chatwoot namespace :widget_api) widget := engine.Group("/widget") widget.Use(middleware.CORS(corsCfg)) registerWidgetRoutes(widget, handlers.Widget) // Widget direct file upload — visitor uploads file before conversation starts // Reference: Chatwoot POST /widget/direct_uploads widget.POST("/direct_uploads", handlers.Upload.DirectUpload) widget.PUT("/direct_uploads/:upload_uuid", handlers.Upload.CompleteWidgetDirectUpload) // Chatwoot widget API routes — public + CORS for reused Chatwoot frontend/widget. // Reference: Chatwoot namespace :api/:v1/:widget at /api/v1/widget/*. widgetV1 := engine.Group("/api/v1/widget") widgetV1.Use(middleware.CORS(corsCfg)) registerChatwootWidgetRoutes(widgetV1, handlers) // Public CSAT survey routes — no auth required (customer submits rating) // Reference: Chatwoot /public/api/v1/conversations/:uuid/csats publicAPI := engine.Group("/public/api/v1") publicAPI.Use(middleware.CORS(corsCfg)) { publicAPI.GET("/conversations/:conversation_uuid/csats", handlers.CsatSurvey.PublicGet) publicAPI.POST("/conversations/:conversation_uuid/csats", handlers.CsatSurvey.PublicUpdate) publicAPI.GET("/csat_survey/:id", handlers.CsatSurvey.PublicGet) publicAPI.PUT("/csat_survey/:id", handlers.CsatSurvey.PublicUpdate) publicAPI.PATCH("/csat_survey/:id", handlers.CsatSurvey.PublicUpdate) // Public API compatibility routes used by Chatwoot public inbox/contact flows. publicInboxes := publicAPI.Group("/inboxes") { publicInboxes.GET("/:inbox_id", handlers.Widget.PublicInboxShow) contacts := publicInboxes.Group("/:inbox_id/contacts") { contacts.POST("", handlers.Widget.PublicCreateContact) contacts.GET("/:contact_id", handlers.Widget.PublicGetContact) contacts.PUT("/:contact_id", handlers.Widget.PublicUpdateContact) contacts.PATCH("/:contact_id", handlers.Widget.PublicUpdateContact) conversations := contacts.Group("/:contact_id/conversations") { conversations.GET("", handlers.Widget.PublicListConversations) conversations.POST("", handlers.Widget.PublicCreateConversation) conversations.GET("/:conversation_id", handlers.Widget.PublicGetConversation) conversations.POST("/:conversation_id/toggle_status", handlers.Widget.PublicToggleStatus) conversations.POST("/:conversation_id/toggle_typing", handlers.Widget.PublicToggleTyping) conversations.POST("/:conversation_id/update_last_seen", handlers.Widget.PublicUpdateLastSeen) messages := conversations.Group("/:conversation_id/messages") { messages.GET("", handlers.Widget.PublicListMessages) messages.POST("", handlers.Widget.PublicCreateMessage) messages.PUT("/:message_id", handlers.Widget.PublicUpdateMessage) messages.PATCH("/:message_id", handlers.Widget.PublicUpdateMessage) } } } } } // API v2 routes — authenticated, account-scoped report APIs. apiV2 := engine.Group("/api/v2") apiV2.Use(middleware.AuthMiddleware(jwtCfg)) registerV2Routes(apiV2, handlers) // Webhook callback routes. // Reference: Chatwoot routes provider-specific public webhook paths and lets // each provider handler verify its own token/signature contract. webhookGroup := engine.Group("/webhooks") // Facebook/Instagram webhook — Meta Business Suite combined endpoint // GET: webhook verification (hub.mode=subscribe, hub.verify_token, hub.challenge) // POST: incoming message/event processing (X-Hub-Signature-256 validated) // Reference: Chatwoot routes at /webhooks/facebook/:page_id if handlers.FacebookWebhook != nil { fbGroup := webhookGroup.Group("/facebook") fbGroup.GET("/:page_id", handlers.FacebookWebhook.HandleFacebookVerification) fbGroup.POST("/:page_id", handlers.FacebookWebhook.HandleFacebookWebhook) } // Telegram webhook — bot token in URL path for routing // Reference: Chatwoot routes at /webhooks/telegram/:bot_token tgGroup := webhookGroup.Group("/telegram") tgGroup.POST("/:bot_token", func(c *gin.Context) { if handlers == nil || handlers.TelegramWebhook == nil { chatwootParityStub(c) return } handlers.TelegramWebhook.HandleTelegramWebhook(c) }) // WhatsApp webhook — Cloud API webhook verification + incoming events // GET: webhook verification (hub.mode=subscribe, hub.verify_token, hub.challenge) // POST: incoming message/event processing (X-Hub-Signature-256 validated) // Reference: Chatwoot routes at /webhooks/whatsapp/:phone_number // Reference: WhatsApp Cloud API https://developers.facebook.com/docs/whatsapp/cloud-api/get-started#verify-webhook waGroup := webhookGroup.Group("/whatsapp") waGroup.GET("/:phone_number", func(c *gin.Context) { if handlers == nil || handlers.WhatsAppWebhook == nil { chatwootParityStub(c) return } handlers.WhatsAppWebhook.HandleWhatsAppVerification(c) }) waGroup.POST("/:phone_number", func(c *gin.Context) { if handlers == nil || handlers.WhatsAppWebhook == nil { chatwootParityStub(c) return } handlers.WhatsAppWebhook.HandleWhatsAppWebhook(c) }) // TikTok webhook — Business API event notifications // GET: webhook URL verification request // POST: incoming message/event processing (X-TikTok-Signature validated) // Reference: TikTok Business API https://business-api.tiktok.com/portal/docs?id=1739584855420928 ttGroup := webhookGroup.Group("/tiktok") ttGroup.POST("", func(c *gin.Context) { if handlers == nil || handlers.TikTokWebhook == nil { chatwootParityStub(c) return } handlers.TikTokWebhook.HandleTikTokWebhook(c) }) ttGroup.GET("/:business_id", func(c *gin.Context) { if handlers == nil || handlers.TikTokWebhook == nil { chatwootParityStub(c) return } handlers.TikTokWebhook.HandleTikTokVerification(c) }) ttGroup.POST("/:business_id", func(c *gin.Context) { if handlers == nil || handlers.TikTokWebhook == nil { chatwootParityStub(c) return } handlers.TikTokWebhook.HandleTikTokWebhook(c) }) // LINE webhook — Messaging API event notifications // POST: incoming message/event processing (X-Line-Signature HMAC-SHA256 validated) // Reference: LINE Messaging API https://developers.line.biz/en/docs/messaging-api/receiving-messages/ lineGroup := webhookGroup.Group("/line") lineGroup.POST("/:line_channel_id", func(c *gin.Context) { if handlers == nil || handlers.LineWebhook == nil { chatwootParityStub(c) return } handlers.LineWebhook.HandleLineWebhook(c) }) // Twilio SMS webhook — inbound SMS/MMS + delivery status callbacks // POST /sms/:phone_number: inbound SMS/MMS messages (form-encoded body, TwiML response) // POST /status/:phone_number: delivery status callbacks (delivered/undelivered/failed) // Reference: Twilio SMS API https://www.twilio.com/docs/sms/api/message-resource webhookGroup.POST("/sms/:phone_number", func(c *gin.Context) { if handlers == nil || handlers.TwilioWebhook == nil { chatwootParityStub(c) return } handlers.TwilioWebhook.HandleTwilioInboundSMS(c) }) twilioGroup := webhookGroup.Group("/twilio") twilioGroup.POST("/sms/:phone_number", func(c *gin.Context) { if handlers == nil || handlers.TwilioWebhook == nil { chatwootParityStub(c) return } handlers.TwilioWebhook.HandleTwilioInboundSMS(c) }) twilioGroup.POST("/status/:phone_number", func(c *gin.Context) { if handlers == nil || handlers.TwilioWebhook == nil { chatwootParityStub(c) return } handlers.TwilioWebhook.HandleTwilioDeliveryStatus(c) }) // Twitter webhook — Account Activity API CRC validation + event processing // GET: CRC challenge response (crc_token query param) // POST: incoming DM/event processing // Reference: Twitter API v2 Account Activity API https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/account-activity-api twWebhookGroup := webhookGroup.Group("/twitter") twWebhookGroup.GET("", func(c *gin.Context) { if handlers == nil || handlers.TwitterChannel == nil { chatwootParityStub(c) return } handlers.TwitterChannel.WebhookCRC(c) }) twWebhookGroup.POST("", func(c *gin.Context) { if handlers == nil || handlers.TwitterChannel == nil { chatwootParityStub(c) return } handlers.TwitterChannel.WebhookEvent(c) }) // Legacy GoChat aliases kept for already configured Twitter webhooks. twWebhookGroup.GET("/webhook", func(c *gin.Context) { if handlers == nil || handlers.TwitterChannel == nil { chatwootParityStub(c) return } handlers.TwitterChannel.WebhookCRC(c) }) twWebhookGroup.POST("/webhook", func(c *gin.Context) { if handlers == nil || handlers.TwitterChannel == nil { chatwootParityStub(c) return } handlers.TwitterChannel.WebhookEvent(c) }) webhookGroup.GET("/instagram", func(c *gin.Context) { if handlers == nil || handlers.FacebookWebhook == nil { chatwootParityStub(c) return } handlers.FacebookWebhook.HandleInstagramVerification(c) }) webhookGroup.POST("/instagram", func(c *gin.Context) { if handlers == nil || handlers.FacebookWebhook == nil { chatwootParityStub(c) return } handlers.FacebookWebhook.HandleInstagramWebhook(c) }) webhookGroup.POST("/shopify", func(c *gin.Context) { if handlers == nil || handlers.ShopifyWebhook == nil { chatwootParityStub(c) return } handlers.ShopifyWebhook.HandleShopifyWebhook(c) }) // Microsoft webhook — Graph API subscription validation + notifications // POST: validation request (returns validationToken) + change notifications // Reference: Microsoft Graph API https://learn.microsoft.com/en-us/graph/api/resources/webhooks if handlers.MicrosoftChannel != nil { msWebhookGroup := webhookGroup.Group("/microsoft") msWebhookGroup.POST("/validation", handlers.MicrosoftChannel.WebhookValidation) msWebhookGroup.POST("/events", handlers.MicrosoftChannel.WebhookEvent) } // Email webhook — IMAP/SMTP inbound email processing + verification // POST: inbound email processing (parsed from IMAP or SMTP relay) // GET: email verification (MX/DNS record validation for custom domain setup) // Reference: Chatwoot email channel webhook https://www.chatwoot.com/docs/product/channels/email if handlers.EmailWebhook != nil { emailWebhookGroup := webhookGroup.Group("/email") emailWebhookGroup.POST("/:inbox_id", handlers.EmailWebhook.HandleEmailWebhook) emailWebhookGroup.GET("/:inbox_id/verification", handlers.EmailWebhook.HandleEmailVerification) } // Generic webhook success fallback was removed for P6.7: provider paths above // should either process/verify the callback or return explicit not-implemented. // WebSocket endpoints (ref: Chatwoot ActionCable mount at /cable) // P9: real-time communication — WebSocket upgrade with JWT/pubsub_token auth wsHandler := ws.NewHandler(hub, wsAuthenticator) engine.GET("/ws", wsHandler.ServeWS) // Primary WebSocket endpoint engine.GET("/cable", wsHandler.ServeCable) // ActionCable-compatible endpoint (Chatwoot convention) } // registerV1Routes maps all API v1 resource routes. // Reference: Chatwoot routes.rb namespace :api, scope :v1 func registerV1Routes(g *gin.RouterGroup, h *Handlers) { // Profile routes — not scoped to account g.GET("/profile", h.Profile.Get) g.PUT("/profile", h.Profile.Update) g.PUT("/profile/avatar", h.Profile.UpdateAvatar) g.DELETE("/profile/avatar", h.Profile.DeleteAvatar) g.POST("/profile/availability", h.Profile.SetAvailability) g.POST("/profile/auto_offline", h.Profile.SetAutoOffline) g.PUT("/profile/set_active_account", h.Profile.SetActiveAccount) g.POST("/profile/resend_confirmation", h.Profile.ResendConfirmation) g.POST("/profile/reset_access_token", h.Profile.ResetAccessToken) // MFA routes under profile scope (Chatwoot: scope module: 'profile' do resource :mfa) // GET /profile/mfa — show status, POST /profile/mfa — create (enable), DELETE /profile/mfa — destroy (disable) // POST /profile/mfa/verify — verify TOTP code, POST /profile/mfa/backup_codes — generate backup codes profileMfa := g.Group("/profile/mfa") { profileMfa.GET("/", h.MFA.MFAStatus) profileMfa.POST("/", h.MFA.EnableMFA) profileMfa.DELETE("/", h.MFA.DisableMFA) profileMfa.POST("/verify", h.MFA.VerifyMFA) profileMfa.POST("/backup_codes", h.MFA.BackupCodes) } // Notification routes — user-scoped, not account-scoped // Reference: Chatwoot also has user-scoped notification routes (not account-scoped) g.GET("/notifications", h.Notification.List) g.GET("/notifications/unread_count", h.Notification.UnreadCount) g.POST("/notifications/read_all", h.Notification.MarkAllRead) g.PUT("/notifications/:id", h.Notification.Update) g.DELETE("/notifications/:id", h.Notification.Destroy) g.POST("/notifications/:id/snooze", h.Notification.Snooze) g.POST("/notifications/:id/unread", h.Notification.Unread) g.DELETE("/notifications/destroy_all", h.Notification.DestroyAll) // notification_settings is now registered under account-scoped routes (see notifSettings group below) // Push subscription routes — user-scoped push notification device management (P4 M8) g.POST("/push_subscriptions", h.PushSubscription.Create) g.GET("/push_subscriptions", h.PushSubscription.List) g.DELETE("/push_subscriptions/:id", h.PushSubscription.Delete) // Notification subscription routes — user-scoped, browser_push/fcm subscription management // Reference: Chatwoot resource :notification_subscriptions, only: [:create, :destroy] g.POST("/notification_subscriptions", h.NotificationSubscription.Create) g.DELETE("/notification_subscriptions/:identifier", h.NotificationSubscription.Destroy) // SSO session management routes — user-scoped, authenticated (M13) // Reference: Chatwoot SsoSession management — list/terminate sessions v1.RegisterSSOSessionRoutes(g.Group("/sso_sessions"), h.SSOSession) // Webhook subscription routes — account-scoped webhook event management (P4 M8) // Reference: Chatwoot api/v1/accounts/:account_id/webhooks g.GET("/accounts/:account_id/webhooks", h.WebhookSubscription.List) g.POST("/accounts/:account_id/webhooks", h.WebhookSubscription.Create) g.GET("/accounts/:account_id/webhooks/:webhook_id", h.WebhookSubscription.Get) g.PUT("/accounts/:account_id/webhooks/:webhook_id", h.WebhookSubscription.Update) g.DELETE("/accounts/:account_id/webhooks/:webhook_id", h.WebhookSubscription.Delete) // Account routes — scoped with AccountScope middleware (ref: Chatwoot namespace :accounts) // GetAll is outside AccountScope — platform admin level listing of all accounts g.GET("/accounts/all", h.Account.GetAll) accounts := g.Group("/accounts") accounts.Use(middleware.AccountScope()) { accounts.GET("/", h.Account.List) accounts.POST("/", h.Account.Create) accounts.GET("/:account_id", h.Account.Get) accounts.PUT("/:account_id", h.Account.Update) accounts.DELETE("/:account_id", h.Account.Delete) // Account settings (ref: Chatwoot accounts#update settings subset) accounts.PUT("/:account_id/settings", h.Account.UpdateSettings) // Account file upload route (ref: Chatwoot api/v1/accounts/:account_id/upload) accounts.POST("/:account_id/upload", h.Upload.Upload) // Account direct file upload — staged upload returning blob UUID for message attachment // Reference: Chatwoot POST /api/v1/accounts/:account_id/direct_uploads accounts.POST("/:account_id/direct_uploads", h.Upload.AccountDirectUpload) // M11: WebWidget offline messages account-level routes // List all pending offline messages across inboxes for this account accounts.GET("/:account_id/web_widgets/offline_messages", h.WebWidgetOffline.ListOfflineMessages) accounts.PUT("/:account_id/web_widgets/offline_messages/:offline_message_id/dismiss", h.WebWidgetOffline.DismissOfflineMessage) accounts.PUT("/:account_id/web_widgets/offline_messages/:offline_message_id/convert", h.WebWidgetOffline.ConvertOfflineMessage) // Account agents (ref: Chatwoot agents_controller.rb — resources :agents, only: [:index, :create, :update, :destroy]) // Chatwoot: GET /api/v1/accounts/:account_id/agents (index) accounts.GET("/:account_id/agents", h.Agent.List) // Chatwoot: POST /api/v1/accounts/:account_id/agents (create) accounts.POST("/:account_id/agents", h.Agent.Create) // Chatwoot: PUT /api/v1/accounts/:account_id/agents/:id (update) accounts.PUT("/:account_id/agents/:agent_id", h.Agent.Update) // Chatwoot: DELETE /api/v1/accounts/:account_id/agents/:id (destroy) accounts.DELETE("/:account_id/agents/:agent_id", h.Agent.Delete) // Chatwoot: GET /api/v1/accounts/:account_id/agents/:id (show) — was missing accounts.GET("/:account_id/agents/:agent_id", h.Agent.Get) // Chatwoot: POST /api/v1/accounts/:account_id/agents/bulk_create accounts.POST("/:account_id/agents/bulk_create", h.Agent.BulkCreate) // Bulk assign/unassign conversations to agents (ref: Chatwoot agents_controller.rb #bulk_assign, #bulk_unassign) accounts.POST("/:account_id/agents/bulk_assign", h.AgentBulk.BulkAssign) accounts.POST("/:account_id/agents/bulk_unassign", h.AgentBulk.BulkUnassign) // Assignable agents — standalone resource (ref: Chatwoot resources :assignable_agents, only: [:index]) // Chatwoot: GET /api/v1/accounts/:account_id/assignable_agents (with inbox_ids[] query param) accounts.GET("/:account_id/assignable_agents", h.AssignableAgent.List) // Bulk actions for conversations/contacts (ref: Chatwoot bulk_actions_controller.rb) accounts.POST("/:account_id/bulk_actions", h.BulkAction.Create) // Account members (ref: Chatwoot namespace :account_users) accounts.GET("/:account_id/users", h.Account.ListUsers) accounts.POST("/:account_id/users", h.Account.AddUser) accounts.DELETE("/:account_id/users/:user_id", h.Account.RemoveUser) // Account extensions (ref: Chatwoot accounts_controller.rb#update_active_at, #cache_keys) accounts.POST("/:account_id/update_active_at", h.Account.UpdateActiveAt) accounts.GET("/:account_id/cache_keys", h.Account.CacheKeys) // Nested resources under :id accountScoped := accounts.Group("/:account_id") { // Inbox routes (ref: Chatwoot nested resources :inboxes) inboxes := accountScoped.Group("/inboxes") { inboxes.GET("/", h.Inbox.List) inboxes.POST("/", h.Inbox.Create) inboxes.GET("/:inbox_id", h.Inbox.Get) inboxes.PUT("/:inbox_id", h.Inbox.Update) inboxes.PATCH("/:inbox_id", h.Inbox.Update) inboxes.DELETE("/:inbox_id", h.Inbox.Delete) // Inbox member-action routes (ref: Chatwoot InboxesController member actions) // POST /api/v1/accounts/:id/inboxes/:inbox_id/set_agent_bot — assign/remove agent bot inboxes.POST("/:inbox_id/set_agent_bot", h.Inbox.SetAgentBot) // GET /api/v1/accounts/:id/inboxes/:inbox_id/health — check channel health inboxes.GET("/:inbox_id/health", h.Inbox.Health) // POST /api/v1/accounts/:id/inboxes/:inbox_id/sync_templates — sync WhatsApp templates inboxes.POST("/:inbox_id/sync_templates", h.Inbox.SyncTemplates) // POST /api/v1/accounts/:id/inboxes/:inbox_id/register_webhook — register channel webhook inboxes.POST("/:inbox_id/register_webhook", h.Inbox.RegisterWebhook) // GET /api/v1/accounts/:id/inboxes/:inbox_id/agent_bot — get currently active agent bot inboxes.GET("/:inbox_id/agent_bot", h.Inbox.GetAgentBot) // DELETE /api/v1/accounts/:id/inboxes/:inbox_id/avatar — remove inbox avatar inboxes.DELETE("/:inbox_id/avatar", h.Inbox.DeleteAvatar) // GET /api/v1/accounts/:id/inboxes/:inbox_id/campaigns — list campaigns for inbox inboxes.GET("/:inbox_id/campaigns", h.Inbox.ListCampaigns) inboxes.POST("/:inbox_id/reset_secret", h.Inbox.ResetSecret) // WebWidget config routes(ref: Chatwoot web_widget_config_controller.rb) // /api/v1/accounts/:id/inboxes/web_widget — create widget inbox inboxes.POST("/web_widget", h.WebWidget.CreateWebWidgetInbox) // /api/v1/accounts/:id/inboxes/:inbox_id/web_widget_config — CRUD widget config inboxes.GET("/:inbox_id/web_widget_config", h.WebWidget.GetWebWidgetConfig) inboxes.PUT("/:inbox_id/web_widget_config", h.WebWidget.UpdateWebWidgetConfig) // /api/v1/accounts/:id/inboxes/:inbox_id/web_widget — delete widget inbox inboxes.DELETE("/:inbox_id/web_widget", h.WebWidget.DeleteWebWidgetInbox) // M11: WebWidget theme_config & pre_chat_form admin routes // Extended configuration beyond basic widget_color inboxes.GET("/:inbox_id/web_widget/theme_config", h.WebWidgetTheme.GetThemeConfig) inboxes.PUT("/:inbox_id/web_widget/theme_config", h.WebWidgetTheme.UpdateThemeConfig) inboxes.DELETE("/:inbox_id/web_widget/theme_config", h.WebWidgetTheme.DeleteThemeConfig) inboxes.GET("/:inbox_id/web_widget/pre_chat_form", h.WebWidgetPreChat.GetPreChatForm) inboxes.PUT("/:inbox_id/web_widget/pre_chat_form", h.WebWidgetPreChat.UpdatePreChatForm) inboxes.DELETE("/:inbox_id/web_widget/pre_chat_form", h.WebWidgetPreChat.DeletePreChatForm) // M11: WebWidget offline messages admin routes // Agents can view pending offline messages and dismiss/convert them inboxes.GET("/:inbox_id/web_widget/offline_messages", h.WebWidgetOffline.ListOfflineMessagesByInbox) // InboxMember (seat assignment) routes (ref: Chatwoot inbox_members_controller) // /api/v1/accounts/:id/inboxes/:inbox_id/members — manage agent-to-inbox assignments inboxes.GET("/:inbox_id/members", h.InboxMember.ListMembers) inboxes.POST("/:inbox_id/members", h.InboxMember.AddMember) inboxes.PATCH("/:inbox_id/members/:user_id", h.InboxMember.UpdateMember) inboxes.PATCH("/:inbox_id/members/update_multiple", h.InboxMember.UpdateMultiple) inboxes.DELETE("/:inbox_id/members/:user_id", h.InboxMember.RemoveMember) // Account-level inbox_members (Chatwoot: resources :inbox_members, param: :inbox_id) inboxMembers := accountScoped.Group("/inbox_members") { inboxMembers.POST("/", h.InboxMember.CreateAccountScoped) inboxMembers.PATCH("/", h.InboxMember.UpdateAccountScoped) inboxMembers.DELETE("/", h.InboxMember.DestroyAccountScoped) inboxMembers.GET("/:inbox_id", h.InboxMember.ShowAccountScoped) } // CSAT template (ref: Chatwoot resource :csat_template, singular per inbox) // GET /api/v1/accounts/:id/inboxes/:inbox_id/csat_template — show template status // POST /api/v1/accounts/:id/inboxes/:inbox_id/csat_template — create/update template // POST /api/v1/accounts/:id/inboxes/:inbox_id/csat_template/analyze — analyze template quality inboxes.GET("/:inbox_id/csat_template", h.InboxCsatTemplate.Show) inboxes.POST("/:inbox_id/csat_template", h.InboxCsatTemplate.Create) inboxes.POST("/:inbox_id/csat_template/analyze", h.InboxCsatTemplate.Analyze) // Inbox limits (ref: Chatwoot resources :inbox_limits, create/update/destroy only) // POST /api/v1/accounts/:id/inboxes/:inbox_id/inbox_limits — create limit // PATCH /api/v1/accounts/:id/inboxes/:inbox_id/inbox_limits/:id — update limit // DELETE /api/v1/accounts/:id/inboxes/:inbox_id/inbox_limits/:id — delete limit inboxes.POST("/:inbox_id/inbox_limits", h.InboxLimit.Create) inboxes.PATCH("/:inbox_id/inbox_limits/:id", h.InboxLimit.Update) inboxes.DELETE("/:inbox_id/inbox_limits/:id", h.InboxLimit.Delete) // Working hours (ref: Chatwoot OutOfOffisable concern) // GET /api/v1/accounts/:id/inboxes/:inbox_id/working_hours — weekly schedule // PUT /api/v1/accounts/:id/inboxes/:inbox_id/working_hours — update schedule // GET /api/v1/accounts/:id/inboxes/:inbox_id/out_of_office — check out-of-office status inboxes.GET("/:inbox_id/working_hours", h.WorkingHour.GetWeeklySchedule) inboxes.PUT("/:inbox_id/working_hours", h.WorkingHour.UpdateWeeklySchedule) inboxes.GET("/:inbox_id/out_of_office", h.WorkingHour.IsOutOfOffice) // Assignable agents (ref: Chatwoot InboxesController #assignable_agents) // GET /api/v1/accounts/:id/inboxes/:inbox_id/assignable_agents — list agents assignable to conversations inboxes.GET("/:inbox_id/assignable_agents", h.AssignableAgent.List) } // Contact merge (ref: Chatwoot ContactsController #merge) // POST /api/v1/accounts/:account_id/contacts/merge — merge two contacts // Also: POST /api/v1/accounts/:account_id/actions/contact_merge — Chatwoot resource path accountScoped.POST("/actions/contact_merge", h.Contact.Merge) accountScoped.POST("/contacts/merge", h.Contact.Merge) accountScoped.POST("/contact_merge", h.Contact.Merge) // Facebook channel routes (ref: Chatwoot channels/facebook_pages_controller) // POST /accounts/:id/channels/facebook_channel → create FB Messenger inbox fbChannels := accountScoped.Group("/channels/facebook_channel") { fbChannels.POST("/", h.FacebookChannel.CreateFacebookPage) fbChannels.GET("/authorization", h.FacebookChannel.Authorization) fbChannels.POST("/oauth_callback", h.FacebookChannel.OAuthCallback) fbChannels.POST("/reauthorize", h.FacebookChannel.ReauthorizeFacebookPage) fbChannels.GET("/:fb_id", h.FacebookChannel.GetFacebookPage) fbChannels.PATCH("/:fb_id", h.FacebookChannel.UpdateFacebookPage) fbChannels.DELETE("/:fb_id", h.FacebookChannel.DeleteFacebookPage) } // Instagram channel routes (ref: Chatwoot channels/instagram_controller) // Twitter channel routes (G10: OAuth 2.0 + webhook CRC) twChannels := accountScoped.Group("/twitter_channels") twChannels.GET("/authorization", h.TwitterChannel.Authorization) twChannels.POST("/oauth_callback", h.TwitterChannel.OAuthCallback) twChannels.DELETE("/:twitter_id", h.TwitterChannel.Delete) // Microsoft channel routes (G10: Azure AD OAuth 2.0) msChannels := accountScoped.Group("/microsoft_channels") msChannels.GET("/authorization", h.MicrosoftChannel.Authorization) msChannels.POST("/oauth_callback", h.MicrosoftChannel.OAuthCallback) msChannels.DELETE("/:ms_id", h.MicrosoftChannel.Delete) // Google channel routes (G10: Google OAuth 2.0) goChannels := accountScoped.Group("/google_channels") goChannels.GET("/authorization", h.GoogleChannel.Authorization) goChannels.POST("/oauth_callback", h.GoogleChannel.OAuthCallback) goChannels.DELETE("/:google_id", h.GoogleChannel.Delete) igChannels := accountScoped.Group("/instagram_channels") { igChannels.POST("/", h.InstagramChannel.CreateInstagramChannel) igChannels.GET("/", h.InstagramChannel.ListInstagramChannels) } igInboxScoped := accountScoped.Group("/inboxes/:inbox_id/instagram_channels") { igInboxScoped.GET("/:ig_id", h.InstagramChannel.GetInstagramChannel) igInboxScoped.PATCH("/:ig_id", h.InstagramChannel.UpdateInstagramChannel) igInboxScoped.DELETE("/:ig_id", h.InstagramChannel.DeleteInstagramChannel) } // === TikTok channel CRUD routes === ttChannels := accountScoped.Group("/tiktok_channels") { ttChannels.POST("/", h.TikTokChannel.CreateTikTokChannel) ttChannels.GET("/", h.TikTokChannel.ListTikTokChannels) } ttInboxScoped := accountScoped.Group("/inboxes/:inbox_id/tiktok_channels") { ttInboxScoped.GET("/:tt_id", h.TikTokChannel.GetTikTokChannel) ttInboxScoped.PATCH("/:tt_id", h.TikTokChannel.UpdateTikTokChannel) ttInboxScoped.DELETE("/:tt_id", h.TikTokChannel.DeleteTikTokChannel) } // === LINE channel CRUD routes === lineChannels := accountScoped.Group("/line_channels") { lineChannels.POST("/", h.LINEChannel.Create) lineChannels.GET("/", h.LINEChannel.List) } lineInboxScoped := accountScoped.Group("/inboxes/:inbox_id/line_channels") { lineInboxScoped.GET("/:line_id", h.LINEChannel.Get) lineInboxScoped.PATCH("/:line_id", h.LINEChannel.Update) lineInboxScoped.DELETE("/:line_id", h.LINEChannel.Delete) } // === Twilio SMS channel CRUD routes === twilioChannels := accountScoped.Group("/twilio_sms_channels") { twilioChannels.POST("/", h.TwilioSMSChannel.Create) twilioChannels.GET("/", h.TwilioSMSChannel.List) } twilioInboxScoped := accountScoped.Group("/inboxes/:inbox_id/twilio_sms_channels") { twilioInboxScoped.GET("/:tw_id", h.TwilioSMSChannel.Get) twilioInboxScoped.PATCH("/:tw_id", h.TwilioSMSChannel.Update) twilioInboxScoped.DELETE("/:tw_id", h.TwilioSMSChannel.Delete) } // === Email channel CRUD routes === emailChannels := accountScoped.Group("/email_channels") { emailChannels.POST("/", h.EmailChannel.Create) emailChannels.GET("/", h.EmailChannel.List) } emailInboxScoped := accountScoped.Group("/inboxes/:inbox_id/email_channels") { emailInboxScoped.GET("/:em_id", h.EmailChannel.Get) emailInboxScoped.PATCH("/:em_id", h.EmailChannel.Update) emailInboxScoped.DELETE("/:em_id", h.EmailChannel.Delete) } // EmailChannelMigration — initiate migration of email channel between inboxes // Reference: Chatwoot resources :email_channel_migrations, only: [:create] v1.RegisterEmailChannelMigrationRoutes(accountScoped.Group("/email_channel_migrations"), h.EmailChannelMigration) // === Chatwoot-style OAuth + Webhook routes (G10) === // These follow Chatwoot's URL convention: /accounts/:id/{channel}/oauth // and /accounts/:id/{channel}/callback, mirroring Chatwoot's // channel_controller.rb pattern for each channel type. // Twitter OAuth + Webhook routes (G10) twOAuth := accountScoped.Group("/twitter") { twOAuth.GET("/oauth", h.TwitterChannel.Authorization) // OAuth authorize URL twOAuth.GET("/callback", h.TwitterChannel.OAuthCallbackGET) // OAuth redirect callback (GET, query params) twOAuth.POST("/webhooks", h.TwitterChannel.RegisterWebhook) // Register webhook twOAuth.GET("/webhooks", h.TwitterChannel.ListWebhooks) // List webhooks } // Instagram OAuth + Webhook routes (G10) igOAuth := accountScoped.Group("/instagram") { igOAuth.GET("/oauth", h.InstagramChannel.Authorization) // OAuth authorize URL igOAuth.GET("/callback", h.InstagramChannel.OAuthCallbackGET) // OAuth redirect callback (GET, query params) igOAuth.POST("/webhooks", h.InstagramChannel.RegisterWebhook) // Register webhook (Facebook Graph API subscribed_apps) igOAuth.GET("/webhooks", h.InstagramChannel.ListWebhooks) // List webhooks } // Microsoft OAuth + Webhook routes (G10) msOAuth := accountScoped.Group("/microsoft") { msOAuth.GET("/oauth", h.MicrosoftChannel.Authorization) // OAuth authorize URL msOAuth.GET("/callback", h.MicrosoftChannel.OAuthCallbackGET) // OAuth redirect callback (GET, query params) msOAuth.POST("/webhooks", h.MicrosoftChannel.RegisterWebhook) // Register webhook (Microsoft Graph API subscription) msOAuth.GET("/webhooks", h.MicrosoftChannel.ListWebhooks) // List webhooks (Microsoft Graph API subscriptions) } // Google OAuth + Webhook routes (G10) goOAuth := accountScoped.Group("/google") { goOAuth.GET("/oauth", h.GoogleChannel.Authorization) // OAuth authorize URL goOAuth.GET("/callback", h.GoogleChannel.OAuthCallbackGET) // OAuth redirect callback (GET, query params) goOAuth.POST("/webhooks", h.GoogleChannel.RegisterWebhook) // Register webhook (Google Chat API spaces.watch) goOAuth.GET("/webhooks", h.GoogleChannel.ListWebhooks) // List webhooks } // Instagram comment management routes (ref: Chatwoot instagram_controller comments) igComments := accountScoped.Group("/inboxes/:inbox_id/instagram_comments") { igComments.GET("/media/:media_id", h.InstagramChannel.GetComments) igComments.GET("/:comment_id/replies", h.InstagramChannel.GetCommentReplies) igComments.POST("/:comment_id/reply", h.InstagramChannel.ReplyToComment) igComments.POST("/:comment_id/hide", h.InstagramChannel.HideComment) igComments.DELETE("/:comment_id", h.InstagramChannel.DeleteComment) } // Conversation routes (ref: Chatwoot resources :conversations) conversations := accountScoped.Group("/conversations") { conversations.GET("/", h.Conversation.List) conversations.POST("/", h.Conversation.Create) conversations.GET("/search", h.Conversation.Search) conversations.POST("/filter", h.Conversation.Filter) conversations.GET("/:conversation_id", h.Conversation.Get) conversations.PATCH("/:conversation_id", h.Conversation.Update) conversations.PUT("/:conversation_id", h.Conversation.Update) conversations.DELETE("/:conversation_id", h.Conversation.Delete) conversations.POST("/:conversation_id/assign", h.Conversation.AssignAgent) conversations.POST("/:conversation_id/toggle_status", h.Conversation.ToggleStatus) conversations.POST("/:conversation_id/toggle_priority", h.Conversation.TogglePriority) conversations.PATCH("/:conversation_id/labels", h.Label.ReplaceConversationLabels) conversations.GET("/:conversation_id/labels", h.Label.GetConversationLabels) conversations.POST("/:conversation_id/labels", h.Label.AddLabelToConversation) conversations.DELETE("/:conversation_id/labels/:tag_id", h.Label.RemoveLabelFromConversation) conversations.POST("/:conversation_id/mute", h.Conversation.Mute) conversations.POST("/:conversation_id/unmute", h.Conversation.Unmute) conversations.PATCH("/:conversation_id/priority", h.Conversation.UpdatePriority) // Conversation meta, unread, transcript, custom_attributes, unread_counts conversations.GET("/meta", h.Conversation.Meta) conversations.GET("/unread_counts", h.Conversation.UnreadCounts) conversations.POST("/:conversation_id/unread", h.Conversation.Unread) conversations.POST("/:conversation_id/transcript", h.Conversation.Transcript) conversations.POST("/:conversation_id/custom_attributes", h.Conversation.UpdateCustomAttributes) // Additional Conversation endpoints matching Chatwoot member routes conversations.GET("/:conversation_id/attachments", h.Conversation.ListAttachments) conversations.POST("/:conversation_id/toggle_typing_status", h.Conversation.ToggleTyping) conversations.POST("/:conversation_id/update_last_seen", h.Conversation.UpdateLastSeen) conversations.POST("/:conversation_id/assignments", h.Conversation.AssignTeam) // Participants nested under conversation participants := conversations.Group("/:conversation_id/participants") { participants.GET("/", h.ConversationParticipant.List) participants.POST("/", h.ConversationParticipant.Add) participants.PATCH("/", h.ConversationParticipant.BatchUpdate) participants.PATCH("/:user_id", h.ConversationParticipant.Update) participants.DELETE("/:user_id", h.ConversationParticipant.Remove) } // Draft messages nested under conversation drafts := conversations.Group("/:conversation_id/draft_messages") { drafts.GET("/", h.DraftMessage.List) drafts.POST("/", h.DraftMessage.Create) drafts.GET("/:draft_id", h.DraftMessage.Get) drafts.PATCH("/:draft_id", h.DraftMessage.Update) drafts.DELETE("/:draft_id", h.DraftMessage.Delete) } // Messages nested under conversation msgs := conversations.Group("/:conversation_id/messages") { msgs.GET("/", h.Conversation.ListMessages) msgs.POST("/", h.Message.Create) msgs.GET("/:message_id", h.Message.Get) msgs.PATCH("/:message_id", h.Message.Update) msgs.PUT("/:message_id", h.Message.Update) msgs.DELETE("/:message_id", h.Message.Delete) msgs.POST("/:message_id/retry", h.Message.Retry) msgs.POST("/:message_id/translate", h.Message.Translate) // Delivery status nested under message deliveryStatus := msgs.Group("/:message_id/delivery_status") { deliveryStatus.GET("/", h.DeliveryStatus.List) } } // WhatsApp calls nested under conversation whatsappCalls := conversations.Group("/:conversation_id/whatsapp_calls") { whatsappCalls.GET("/", h.WhatsAppCall.List) whatsappCalls.POST("/", h.WhatsAppCall.Create) whatsappCalls.GET("/:call_id", h.WhatsAppCall.Get) whatsappCalls.PUT("/:call_id", h.WhatsAppCall.Update) whatsappCalls.DELETE("/:call_id", h.WhatsAppCall.Delete) } } // Tag/Label routes (ref: Chatwoot resources :labels) tags := accountScoped.Group("/tags") { tags.GET("/", h.Label.ListTags) tags.POST("/", h.Label.CreateTag) tags.GET("/:tag_id", h.Label.GetTag) tags.PUT("/:tag_id", h.Label.UpdateTag) tags.DELETE("/:tag_id", h.Label.DeleteTag) tags.GET("/:tag_id/conversations", h.Label.GetConversationsByTag) } // Labels alias (Chatwoot uses /labels instead of /tags) labels := accountScoped.Group("/labels") { labels.GET("/", h.Label.ListTags) labels.POST("/", h.Label.CreateTag) labels.GET("/:tag_id", h.Label.GetTag) labels.PUT("/:tag_id", h.Label.UpdateTag) labels.DELETE("/:tag_id", h.Label.DeleteTag) labels.GET("/:tag_id/conversations", h.Label.GetConversationsByTag) } // Batch label operations accountScoped.POST("/labels/batch_add", h.Label.BatchAddLabel) accountScoped.POST("/labels/batch_remove", h.Label.BatchRemoveLabel) // Contact routes (ref: Chatwoot resources :contacts) contacts := accountScoped.Group("/contacts") { contacts.GET("/", h.Contact.List) contacts.POST("/", h.Contact.Create) contacts.GET("/search", h.Contact.Search) contacts.POST("/filter", h.Contact.Filter) contacts.GET("/active", h.Contact.Active) contacts.GET("/export", h.Contact.Export) contacts.POST("/export", h.Contact.ExportRequest) contacts.GET("/export/:export_id/download", h.Contact.DownloadExport) contacts.POST("/import", h.Contact.Import) contacts.GET("/:contact_id", h.Contact.Get) contacts.PUT("/:contact_id", h.Contact.Update) contacts.PATCH("/:contact_id", h.Contact.Update) contacts.DELETE("/:contact_id", h.Contact.Delete) contacts.DELETE("/:contact_id/avatar", h.Contact.DeleteAvatar) // M4 G3: Contact extension routes (active, export, import, contactable_inboxes, custom_attributes) contacts.GET("/:contact_id/conversations", h.Contact.ListConversations) contacts.GET("/:contact_id/contactable_inboxes", h.Contact.ContactableInboxes) contacts.GET("/:contact_id/labels", h.Contact.ListLabels) contacts.GET("/:contact_id/labels/", h.Contact.ListLabels) contacts.POST("/:contact_id/labels", h.Contact.UpdateLabels) contacts.POST("/:contact_id/labels/", h.Contact.UpdateLabels) contacts.DELETE("/:contact_id/custom_attributes", h.Contact.DeleteCustomAttributes) contacts.POST("/:contact_id/destroy_custom_attributes", h.Contact.DestroyCustomAttributes) // Contact notes (ref: Chatwoot nested notes under contacts) contacts.GET("/:contact_id/notes", h.Contact.ListNotes) contacts.POST("/:contact_id/notes", h.Contact.CreateNote) contacts.GET("/:contact_id/notes/:note_id", h.Contact.ShowNote) contacts.PUT("/:contact_id/notes/:note_id", h.Contact.UpdateNote) contacts.PATCH("/:contact_id/notes/:note_id", h.Contact.UpdateNote) contacts.DELETE("/:contact_id/notes/:note_id", h.Contact.DestroyNote) // Contact inboxes (ref: Chatwoot nested contact_inboxes under contacts) contacts.GET("/:contact_id/contact_inboxes", h.Contact.ListContactInboxes) contacts.POST("/:contact_id/contact_inboxes", h.Contact.CreateContactInbox) contacts.POST("/:contact_id/contact_inboxes/", h.Contact.CreateContactInbox) contacts.DELETE("/:contact_id/contact_inboxes/:inbox_id", h.Contact.DeleteContactInbox) } // M4 G3: ContactInbox filter route (account-scope, not nested under contact) // Reference: Chatwoot contact_inboxes_controller#filter contactInboxes := accountScoped.Group("/contact_inboxes") { contactInboxes.GET("/filter", h.ContactInboxFilter.Filter) } // G4: Company routes (CRUD + search + nested contacts/conversations/notes) companies := accountScoped.Group("/companies") { companies.GET("/", h.Company.List) companies.POST("/", h.Company.Create) companies.GET("/search", h.Company.Search) companies.GET("/:company_id", h.Company.Get) companies.PUT("/:company_id", h.Company.Update) companies.PATCH("/:company_id", h.Company.Update) companies.DELETE("/:company_id", h.Company.Delete) companies.POST("/:company_id/destroy_custom_attributes", h.Company.DestroyCustomAttributes) companies.DELETE("/:company_id/avatar", h.Company.DeleteAvatar) // Nested contacts under a company companies.GET("/:company_id/contacts", h.Company.ListContacts) companies.GET("/:company_id/contacts/search", h.Company.SearchContacts) // Nested conversations under a company (via contacts) companies.GET("/:company_id/conversations", h.Company.ListConversations) // Nested notes under a company companies.GET("/:company_id/notes", h.Company.ListNotes) companies.POST("/:company_id/notes", h.Company.CreateNote) companies.DELETE("/:company_id/notes/:note_id", h.Company.DeleteNote) // Nested contacts management under a company companies.POST("/:company_id/contacts", h.Company.AddContact) companies.POST("/:company_id/contacts/:contact_id", h.Company.AddContact) companies.DELETE("/:company_id/contacts/:contact_id", h.Company.RemoveContact) } // Team routes (ref: Chatwoot resources :teams) teams := accountScoped.Group("/teams") { teams.GET("/", h.Team.List) teams.POST("/", h.Team.Create) teams.GET("/:team_id", h.Team.Get) teams.PUT("/:team_id", h.Team.Update) teams.DELETE("/:team_id", h.Team.Delete) // Team members (ref: Chatwoot nested resources :team_members) teamMembers := teams.Group("/:team_id/team_members") { teamMembers.GET("/", h.Team.ListMembers) teamMembers.POST("/", h.Team.AddMembers) teamMembers.PATCH("/", h.Team.UpdateMembers) teamMembers.DELETE("/", h.Team.RemoveMembers) } } // Platform app routes (account-level access) platformApps := accountScoped.Group("/platform_apps") { platformApps.GET("/", h.PlatformApp.List) platformApps.POST("/", h.PlatformApp.Create) platformApps.GET("/search", h.PlatformApp.Search) platformApps.GET("/:platform_app_id", h.PlatformApp.Get) platformApps.PUT("/:platform_app_id", h.PlatformApp.Update) platformApps.DELETE("/:platform_app_id", h.PlatformApp.Delete) platformApps.POST("/:platform_app_id/regenerate_access_token", h.PlatformApp.RegenerateAccessToken) platformApps.GET("/:platform_app_id/access_tokens", h.PlatformApp.ListAccessTokens) platformApps.GET("/:platform_app_id/permissibles", h.PlatformApp.ListPermissibles) platformApps.POST("/:platform_app_id/permissibles", h.PlatformApp.AddPermissible) platformApps.DELETE("/:platform_app_id/permissibles/:permissible_id", h.PlatformApp.RemovePermissible) } // Agent Bot routes (account-level + global bots) // Reference: Chatwoot namespace :agent_bots under :account agentBots := accountScoped.Group("/agent_bots") { agentBots.GET("/", h.AgentBot.List) agentBots.POST("/", h.AgentBot.Create) agentBots.GET("/:agent_bot_id", h.AgentBot.Get) agentBots.PUT("/:agent_bot_id", h.AgentBot.Update) agentBots.DELETE("/:agent_bot_id", h.AgentBot.Delete) agentBots.POST("/:agent_bot_id/reset_token", h.AgentBot.ResetToken) agentBots.POST("/:agent_bot_id/reset_secret", h.AgentBot.ResetSecret) agentBots.POST("/:agent_bot_id/delete_avatar", h.AgentBot.DeleteAvatar) // Chatwoot-compatible member action routes (ref: Chatwoot agent_bots member actions) // Chatwoot: DELETE /accounts/:account_id/agent_bots/:id/avatar agentBots.DELETE("/:agent_bot_id/avatar", h.AgentBot.DeleteAvatar) // Chatwoot: POST /accounts/:account_id/agent_bots/:id/reset_access_token agentBots.POST("/:agent_bot_id/reset_access_token", h.AgentBot.ResetToken) } // Agent Bot-Inbox binding routes // Reference: Chatwoot namespace :agent_bot_inboxes under :account agentBotInboxes := accountScoped.Group("/agent_bot_inboxes") { agentBotInboxes.POST("/", h.AgentBotInbox.Bind) agentBotInboxes.DELETE("/:agent_bot_inbox_id", h.AgentBotInbox.Unbind) agentBotInboxes.PATCH("/:agent_bot_inbox_id/status", h.AgentBotInbox.UpdateStatus) agentBotInboxes.GET("/", h.AgentBotInbox.ListByInbox) // ?inbox_id=X agentBotInboxes.GET("/by_bot", h.AgentBotInbox.ListByBot) // ?agent_bot_id=X } // P9: AgentBot rule engine + trigger config routes // Reference: Chatwoot AutomationRulesController routes under :account/:agent_bot botRules := agentBots.Group("/:agent_bot_id/bot_rules") { botRules.GET("/", h.BotRule.ListByBot) botRules.POST("/", h.BotRule.Create) botRules.GET("/:rule_id", h.BotRule.Get) botRules.PUT("/:rule_id", h.BotRule.Update) botRules.DELETE("/:rule_id", h.BotRule.Delete) } triggerConfigs := agentBots.Group("/:agent_bot_id/trigger_configs") { triggerConfigs.GET("/", h.BotTriggerConfig.ListByBot) triggerConfigs.POST("/", h.BotTriggerConfig.Create) triggerConfigs.GET("/:trigger_config_id", h.BotTriggerConfig.Get) triggerConfigs.PUT("/:trigger_config_id", h.BotTriggerConfig.Update) triggerConfigs.DELETE("/:trigger_config_id", h.BotTriggerConfig.Delete) } // Captain AI routes (ref: Chatwoot namespace :captain) // P10 (M10): Captain Assistant + Copilot features captain := accountScoped.Group("/captain") { // Assistant CRUD assistants := captain.Group("/assistants") { assistants.GET("/", h.CaptainAssistant.List) assistants.POST("/", h.CaptainAssistant.Create) assistants.GET("/:assistant_id", h.CaptainAssistant.Get) assistants.PUT("/:assistant_id", h.CaptainAssistant.Update) assistants.DELETE("/:assistant_id", h.CaptainAssistant.Delete) // Inbox bindings assistants.POST("/:assistant_id/inboxes", h.CaptainAssistant.AssociateInbox) assistants.DELETE("/:assistant_id/inboxes/:inbox_id", h.CaptainAssistant.DissociateInbox) // Chatwoot member/collection routes assistants.GET("/:assistant_id/inboxes", h.CaptainAssistant.GetConfig) assistants.GET("/tools", h.CaptainAssistant.GetConfig) assistants.POST("/:assistant_id/playground", h.CaptainAssistant.GenerateResponse) // Documents nested under assistant assistantDocs := assistants.Group("/:assistant_id/documents") { assistantDocs.GET("/", h.CaptainDocument.List) assistantDocs.POST("/", h.CaptainDocument.Create) assistantDocs.GET("/:document_id", h.CaptainDocument.Get) assistantDocs.DELETE("/:document_id", h.CaptainDocument.Delete) } // Scenarios nested under assistant assistantScenarios := assistants.Group("/:assistant_id/scenarios") { assistantScenarios.GET("/", h.CaptainScenario.List) assistantScenarios.POST("/", h.CaptainScenario.Create) assistantScenarios.GET("/:scenario_id", h.CaptainScenario.Get) assistantScenarios.PUT("/:scenario_id", h.CaptainScenario.Update) assistantScenarios.DELETE("/:scenario_id", h.CaptainScenario.Delete) } } // Custom tools (account-level) customTools := captain.Group("/custom_tools") { customTools.GET("/", h.CaptainCustomTool.List) customTools.POST("/", h.CaptainCustomTool.Create) customTools.GET("/:tool_id", h.CaptainCustomTool.Get) customTools.PUT("/:tool_id", h.CaptainCustomTool.Update) customTools.DELETE("/:tool_id", h.CaptainCustomTool.Delete) // Test custom tool before enabling customTools.POST("/test", h.CaptainCustomTool.TestTool) } // Flat document routes (Chatwoot: resources :documents, only: [:index, :show, :create, :destroy]) documents := captain.Group("/documents") { documents.GET("/", h.CaptainDocument.List) documents.POST("/", h.CaptainDocument.Create) documents.GET("/:document_id", h.CaptainDocument.Get) documents.DELETE("/:document_id", h.CaptainDocument.Delete) documents.POST("/:document_id/sync", h.CaptainDocument.SyncDocument) } // Flat scenario routes (Chatwoot: resources :scenarios) scenarios := captain.Group("/scenarios") { scenarios.GET("/", h.CaptainScenario.List) scenarios.POST("/", h.CaptainScenario.Create) scenarios.GET("/:scenario_id", h.CaptainScenario.Get) scenarios.PUT("/:scenario_id", h.CaptainScenario.Update) scenarios.DELETE("/:scenario_id", h.CaptainScenario.Delete) } // Copilot features (ref: Chatwoot Captain::Copilot) copilotThreads := captain.Group("/copilot_threads") { copilotThreads.GET("/", h.Copilot.ListThreads) copilotThreads.POST("/", h.Copilot.CreateThread) copilotThreads.GET("/:thread_id", h.Copilot.GetThread) copilotThreads.DELETE("/:thread_id", h.Copilot.DeleteThread) // Nested copilot_messages (Chatwoot: resources :copilot_messages, only: [:index, :create]) copilotThreadMessages := copilotThreads.Group("/:thread_id/copilot_messages") { copilotThreadMessages.GET("/", h.Copilot.ListSuggestionMessages) copilotThreadMessages.POST("/", h.Copilot.SendMessage) } copilotThreads.POST("/:thread_id/messages", h.Copilot.SendMessage) } // Copilot suggestion messages (conversation-level) // GET/POST /api/v1/accounts/:account_id/copilot_messages copilotMessages := captain.Group("/copilot_messages") { copilotMessages.GET("/", h.Copilot.ListSuggestionMessages) copilotMessages.POST("/", h.Copilot.CreateSuggestionMessage) } // Copilot AI actions captain.POST("/copilot/suggest_replies", h.Copilot.GetSuggestedReplies) captain.POST("/copilot/summarize", h.Copilot.SummarizeConversation) captain.POST("/copilot/translate", h.Copilot.TranslateMessage) // Captain AI task endpoints (M12 — standalone AI suggestion tasks) // Reference: Chatwoot Captain::TasksController tasks := captain.Group("/tasks") { tasks.POST("/reply_suggestion", h.CaptainTask.ReplySuggestion) tasks.POST("/summarize", h.CaptainTask.Summarize) tasks.POST("/rewrite", h.CaptainTask.Rewrite) tasks.POST("/label_suggestion", h.CaptainTaskExtended.LabelSuggestion) tasks.POST("/follow_up", h.CaptainTaskExtended.FollowUp) // M12: SSE streaming variants of Captain task endpoints tasks.POST("/reply_suggestion/stream", h.CaptainTask.StreamReplySuggestion) tasks.POST("/summarize/stream", h.CaptainTask.StreamSummarize) tasks.POST("/rewrite/stream", h.CaptainTask.StreamRewrite) } // M12: SSE streaming for Copilot messages captain.GET("/copilot/stream", h.SSEStream.StreamCopilotMessage) // M12: Conversation insight endpoints // Reference: Chatwoot Captain::ConversationInsightController insights := captain.Group("/conversation_insights") { insights.POST("/:conversation_id/analyze_participants", h.ConversationInsight.AnalyzeParticipants) insights.POST("/:conversation_id/extract_action_items", h.ConversationInsight.ExtractActionItems) insights.POST("/:conversation_id/suggest_labels", h.ConversationInsight.SuggestLabels) } // M12: Captain Preferences (per-account AI configuration) // Reference: Chatwoot Captain::PreferencesController preferences := captain.Group("/preferences") { preferences.POST("/", h.CaptainPreference.Create) preferences.GET("/", h.CaptainPreference.Get) preferences.PUT("/", h.CaptainPreference.Update) preferences.DELETE("/", h.CaptainPreference.Delete) } // M12: Label Suggestion + Follow Up (GET endpoints) // Reference: Chatwoot Captain::ConversationInsightController tasks.GET("/label_suggestion", h.CaptainTaskExtended.LabelSuggestion) tasks.GET("/follow_up", h.CaptainTaskExtended.FollowUp) // M12: Assistant Response generation (single conversation) // Reference: Chatwoot Captain::AssistantResponsesController assistantResponses := captain.Group("/assistant_responses") { // Chatwoot: resources :assistant_responses (standard CRUD) assistantResponses.POST("/", h.CaptainAssistantResponse.Create) assistantResponses.GET("/", h.CaptainAssistantResponse.List) assistantResponses.GET("/:response_id", h.CaptainAssistantResponse.Get) assistantResponses.PUT("/:response_id", h.CaptainAssistantResponse.Update) assistantResponses.DELETE("/:response_id", h.CaptainAssistantResponse.Delete) // Extended: ProcessResponse (AI generation + optional store) assistantResponses.POST("/process", h.CaptainAssistantResponse.ProcessResponse) } // M12: Bulk AI actions (batch operations on multiple conversations) // Reference: Chatwoot Captain::BulkActionsController bulkActions := captain.Group("/bulk_actions") { bulkActions.POST("/", h.CaptainBulkAction.Execute) } } // Draft messages — account-scoped search & count draftMessages := accountScoped.Group("/draft_messages") { draftMessages.GET("/search", h.DraftMessage.Search) draftMessages.GET("/count", h.DraftMessage.Count) } // Reports/Analytics routes (P11 — Reports/Analytics) // Reference: Chatwoot reports_controller.rb + live_reports_controller.rb + summary_reports_controller.rb reports := accountScoped.Group("/reports") { reports.GET("/summary", h.Analytics.Summary) reports.GET("/bot_summary", h.Analytics.BotSummary) reports.GET("/agents", h.Analytics.AgentMetrics) reports.GET("/inboxes", h.Analytics.InboxMetrics) reports.GET("/labels", h.Analytics.LabelMetrics) reports.GET("/teams", h.Analytics.TeamMetrics) reports.GET("/conversations", h.Analytics.Conversations) reports.GET("/conversations_summary", h.Analytics.ConversationsSummary) reports.GET("/conversation_traffic", h.Analytics.ConversationTraffic) reports.GET("/bot_metrics", h.Analytics.BotMetrics) reports.GET("/inbox_label_matrix", h.Analytics.InboxLabelMatrix) reports.GET("/first_response_time_distribution", h.Analytics.FirstResponseTimeDistribution) reports.GET("/outgoing_messages_count", h.Analytics.OutgoingMessagesCount) } // Live reports // Reference: Chatwoot live_reports_controller.rb liveReports := accountScoped.Group("/live_reports") { liveReports.GET("/conversation_metrics", h.LiveReport.ConversationMetrics) liveReports.GET("/grouped_conversation_metrics", h.LiveReport.GroupedConversationMetrics) } // Reporting events (P11 — raw analytics events) // GET /api/v1/accounts/:account_id/reporting_events?since=...&until=...&metric=... accountScoped.GET("/reporting_events", h.ReportingEvent.List) // Summary reports — read-only collection GET for agent/team/inbox/label summaries // Reference: Chatwoot resources :summary_reports, only: [] do collection do get :agent, :team, :inbox, :label end end v1.RegisterSummaryReportRoutes(accountScoped, h.SummaryReport) // Banner — read-only list of active banners visible to account users // Reference: Chatwoot api/v1/accounts/:account_id/banners — only active banners accountScoped.GET("/banners", h.Banner.ListActive) // Dashboard apps (custom dashboards) dashboardApps := accountScoped.Group("/dashboard_apps") { dashboardApps.GET("", h.DashboardApp.List) dashboardApps.POST("", h.DashboardApp.Create) dashboardApps.GET("/search", h.DashboardApp.Search) dashboardApps.GET("/:dashboard_app_id", h.DashboardApp.Get) dashboardApps.PUT("/:dashboard_app_id", h.DashboardApp.Update) dashboardApps.PATCH("/:dashboard_app_id", h.DashboardApp.Patch) dashboardApps.DELETE("/:dashboard_app_id", h.DashboardApp.Delete) // Widget management (CRUD on content jsonb array) dashboardApps.GET("/:dashboard_app_id/widgets", h.DashboardApp.GetWidgets) dashboardApps.POST("/:dashboard_app_id/widgets", h.DashboardApp.AddWidget) dashboardApps.PUT("/:dashboard_app_id/widgets/:widget_index", h.DashboardApp.UpdateWidget) dashboardApps.DELETE("/:dashboard_app_id/widgets/:widget_index", h.DashboardApp.RemoveWidget) } // Knowledge Base / Help Center routes (M9) // Reference: Chatwoot knowledge_base routes // Feature gate: knowledge_base must be enabled on the account portals := accountScoped.Group("/portals", middleware.FeatureFlagCheck(middleware.FeatureKnowledgeBase)) { portals.GET("/", h.Portal.List) portals.POST("/", h.Portal.Create) portals.GET("/:portal_id", h.Portal.Get) portals.PUT("/:portal_id", h.Portal.Update) portals.DELETE("/:portal_id", h.Portal.Delete) portals.POST("/:portal_id/archive", h.Portal.Archive) portals.DELETE("/:portal_id/logo", h.Portal.RemoveLogo) portals.POST("/:portal_id/send_instructions", h.Portal.SendInstructions) portals.GET("/:portal_id/ssl_status", h.Portal.SSLStatus) // Categories nested under portal categories := portals.Group("/:portal_id/categories") { categories.GET("/", h.Category.List) categories.POST("/", h.Category.Create) categories.GET("/:category_id", h.Category.Get) categories.PUT("/:category_id", h.Category.Update) categories.DELETE("/:category_id", h.Category.Delete) categories.POST("/reorder", h.Category.Reorder) } // Articles nested under portal articles := portals.Group("/:portal_id/articles") { articles.GET("/", h.Article.List) articles.POST("/", h.Article.Create) articles.GET("/search", h.Article.Search) articles.GET("/status_counts", h.Article.StatusCounts) articles.POST("/reorder", h.Article.Reorder) articles.POST("/bulk_update_status", h.Article.BulkUpdateStatus) articles.POST("/bulk_delete", h.Article.BulkDelete) articles.POST("/bulk_actions", h.Article.BulkActions) articles.GET("/:article_id", h.Article.Get) articles.GET("/:article_id/edit", h.Article.Edit) articles.PUT("/:article_id", h.Article.Update) articles.DELETE("/:article_id", h.Article.Delete) } // Article listing by category (nested) portals.GET("/:portal_id/categories/:category_id/articles", h.Article.ListByCategory) // Folders nested under portal folders := portals.Group("/:portal_id/folders") { folders.GET("/", h.Folder.List) folders.POST("/", h.Folder.Create) folders.GET("/:folder_id", h.Folder.Get) folders.PUT("/:folder_id", h.Folder.Update) folders.DELETE("/:folder_id", h.Folder.Delete) } // Portal members members := portals.Group("/:portal_id/members") { members.GET("/", h.PortalMember.List) members.POST("/", h.PortalMember.Create) members.GET("/:member_id", h.PortalMember.Get) members.PUT("/:member_id", h.PortalMember.Update) members.DELETE("/:member_id", h.PortalMember.Delete) } } // Automation Rules — CRUD + clone // Reference: Chatwoot namespace :automation_rules automationRules := accountScoped.Group("/automation_rules") { automationRules.GET("/", h.AutomationRule.List) automationRules.POST("/", h.AutomationRule.Create) automationRules.GET("/:automation_id", h.AutomationRule.Get) automationRules.PUT("/:automation_id", h.AutomationRule.Update) automationRules.DELETE("/:automation_id", h.AutomationRule.Delete) automationRules.POST("/:automation_id/clone", h.AutomationRule.Clone) automationRules.POST("/:automation_id/toggle_active", h.AutomationRule.ToggleActive) } // Macros — CRUD + execute // Reference: Chatwoot namespace :macros macros := accountScoped.Group("/macros") { macros.GET("/", h.Macro.List) macros.POST("/", h.Macro.Create) macros.GET("/:macro_id", h.Macro.Get) macros.PUT("/:macro_id", h.Macro.Update) macros.DELETE("/:macro_id", h.Macro.Delete) macros.POST("/:macro_id/execute", h.Macro.Execute) macros.POST("/:macro_id/clone", h.Macro.Clone) macros.POST("/:macro_id/toggle_active", h.Macro.ToggleActive) } // G16: Third-party Integrations — IntegrationHook CRUD + Slack/Shopify/Linear/Notion // Reference: Chatwoot namespace :integrations under :account integrations := accountScoped.Group("/integrations") { // IntegrationHook CRUD (generic hooks) v1.RegisterIntegrationHookRoutes(integrations, h.IntegrationHook) // Slack integration v1.RegisterSlackIntegrationRoutes(integrations, h.SlackIntegration) // Shopify integration v1.RegisterShopifyIntegrationRoutes(integrations, h.ShopifyIntegration) // Linear integration v1.RegisterLinearIntegrationRoutes(integrations, h.LinearIntegration) // Notion integration v1.RegisterNotionIntegrationRoutes(integrations, h.NotionIntegration) } // CSAT Survey Responses — list, metrics, review notes // Reference: Chatwoot namespace :csat_survey_responses csats := accountScoped.Group("/csat_survey_responses") { csats.GET("/", h.CsatSurvey.List) csats.GET("/metrics", h.CsatSurvey.Metrics) csats.GET("/download", h.CsatSurvey.Download) csats.PATCH("/:id", h.CsatSurvey.Update) csats.POST("/:id/update_review_notes", h.CsatSurvey.UpdateReviewNotes) } // Enterprise: AuditLog — list + get single entry v1.RegisterAuditRoutes(accountScoped, h.Audit) // Enterprise: AgentCapacityPolicy — CRUD for capacity policies v1.RegisterAgentCapacityRoutes(accountScoped, h.AgentCapacity) // NOTE: CSAT metrics routes (/csat_survey_responses/metrics, /download) already registered // in csats group above (line 1243-1244). RegisterCsatMetricsRoutes is NOT called here // to avoid Gin duplicate route panic. // Enterprise: CustomRole — CRUD for account-scoped custom roles v1.RegisterCustomRoleRoutes(accountScoped, h.CustomRole) // Notification settings — per-account notification preference show/update // Reference: Chatwoot resource :notification_settings, only: [:show, :update] notifSettings := accountScoped.Group("/notification_settings") { notifSettings.GET("/", h.NotificationSetting.Show) notifSettings.PATCH("/", h.NotificationSetting.Update) notifSettings.PUT("/", h.NotificationSetting.Update) } // Notifications — account-scoped Chatwoot routes used by the dashboard frontend. notifications := accountScoped.Group("/notifications") { notifications.GET("/", h.Notification.List) notifications.POST("/read_all", h.Notification.MarkAllRead) notifications.GET("/unread_count", h.Notification.UnreadCount) notifications.POST("/destroy_all", h.Notification.DestroyAll) notifications.DELETE("/destroy_all", h.Notification.DestroyAll) notifications.GET("/:notification_id", h.Notification.Get) notifications.PUT("/:notification_id", h.Notification.Update) notifications.PATCH("/:notification_id", h.Notification.Update) notifications.DELETE("/:notification_id", h.Notification.Destroy) notifications.POST("/:notification_id/snooze", h.Notification.Snooze) notifications.POST("/:notification_id/unread", h.Notification.Unread) } // Notification subscriptions (account-scoped) // Reference: Chatwoot resources :notification_subscriptions, only: [:create, :destroy] notifSubs := accountScoped.Group("/notification_subscriptions") { notifSubs.POST("/", h.NotificationSubscription.Create) notifSubs.DELETE("/:identifier", h.NotificationSubscription.Destroy) } // Canned Responses — CRUD + search // Reference: Chatwoot namespace :canned_responses cannedResponses := accountScoped.Group("/canned_responses") { cannedResponses.GET("/", h.CannedResponse.List) cannedResponses.POST("/", h.CannedResponse.Create) cannedResponses.GET("/:id", h.CannedResponse.Get) cannedResponses.PUT("/:id", h.CannedResponse.Update) cannedResponses.DELETE("/:id", h.CannedResponse.Delete) cannedResponses.GET("/search", h.CannedResponse.Search) } // Integration hooks (Chatwoot: resources :hooks) // Reference: Chatwoot hooks_controller — CRUD + process_event hooks := accountScoped.Group("/hooks") { hooks.GET("/", h.IntegrationHook.ListHooks) hooks.GET("/:id", h.IntegrationHook.GetHook) hooks.POST("/", h.IntegrationHook.CreateHook) hooks.PUT("/:id", h.IntegrationHook.UpdateHook) hooks.DELETE("/:id", h.IntegrationHook.DeleteHook) hooks.POST("/:id/process_event", h.IntegrationHook.ProcessHookEvent) } // Campaigns — CRUD + start/stop actions // Reference: Chatwoot namespace :campaigns campaigns := accountScoped.Group("/campaigns") { campaigns.GET("/", h.Campaign.List) campaigns.POST("/", h.Campaign.Create) campaigns.GET("/:campaign_id", h.Campaign.Get) campaigns.PUT("/:campaign_id", h.Campaign.Update) campaigns.DELETE("/:campaign_id", h.Campaign.Delete) campaigns.POST("/:campaign_id/start", h.Campaign.Start) campaigns.POST("/:campaign_id/stop", h.Campaign.Stop) } // Assignment Policy — account-level + inbox-level overrides // Reference: Chatwoot assignment_policy / inbox_assignment_policy accountScoped.GET("/assignment_policy", h.AssignmentPolicy.GetAccountPolicy) accountScoped.POST("/assignment_policy", h.AssignmentPolicy.CreateAccountPolicy) accountScoped.PUT("/assignment_policy/:policy_id", h.AssignmentPolicy.UpdateAccountPolicy) accountScoped.DELETE("/assignment_policy/:policy_id", h.AssignmentPolicy.DeleteAccountPolicy) // Assignment policies (plural, Chatwoot-compatible alias) // Reference: Chatwoot resources :assignment_policies assignmentPolicies := accountScoped.Group("/assignment_policies") { assignmentPolicies.GET("/", h.AssignmentPolicy.GetAccountPolicy) assignmentPolicies.POST("/", h.AssignmentPolicy.CreateAccountPolicy) assignmentPolicies.GET("/:policy_id", h.AssignmentPolicy.GetAccountPolicy) assignmentPolicies.PUT("/:policy_id", h.AssignmentPolicy.UpdateAccountPolicy) assignmentPolicies.DELETE("/:policy_id", h.AssignmentPolicy.DeleteAccountPolicy) // Nested inboxes (Chatwoot: resources :assignment_policy_inboxes) assignmentPolicies.GET("/:policy_id/inboxes", h.AssignmentPolicy.GetInboxPolicy) assignmentPolicies.POST("/:policy_id/inboxes", h.AssignmentPolicy.CreateInboxPolicy) assignmentPolicies.DELETE("/:policy_id/inboxes/:inbox_id", h.AssignmentPolicy.DeleteInboxPolicy) } // Inbox-level assignment policy overrides (nested under inboxes) inboxAssignment := accountScoped.Group("/inboxes/:inbox_id/assignment_policy") { inboxAssignment.GET("/", h.AssignmentPolicyV2.GetInboxPolicy) inboxAssignment.POST("/", h.AssignmentPolicyV2.SetInboxPolicy) inboxAssignment.DELETE("/", h.AssignmentPolicyV2.DeleteInboxPolicy) inboxAssignment.PUT("/:policy_id", h.AssignmentPolicy.UpdateInboxPolicy) inboxAssignment.DELETE("/:policy_id", h.AssignmentPolicy.DeleteInboxPolicy) } // SLA Policy — CRUD + applied SLA metrics/download + inbox associations (M11) // Reference: Chatwoot sla_policies_controller, applied_slas_controller accountScoped.GET("/sla_policies", h.SlaPolicy.List) accountScoped.POST("/sla_policies", h.SlaPolicy.Create) accountScoped.GET("/sla_policies/:id", h.SlaPolicy.Get) accountScoped.PUT("/sla_policies/:id", h.SlaPolicy.Update) accountScoped.DELETE("/sla_policies/:id", h.SlaPolicy.Delete) // SLA inbox associations accountScoped.GET("/sla_policies/:id/inboxes", h.SlaPolicy.ListInboxes) accountScoped.POST("/sla_policies/:id/inboxes", h.SlaPolicy.AddInbox) accountScoped.DELETE("/sla_policies/:id/inboxes/:inbox_id", h.SlaPolicy.RemoveInbox) // Applied SLA metrics & download accountScoped.GET("/applied_slas", h.SlaPolicy.ListAppliedSlas) accountScoped.GET("/applied_slas/metrics", h.SlaPolicy.GetAppliedSlaMetrics) accountScoped.GET("/applied_slas/download", h.SlaPolicy.GetAppliedSlaDownload) // Assignment Policy V2 — enhanced with Type field + inbox join model (M11) // Reference: Chatwoot assignment_policy V2 (round_robin / fair / best_skill_match) accountScoped.GET("/assignment_policies_v2", h.AssignmentPolicyV2.List) accountScoped.POST("/assignment_policies_v2", h.AssignmentPolicyV2.Create) accountScoped.GET("/assignment_policies_v2/:id", h.AssignmentPolicyV2.Get) accountScoped.PUT("/assignment_policies_v2/:id", h.AssignmentPolicyV2.Update) accountScoped.DELETE("/assignment_policies_v2/:id", h.AssignmentPolicyV2.Delete) // V2 inbox associations accountScoped.GET("/assignment_policies_v2/:id/inboxes", h.AssignmentPolicyV2.ListInboxes) accountScoped.POST("/assignment_policies_v2/:id/inboxes", h.AssignmentPolicyV2.AddInbox) accountScoped.DELETE("/assignment_policies_v2/:id/inboxes/:inbox_id", h.AssignmentPolicyV2.RemoveInbox) // V2 inbox-level assignment policy (reverse lookup: inbox → policy) inboxAssignmentV2 := accountScoped.Group("/inboxes/:inbox_id/assignment_policy_v2") { inboxAssignmentV2.GET("/", h.AssignmentPolicyV2.GetInboxPolicy) inboxAssignmentV2.POST("/", h.AssignmentPolicyV2.SetInboxPolicy) inboxAssignmentV2.DELETE("/", h.AssignmentPolicyV2.DeleteInboxPolicy) } // Search routes — Global Search + Advanced Filter (M14) // Reference: Chatwoot GlobalSearchService — cross-entity search accountScoped.GET("/search", h.Search.GlobalSearch) accountScoped.GET("/search/conversations", h.Search.SearchConversations) accountScoped.GET("/search/messages", h.Search.SearchMessages) accountScoped.GET("/search/contacts", h.Search.SearchContacts) accountScoped.GET("/search/articles", h.Search.SearchArticles) // Account SAML settings — account-scoped SAML config admin API (M13) // Only accessible to account administrators. // Reference: Chatwoot AccountSamlSettings — enterprise SSO configuration v1.RegisterAccountSamlSettingsRoutes(accountScoped.Group("/saml_settings"), h.AccountSamlSettings) // Custom Attribute Definitions — CRUD for attribute schema definitions // Reference: Chatwoot custom_attribute_definitions_controller.rb customAttrDefs := accountScoped.Group("/custom_attribute_definitions") { customAttrDefs.GET("/", h.CustomAttributeDefinition.List) customAttrDefs.POST("/", h.CustomAttributeDefinition.Create) customAttrDefs.GET("/:id", h.CustomAttributeDefinition.Get) customAttrDefs.PUT("/:id", h.CustomAttributeDefinition.Update) customAttrDefs.PATCH("/:id", h.CustomAttributeDefinition.Update) // Chatwoot uses PATCH customAttrDefs.DELETE("/:id", h.CustomAttributeDefinition.Delete) } // Custom Attribute Values — set/remove specific attribute values on conversations/contacts // Reference: Chatwoot custom_attribute_definitions_controller.rb (value management) // Conversation attribute values: POST to set, DELETE to remove a specific key conversationAttrs := accountScoped.Group("/conversations/:conversation_id/custom_attributes") { conversationAttrs.POST("/", h.CustomAttributeValue.SetConversationAttribute) conversationAttrs.DELETE("/:attribute_name", h.CustomAttributeValue.RemoveConversationAttribute) } // Contact attribute values: POST to set, DELETE to remove a specific key contactAttrs := accountScoped.Group("/contacts/:contact_id/custom_attributes") { contactAttrs.POST("/", h.CustomAttributeValue.SetContactAttribute) contactAttrs.DELETE("/:attribute_name", h.CustomAttributeValue.RemoveContactAttribute) } // Custom Filters — CRUD for saved filter queries // Reference: Chatwoot custom_filters_controller.rb customFilters := accountScoped.Group("/custom_filters") { customFilters.GET("/", h.CustomFilter.List) customFilters.POST("/", h.CustomFilter.Create) customFilters.GET("/:id", h.CustomFilter.Get) customFilters.PUT("/:id", h.CustomFilter.Update) customFilters.PATCH("/:id", h.CustomFilter.Update) // Chatwoot uses PATCH customFilters.DELETE("/:id", h.CustomFilter.Delete) } } } } // registerV2Routes maps Chatwoot API v2 account-scoped report routes. // Reference: Chatwoot config/routes.rb namespace :api/:v2. func registerV2Routes(g *gin.RouterGroup, h *Handlers) { accounts := g.Group("/accounts") accounts.Use(middleware.AccountScope()) { accounts.POST("/", h.Account.Create) accountScoped := accounts.Group("/:account_id") { v1.RegisterSummaryReportRoutes(accountScoped, h.SummaryReport) reports := accountScoped.Group("/reports") { reports.GET("", h.Analytics.Summary) reports.GET("/summary", h.Analytics.Summary) reports.GET("/bot_summary", h.Analytics.BotSummary) reports.GET("/agents", h.Analytics.AgentMetrics) reports.GET("/inboxes", h.Analytics.InboxMetrics) reports.GET("/labels", h.Analytics.LabelMetrics) reports.GET("/teams", h.Analytics.TeamMetrics) reports.GET("/conversations", h.Analytics.Conversations) reports.GET("/conversations_summary", h.Analytics.ConversationsSummary) reports.GET("/conversation_traffic", h.Analytics.ConversationTraffic) reports.GET("/bot_metrics", h.Analytics.BotMetrics) reports.GET("/inbox_label_matrix", h.Analytics.InboxLabelMatrix) reports.GET("/first_response_time_distribution", h.Analytics.FirstResponseTimeDistribution) reports.GET("/outgoing_messages_count", h.Analytics.OutgoingMessagesCount) } liveReports := accountScoped.Group("/live_reports") { liveReports.GET("/conversation_metrics", h.LiveReport.ConversationMetrics) liveReports.GET("/grouped_conversation_metrics", h.LiveReport.GroupedConversationMetrics) } } } } // registerPlatformRoutes maps super-admin platform routes. // Reference: Chatwoot namespace :platform_app (super_admin only) func registerPlatformRoutes(g *gin.RouterGroup, h *Handlers) { // PlatformApp CRUD g.GET("/apps", h.PlatformApp.List) g.POST("/apps", h.PlatformApp.Create) g.GET("/apps/search", h.PlatformApp.Search) g.GET("/apps/:id", h.PlatformApp.Get) g.PUT("/apps/:id", h.PlatformApp.Update) g.DELETE("/apps/:id", h.PlatformApp.Delete) // AccessToken management (Chatwoot AccessTokenable concern) g.POST("/apps/:id/regenerate_access_token", h.PlatformApp.RegenerateAccessToken) g.GET("/apps/:id/access_tokens", h.PlatformApp.ListAccessTokens) // Permissible management (Chatwoot PlatformAppPermissible) g.GET("/apps/:id/permissibles", h.PlatformApp.ListPermissibles) g.POST("/apps/:id/permissibles", h.PlatformApp.AddPermissible) g.DELETE("/apps/:id/permissibles/:permissible_id", h.PlatformApp.RemovePermissible) // Agent Bot CRUD (super-admin, global bots without account_id) // Reference: Chatwoot namespace :agent_bots under :platform_app g.GET("/agent_bots", h.AgentBot.List) g.POST("/agent_bots", h.AgentBot.Create) g.GET("/agent_bots/:id", h.AgentBot.Get) g.PUT("/agent_bots/:id", h.AgentBot.Update) g.DELETE("/agent_bots/:id", h.AgentBot.Delete) g.POST("/agent_bots/:id/reset_token", h.AgentBot.ResetToken) g.POST("/agent_bots/:id/reset_secret", h.AgentBot.ResetSecret) g.POST("/agent_bots/:id/delete_avatar", h.AgentBot.DeleteAvatar) g.PUT("/agent_bots/:id/avatar", h.AgentBot.PlatformUpdateAvatar) g.POST("/agent_bots/:id/reset", h.AgentBot.PlatformResetConfig) // G16: Platform Users SSO — super-admin initiates SSO for a user // Reference: Chatwoot Platform::Api::V1::UsersController #login, #token v1.RegisterPlatformUserSSORoutes(g, h.PlatformUserSSO) // InstallationConfig CRUD (super-admin, global key-value config) // Reference: Chatwoot Platform::Api::V1::InstallationConfigsController g.GET("/installation_configs", h.InstallationConfig.List) g.POST("/installation_configs", h.InstallationConfig.Create) g.GET("/installation_configs/:id", h.InstallationConfig.Get) g.PUT("/installation_configs/:id", h.InstallationConfig.Update) g.DELETE("/installation_configs/:id", h.InstallationConfig.Delete) // Banner CRUD (super-admin, global announcement/alert/update banners) // Reference: Chatwoot Platform::Api::V1::BannersController g.GET("/banners", h.Banner.List) g.POST("/banners", h.Banner.Create) g.GET("/banners/:id", h.Banner.Get) g.PUT("/banners/:id", h.Banner.Update) g.DELETE("/banners/:id", h.Banner.Delete) // WidgetTest read-only endpoints (platform scope, super-admin) // Reference: Chatwoot resources :widget_tests, only: [:index] — not in production v1.RegisterWidgetTestRoutes(g, h.WidgetTest) } // registerPlatformTokenRoutes maps AccessToken-authenticated Platform API routes. // Reference: Chatwoot PlatformController — AccessTokenable concern (api_access_token header) // These routes use PlatformApp authentication (not SuperAdmin), distinct from registerPlatformRoutes. func registerPlatformTokenRoutes(g *gin.RouterGroup, h *Handlers) { // Platform Users — AccessToken authenticated // Reference: Chatwoot Platform::Api::V1::UsersController (AccessToken auth) g.GET("/users", h.PlatformUser.List) g.GET("/users/:id", h.PlatformUser.Show) g.POST("/users", h.PlatformUser.Create) g.POST("/users/:id/login", h.PlatformUser.Login) g.POST("/users/:id/token", h.PlatformUser.Token) g.PATCH("/users/:id", h.PlatformUser.Update) g.DELETE("/users/:id", h.PlatformUser.Destroy) // Platform Accounts — AccessToken authenticated // Reference: Chatwoot Platform::Api::V1::AccountsController (AccessToken auth) g.GET("/accounts", h.PlatformAccount.List) g.GET("/accounts/:account_id", h.PlatformAccount.Show) g.POST("/accounts", h.PlatformAccount.Create) g.PATCH("/accounts/:account_id", h.PlatformAccount.Update) g.DELETE("/accounts/:account_id", h.PlatformAccount.Destroy) // NOTE: Platform Agent Bots routes already registered in registerPlatformRoutes (super-admin scope). // PlatformAgentBot handlers share the same /agent_bots paths but with AccessToken auth. // Since both route sets are now merged into one group, these are NOT re-registered here. // Platform Account Users — AccessToken authenticated // Reference: Chatwoot Platform::Api::V1::AccountUsersController (nested under accounts) g.GET("/accounts/:account_id/account_users", h.PlatformAccountUser.Index) g.POST("/accounts/:account_id/account_users", h.PlatformAccountUser.Create) g.DELETE("/accounts/:account_id/account_users/:user_id", h.PlatformAccountUser.Destroy) } // registerChatwootWidgetRoutes maps Chatwoot's /api/v1/widget namespace. // Widget behavior is backed by Chatwoot-compatible handlers; public inbox APIs are tracked separately. func registerChatwootWidgetRoutes(g *gin.RouterGroup, h *Handlers) { g.POST("/direct_uploads", h.Upload.DirectUpload) g.PUT("/direct_uploads/:upload_uuid", h.Upload.CompleteWidgetDirectUpload) g.POST("/config", h.Widget.Config) g.GET("/campaigns", h.Widget.ListCampaigns) g.POST("/events", h.Widget.CreateEvent) g.GET("/messages", h.Widget.GetLatestMessages) g.POST("/messages", h.Widget.SendMessage) g.PUT("/messages/:message_id", h.Widget.UpdateMessage) g.PATCH("/messages/:message_id", h.Widget.UpdateMessage) g.GET("/conversations", h.Widget.GetConversations) g.POST("/conversations", h.Widget.CreateConversation) g.POST("/conversations/destroy_custom_attributes", h.Widget.DestroyConversationCustomAttributes) g.POST("/conversations/set_custom_attributes", h.Widget.SetConversationCustomAttributes) g.POST("/conversations/update_last_seen", h.Widget.UpdateLastSeen) g.POST("/conversations/toggle_typing", h.Widget.ToggleTyping) g.POST("/conversations/transcript", h.Widget.SendTranscript) g.GET("/conversations/toggle_status", h.Widget.ToggleStatus) g.GET("/contact", h.Widget.GetContact) g.PUT("/contact", h.Widget.UpdateContact) g.PATCH("/contact", h.Widget.UpdateContact) g.POST("/contact/destroy_custom_attributes", h.Widget.DestroyContactCustomAttributes) g.PATCH("/contact/set_user", h.Widget.SetUser) g.GET("/inbox_members", h.Widget.ListInboxMembers) g.POST("/labels", h.Widget.AddLabel) g.DELETE("/labels/:label_id", h.Widget.RemoveLabel) g.POST("/integrations/dyte/add_participant_to_meeting", h.Widget.AddDyteParticipantToMeeting) } // registerWidgetRoutes maps widget API routes (public, for embed). // Reference: Chatwoot namespace :widget_api — no authentication required func registerWidgetRoutes(g *gin.RouterGroup, h *widget.WidgetHandler) { g.POST("/init", h.Init) g.PATCH("/contact", h.UpdateContact) g.GET("/conversations", h.GetConversations) g.POST("/messages", h.SendMessage) g.GET("/cable_token", h.GetCableToken) g.GET("/conversations/:id/messages", h.GetMessages) g.POST("/conversations/:id/toggle_typing", h.ToggleTyping) // M11: Widget theme, pre-chat form, and file upload public routes // Widget SDK fetches theme + pre-chat form before conversation starts // These routes use /widget/ prefix to avoid Gin radix tree conflict with /:portal_id (portals) widget := g.Group("/widget") widget.GET("/:website_token/theme_config", h.GetThemeConfig) widget.GET("/:website_token/pre_chat_form", h.GetPreChatForm) // File upload staged via website_token auth; attached to conversation after init widget.POST("/:website_token/uploads", h.StageFileUpload) widget.GET("/:website_token/uploads/:upload_uuid", h.GetFileUploadStatus) // M11: Offline message — visitors submit when no agents available widget.POST("/:website_token/offline_message", h.SubmitOfflineMessage) } func chatwootParityStub(c *gin.Context) { c.JSON(501, gin.H{ "error": "not implemented", "message": "Chatwoot parity route is registered; behavior is tracked for implementation", }) } // --- Placeholder handlers for webhooks --- // Full implementation deferred to P7 (channels) func webhookStub(c *gin.Context) { c.JSON(200, gin.H{"message": "Webhook callback placeholder (P7)"}) } func healthCheck(c *gin.Context) { uptime := time.Since(startTime) c.JSON(200, gin.H{ "status": "ok", "service": "gochat", "version": config.Version, "commit": config.CommitSHA, "buildDate": config.BuildDate, "uptime": uptime.String(), "uptimeSeconds": uint64(uptime.Seconds()), }) }