- anthropic_provider.go (new): AnthropicProvider implementing the Provider
interface using Claude's messages API. Handles:
- System prompt as top-level param (not in messages array)
- Content blocks response format → extract text
- SSE streaming with Anthropic event types (content_block_delta, message_stop)
- Anthropic-specific headers (x-api-key, anthropic-version)
- Retry with exponential backoff (shared logic with OpenAI provider)
- Embedding API returns error (Anthropic has no embeddings; OpenAI-compat
provider should be used for embeddings)
- NewProviderFromConfig factory function: selects AnthropicProvider for
provider="anthropic"/"claude", OpenAIProvider for all others
- bootstrap.go: use NewProviderFromConfig instead of hardcoded NewOpenAIProvider,
allowing config.captain.llm_provider to switch between providers
Note: domestic providers (Volcengine/Doubao/Qwen) use OpenAI-compatible API
and work with the existing OpenAIProvider by setting llm_base_url.
Verified: go build + go vet + go test all pass
1007 lines
54 KiB
Go
1007 lines
54 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/gochat/gochat/internal/auth"
|
|
"github.com/gochat/gochat/internal/autoassignment"
|
|
"github.com/gochat/gochat/internal/automation"
|
|
"github.com/gochat/gochat/internal/campaign"
|
|
"github.com/gochat/gochat/internal/canned"
|
|
"github.com/gochat/gochat/internal/channel"
|
|
emailchannel "github.com/gochat/gochat/internal/channel/email"
|
|
facebookchannel "github.com/gochat/gochat/internal/channel/facebook"
|
|
googlechannel "github.com/gochat/gochat/internal/channel/google"
|
|
linechannel "github.com/gochat/gochat/internal/channel/line"
|
|
microsoftchannel "github.com/gochat/gochat/internal/channel/microsoft"
|
|
channelprovider "github.com/gochat/gochat/internal/channel/provider"
|
|
telegramchannel "github.com/gochat/gochat/internal/channel/telegram"
|
|
tiktokchannel "github.com/gochat/gochat/internal/channel/tiktok"
|
|
twiliochannel "github.com/gochat/gochat/internal/channel/twilio"
|
|
twitterchannel "github.com/gochat/gochat/internal/channel/twitter"
|
|
whatsappchannel "github.com/gochat/gochat/internal/channel/whatsapp"
|
|
"github.com/gochat/gochat/internal/config"
|
|
"github.com/gochat/gochat/internal/database"
|
|
v1 "github.com/gochat/gochat/internal/handler/api/v1"
|
|
webhook "github.com/gochat/gochat/internal/handler/webhook"
|
|
widget "github.com/gochat/gochat/internal/handler/widget"
|
|
ws "github.com/gochat/gochat/internal/handler/ws"
|
|
llm "github.com/gochat/gochat/internal/llm"
|
|
"github.com/gochat/gochat/internal/middleware"
|
|
"github.com/gochat/gochat/internal/pubsub"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/gochat/gochat/internal/router"
|
|
"github.com/gochat/gochat/internal/search"
|
|
"github.com/gochat/gochat/internal/security"
|
|
"github.com/gochat/gochat/internal/service"
|
|
"github.com/gochat/gochat/internal/worker"
|
|
wspkg "github.com/gochat/gochat/internal/ws"
|
|
"github.com/gochat/gochat/internal/wsevent"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
"github.com/spf13/viper"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// dbProvider wraps a *gorm.DB to satisfy the DBProvider interface
|
|
// used by automation, canned, and other service packages.
|
|
type dbProvider struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
func (d *dbProvider) DB() *gorm.DB { return d.db }
|
|
|
|
// Bootstrap initializes all application dependencies in order.
|
|
// Pattern follows Chatwoot's config/initializers/ + config/application.rb boot sequence:
|
|
// 1. Load & validate config
|
|
// 2. Init logger
|
|
// 3. Connect database
|
|
// 4. Connect Redis
|
|
// 5. Wire auth services (JWT, OAuth, MFA, refresh tokens)
|
|
// 6. Wire repositories -> services -> handlers (dependency injection)
|
|
// 7. Setup HTTP router + middleware chain
|
|
func Bootstrap(env string) (*App, error) {
|
|
applogger.L().Info("Bootstrapping GoChat application...")
|
|
|
|
// Step 1: Load configuration with environment overlay support
|
|
cfg, err := config.LoadWithEnv(env)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("config load failed: %w", err)
|
|
}
|
|
|
|
// Step 2: Validate configuration
|
|
if err := config.Validate(cfg); err != nil {
|
|
return nil, fmt.Errorf("config validation failed: %w", err)
|
|
}
|
|
|
|
// Step 3: Initialize logger (ref: Chatwoot config/initializers/logging.rb)
|
|
if err := applogger.Init(applogger.Config{
|
|
Level: cfg.Log.Level,
|
|
Format: cfg.Log.Format,
|
|
Output: "stdout",
|
|
ErrorOutput: "stderr",
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("logger init failed: %w", err)
|
|
}
|
|
|
|
applogger.L().Infof("Configuration loaded (env=%s, mode=%s)", env, cfg.Server.Mode)
|
|
|
|
// Step 4: Connect to PostgreSQL (ref: Chatwoot config/database.yml)
|
|
db, err := NewDatabase(&cfg.Database, cfg.Log.Level)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("database init failed: %w", err)
|
|
}
|
|
|
|
// Step 4b: Run database migrations if enabled (ref: Chatwoot ActiveRecord migration pattern)
|
|
if cfg.Database.RunMigrations {
|
|
applogger.L().Info("Running database migrations...")
|
|
if err := database.RunMigrations(cfg.Database.MigrateDSN(), cfg.Database.GetMigrationsPath()); err != nil {
|
|
return nil, fmt.Errorf("database migrations failed: %w", err)
|
|
}
|
|
applogger.L().Info("Database migrations completed successfully")
|
|
}
|
|
|
|
workerPool := worker.NewWorkerPool(db)
|
|
automation.RegisterActionDeliveryJobs(workerPool, &dbProvider{db: db})
|
|
|
|
// Step 5: Connect to Redis (ref: Chatwoot config/cable.yml)
|
|
rdb, err := NewRedisClient(&cfg.Redis)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("redis init failed: %w", err)
|
|
}
|
|
|
|
// Step 5b: Initialize PubSub — Redis-backed when Redis is available, in-memory fallback
|
|
var ps pubsub.PubSub
|
|
redisPubSub, redisErr := pubsub.NewRedisPubSub(rdb)
|
|
if redisErr != nil {
|
|
applogger.L().Warnf("Redis PubSub init failed, falling back to in-memory: %v", redisErr)
|
|
ps = pubsub.NewInMemoryPubSub()
|
|
} else {
|
|
ps = redisPubSub
|
|
applogger.L().Info("Redis PubSub initialized (watermill-redisstream)")
|
|
}
|
|
|
|
// Step 6: Wire auth infrastructure (P2E §1)
|
|
jwtService := auth.NewJWTService(&cfg.JWT)
|
|
refreshStore := auth.NewRefreshTokenStore(rdb, &cfg.JWT)
|
|
sessionStore := auth.NewSessionStore(&cfg.Session) // session management (ref: Chatwoot Devise sessions)
|
|
oauthService := auth.NewOAuthService(db, cfg)
|
|
mfaService := auth.NewMFAService(db)
|
|
webhookRegistry := auth.NewWebhookTokenRegistry()
|
|
|
|
// Step 6b: Wire SAML service (if enabled)
|
|
var samlService *auth.SAMLService
|
|
if cfg.SAML.Enabled {
|
|
samlSvc, err := auth.NewSAMLService(&cfg.SAML, rdb, db)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("saml service init failed: %w", err)
|
|
}
|
|
samlService = samlSvc
|
|
applogger.L().Info("SAML service initialized (enabled)")
|
|
} else {
|
|
samlService, _ = auth.NewSAMLService(&cfg.SAML, nil, nil)
|
|
applogger.L().Info("SAML service initialized (disabled)")
|
|
}
|
|
|
|
// Step 6c: Wire SSO session store (M13 — SSO session tracking + SLO support)
|
|
ssoSessionStore := auth.NewSSOSessionStore(rdb, 24*time.Hour)
|
|
|
|
// Step 6d: Wire LDAP service (if enabled)
|
|
var ldapService *auth.LDAPService
|
|
if cfg.LDAP.Enabled {
|
|
ldapSvc, err := auth.NewLDAPService(&cfg.LDAP, rdb, db)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ldap service init failed: %w", err)
|
|
}
|
|
ldapService = ldapSvc
|
|
applogger.L().Info("LDAP service initialized (enabled)")
|
|
} else {
|
|
ldapService, _ = auth.NewLDAPService(&cfg.LDAP, nil, nil)
|
|
applogger.L().Info("LDAP service initialized (disabled)")
|
|
}
|
|
|
|
// Step 6e: Wire OIDC service (if enabled)
|
|
var oidcService *auth.OIDCService
|
|
if cfg.OIDC.Enabled {
|
|
oidcSvc, err := auth.NewOIDCService(&cfg.OIDC, rdb, db)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("oidc service init failed: %w", err)
|
|
}
|
|
oidcService = oidcSvc
|
|
applogger.L().Info("OIDC service initialized (enabled)")
|
|
} else {
|
|
oidcService, _ = auth.NewOIDCService(&cfg.OIDC, nil, nil)
|
|
applogger.L().Info("OIDC service initialized (disabled)")
|
|
}
|
|
|
|
// Step 6f: Wire unified SSO middleware (M13 — provider routing + post-auth processing)
|
|
ssoMiddleware := auth.NewSSOMiddleware(db, rdb, cfg, samlService, ldapService, oidcService)
|
|
|
|
// Step 7: Wire repositories (data access layer)
|
|
accountRepo := repository.NewAccountRepo(db)
|
|
userRepo := repository.NewUserRepo(db)
|
|
contactRepo := repository.NewContactRepo(db)
|
|
contactNoteRepo := repository.NewContactNoteRepo(db)
|
|
contactInboxRepo := repository.NewContactInboxRepo(db)
|
|
conversationRepo := repository.NewConversationRepo(db)
|
|
inboxRepo := repository.NewInboxRepo(db)
|
|
inboxMemberRepo := repository.NewInboxMemberRepo(db)
|
|
messageRepo := repository.NewMessageRepo(db)
|
|
notificationRepo := repository.NewNotificationRepo(db)
|
|
notificationPrefRepo := repository.NewNotificationPreferenceRepo(db)
|
|
pushTokenRepo := repository.NewPushTokenRepo(db)
|
|
notificationSubscriptionRepo := repository.NewNotificationSubscriptionRepo(db)
|
|
webhookSubRepo := repository.NewWebhookSubscriptionRepo(db)
|
|
conversationParticipantRepo := repository.NewConversationParticipantRepo(db)
|
|
draftMessageRepo := repository.NewDraftMessageRepo(db)
|
|
|
|
// M11: Pre-chat form repo (needed by WidgetService)
|
|
preChatFormRepo := repository.NewPreChatFormRepo(db)
|
|
|
|
// Captain repos (P10 M10 — Captain AI + Copilot)
|
|
captainAssistantRepo := repository.NewCaptainAssistantRepo(db)
|
|
captainDocumentRepo := repository.NewCaptainDocumentRepo(db)
|
|
captainScenarioRepo := repository.NewCaptainScenarioRepo(db)
|
|
captainInboxRepo := repository.NewCaptainInboxRepo(db)
|
|
captainCustomToolRepo := repository.NewCaptainCustomToolRepo(db)
|
|
captainAssistantResponseRepo := repository.NewCaptainAssistantResponseRepo(db)
|
|
captainPreferenceRepo := repository.NewCaptainPreferenceRepo(db)
|
|
captainAutoReplyRuleRepo := repository.NewCaptainAutoReplyRuleRepo(db)
|
|
copilotThreadRepo := repository.NewCopilotThreadRepo(db)
|
|
copilotMessageRepo := repository.NewCopilotMessageRepo(db)
|
|
copilotSuggestionRepo := repository.NewCopilotSuggestionRepo(db)
|
|
|
|
// Team repos (P5 — Teams + Team Members)
|
|
teamRepo := repository.NewTeamRepo(db)
|
|
teamMemberRepo := repository.NewTeamMemberRepo(db)
|
|
|
|
// Analytics repos (P11 — Reports/Analytics)
|
|
reportingEventRepo := repository.NewReportingEventRepo(db)
|
|
reportingEventsRollupRepo := repository.NewReportingEventsRollupRepo(db)
|
|
dashboardAppRepo := repository.NewDashboardAppRepo(db)
|
|
platformAppRepo := repository.NewPlatformAppRepo(db)
|
|
accessTokenRepo := repository.NewAccessTokenRepo(db)
|
|
permissibleRepo := repository.NewPermissibleRepo(db)
|
|
// M12: AgentBot repos (AgentBot + AgentBotInbox)
|
|
agentBotRepo := repository.NewAgentBotRepo(db)
|
|
agentBotInboxRepo := repository.NewAgentBotInboxRepo(db)
|
|
|
|
// Platform: InstallationConfig repo (global key-value config for super-admin)
|
|
installationConfigRepo := repository.NewInstallationConfigRepo(db)
|
|
|
|
// Banner repo (Platform announcement banners)
|
|
bannerRepo := repository.NewBannerRepo(db)
|
|
|
|
// WidgetTest repo (Platform read-only, widget/UI integration testing)
|
|
widgetTestRepo := repository.NewWidgetTestRepo(db)
|
|
|
|
// EmailChannelMigration repo (account-scoped email channel migration)
|
|
emailChannelMigrationRepo := repository.NewEmailChannelMigrationRepo(db)
|
|
|
|
// Knowledge Base repos (M9 — Help Center / Knowledge Base)
|
|
portalRepo := repository.NewPortalRepo(db)
|
|
categoryRepo := repository.NewCategoryRepo(db)
|
|
relatedCategoryRepo := repository.NewRelatedCategoryRepo(db)
|
|
articleRepo := repository.NewArticleRepo(db)
|
|
articleEmbeddingRepo := repository.NewArticleEmbeddingRepo(db)
|
|
folderRepo := repository.NewFolderRepo(db)
|
|
portalMemberRepo := repository.NewPortalMemberRepo(db)
|
|
|
|
// Campaign + AutoAssignment repos
|
|
campaignRepo := repository.NewCampaignRepo(db)
|
|
assignmentPolicyRepo := repository.NewAssignmentPolicyRepo(db)
|
|
inboxAssignmentPolicyRepo := repository.NewInboxAssignmentPolicyRepo(db)
|
|
|
|
// SLA Policy repos (M11 — SLA Policy + AppliedSLA + SlaEvent + SlaPolicyInbox)
|
|
slaPolicyRepo := repository.NewSlaPolicyRepo(db)
|
|
appliedSlaRepo := repository.NewAppliedSlaRepo(db)
|
|
slaEventRepo := repository.NewSlaEventRepo(db)
|
|
slaPolicyInboxRepo := repository.NewSlaPolicyInboxRepo(db)
|
|
|
|
// Assignment Policy V2 repo (M11 — enhanced with Type + inbox join)
|
|
assignmentPolicyV2Repo := repository.NewAssignmentPolicyV2Repo(db)
|
|
assignmentPolicyInboxRepo := repository.NewAssignmentPolicyInboxRepo(db)
|
|
|
|
// Tag/Label repos (M2 — Label+Tag system)
|
|
tagRepo := repository.NewTagRepo(db)
|
|
convLabelRepo := repository.NewConversationLabelRepo(db)
|
|
|
|
// S6: WorkingHour repos (out-of-office / business hours)
|
|
workingHourRepo := repository.NewWorkingHourRepo(db)
|
|
|
|
// Search repo (M14 — Global Search + Advanced Filter)
|
|
searchRepo := repository.NewSearchRepo(db)
|
|
|
|
// G4: Company repo (CRUD + search + nested contacts/conversations/notes)
|
|
companyRepo := repository.NewCompanyRepo(db)
|
|
|
|
// G16: Third-party integration repos (IntegrationHook + IntegrationApp)
|
|
integrationHookRepo := repository.NewIntegrationHookRepo(db)
|
|
integrationAppRepo := repository.NewIntegrationAppRepo(db)
|
|
|
|
// M13 repos: SAML IdP config + Account SAML settings (enterprise SSO)
|
|
samlIdPConfigRepo := repository.NewSAMLIdPConfigRepo(db)
|
|
accountSamlSettingsRepo := repository.NewAccountSamlSettingsRepo(db)
|
|
_ = samlIdPConfigRepo // used by SAML service later
|
|
|
|
// M13 repos: LDAP + OIDC account settings (enterprise SSO)
|
|
accountLDAPSettingsRepo := repository.NewAccountLDAPSettingsRepo(db)
|
|
accountOIDCSettingsRepo := repository.NewAccountOIDCSettingsRepo(db)
|
|
_ = accountLDAPSettingsRepo // used by LDAP handler later
|
|
_ = accountOIDCSettingsRepo // used by OIDC handler later
|
|
|
|
// Custom attribute definition + custom filter repos
|
|
customAttributeDefinitionRepo := repository.NewCustomAttributeDefinitionRepo(db)
|
|
customFilterRepo := repository.NewCustomFilterRepo(db)
|
|
|
|
// Lane C: CSAT template + Inbox limit repos
|
|
csatTemplateRepo := repository.NewCsatTemplateRepo(db)
|
|
inboxLimitRepo := repository.NewInboxLimitRepo(db)
|
|
|
|
// Enterprise: AuditLog, CustomRole, AgentCapacityPolicy repos
|
|
auditRepo := repository.NewAuditRepo(db)
|
|
customRoleRepo := repository.NewCustomRoleRepo(db)
|
|
agentCapacityPolicyRepo := repository.NewAgentCapacityPolicyRepo(db)
|
|
|
|
// Step 8: Wire services (business logic layer)
|
|
authService := service.NewAuthService(db, jwtService, refreshStore, oauthService, mfaService)
|
|
accountService := service.NewAccountService(accountRepo)
|
|
contactInboxService := service.NewContactInboxService(contactInboxRepo)
|
|
noteRepo := repository.NewNoteRepo(db)
|
|
contactNoteService := service.NewContactNoteService(contactRepo, contactNoteRepo)
|
|
contactService := service.NewContactService(contactRepo, contactInboxService, noteRepo)
|
|
contactService.SetContactExportMailer(service.NewEnvContactExportMailer())
|
|
contactService.SetWorkerPool(workerPool)
|
|
// G4: Company service (depends on companyRepo, contactRepo, conversationRepo for nested queries)
|
|
companyService := service.NewCompanyService(companyRepo, contactRepo, conversationRepo)
|
|
|
|
// G16: Third-party integration services
|
|
integrationHookService := service.NewIntegrationHookService(integrationHookRepo, integrationAppRepo, nil) // registry created after webhookSignatureService
|
|
slackIntegrationService := service.NewSlackIntegrationService(integrationHookRepo)
|
|
shopifyIntegrationService := service.NewShopifyIntegrationService(integrationHookRepo)
|
|
linearIntegrationService := service.NewLinearIntegrationService(integrationHookRepo)
|
|
notionIntegrationService := service.NewNotionIntegrationService(integrationHookRepo)
|
|
dyteIntegrationService := service.NewDyteIntegrationService(integrationHookRepo, messageRepo)
|
|
|
|
// Create channel dispatcher for event-driven architecture (ref: Chatwoot Dispatcher)
|
|
channelDispatcher := channel.NewDispatcher(workerPool)
|
|
service.RegisterConversationMaintenanceJobs(workerPool, db, channelDispatcher)
|
|
if _, err := service.EnqueueScheduledItemsTrigger(context.Background(), workerPool, time.Now()); err != nil {
|
|
applogger.L().Warnf("failed to enqueue initial scheduled item trigger: %v", err)
|
|
}
|
|
|
|
// P9: AgentBot rule engine services
|
|
botRuleService := automation.NewBotRuleService(&dbProvider{db: db})
|
|
botTriggerConfigService := automation.NewBotTriggerConfigService(&dbProvider{db: db})
|
|
|
|
// Step 8b: Wire channel providers (Facebook, Instagram, Telegram, Twitter, Microsoft, Google)
|
|
// Reference: Chatwoot registers channel providers in config/initializers/channels.rb
|
|
fbProvider := facebookchannel.NewFacebookProvider()
|
|
igProvider := facebookchannel.NewInstagramProvider()
|
|
tgProvider := channelprovider.NewTelegramProvider()
|
|
twProvider := twitterchannel.NewTwitterProvider(twitterchannel.TwitterOAuth2Config{
|
|
ClientID: cfg.OAuth.Twitter.ClientID,
|
|
ClientSecret: cfg.OAuth.Twitter.ClientSecret,
|
|
RedirectURL: cfg.OAuth.Twitter.RedirectURL,
|
|
Scopes: cfg.OAuth.Twitter.Scopes,
|
|
})
|
|
msProvider := microsoftchannel.NewMicrosoftProvider(microsoftchannel.MicrosoftOAuthConfig{
|
|
ClientID: cfg.OAuth.Microsoft.ClientID,
|
|
ClientSecret: cfg.OAuth.Microsoft.ClientSecret,
|
|
TenantID: cfg.OAuth.Microsoft.TenantID,
|
|
RedirectURL: cfg.OAuth.Microsoft.RedirectURL,
|
|
Scopes: cfg.OAuth.Microsoft.Scopes,
|
|
})
|
|
goProvider := googlechannel.NewGoogleProvider(googlechannel.GoogleOAuthConfig{
|
|
ClientID: cfg.OAuth.Google.ClientID,
|
|
ClientSecret: cfg.OAuth.Google.ClientSecret,
|
|
RedirectURL: cfg.OAuth.Google.RedirectURL,
|
|
Scopes: cfg.OAuth.Google.Scopes,
|
|
})
|
|
|
|
// Register channel providers with the global registry so outgoing messages route correctly.
|
|
// The provider/ init() package already registers these, so we use Register() which
|
|
// returns an error on duplicates rather than MustRegister() which panics.
|
|
if err := channel.Register(fbProvider); err != nil {
|
|
applogger.L().Warnf("FB provider already registered: %v", err)
|
|
}
|
|
if err := channel.Register(igProvider); err != nil {
|
|
applogger.L().Warnf("IG provider already registered: %v", err)
|
|
}
|
|
if err := channel.Register(tgProvider); err != nil {
|
|
applogger.L().Warnf("Telegram provider already registered: %v", err)
|
|
}
|
|
if err := channel.Register(twProvider); err != nil {
|
|
applogger.L().Warnf("Twitter provider already registered: %v", err)
|
|
}
|
|
if err := channel.Register(msProvider); err != nil {
|
|
applogger.L().Warnf("Microsoft provider already registered: %v", err)
|
|
}
|
|
if err := channel.Register(goProvider); err != nil {
|
|
applogger.L().Warnf("Google provider already registered: %v", err)
|
|
}
|
|
|
|
// Create Facebook channel repository and service (for listener DB access)
|
|
fbRepo := facebookchannel.NewRepository(db)
|
|
fbService := facebookchannel.NewService(fbRepo)
|
|
|
|
// Create Facebook/Instagram event listeners for incoming event processing
|
|
// (delegates to provider ProcessIncoming, handles contact/inbox resolution)
|
|
fbListener := facebookchannel.NewFacebookEventListener(db, fbService, fbRepo, fbProvider)
|
|
igListener := facebookchannel.NewInstagramEventListener(db, fbService, fbRepo, igProvider)
|
|
|
|
// Register listeners on the channel dispatcher (keyed by Name(), not channel type)
|
|
channelDispatcher.Register(fbListener)
|
|
channelDispatcher.Register(igListener)
|
|
channelDispatcher.Register(channel.NewWebhookListenerWithDB(db))
|
|
// Create Facebook webhook handler (Gin HTTP handler for FB/IG webhook endpoints)
|
|
facebookWebhookHandler := webhook.NewFacebookWebhookHandler(fbProvider, igProvider, db, channelDispatcher)
|
|
facebookWebhookHandler.WithWorkerPool(workerPool)
|
|
shopifyWebhookHandler := webhook.NewShopifyWebhookHandler(db, "")
|
|
|
|
// Create Telegram webhook handler (Gin HTTP handler for Telegram webhook endpoint)
|
|
telWebhook := telegramchannel.NewWebhookHandler(tgProvider)
|
|
telegramWebhookHandler := webhook.NewTelegramWebhookHandler(tgProvider, telWebhook, db, channelDispatcher)
|
|
telegramWebhookHandler.WithWorkerPool(workerPool)
|
|
|
|
// Step 8c: Wire WhatsApp channel provider (Cloud API / 360dialog)
|
|
// Reference: Chatwoot registers WhatsApp channel providers in config/initializers/channels.rb
|
|
// WhatsAppProvider requires service/repo/pipeline deps (unlike FB/IG/TG which are self-contained),
|
|
// so we cannot use init() registration — must wire here in bootstrap.
|
|
waRepo := whatsappchannel.NewRepository(db)
|
|
waService := whatsappchannel.NewWhatsAppService(waRepo)
|
|
waPipeline := whatsappchannel.NewIncomingPipeline(waService)
|
|
waProvider := whatsappchannel.NewWhatsAppProvider(waService, waRepo, waPipeline)
|
|
|
|
// Register WhatsApp provider with the global registry for outgoing message routing.
|
|
if err := channel.Register(waProvider); err != nil {
|
|
applogger.L().Warnf("WhatsApp provider already registered: %v", err)
|
|
}
|
|
|
|
// Create WhatsApp webhook handler (Gin HTTP handler for WA webhook endpoints)
|
|
waWebhook := whatsappchannel.NewWebhookHandler(waProvider)
|
|
whatsappWebhookHandler := webhook.NewWhatsAppWebhookHandler(waProvider, waWebhook, db, channelDispatcher)
|
|
whatsappWebhookHandler.WithWorkerPool(workerPool)
|
|
|
|
// Step 8d: Wire TikTok channel provider (Business API)
|
|
// TikTok requires service/repo/pipeline deps (like WhatsApp), so wire here in bootstrap.
|
|
ttRepo := tiktokchannel.NewRepository(db)
|
|
ttService := tiktokchannel.NewTikTokService(ttRepo)
|
|
ttPipeline := tiktokchannel.NewIncomingProcessor(ttService, ttRepo)
|
|
ttProvider := tiktokchannel.NewTikTokProvider(ttService, ttRepo, ttPipeline)
|
|
|
|
// Register TikTok provider with the global registry for outgoing message routing.
|
|
if err := channel.Register(ttProvider); err != nil {
|
|
applogger.L().Warnf("TikTok provider already registered: %v", err)
|
|
}
|
|
|
|
// Create TikTok webhook handler (Gin HTTP handler for TikTok webhook endpoints)
|
|
ttWebhook := tiktokchannel.NewWebhookHandler(ttService, ttPipeline)
|
|
tiktokWebhookHandler := webhook.NewTikTokWebhookHandler(ttWebhook, ttPipeline, db, channelDispatcher)
|
|
tiktokWebhookHandler.WithWorkerPool(workerPool)
|
|
|
|
// Step 8e: Wire LINE channel provider (Messaging API)
|
|
// LINE requires service/repo/pipeline deps (like WhatsApp), so wire here in bootstrap.
|
|
lineRepo := linechannel.NewRepository(db)
|
|
lineService := linechannel.NewLineService(lineRepo)
|
|
linePipeline := linechannel.NewIncomingProcessor(lineService)
|
|
lineProvider := linechannel.NewLineProvider(lineService, lineRepo, linePipeline)
|
|
|
|
// Register LINE provider with the global registry for outgoing message routing.
|
|
if err := channel.Register(lineProvider); err != nil {
|
|
applogger.L().Warnf("LINE provider already registered: %v", err)
|
|
}
|
|
|
|
// Create LINE webhook handler (Gin HTTP handler for LINE webhook endpoints)
|
|
lineWebhook := linechannel.NewWebhookHandler(linePipeline, lineService)
|
|
lineWebhookHandler := webhook.NewLineWebhookHandler(lineWebhook, linePipeline, lineService, db, channelDispatcher)
|
|
lineWebhookHandler.WithWorkerPool(workerPool)
|
|
|
|
// Step 8f: Wire Twilio SMS channel provider (Twilio SMS API)
|
|
// Twilio SMS requires service/repo/pipeline deps (like WhatsApp), so wire here in bootstrap.
|
|
twilioRepo := twiliochannel.NewRepository(db)
|
|
twilioService := twiliochannel.NewTwilioService(twilioRepo)
|
|
twilioPipeline := twiliochannel.NewIncomingProcessor(twilioService)
|
|
twilioProvider := twiliochannel.NewTwilioProvider(twilioService, twilioRepo, twilioPipeline)
|
|
|
|
// Register Twilio SMS provider with the global registry for outgoing message routing.
|
|
if err := channel.Register(twilioProvider); err != nil {
|
|
applogger.L().Warnf("Twilio SMS provider already registered: %v", err)
|
|
}
|
|
|
|
// Create Twilio webhook handler (Gin HTTP handler for Twilio SMS webhook endpoints)
|
|
twilioWebhook := twiliochannel.NewWebhookHandler(twilioPipeline, twilioService)
|
|
twilioWebhookHandler := webhook.NewTwilioWebhookHandler(twilioWebhook, db, channelDispatcher)
|
|
twilioWebhookHandler.WithWorkerPool(workerPool)
|
|
|
|
// Step 8g: Wire Email channel provider (IMAP/SMTP)
|
|
// Email requires service/repo/pipeline deps, so wire here in bootstrap.
|
|
emailRepo := emailchannel.NewRepository(db)
|
|
emailSvc := emailchannel.NewService()
|
|
emailProvider := emailchannel.NewEmailProvider(emailSvc, emailRepo, nil)
|
|
emailPipeline := emailchannel.NewIncomingProcessor(emailProvider)
|
|
emailProvider.SetPipeline(emailPipeline)
|
|
|
|
// Register Email provider with the global registry for outgoing message routing.
|
|
if err := channel.Register(emailProvider); err != nil {
|
|
applogger.L().Warnf("Email provider already registered: %v", err)
|
|
}
|
|
|
|
// Create Email webhook handler (Gin HTTP handler for Email webhook endpoints)
|
|
emailWebhook := emailchannel.NewWebhookHandler()
|
|
emailWebhookHandler := webhook.NewEmailWebhookHandler(emailWebhook, emailPipeline, db)
|
|
|
|
inboxMemberService := service.NewInboxMemberService(inboxMemberRepo)
|
|
accountUserRepo := repository.NewAccountUserRepo(db)
|
|
agentRepo := repository.NewAgentRepo(db)
|
|
conversationService := service.NewConversationService(conversationRepo, messageRepo, channelDispatcher, inboxMemberService, accountUserRepo, teamRepo, teamMemberRepo)
|
|
conversationService.SetWorkerPool(workerPool)
|
|
appliedSlaService := service.NewAppliedSlaService(appliedSlaRepo, slaEventRepo, slaPolicyRepo, conversationRepo)
|
|
conversationService.SetAppliedSlaService(appliedSlaService)
|
|
service.RegisterSlaProcessingJobs(workerPool, db, appliedSlaService)
|
|
if _, err := service.EnqueueSlaAccountsScan(context.Background(), workerPool, time.Now()); err != nil {
|
|
applogger.L().Warnf("failed to enqueue initial SLA scan: %v", err)
|
|
}
|
|
conversationParticipantService := service.NewConversationParticipantService(conversationParticipantRepo, conversationRepo)
|
|
draftMessageService := service.NewDraftMessageService(draftMessageRepo, conversationRepo)
|
|
inboxService := service.NewInboxService(inboxRepo, agentBotInboxRepo, agentBotRepo, campaignRepo, webhookSubRepo, waService, waRepo)
|
|
inboxService.SetWorkerPool(workerPool)
|
|
service.RegisterInboxTemplateSyncJobs(workerPool, inboxService)
|
|
igRepo := repository.NewChannelInstagramRepo(db)
|
|
igService := service.NewChannelInstagramService(igRepo, igProvider)
|
|
fbChannelRepo := repository.NewChannelFacebookRepo(db)
|
|
fbChannelService := service.NewChannelFacebookService(fbChannelRepo)
|
|
twRepo := repository.NewChannelTwitterRepo(db)
|
|
twService := service.NewChannelTwitterService(twRepo)
|
|
msRepo := repository.NewChannelMicrosoftRepo(db)
|
|
msService := service.NewChannelMicrosoftService(msRepo)
|
|
goRepo := repository.NewChannelGoogleRepo(db)
|
|
goService := service.NewChannelGoogleService(goRepo)
|
|
|
|
// Step 8h: Wire channel CRUD service/repo layers for TikTok, LINE, Twilio SMS, Email
|
|
// These service/repo pairs are for the v1 API channel handlers (CRUD operations),
|
|
// distinct from the channel-internal service/repo (which handle message pipeline processing).
|
|
ttChannelRepo := repository.NewChannelTikTokRepo(db)
|
|
ttChannelSvc := service.NewChannelTikTokService(ttChannelRepo)
|
|
lineChannelRepo := repository.NewChannelLINERepo(db)
|
|
lineChannelSvc := service.NewChannelLINEService(lineChannelRepo)
|
|
twilioSMSRepo := repository.NewChannelTwilioSMSRepo(db)
|
|
twilioSMSSvc := service.NewChannelTwilioSMSService(twilioSMSRepo)
|
|
emailChannelRepo := repository.NewChannelEmailRepo(db)
|
|
emailChannelSvc := service.NewChannelEmailService(emailChannelRepo)
|
|
|
|
// LLM provider for Captain AI + Copilot features (must be created before services that depend on it)
|
|
// Provider is selected by config.captain.llm_provider: "openai" (default, also works for
|
|
// Azure/custom/compatible domestic providers), "anthropic" (Claude API format).
|
|
llmProvider := llm.NewProviderFromConfig(
|
|
cfg.Captain.LLMProvider,
|
|
cfg.Captain.LLMAPIKey,
|
|
cfg.Captain.LLMBaseURL,
|
|
cfg.Captain.LLMModel,
|
|
cfg.Captain.EmbeddingModel,
|
|
)
|
|
|
|
messageService := service.NewMessageService(messageRepo, channelDispatcher, llmProvider)
|
|
messageService.SetWorkerPool(workerPool)
|
|
notificationService := service.NewNotificationService(db, notificationRepo, notificationPrefRepo)
|
|
|
|
// M2 — Tag+Label services
|
|
tagService := service.NewTagService(tagRepo)
|
|
labelService := service.NewLabelService(convLabelRepo, tagRepo)
|
|
|
|
// P4 Notification+Webhook services
|
|
pushSubscriptionService := service.NewPushSubscriptionService(pushTokenRepo)
|
|
notificationSubscriptionService := service.NewNotificationSubscriptionService(notificationSubscriptionRepo)
|
|
pushDeliveryService := service.NewPushDeliveryService(pushTokenRepo, cfg.Push.VapidPublicKey, cfg.Push.VapidPrivateKey, cfg.Push.VapidSubject)
|
|
webhookSubscriptionService := service.NewWebhookSubscriptionService(webhookSubRepo)
|
|
webhookDeliveryService := service.NewWebhookDeliveryService(webhookSubRepo)
|
|
webhookSignatureService := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
|
webhookProcessorRegistry := service.NewWebhookProcessorRegistry(webhookSignatureService, integrationHookRepo, accountRepo)
|
|
// Wire registry into integrationHookService (created earlier with nil)
|
|
integrationHookService.SetRegistry(webhookProcessorRegistry)
|
|
_ = pushDeliveryService
|
|
_ = webhookDeliveryService
|
|
_ = webhookSignatureService
|
|
|
|
// Notification delivery pipeline — subscribes to EventBus topics and delivers via push/email/webhook (P4 M8)
|
|
notificationDeliverySvc, err := service.NewNotificationDeliveryService(
|
|
notificationService,
|
|
pushDeliveryService,
|
|
webhookDeliveryService,
|
|
webhookSignatureService,
|
|
notificationPrefRepo,
|
|
pushTokenRepo,
|
|
webhookSubRepo,
|
|
rdb,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to init notification delivery service: %w", err)
|
|
}
|
|
|
|
// Captain services (P10 M10 — Captain AI + Copilot)
|
|
captainAssistantService := service.NewCaptainAssistantService(captainAssistantRepo, captainInboxRepo, captainDocumentRepo, captainAssistantResponseRepo, llmProvider)
|
|
captainDocumentService := service.NewCaptainDocumentService(captainDocumentRepo, llmProvider, captainAssistantRepo)
|
|
captainDocumentService.SetResponseRepo(captainAssistantResponseRepo)
|
|
captainDocumentService.SetWorkerPool(workerPool)
|
|
if _, err := service.EnqueueCaptainDocumentScheduleSyncs(context.Background(), workerPool, time.Now()); err != nil {
|
|
applogger.L().Warnf("failed to enqueue initial Captain document sync scheduler: %v", err)
|
|
}
|
|
captainScenarioService := service.NewCaptainScenarioService(captainScenarioRepo, captainAssistantRepo)
|
|
captainCustomToolService := service.NewCaptainCustomToolService(captainCustomToolRepo, accountRepo)
|
|
copilotService := service.NewCopilotService(copilotThreadRepo, copilotMessageRepo, copilotSuggestionRepo, llmProvider, captainAssistantRepo)
|
|
copilotService.SetWorkerPool(workerPool)
|
|
captainConversationService := service.NewCaptainConversationService(db, llmProvider)
|
|
captainConversationService.SetWorkerPool(workerPool)
|
|
|
|
// Tool execution service — LLM function calling (tool_call loop)
|
|
toolExecutionService := service.NewToolExecutionService(captainCustomToolRepo, llmProvider)
|
|
captainConversationService.SetToolExecutionService(toolExecutionService)
|
|
copilotContextService := service.NewCopilotContextService(messageRepo, conversationRepo, contactRepo, llmProvider)
|
|
captainTaskService := service.NewCaptainTaskService(captainAssistantRepo, captainAssistantResponseRepo, captainCustomToolRepo, conversationRepo, messageRepo, llmProvider, copilotContextService, copilotSuggestionRepo)
|
|
conversationInsightService := service.NewConversationInsightService(conversationRepo, messageRepo, captainAssistantRepo, llmProvider)
|
|
captainPreferenceService := service.NewCaptainPreferenceService(captainPreferenceRepo, accountRepo)
|
|
captainTaskExtendedService := service.NewCaptainTaskExtendedService(conversationRepo, messageRepo, captainAssistantRepo, captainPreferenceRepo, llmProvider, copilotSuggestionRepo)
|
|
captainAssistantResponseService := service.NewCaptainAssistantResponseService(captainAssistantRepo, captainAssistantResponseRepo, conversationRepo, messageRepo, captainPreferenceRepo, llmProvider)
|
|
captainBulkActionService := service.NewCaptainBulkActionService(conversationRepo, messageRepo, captainAssistantRepo, captainPreferenceRepo, llmProvider, captainTaskExtendedService, captainAssistantResponseService)
|
|
captainBulkActionService.SetCaptainResourceRepos(captainAssistantResponseRepo, captainDocumentRepo)
|
|
|
|
// RAG service — knowledge base Q&A (embedding search + LLM generation)
|
|
ragService := service.NewRAGService(captainAssistantResponseRepo, captainAssistantRepo, llmProvider)
|
|
|
|
// Auto-reply rule service — CRUD + condition matching + LLM reply composition
|
|
autoReplyRuleService := service.NewAutoReplyRuleService(captainAutoReplyRuleRepo, captainAssistantRepo, conversationRepo, llmProvider)
|
|
|
|
// Analytics services (P11 — Reports/Analytics)
|
|
analyticsService := service.NewAnalyticsService(reportingEventRepo, reportingEventsRollupRepo)
|
|
analyticsService.SetWorkerPool(workerPool)
|
|
yearInReviewService := service.NewYearInReviewService(db)
|
|
summaryReportService := service.NewSummaryReportService(reportingEventsRollupRepo)
|
|
dashboardAppService := service.NewDashboardAppService(dashboardAppRepo)
|
|
platformAppService := service.NewPlatformAppService(platformAppRepo, accessTokenRepo, permissibleRepo)
|
|
platformUserService := service.NewPlatformUserService(userRepo, permissibleRepo, accessTokenRepo, accountUserRepo)
|
|
// M12: AgentBot services (AgentBot + AgentBotInbox + Listener)
|
|
agentBotService := service.NewAgentBotService(agentBotRepo)
|
|
agentBotInboxService := service.NewAgentBotInboxService(agentBotInboxRepo, agentBotRepo)
|
|
agentBotListener := service.NewAgentBotListener(agentBotInboxRepo, agentBotRepo, conversationRepo, messageRepo)
|
|
agentBotListener.SetCaptainConversationService(captainConversationService)
|
|
channelDispatcher.Register(agentBotListener)
|
|
|
|
// Auto-reply listener — evaluates rules on incoming messages and sends auto-replies
|
|
autoReplyListener := service.NewAutoReplyListener(captainAutoReplyRuleRepo, autoReplyRuleService, conversationRepo, messageRepo, messageService)
|
|
channelDispatcher.Register(autoReplyListener)
|
|
|
|
// Platform: InstallationConfig service (global key-value config for super-admin)
|
|
installationConfigService := service.NewInstallationConfigService(installationConfigRepo)
|
|
|
|
// Banner service (Platform CRUD + account read-only)
|
|
bannerService := service.NewBannerService(bannerRepo)
|
|
|
|
// WidgetTest service (Platform read-only, widget/UI integration testing)
|
|
widgetTestService := service.NewWidgetTestService(widgetTestRepo)
|
|
|
|
// EmailChannelMigration service (account-scoped create-only)
|
|
emailChannelMigrationService := service.NewEmailChannelMigrationService(emailChannelMigrationRepo)
|
|
|
|
// Knowledge Base services (M9 — Help Center / Knowledge Base)
|
|
portalService := service.NewPortalService(portalRepo)
|
|
categoryService := service.NewCategoryService(categoryRepo, relatedCategoryRepo)
|
|
articleService := service.NewArticleService(articleRepo)
|
|
articleService.SetWorkerPool(workerPool)
|
|
articleService.SetArticleTranslationBackend(service.NewLLMArticleTranslationBackend(llmProvider))
|
|
articleService.SetEmbeddingRepo(articleEmbeddingRepo)
|
|
articleService.SetLLMProvider(llmProvider)
|
|
folderService := service.NewFolderService(folderRepo)
|
|
portalMemberService := service.NewPortalMemberService(portalMemberRepo)
|
|
|
|
// Automation services (P6 — Automation Rules + Macros + CSAT)
|
|
automationRuleService := automation.NewAutomationRuleServiceWithWorker(&dbProvider{db: db}, workerPool)
|
|
macroService := automation.NewMacroServiceWithWorker(&dbProvider{db: db}, workerPool)
|
|
csatSurveyService := automation.NewCsatSurveyService(&dbProvider{db: db})
|
|
cannedResponseService := canned.NewCannedResponseService(&dbProvider{db: db})
|
|
|
|
// Team + Profile services (P5 — Teams + Team Members + User Profiles)
|
|
teamService := service.NewTeamService(teamRepo, teamMemberRepo, db)
|
|
agentService := service.NewAgentService(agentRepo, db)
|
|
agentService.SetConfirmationMailer(service.NewEnvProfileConfirmationMailer())
|
|
profileService := service.NewProfileService(userRepo, accountUserRepo, accessTokenRepo, installationConfigRepo)
|
|
profileService.SetConfirmationMailer(service.NewEnvProfileConfirmationMailer())
|
|
|
|
// Campaign + AutoAssignment services
|
|
campaignInternalSvc := campaign.NewCampaignService(db, channelDispatcher)
|
|
campaignService := service.NewCampaignService(campaignInternalSvc, campaignRepo)
|
|
assignmentInternalSvc := autoassignment.NewAssignmentService(db, rdb)
|
|
assignmentPolicyService := service.NewAssignmentPolicyService(assignmentPolicyRepo, inboxAssignmentPolicyRepo, assignmentInternalSvc)
|
|
|
|
// SLA Policy service (M11 — SLA Policy + AppliedSLA + SlaEvent + inbox associations)
|
|
slaPolicyService := service.NewSlaPolicyService(slaPolicyRepo, appliedSlaRepo, slaEventRepo, slaPolicyInboxRepo)
|
|
|
|
// Assignment Policy V2 service (M11 — enhanced with Type + inbox join)
|
|
assignmentPolicyV2Service := service.NewAssignmentPolicyV2Service(assignmentPolicyV2Repo, assignmentPolicyInboxRepo)
|
|
|
|
// Search service (M14 — Global Search + Advanced Filter)
|
|
searchEngine, err := search.NewSearchEngine(search.EngineConfig{
|
|
Engine: cfg.Search.Engine,
|
|
Host: cfg.Search.Host,
|
|
APIKey: cfg.Search.APIKey,
|
|
IndexPrefix: cfg.Search.IndexPrefix,
|
|
TimeoutSeconds: cfg.Search.TimeoutSeconds,
|
|
}, searchRepo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("search engine init failed: %w", err)
|
|
}
|
|
searchService := search.NewSearchServiceWithEngine(searchEngine, searchRepo)
|
|
searchIndexer := service.NewDurableSearchIndexer(db, workerPool, searchService)
|
|
service.RegisterConversationMaintenanceSearchIndexer(workerPool, db, searchIndexer)
|
|
conversationService.SetSearchIndexer(searchIndexer)
|
|
messageService.SetSearchIndexer(searchIndexer)
|
|
contactService.SetSearchIndexer(searchIndexer)
|
|
contactService.SetSearchReader(searchService)
|
|
companyService.SetSearchIndexer(searchIndexer)
|
|
companyService.SetSearchReader(searchService)
|
|
articleService.SetSearchIndexer(searchIndexer)
|
|
automationRuleService.SetSearchIndexer(searchIndexer)
|
|
macroService.SetSearchIndexer(searchIndexer)
|
|
csatSurveyService.SetSearchIndexer(searchIndexer)
|
|
botRuleService.SetSearchIndexer(searchIndexer)
|
|
facebookWebhookHandler.WithSearchIndexer(searchIndexer)
|
|
telegramWebhookHandler.WithSearchIndexer(searchIndexer)
|
|
whatsappWebhookHandler.WithSearchIndexer(searchIndexer)
|
|
tiktokWebhookHandler.WithSearchIndexer(searchIndexer)
|
|
lineWebhookHandler.WithSearchIndexer(searchIndexer)
|
|
twilioWebhookHandler.WithSearchIndexer(searchIndexer)
|
|
// Event-triggered automation/AgentBot listeners are registered after the durable
|
|
// search indexer exists so action side effects keep Meilisearch current.
|
|
channelDispatcher.Register(automation.NewAgentBotRuleListenerWithSearchIndexer(&dbProvider{db: db}, searchIndexer))
|
|
automation.RegisterAutomationRuleListenerWithWorkerAndSearchIndexer(channelDispatcher, &dbProvider{db: db}, workerPool, searchIndexer)
|
|
// M6: Register CSAT survey listener on the dispatcher for conversation.resolved events
|
|
channelDispatcher.Register(automation.NewCsatSurveyListenerWithWorkerAndSearchIndexer(&dbProvider{db: db}, workerPool, searchIndexer))
|
|
|
|
// Custom attribute definition + custom filter + custom attribute value services
|
|
customAttributeDefinitionService := service.NewCustomAttributeDefinitionService(customAttributeDefinitionRepo)
|
|
customFilterService := service.NewCustomFilterService(customFilterRepo)
|
|
customAttributeValueService := service.NewCustomAttributeValueService(customAttributeDefinitionRepo, conversationRepo, contactRepo)
|
|
|
|
// Lane C: CSAT template + Inbox limit services
|
|
csatTemplateService := service.NewCsatTemplateService(csatTemplateRepo)
|
|
csatTemplateService.SetWorkerPool(workerPool)
|
|
inboxLimitService := service.NewInboxLimitService(inboxLimitRepo)
|
|
|
|
// S6: WorkingHour service (out-of-office / business hours per inbox)
|
|
workingHourService := service.NewWorkingHourService(workingHourRepo, inboxRepo, accountRepo)
|
|
|
|
// Lane B: AssignableAgent service (find agents available for assignment)
|
|
assignableAgentService := service.NewAssignableAgentService(inboxMemberRepo, userRepo, accountRepo, conversationRepo)
|
|
|
|
// Enterprise: AuditLog, CustomRole, AgentCapacityPolicy, CsatMetrics services
|
|
auditService := service.NewAuditService(auditRepo)
|
|
customRoleService := service.NewCustomRoleService(customRoleRepo)
|
|
agentCapacityPolicyService := service.NewAgentCapacityPolicyService(agentCapacityPolicyRepo)
|
|
csatMetricsService := service.NewCsatMetricsService(&dbProvider{db: db})
|
|
|
|
// Step 11 (early): Create WebSocket hub (ref: Chatwoot ActionCable)
|
|
// Must be created before handlers so the hubTypingAdapter can reference it.
|
|
wsHub := ws.NewHubSimple()
|
|
wsRelay := wspkg.NewBroadcastRelay(rdb, wsHub)
|
|
eventPublisher := wspkg.NewEventPublisher(wsHub, nil, wsRelay)
|
|
presenceTracker := wspkg.NewPresenceTracker(rdb, wsRelay)
|
|
|
|
// Bridge channel dispatcher events to the WebSocket EventPublisher so that
|
|
// message/conversation/contact events reach connected WS/SSE clients.
|
|
channelDispatcher.Register(wsevent.New(eventPublisher))
|
|
|
|
// Widget service + handler (M11 — WebWidget channel completion)
|
|
// hubTypingAdapter delegates typing events to the WS hub's direct broadcast,
|
|
// avoiding Redis dependency (the full TypingTracker requires Redis).
|
|
widgetTypingAdapter := &hubTypingAdapter{hub: wsHub}
|
|
widgetThemeConfigRepo := repository.NewWidgetThemeConfigRepo(db)
|
|
widgetFileUploadRepo := repository.NewWidgetFileUploadRepo(db)
|
|
widgetOfflineMessageRepo := repository.NewWidgetOfflineMessageRepo(db)
|
|
widgetService := service.NewWidgetService(inboxRepo, contactRepo, contactInboxRepo, conversationRepo, messageRepo, widgetTypingAdapter, widgetThemeConfigRepo, preChatFormRepo, widgetFileUploadRepo, widgetOfflineMessageRepo, inboxMemberRepo, tagRepo, campaignRepo)
|
|
widgetService.SetWorkerPool(workerPool)
|
|
widgetHandler := widget.NewHandler(widgetService).WithEventPublisher(eventPublisher)
|
|
|
|
// Upload: DirectUpload repo + service + handler (account-level + widget direct uploads)
|
|
directUploadRepo := repository.NewDirectUploadRepo(db)
|
|
uploadService := service.NewUploadService(directUploadRepo, cfg).
|
|
WithWidgetAuth(inboxRepo, contactInboxRepo).
|
|
WithConversationRepo(conversationRepo)
|
|
uploadHandler := v1.NewUploadHandler(uploadService)
|
|
|
|
// Step 9: Wire handlers (HTTP presentation layer)
|
|
contactMergeRepo := repository.NewContactMergeRepo(db)
|
|
contactMergeService := service.NewContactMergeService(contactMergeRepo, db)
|
|
handlers := &router.Handlers{
|
|
Auth: v1.NewAuthHandler(authService, oauthService, profileService),
|
|
MFA: v1.NewMFAHandler(mfaService),
|
|
SAML: v1.NewSAMLHandler(samlService, jwtService, refreshStore, ssoSessionStore, &cfg.SAML),
|
|
Account: v1.NewAccountHandler(accountService),
|
|
EnterpriseAccount: v1.NewEnterpriseAccountHandler(accountService),
|
|
Contact: v1.NewContactHandler(contactService, contactInboxService, contactMergeService, contactNoteService, conversationService).WithContactPresence(presenceTracker).WithEventPublisher(eventPublisher),
|
|
Conversation: v1.NewConversationHandler(conversationService, messageService).WithAuditService(auditService).WithContactPresence(presenceTracker),
|
|
Inbox: v1.NewInboxHandler(inboxService).WithAuditService(auditService),
|
|
InboxMember: v1.NewInboxMemberHandler(inboxMemberService),
|
|
WebWidget: v1.NewWebWidgetHandler(inboxService),
|
|
WebWidgetTheme: v1.NewWebWidgetThemeHandler(widgetService, inboxService),
|
|
WebWidgetPreChat: v1.NewWebWidgetPreChatHandler(widgetService, inboxService),
|
|
WebWidgetOffline: v1.NewWebWidgetOfflineHandler(widgetService, inboxService),
|
|
InstagramChannel: v1.NewInstagramChannelHandler(igService, igProvider, inboxService, igRepo),
|
|
FacebookChannel: v1.NewFacebookChannelHandler(fbChannelService, fbProvider, inboxService, fbChannelRepo),
|
|
TwitterChannel: v1.NewTwitterChannelHandler(twService, twProvider, inboxService, twRepo),
|
|
MicrosoftChannel: v1.NewMicrosoftChannelHandler(msService, msProvider, inboxService, msRepo),
|
|
GoogleChannel: v1.NewGoogleChannelHandler(goService, goProvider, inboxService, goRepo),
|
|
TikTokChannel: v1.NewTikTokChannelHandler(ttChannelSvc, ttProvider, inboxService, ttChannelRepo),
|
|
LINEChannel: v1.NewLINEChannelHandler(lineChannelSvc, lineProvider, inboxService, lineChannelRepo),
|
|
TwilioSMSChannel: v1.NewTwilioChannelHandler(twilioSMSSvc, inboxService, twilioSMSRepo),
|
|
EmailChannel: v1.NewEmailChannelHandler(emailChannelSvc, inboxService, emailChannelRepo),
|
|
EmailWebhook: emailWebhookHandler,
|
|
Message: v1.NewMessageHandler(messageService),
|
|
Profile: v1.NewProfileHandler(profileService),
|
|
Notification: v1.NewNotificationHandler(notificationService).WithEventPublisher(eventPublisher),
|
|
PlatformApp: v1.NewPlatformAppHandler(platformAppService),
|
|
Team: v1.NewTeamHandler(teamService),
|
|
CaptainAssistant: v1.NewCaptainAssistantHandler(captainAssistantService),
|
|
CaptainDocument: v1.NewCaptainDocumentHandler(captainDocumentService),
|
|
CaptainScenario: v1.NewCaptainScenarioHandler(captainScenarioService),
|
|
CaptainCustomTool: v1.NewCaptainCustomToolHandler(captainCustomToolService),
|
|
CaptainTask: v1.NewCaptainTaskHandler(captainTaskService),
|
|
CaptainPreference: v1.NewCaptainPreferenceHandler(captainPreferenceService),
|
|
CaptainTaskExtended: v1.NewCaptainTaskExtendedHandler(captainTaskExtendedService),
|
|
CaptainAssistantResponse: v1.NewCaptainAssistantResponseHandler(captainAssistantResponseService),
|
|
CaptainBulkAction: v1.NewCaptainBulkActionHandler(captainBulkActionService),
|
|
RAG: v1.NewRAGHandler(ragService),
|
|
CaptainConversation: v1.NewCaptainConversationHandler(captainConversationService),
|
|
AutoReplyRule: v1.NewAutoReplyRuleHandler(autoReplyRuleService),
|
|
Copilot: v1.NewCopilotHandler(copilotService),
|
|
Analytics: v1.NewAnalyticsHandler(analyticsService),
|
|
LiveReport: v1.NewLiveReportHandler(analyticsService),
|
|
YearInReview: v1.NewYearInReviewHandler(yearInReviewService),
|
|
DashboardApp: v1.NewDashboardAppHandler(dashboardAppService),
|
|
Portal: v1.NewPortalHandler(portalService),
|
|
Category: v1.NewCategoryHandler(categoryService, portalService),
|
|
Article: v1.NewArticleHandler(articleService, portalService),
|
|
Folder: v1.NewFolderHandler(folderService),
|
|
PortalMember: v1.NewPortalMemberHandler(portalMemberService),
|
|
AutomationRule: v1.NewAutomationRuleHandler(automationRuleService).WithAuditService(auditService),
|
|
Macro: v1.NewMacroHandler(macroService).WithAuditService(auditService),
|
|
CsatSurvey: v1.NewCsatSurveyHandler(csatSurveyService).WithAuditService(auditService),
|
|
CannedResponse: v1.NewCannedResponseHandler(cannedResponseService),
|
|
PushSubscription: v1.NewPushSubscriptionHandler(pushSubscriptionService),
|
|
NotificationSubscription: v1.NewNotificationSubscriptionHandler(notificationSubscriptionService),
|
|
WebhookSubscription: v1.NewWebhookSubscriptionHandler(webhookSubscriptionService),
|
|
TelegramWebhook: telegramWebhookHandler,
|
|
FacebookWebhook: facebookWebhookHandler,
|
|
WhatsAppWebhook: whatsappWebhookHandler,
|
|
TikTokWebhook: tiktokWebhookHandler,
|
|
LineWebhook: lineWebhookHandler,
|
|
TwilioWebhook: twilioWebhookHandler,
|
|
ShopifyWebhook: shopifyWebhookHandler,
|
|
Label: v1.NewLabelHandler(tagService, labelService),
|
|
Campaign: v1.NewCampaignHandler(campaignService),
|
|
AssignmentPolicy: v1.NewAssignmentPolicyHandler(assignmentPolicyService),
|
|
SlaPolicy: v1.NewSlaPolicyHandler(slaPolicyService).WithAuditService(auditService),
|
|
AssignmentPolicyV2: v1.NewAssignmentPolicyV2Handler(assignmentPolicyV2Service),
|
|
// Enterprise: AuditLog, CustomRole, AgentCapacityPolicy, CsatMetrics handlers
|
|
Audit: v1.NewAuditHandler(auditService),
|
|
CustomRole: v1.NewCustomRoleHandler(customRoleService).WithAuditService(auditService),
|
|
AgentCapacity: v1.NewAgentCapacityHandler(agentCapacityPolicyService).WithAuditService(auditService),
|
|
CsatMetrics: v1.NewCsatMetricsHandler(csatMetricsService),
|
|
Search: v1.NewSearchHandler(searchService, db),
|
|
Widget: widgetHandler,
|
|
// M13: SSO/SAML enterprise authentication handlers
|
|
AccountSamlSettings: v1.NewAccountSamlSettingsHandler(accountSamlSettingsRepo),
|
|
SSOSession: v1.NewSSOSessionHandler(ssoSessionStore),
|
|
// M13: LDAP/OIDC enterprise authentication handlers
|
|
LDAP: v1.NewLDAPHandler(ldapService, ssoMiddleware, jwtService, refreshStore, ssoSessionStore, &cfg.LDAP, db),
|
|
OIDC: v1.NewOIDCHandler(oidcService, ssoMiddleware, jwtService, refreshStore, &cfg.OIDC),
|
|
SSOMiddleware: ssoMiddleware,
|
|
// M12: AgentBot handlers (platform-level + account-level bots)
|
|
AgentBot: v1.NewAgentBotHandler(agentBotService),
|
|
InstallationConfig: v1.NewInstallationConfigHandler(installationConfigService),
|
|
WidgetTest: v1.NewWidgetTestHandler(widgetTestService),
|
|
AgentBotInbox: v1.NewAgentBotInboxHandler(agentBotInboxService),
|
|
// P9: AgentBot rule engine + trigger config handlers
|
|
BotRule: v1.NewBotRuleHandler(botRuleService),
|
|
BotTriggerConfig: v1.NewBotTriggerConfigHandler(botTriggerConfigService),
|
|
// M12: SSE streaming + conversation insight handlers
|
|
SSEStream: v1.NewSSEStreamHandler(copilotService, llmProvider),
|
|
ConversationInsight: v1.NewConversationInsightHandler(conversationInsightService),
|
|
// M4 G3: ContactInbox filter handler (account-scope)
|
|
ContactInboxFilter: v1.NewContactInboxHandler(contactInboxService),
|
|
// Conversation participant + draft message handlers
|
|
ConversationParticipant: v1.NewConversationParticipantHandler(conversationParticipantService),
|
|
DraftMessage: v1.NewDraftMessageHandler(draftMessageService),
|
|
// G4: Companies module (CRUD + search + nested contacts/conversations/notes)
|
|
Company: v1.NewCompanyHandler(companyService).WithEventPublisher(eventPublisher),
|
|
// Custom attributes + custom filters
|
|
CustomAttributeDefinition: v1.NewCustomAttributeDefinitionHandler(customAttributeDefinitionService),
|
|
CustomAttributeValue: v1.NewCustomAttributeValueHandler(customAttributeValueService),
|
|
CustomFilter: v1.NewCustomFilterHandler(customFilterService),
|
|
// G16: Third-party integration handlers (IntegrationHook CRUD + Slack/Shopify/Linear/Notion)
|
|
IntegrationHook: v1.NewIntegrationHookHandler(integrationHookService),
|
|
SlackIntegration: v1.NewSlackIntegrationHandler(slackIntegrationService),
|
|
ShopifyIntegration: v1.NewShopifyIntegrationHandler(shopifyIntegrationService),
|
|
LinearIntegration: v1.NewLinearIntegrationHandler(linearIntegrationService),
|
|
NotionIntegration: v1.NewNotionIntegrationHandler(notionIntegrationService),
|
|
DyteIntegration: v1.NewDyteIntegrationHandler(dyteIntegrationService),
|
|
PlatformUserSSO: v1.NewPlatformUserSSOHandler(),
|
|
// Platform API AccessToken-authenticated handlers
|
|
PlatformUser: v1.NewPlatformUserHandler(platformUserService),
|
|
PlatformAccount: v1.NewPlatformAccountHandler(accountRepo, permissibleRepo, accountService),
|
|
PlatformAgentBot: v1.NewPlatformAgentBotHandler(agentBotRepo, permissibleRepo),
|
|
PlatformAccountUser: v1.NewPlatformAccountUserHandler(accountRepo, userRepo, permissibleRepo),
|
|
Upload: uploadHandler,
|
|
// Lane B: AssignableAgent handler (find agents available for assignment)
|
|
AssignableAgent: v1.NewAssignableAgentHandler(assignableAgentService),
|
|
Agent: v1.NewAgentHandler(agentService),
|
|
AgentBulk: v1.NewAgentBulkHandler(conversationService),
|
|
BulkAction: v1.NewBulkActionHandler(conversationService, contactService).WithWorkerPool(workerPool),
|
|
// Lane C: CSAT template (singular per inbox) + Inbox limits
|
|
InboxCsatTemplate: v1.NewInboxCsatTemplateHandler(csatTemplateService),
|
|
InboxLimit: v1.NewInboxLimitHandler(inboxLimitService),
|
|
WorkingHour: v1.NewWorkingHourHandler(workingHourService),
|
|
// Banner handler (Platform CRUD + account read-only)
|
|
Banner: v1.NewBannerHandler(bannerService),
|
|
// EmailChannelMigration handler (account-scoped create-only)
|
|
EmailChannelMigration: v1.NewEmailChannelMigrationHandler(emailChannelMigrationService),
|
|
// SummaryReport handler (read-only reporting resource — agent/team/inbox/label summaries)
|
|
SummaryReport: v1.NewSummaryReportHandler(summaryReportService),
|
|
}
|
|
// Step 10: Setup Gin router + middleware chain
|
|
// (ref: Chatwoot Rails middleware stack in config/application.rb)
|
|
gin.SetMode(cfg.Server.Mode)
|
|
engine := gin.New()
|
|
|
|
// Global middleware — applies to ALL routes
|
|
corsMiddleware := middleware.CORS(middleware.CORSConfigFromAppConfig(cfg))
|
|
engine.Use(middleware.Recovery()) // panic recovery
|
|
engine.Use(middleware.RequestLogger()) // structured request logging
|
|
engine.Use(middleware.RateLimit(cfg, rdb)) // rate limiting (ref: Chatwoot rack-attack)
|
|
engine.Use(corsMiddleware) // CORS with configurable whitelist
|
|
engine.Use(middleware.SecurityHeaders(middleware.DefaultSecurityHeadersConfig())) // security headers (ref: P14 deliverable #11)
|
|
|
|
// CSRF protection — double-submit cookie pattern (ref: OWASP CSRF Prevention)
|
|
// Applies globally; safe methods (GET/HEAD/OPTIONS) auto-set token cookie,
|
|
// unsafe methods validate X-CSRF-Token header against cookie value.
|
|
csrfCfg := middleware.CSRFConfigFromAppConfig(cfg)
|
|
engine.Use(middleware.CSRF(csrfCfg))
|
|
|
|
// Session middleware — loads and refreshes user sessions (ref: Chatwoot Devise sessions)
|
|
// Complementary to AuthMiddleware: JWT is primary auth, session provides continuity + force logout.
|
|
if cfg.Session.Enabled {
|
|
sessionMwCfg := middleware.SessionMiddlewareConfigFromAppConfig(cfg)
|
|
engine.Use(middleware.SessionMiddleware(sessionStore, sessionMwCfg))
|
|
}
|
|
|
|
// Step 11: WebSocket hub already created above (before handler wiring)
|
|
// so handlers can reference it via the hubTypingAdapter.
|
|
|
|
// Create WS authenticator for dual JWT + pubsub_token auth
|
|
wsAuthenticator := wspkg.NewWSAuthenticator(jwtService, contactInboxRepo)
|
|
|
|
// Register all routes with handlers + WS hub + authenticator
|
|
router.RegisterRoutes(engine, jwtService, refreshStore, webhookRegistry, handlers, wsHub, wsAuthenticator, &cfg.JWT, middleware.CORSConfigFromAppConfig(cfg), db)
|
|
|
|
// Step 12: Setup config hot-reloader (watches config file for runtime changes)
|
|
reloader, err := config.NewConfigReloader(cfg, env)
|
|
if err != nil {
|
|
applogger.L().Warnf("Config hot-reloader init failed (non-critical, continuing): %v", err)
|
|
} else {
|
|
// Register callback to dynamically adjust log level when config changes
|
|
reloader.OnChange(func(old, new *config.Config) {
|
|
if old.Log.Level != new.Log.Level {
|
|
applogger.L().Infof("Log level changed: %s → %s", old.Log.Level, new.Log.Level)
|
|
_ = applogger.Init(applogger.Config{Level: new.Log.Level, Format: new.Log.Format, Output: "stdout", ErrorOutput: "stderr"})
|
|
}
|
|
})
|
|
applogger.L().Infof("Config hot-reloader initialized (env=%s, watching %s)", env, viper.ConfigFileUsed())
|
|
}
|
|
|
|
applogger.L().Info("All dependencies wired successfully")
|
|
|
|
return &App{
|
|
config: cfg,
|
|
reloader: reloader,
|
|
db: db,
|
|
pubsub: ps,
|
|
engine: engine,
|
|
wsHub: wsHub,
|
|
notificationDeliverySvc: notificationDeliverySvc,
|
|
workerPool: workerPool,
|
|
}, nil
|
|
}
|
|
|
|
// hubTypingAdapter implements service.TypingIndicator by broadcasting
|
|
// typing_on/typing_off events directly through the WS hub's SendToAccount method.
|
|
// This avoids the Redis dependency required by the full ws.TypingTracker.
|
|
type hubTypingAdapter struct {
|
|
hub *ws.Hub
|
|
}
|
|
|
|
func (a *hubTypingAdapter) SetTypingOn(ctx context.Context, accountID, conversationID uint, performer *wspkg.Performer) error {
|
|
msg := &wspkg.WSMessage{
|
|
Event: wspkg.EventConversationTypingOn,
|
|
Data: map[string]any{"conversation_id": conversationID},
|
|
AccountID: accountID,
|
|
Performer: performer,
|
|
}
|
|
data, _ := json.Marshal(msg)
|
|
a.hub.SendToAccount(accountID, data)
|
|
return nil
|
|
}
|
|
|
|
func (a *hubTypingAdapter) SetTypingOff(ctx context.Context, accountID, conversationID uint, performer *wspkg.Performer) error {
|
|
msg := &wspkg.WSMessage{
|
|
Event: wspkg.EventConversationTypingOff,
|
|
Data: map[string]any{"conversation_id": conversationID},
|
|
AccountID: accountID,
|
|
Performer: performer,
|
|
}
|
|
data, _ := json.Marshal(msg)
|
|
a.hub.SendToAccount(accountID, data)
|
|
return nil
|
|
}
|