Files
gochat/internal/app/app.go
T

269 lines
7.8 KiB
Go

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
}