269 lines
7.8 KiB
Go
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
|
|
}
|