package app import ( "context" "fmt" "log" "net/http" "time" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/automation" "github.com/gochat/gochat/internal/canned" "github.com/gochat/gochat/internal/config" ws "github.com/gochat/gochat/internal/handler/ws" "github.com/gochat/gochat/internal/model" channelmodel "github.com/gochat/gochat/internal/model/channel" "github.com/gochat/gochat/internal/pubsub" "github.com/gochat/gochat/internal/service" "github.com/gochat/gochat/internal/worker" applogger "github.com/gochat/gochat/pkg/logger" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" ) // App is the central application container holding all shared services. type App struct { config *config.Config reloader *config.ConfigReloader db *gorm.DB pubsub pubsub.PubSub engine *gin.Engine wsHub *ws.Hub notificationDeliverySvc *service.NotificationDeliveryService workerPool *worker.WorkerPool } // New creates and initializes the application. func New(cfg *config.Config) (*App, error) { // Initialize database db, err := initDB(cfg) if err != nil { return nil, fmt.Errorf("failed to init database: %w", err) } // Auto-migrate core models if err := autoMigrate(db); err != nil { return nil, fmt.Errorf("failed to auto-migrate: %w", err) } // Initialize gin engine gin.SetMode(cfg.Server.Mode) engine := gin.New() engine.Use(gin.Recovery()) // Initialize pub/sub — Redis-backed when Redis is configured, in-memory fallback var ps pubsub.PubSub rdb, redisErr := NewRedisClient(&cfg.Redis) if redisErr != nil { applogger.L().Warnf("Redis unavailable, using in-memory pubsub: %v", redisErr) ps = pubsub.NewInMemoryPubSub() } else { redisPubSub, pubsubErr := pubsub.NewRedisPubSub(rdb) if pubsubErr != nil { applogger.L().Warnf("Redis PubSub init failed, falling back to in-memory: %v", pubsubErr) ps = pubsub.NewInMemoryPubSub() } else { ps = redisPubSub applogger.L().Info("Redis PubSub initialized (watermill-redisstream)") } } application := &App{ config: cfg, db: db, pubsub: ps, engine: engine, } return application, nil } // Run starts the notification delivery pipeline and the HTTP server. func (a *App) Run() error { if a.workerPool != nil { if err := a.workerPool.Start(); err != nil { return fmt.Errorf("failed to start background worker: %w", err) } applogger.L().Info("Background worker started") } // Start notification delivery service (Watermill router) in background if a.notificationDeliverySvc != nil { go func() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := a.notificationDeliverySvc.Start(ctx); err != nil { applogger.L().Errorf("Notification delivery service start error: %v", err) } }() applogger.L().Info("Notification delivery pipeline started") } addr := fmt.Sprintf("%s:%d", a.config.Server.Host, a.config.Server.Port) log.Printf("Starting gochat server on %s", addr) return a.engine.Run(addr) } // Address returns the server listen address. func (a *App) Address() string { return fmt.Sprintf("%s:%d", a.config.Server.Host, a.config.Server.Port) } // Handler returns the gin engine as http.Handler for use with http.Server. func (a *App) Handler() http.Handler { return a.engine } // Config returns the application config. func (a *App) Config() *config.Config { return a.config } // DB returns the gorm.DB instance. func (a *App) DB() *gorm.DB { return a.db } // PubSub returns the pub/sub instance. func (a *App) PubSub() pubsub.PubSub { return a.pubsub } // Reloader returns the config hot-reloader (may be nil if not initialized). func (a *App) Reloader() *config.ConfigReloader { return a.reloader } func initDB(cfg *config.Config) (*gorm.DB, error) { gormLogger := logger.New( log.Default(), logger.Config{LogLevel: logger.Info}, ) db, err := gorm.Open(postgres.Open(cfg.Database.DSN()), &gorm.Config{ Logger: gormLogger, }) if err != nil { return nil, err } sqlDB, err := db.DB() if err != nil { return nil, err } sqlDB.SetMaxIdleConns(cfg.Database.MaxIdleConns) sqlDB.SetMaxOpenConns(cfg.Database.MaxOpenConns) return db, nil } func autoMigrate(db *gorm.DB) error { models := []interface{}{ &model.Account{}, &model.User{}, &model.Conversation{}, &model.Message{}, &model.Contact{}, &model.Inbox{}, &model.Attachment{}, &model.ContactInbox{}, &model.InboxMember{}, &model.Team{}, &model.TeamMember{}, &model.ReportingEvent{}, &model.ReportingEventsRollup{}, &model.DashboardApp{}, // G4: Company module models (CRUD + search + nested contacts/conversations/notes) &model.Company{}, &model.CompanyNote{}, &automation.CsatSurveyResponse{}, &canned.CannedResponse{}, &model.PushToken{}, &model.WebhookSubscription{}, &model.WebhookDelivery{}, // M13: SAML/SSO enterprise authentication models &model.SAMLIdPConfig{}, &model.AccountSamlSettings{}, // M13: LDAP enterprise authentication model &model.AccountLDAPSettings{}, // M13: OIDC enterprise authentication model &model.AccountOIDCSettings{}, // P9: AgentBot rule engine models (defined in automation package to avoid import cycle) &automation.BotRule{}, &automation.BotTriggerConfig{}, // M6: Automation & Macro models &automation.AutomationRule{}, &automation.AutomationExecution{}, &automation.Macro{}, &automation.MacroExecution{}, // M11: WebWidget channel models — theme config, pre-chat form, file upload, offline messages &model.WidgetThemeConfig{}, &model.PreChatForm{}, &model.WidgetFileUpload{}, &model.WidgetOfflineMessage{}, // M11: SLA Policy models — policy, applied SLA, events, inbox associations &model.SlaPolicy{}, &model.SlaPolicyInbox{}, &model.AppliedSLA{}, &model.SlaEvent{}, // Chatwoot assignment policies + inbox association. &model.AssignmentPolicy{}, &model.InboxAssignmentPolicy{}, // Legacy enhanced assignment policy aliases kept for local compatibility. &model.AssignmentPolicyV2{}, &model.AssignmentPolicyInbox{}, // Enterprise: Audit log model (read-only log of account actions) &model.Audit{}, // Enterprise: Custom role for fine-grained permissions &model.CustomRole{}, // Enterprise: Agent capacity policies + inbox capacity limits &model.AgentCapacityPolicy{}, &model.InboxCapacityLimit{}, // Upload: Direct upload model (account-level + widget direct uploads) &model.DirectUpload{}, // Channel models — existing + new channels (TikTok, LINE, Twilio SMS) &channelmodel.ChannelTelegram{}, &channelmodel.ChannelWebWidget{}, &channelmodel.ChannelFacebook{}, &channelmodel.ChannelWhatsApp{}, &channelmodel.ChannelEmail{}, &channelmodel.ChannelTikTok{}, &channelmodel.ChannelLINE{}, &channelmodel.ChannelTwilioSMS{}, // W0: Additional channel models not previously registered &channelmodel.ChannelAPI{}, &channelmodel.ChannelGoogle{}, &channelmodel.ChannelMicrosoft{}, &channelmodel.ChannelTwitter{}, // Platform: InstallationConfig — global key-value config for super-admin &model.InstallationConfig{}, // Banner — platform announcement banners &model.Banner{}, // WidgetTest — platform read-only, widget/UI integration testing &model.WidgetTest{}, // W0: Activated stub models — previously .go.txt, now compiled &model.AutomationAction{}, &model.Call{}, &model.CaptainAssistantInbox{}, &model.Contactable{}, &model.ContactExport{}, &model.DataImport{}, &model.EmailTemplate{}, &model.MessageReaction{}, &model.Report{}, &model.BackgroundJob{}, // S6: WorkingHour — out-of-office / business hours per inbox &model.WorkingHour{}, } for _, m := range models { if err := db.AutoMigrate(m); err != nil { return fmt.Errorf("failed to auto-migrate %T: %w", m, err) } } return nil }