package main import ( "fmt" "net" "os" "os/signal" "strconv" "strings" "syscall" "time" "git.solsynth.dev/goatworks/turbine/pkg/ring/clients" // Add this import "git.solsynth.dev/goatworks/turbine/pkg/ring/infra" "git.solsynth.dev/goatworks/turbine/pkg/ring/routes" "git.solsynth.dev/goatworks/turbine/pkg/ring/services" "git.solsynth.dev/goatworks/turbine/pkg/ring/websocket" // Add this import "git.solsynth.dev/goatworks/turbine/pkg/shared/hash" pb "git.solsynth.dev/goatworks/turbine/pkg/shared/proto/gen" "git.solsynth.dev/goatworks/turbine/pkg/shared/registrar" fiber_websocket "github.com/gofiber/contrib/v3/websocket" // Add this alias import "github.com/gofiber/fiber/v3" "github.com/google/uuid" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/samber/lo" "github.com/spf13/viper" "google.golang.org/grpc" ) func init() { log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) } func main() { log.Info().Msg("Starting Turbine Ring...") viper.SetConfigName("settings") viper.AddConfigPath(".") viper.AddConfigPath("/etc/turbine/") viper.SetConfigType("toml") log.Info().Msg("Reading configuration...") if err := viper.ReadInConfig(); err != nil { log.Fatal().Err(err).Msg("Failed to read config file...") } log.Info().Msg("Configuration loaded.") clients.InitPushClients() // Initialize APNs and Firebase clients clients.InitSMTPSettings() // Initialize SMTP client settings clients.InitNATSClient() // Initialize NATS client // --- gRPC Server --- grpcListenAddr := viper.GetString("grpc.listen") if grpcListenAddr == "" { log.Fatal().Msg("grpc.listen not configured") } grpcServer := grpc.NewServer() wsManager := websocket.NewManager(clients.GetNATSClient()) // Pass NATS client to WebSocket Manager ringService := &services.RingServiceServerImpl{ WsManager: wsManager, // Inject WebSocket Manager into RingService } pb.RegisterRingServiceServer(grpcServer, ringService) pb.RegisterRingHandlerServiceServer(grpcServer, ringService) grpcLis, err := net.Listen("tcp", grpcListenAddr) if err != nil { log.Fatal().Err(err).Msgf("Failed to listen for gRPC: %s", grpcListenAddr) } go func() { log.Info().Msgf("gRPC server listening on %s", grpcListenAddr) if err := grpcServer.Serve(grpcLis); err != nil { log.Fatal().Err(err).Msg("Failed to serve gRPC") } }() // --- Service Registration --- etcdEndpoints := viper.GetStringSlice("etcd.endpoints") if len(etcdEndpoints) == 0 { log.Fatal().Msg("etcd.endpoints not configured") } if err := infra.ConnectDb(); err != nil { log.Fatal().Err(err).Msg("Failed to connect database.") } else { infra.Db.AutoMigrate() } if viper.GetBool("etcd.insecure") { for i, ep := range etcdEndpoints { if !strings.HasPrefix(ep, "http://") && !strings.HasPrefix(ep, "https://") { etcdEndpoints[i] = "http://" + ep } } } serviceReg, err := registrar.NewServiceRegistrar(etcdEndpoints) if err != nil { log.Fatal().Err(err).Msg("Failed to create service registrar") } listenAddr := viper.GetString("listen") host := viper.GetString("host") if host == "" { log.Fatal().Msg("host not configured") } portStr := strings.TrimPrefix(listenAddr, ":") port, err := strconv.Atoi(portStr) if err != nil { log.Fatal().Err(err).Msg("Invalid listen address") } serviceName := "ring" // This should probably be "ring" instanceID := fmt.Sprint(hash.Hash(fmt.Sprintf("%s-%s-%d", serviceName, host, port)))[:8] err = serviceReg.Register(serviceName, "http", instanceID, host, port, 30) if err != nil { log.Fatal().Err(err).Msg("Failed to register service") } log.Info().Str("service", serviceName).Str("instanceID", instanceID).Msg("Service registered successfully") // --- Web Server --- app := fiber.New(fiber.Config{ ServerHeader: "Turbine Ring", }) api := app.Group("/api") { api.Post("/notifications/subscription", routes.CreatePushSubscription) } // Initialize WebSocket Controller wsController := websocket.NewWebSocketController(wsManager) // WebSocket endpoint app.Use("/ws", func(c fiber.Ctx) error { // Mock authentication based on C# example // In a real scenario, you'd extract user/session from JWT or similar // and set c.Locals("currentUser") and c.Locals("currentSession") c.Locals("currentUser", &pb.Account{Id: uuid.New().String(), Name: "mock_user", Nick: "Mock User"}) c.Locals("currentSession", &pb.AuthSession{ClientId: lo.ToPtr(uuid.New().String())}) if fiber_websocket.IsWebSocketUpgrade(c) { return c.Next() } return fiber.ErrUpgradeRequired }) app.Get("/ws", fiber_websocket.New(wsController.HandleWebSocket)) // Graceful shutdown c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c log.Info().Msg("Shutting down servers...") if err := app.ShutdownWithTimeout(5 * time.Second); err != nil { log.Error().Err(err).Msg("Fiber server shutdown error") } grpcServer.GracefulStop() log.Info().Msg("Servers gracefully stopped") }() err = app.Listen(listenAddr, fiber.ListenConfig{DisableStartupMessage: true}) if err != nil { log.Fatal().Err(err).Msg("Failed to start the server...") } }