204 lines
6.3 KiB
Go
204 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
chiMiddleware "github.com/go-chi/chi/v5/middleware"
|
|
"github.com/go-chi/cors"
|
|
"github.com/joho/godotenv"
|
|
"github.com/zs/InsightReply/internal/handler"
|
|
appMiddleware "github.com/zs/InsightReply/internal/middleware"
|
|
"github.com/zs/InsightReply/internal/repository"
|
|
"github.com/zs/InsightReply/internal/service"
|
|
"github.com/zs/InsightReply/internal/worker"
|
|
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func main() {
|
|
// 1. Load environment variables
|
|
if err := godotenv.Load(); err != nil {
|
|
log.Println("No .env file found, relying on system environment variables")
|
|
}
|
|
|
|
// 1.5 Setup application file logging
|
|
if logPath := os.Getenv("LOG_FILE_PATH"); logPath != "" {
|
|
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
|
if err == nil {
|
|
multiWriter := io.MultiWriter(os.Stdout, logFile)
|
|
log.SetOutput(multiWriter)
|
|
log.Printf("Application logs are now being mirrored to: %s", logPath)
|
|
} else {
|
|
log.Printf("Failed to open log file %s: %v", logPath, err)
|
|
}
|
|
}
|
|
|
|
// 2. Database connection setup
|
|
dsn := os.Getenv("DATABASE_URL")
|
|
if dsn == "" {
|
|
log.Fatal("DATABASE_URL environment variable is required")
|
|
}
|
|
|
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
|
if err != nil {
|
|
log.Fatal("failed to connect database:", err)
|
|
}
|
|
fmt.Println("Database connection established")
|
|
|
|
// 3. Initialize Layers
|
|
userRepo := repository.NewUserRepository(db)
|
|
userSvc := service.NewUserService(userRepo)
|
|
userHandler := handler.NewUserHandler(userSvc)
|
|
|
|
authSvc := service.NewAuthService(userRepo)
|
|
authHandler := handler.NewAuthHandler(authSvc)
|
|
|
|
profileRepo := repository.NewProductProfileRepository(db)
|
|
profileSvc := service.NewProductProfileService(profileRepo)
|
|
profileHandler := handler.NewProductProfileHandler(profileSvc)
|
|
|
|
strategyRepo := repository.NewCustomStrategyRepository(db)
|
|
strategySvc := service.NewCustomStrategyService(strategyRepo)
|
|
strategyHandler := handler.NewCustomStrategyHandler(strategySvc)
|
|
|
|
monitorRepo := repository.NewCompetitorMonitorRepository(db)
|
|
monitorSvc := service.NewCompetitorMonitorService(monitorRepo)
|
|
monitorHandler := handler.NewCompetitorMonitorHandler(monitorSvc)
|
|
|
|
// AI Service (Multi-LLM Routing Support)
|
|
aiSvc := service.NewAIService()
|
|
aiHandler := handler.NewAIHandler(aiSvc, profileSvc, strategySvc)
|
|
|
|
// Start Background Workers & Tweet Handling
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel() // Ensure cleanup on exit
|
|
tweetRepo := repository.NewTweetRepository(db)
|
|
replyRepo := repository.NewReplyRepository(db)
|
|
|
|
tweetHandler := handler.NewTweetHandler(tweetRepo) // Mount API handler
|
|
replyHandler := handler.NewReplyHandler(replyRepo)
|
|
|
|
monitorWorker := worker.NewMonitorWorker(monitorRepo, tweetRepo)
|
|
go monitorWorker.Start(ctx, 15*time.Minute)
|
|
|
|
performanceWorker := worker.NewPerformanceWorker(replyRepo, aiSvc)
|
|
go performanceWorker.Start(ctx, 30*time.Minute) // Runs every 30 minutes to check 24h+ old replies
|
|
|
|
// 4. Router setup
|
|
r := chi.NewRouter()
|
|
r.Use(chiMiddleware.Logger)
|
|
r.Use(chiMiddleware.Recoverer)
|
|
r.Use(chiMiddleware.StripSlashes)
|
|
|
|
// CORS Configuration
|
|
corsOrigins := os.Getenv("CORS_ORIGINS")
|
|
if corsOrigins == "" {
|
|
corsOrigins = "*" // default fallback
|
|
}
|
|
r.Use(cors.Handler(cors.Options{
|
|
AllowedOrigins: []string{corsOrigins},
|
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
|
ExposedHeaders: []string{"Link"},
|
|
AllowCredentials: true,
|
|
MaxAge: 300,
|
|
}))
|
|
|
|
// 5. Routes
|
|
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"code":200,"message":"ok","data":{"status":"healthy","version":"1.0.0"}}`))
|
|
})
|
|
|
|
r.Route("/api/v1", func(r chi.Router) {
|
|
// Public routes
|
|
r.Post("/users/register", userHandler.Register)
|
|
r.Post("/auth/login", authHandler.Login)
|
|
|
|
// Protected routes
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(appMiddleware.JWTAuth)
|
|
r.Use(appMiddleware.RateLimit(db))
|
|
|
|
// User and Profile APIs
|
|
r.Get("/users/me", userHandler.GetProfile)
|
|
r.Put("/users/me/preferences", userHandler.UpdatePreferences)
|
|
r.Get("/users/me/product_profiles", profileHandler.GetProfile)
|
|
r.Put("/users/me/product_profiles", profileHandler.SaveProfile)
|
|
|
|
// Strategy APIs
|
|
r.Get("/users/me/strategies", strategyHandler.ListStrategies)
|
|
r.Post("/users/me/strategies", strategyHandler.CreateStrategy)
|
|
r.Delete("/users/me/strategies/{id}", strategyHandler.DeleteStrategy)
|
|
|
|
// Monitor APIs
|
|
r.Get("/monitors/competitors", monitorHandler.ListMonitors)
|
|
r.Post("/monitors/competitors", monitorHandler.CreateMonitor)
|
|
r.Delete("/monitors/competitors/{id}", monitorHandler.DeleteMonitor)
|
|
|
|
// Hot Opportunity Tweers API
|
|
r.Get("/tweets/hot", tweetHandler.GetHotTweets)
|
|
r.Get("/tweets/search", tweetHandler.GetSearchTweets)
|
|
|
|
// AI APIs
|
|
r.Get("/ai/test", aiHandler.Test)
|
|
r.Post("/ai/generate", aiHandler.Generate)
|
|
r.Post("/replies/record", replyHandler.RecordReply)
|
|
r.Get("/replies", replyHandler.GetReplies)
|
|
})
|
|
})
|
|
|
|
// 6. Graceful Shutdown Setup
|
|
port := os.Getenv("SERVER_PORT")
|
|
if port == "" {
|
|
port = "8080"
|
|
}
|
|
srv := &http.Server{
|
|
Addr: ":" + port,
|
|
Handler: r,
|
|
}
|
|
|
|
serverErrors := make(chan error, 1)
|
|
|
|
// Start the server
|
|
go func() {
|
|
fmt.Printf("Server starting on :%s\n", port)
|
|
serverErrors <- srv.ListenAndServe()
|
|
}()
|
|
|
|
// Channel to listen for an interrupt or terminate signal from the OS.
|
|
shutdown := make(chan os.Signal, 1)
|
|
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
|
|
|
|
// Blocking main and waiting for shutdown.
|
|
select {
|
|
case err := <-serverErrors:
|
|
log.Fatalf("Error starting server: %v", err)
|
|
case sig := <-shutdown:
|
|
log.Printf("Start shutdown... signal: %v", sig)
|
|
|
|
// Create context with timeout for shutdown
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
if err := srv.Shutdown(ctx); err != nil {
|
|
log.Printf("Graceful shutdown did not complete in time: %v", err)
|
|
if err := srv.Close(); err != nil {
|
|
log.Fatalf("Could not stop server gracefully: %v", err)
|
|
}
|
|
}
|
|
}
|
|
log.Println("Server stopped")
|
|
}
|