1966 lines
93 KiB
Go
1966 lines
93 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Public help-center routes consumed by the reused Chatwoot widget and portal.
|
|
// Reference: Chatwoot /hc/:slug and /hc/:slug/:locale/*.
|
|
helpCenter := engine.Group("/hc")
|
|
helpCenter.Use(middleware.CORS(corsCfg))
|
|
{
|
|
helpCenter.GET("/:slug", handlers.Portal.PublicRedirectDefaultLocale)
|
|
helpCenter.GET("/:slug/sitemap.xml", handlers.Portal.PublicSitemap)
|
|
helpCenter.GET("/:slug/articles/:article_slug", handlers.Article.PublicArticle)
|
|
helpCenter.GET("/:slug/:locale", handlers.Portal.PublicGet)
|
|
helpCenter.GET("/:slug/:locale/search", handlers.Article.PublicSearch)
|
|
helpCenter.GET("/:slug/:locale/articles", handlers.Article.PublicList)
|
|
helpCenter.GET("/:slug/:locale/articles.json", handlers.Article.PublicList)
|
|
helpCenter.GET("/:slug/:locale/categories", handlers.Category.PublicList)
|
|
helpCenter.GET("/:slug/:locale/categories.json", handlers.Category.PublicList)
|
|
helpCenter.GET("/:slug/:locale/categories/:category_slug", handlers.Category.PublicGet)
|
|
helpCenter.GET("/:slug/:locale/categories/:category_slug/articles", handlers.Article.PublicList)
|
|
helpCenter.GET("/:slug/:locale/categories/:category_slug/articles.json", handlers.Article.PublicList)
|
|
}
|
|
|
|
// 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)
|
|
})
|
|
twilioGroup.POST("/delivery_status", func(c *gin.Context) {
|
|
if handlers == nil || handlers.TwilioWebhook == nil {
|
|
chatwootParityStub(c)
|
|
return
|
|
}
|
|
handlers.TwilioWebhook.HandleTwilioDeliveryStatus(c)
|
|
})
|
|
engine.POST("/twilio/delivery_status", 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.PATCH("/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: PATCH/PUT /api/v1/accounts/:account_id/agents/:id (update)
|
|
accounts.PATCH("/:account_id/agents/:agent_id", h.Agent.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)
|
|
contacts.POST("/:contact_id/call", h.Contact.InitiateCall)
|
|
|
|
// M4 G3: Contact extension routes (active, export, import, contactable_inboxes, attachments, custom_attributes)
|
|
contacts.GET("/:contact_id/conversations", h.Contact.ListConversations)
|
|
contacts.GET("/:contact_id/contactable_inboxes", h.Contact.ContactableInboxes)
|
|
contacts.GET("/:contact_id/attachments", h.Contact.ListAttachments)
|
|
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.GET("/", h.AgentBot.List)
|
|
agentBots.POST("", h.AgentBot.Create)
|
|
agentBots.POST("/", h.AgentBot.Create)
|
|
agentBots.GET("/:agent_bot_id", h.AgentBot.Get)
|
|
agentBots.PUT("/:agent_bot_id", h.AgentBot.Update)
|
|
agentBots.PATCH("/: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("/tools", h.CaptainAssistant.Tools)
|
|
assistants.GET("/:assistant_id", h.CaptainAssistant.Get)
|
|
assistants.PUT("/:assistant_id", h.CaptainAssistant.Update)
|
|
assistants.DELETE("/:assistant_id", h.CaptainAssistant.Delete)
|
|
|
|
// Inbox bindings
|
|
assistants.GET("/:assistant_id/inboxes", h.CaptainAssistant.ListInboxes)
|
|
assistants.POST("/:assistant_id/inboxes", h.CaptainAssistant.AssociateInbox)
|
|
assistants.DELETE("/:assistant_id/inboxes/:inbox_id", h.CaptainAssistant.DissociateInbox)
|
|
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.GET("", h.CaptainPreference.Get)
|
|
preferences.GET("/", h.CaptainPreference.Get)
|
|
preferences.PUT("", h.CaptainPreference.Update)
|
|
preferences.PUT("/", h.CaptainPreference.Update)
|
|
}
|
|
|
|
// 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("", h.Analytics.Index)
|
|
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.GET("/", h.Portal.List)
|
|
portals.POST("", h.Portal.Create)
|
|
portals.POST("/", h.Portal.Create)
|
|
portals.GET("/:portal_id", h.Portal.Get)
|
|
portals.PATCH("/:portal_id", h.Portal.Update)
|
|
portals.PUT("/:portal_id", h.Portal.Update)
|
|
portals.DELETE("/:portal_id", h.Portal.Delete)
|
|
portals.PATCH("/:portal_id/archive", h.Portal.Archive)
|
|
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.GET("/", h.Category.List)
|
|
categories.POST("", h.Category.Create)
|
|
categories.POST("/", h.Category.Create)
|
|
categories.GET("/:category_id", h.Category.Get)
|
|
categories.PATCH("/:category_id", h.Category.Update)
|
|
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.GET("/", h.Article.List)
|
|
articles.POST("", h.Article.Create)
|
|
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.PATCH("/bulk_actions/update_status", h.Article.BulkUpdateStatus)
|
|
articles.PATCH("/bulk_actions/update_category", h.Article.BulkUpdateCategory)
|
|
articles.DELETE("/bulk_actions/delete_articles", h.Article.BulkDelete)
|
|
articles.POST("/bulk_actions/translate", h.Article.BulkTranslate)
|
|
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.PATCH("/:article_id", h.Article.Update)
|
|
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.GET("/", h.NotificationSetting.Show)
|
|
notifSettings.PATCH("", h.NotificationSetting.Update)
|
|
notifSettings.PATCH("/", h.NotificationSetting.Update)
|
|
notifSettings.PUT("", 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.GET("/", h.Campaign.List)
|
|
campaigns.POST("", h.Campaign.Create)
|
|
campaigns.POST("/", h.Campaign.Create)
|
|
campaigns.GET("/:campaign_id", h.Campaign.Get)
|
|
campaigns.PATCH("/:campaign_id", h.Campaign.Update)
|
|
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.Index)
|
|
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/destroy", h.PlatformAccountUser.Destroy)
|
|
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()),
|
|
})
|
|
}
|