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" 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}) service.RegisterConversationMaintenanceJobs(workerPool, db) if _, err := service.EnqueueScheduledItemsTrigger(context.Background(), workerPool, time.Now()); err != nil { applogger.L().Warnf("failed to enqueue initial scheduled item trigger: %v", err) } // 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) 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) 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) // Create channel dispatcher for event-driven architecture (ref: Chatwoot Dispatcher) channelDispatcher := channel.NewDispatcher(workerPool) // P9: AgentBot rule engine services (declared early so listener can be registered on dispatcher) botRuleService := automation.NewBotRuleService(&dbProvider{db: db}) botTriggerConfigService := automation.NewBotTriggerConfigService(&dbProvider{db: db}) agentBotRuleListener := automation.NewAgentBotRuleListener(&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) // P9: Register AgentBot rule listener on the dispatcher channelDispatcher.Register(agentBotRuleListener) // M6: Register automation rule listener on the dispatcher for event-triggered automation automation.RegisterAutomationRuleListenerWithWorker(channelDispatcher, &dbProvider{db: db}, workerPool) // M6: Register CSAT survey listener on the dispatcher for conversation.resolved events channelDispatcher.Register(automation.NewCsatSurveyListenerWithWorker(&dbProvider{db: db}, workerPool)) // 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) conversationService := service.NewConversationService(conversationRepo, messageRepo, channelDispatcher, inboxMemberService, accountUserRepo, teamRepo, teamMemberRepo) 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) 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) llmProvider := llm.NewOpenAIProvider(llm.OpenAIProviderConfig{ APIKey: cfg.Captain.LLMAPIKey, Model: cfg.Captain.LLMModel, BaseURL: cfg.Captain.LLMBaseURL, EmbedModel: 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) copilotService := service.NewCopilotService(copilotThreadRepo, copilotMessageRepo, copilotSuggestionRepo, llmProvider, captainAssistantRepo) 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) // Analytics services (P11 — Reports/Analytics) analyticsService := service.NewAnalyticsService(reportingEventRepo, reportingEventsRollupRepo) summaryReportService := service.NewSummaryReportService(reportingEventsRollupRepo) dashboardAppService := service.NewDashboardAppService(dashboardAppRepo) platformAppService := service.NewPlatformAppService(platformAppRepo, accessTokenRepo, permissibleRepo) platformUserService := service.NewPlatformUserService(userRepo, permissibleRepo) // M12: AgentBot services (AgentBot + AgentBotInbox + Listener) agentBotService := service.NewAgentBotService(agentBotRepo) agentBotInboxService := service.NewAgentBotInboxService(agentBotInboxRepo, agentBotRepo) agentBotListener := service.NewAgentBotListener(agentBotInboxRepo, agentBotRepo) // 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) 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) profileService := service.NewProfileService(userRepo, accountUserRepo, accessTokenRepo) // Campaign + AutoAssignment services campaignInternalSvc := campaign.NewCampaignService(db) 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) conversationService.SetSearchIndexer(searchIndexer) messageService.SetSearchIndexer(searchIndexer) contactService.SetSearchIndexer(searchIndexer) contactService.SetSearchReader(searchService) companyService.SetSearchIndexer(searchIndexer) companyService.SetSearchReader(searchService) articleService.SetSearchIndexer(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() // 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) // Upload: DirectUpload repo + service + handler (account-level + widget direct uploads) directUploadRepo := repository.NewDirectUploadRepo(db) uploadService := service.NewUploadService(directUploadRepo, cfg).WithWidgetAuth(inboxRepo, contactInboxRepo) 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), Contact: v1.NewContactHandler(contactService, contactInboxService, contactMergeService, contactNoteService, conversationService), Conversation: v1.NewConversationHandler(conversationService, messageService).WithAuditService(auditService), 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), 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), Copilot: v1.NewCopilotHandler(copilotService), Analytics: v1.NewAnalyticsHandler(analyticsService), LiveReport: v1.NewLiveReportHandler(analyticsService), DashboardApp: v1.NewDashboardAppHandler(dashboardAppService), Portal: v1.NewPortalHandler(portalService), Category: v1.NewCategoryHandler(categoryService), Article: v1.NewArticleHandler(articleService), 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), 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), // 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), 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), 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), } _ = agentBotListener // M12: subscribed to message events via service calls // 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 }