diff --git a/cmd/server/main.go b/cmd/server/main.go index 53ac90c..eba0447 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "log" "net/http" "os" @@ -18,8 +19,12 @@ func main() { db := database.Connect() defer db.Close() - // --- Собираем наши зависимости (Dependency Injection) --- userRepo := &database.UserRepository{DB: db} + serverRepo := &database.ServerRepository{DB: db} + serverPoller := &core.ServerPoller{Repo: serverRepo} + + // Запускаем поллер в фоновой горутине + go serverPoller.Start(context.Background()) // Сервисы userService := &core.UserService{Repo: userRepo} @@ -39,9 +44,9 @@ func main() { } // Хендлеры userHandler := &api.UserHandler{Service: userService} - authHandler := &api.AuthHandler{Service: authService} // Новый хендлер - profileHandler := &api.ProfileHandler{Service: profileService} // Новый хендлер - + authHandler := &api.AuthHandler{Service: authService} + profileHandler := &api.ProfileHandler{Service: profileService} + serverHandler := &api.ServerHandler{Repo: serverRepo} // --- Настраиваем роутер --- r := chi.NewRouter() r.Use(middleware.Logger) @@ -51,7 +56,7 @@ func main() { r.Route("/api", func(r chi.Router) { r.Post("/register", userHandler.Register) r.Post("/login", authHandler.Login) - // Здесь будет публичный эндпоинт для логина в веб-интерфейс + r.Get("/servers", serverHandler.GetServers) }) r.Route("/authserver", func(r chi.Router) { r.Post("/authenticate", authHandler.Authenticate) diff --git a/go.mod b/go.mod index e9e474c..8be194f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module gitea.mrixs.me/minecraft-platform/backend go 1.24.1 require ( + github.com/Tnze/go-mc v1.20.2 // indirect github.com/go-chi/chi/v5 v5.2.1 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/go.sum b/go.sum index 3afc06c..ce7064d 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Tnze/go-mc v1.20.2 h1:arHCE/WxLCxY73C/4ZNLdOymRYtdwoXE05ohB7HVN6Q= +github.com/Tnze/go-mc v1.20.2/go.mod h1:geoRj2HsXSkB3FJBuhr7wCzXegRlzWsVXd7h7jiJ6aQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= diff --git a/internal/api/server_handler.go b/internal/api/server_handler.go new file mode 100644 index 0000000..1427dcc --- /dev/null +++ b/internal/api/server_handler.go @@ -0,0 +1,22 @@ +package api + +import ( + "encoding/json" + "net/http" + + "gitea.mrixs.me/minecraft-platform/backend/internal/database" +) + +type ServerHandler struct { + Repo *database.ServerRepository +} + +func (h *ServerHandler) GetServers(w http.ResponseWriter, r *http.Request) { + servers, err := h.Repo.GetAllWithStatus(r.Context()) + if err != nil { + http.Error(w, "Failed to get servers", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(servers) +} diff --git a/internal/core/server_poller.go b/internal/core/server_poller.go new file mode 100644 index 0000000..be7958e --- /dev/null +++ b/internal/core/server_poller.go @@ -0,0 +1,88 @@ +package core + +import ( + "context" + "encoding/json" + "log" + "time" + + "gitea.mrixs.me/minecraft-platform/backend/internal/database" + "gitea.mrixs.me/minecraft-platform/backend/internal/models" + "github.com/Tnze/go-mc/bot/basic" + "github.com/Tnze/go-mc/net" +) + +type ServerPoller struct { + Repo *database.ServerRepository +} + +func (p *ServerPoller) Start(ctx context.Context) { + log.Println("Starting server poller...") + ticker := time.NewTicker(60 * time.Second) + defer ticker.Stop() + + p.pollAllServers(ctx) + + for { + select { + case <-ticker.C: + p.pollAllServers(ctx) + case <-ctx.Done(): + log.Println("Stopping server poller...") + return + } + } +} + +func (p *ServerPoller) pollAllServers(ctx context.Context) { + servers, err := p.Repo.GetAllEnabledServers(ctx) + if err != nil { + log.Printf("Poller: failed to get servers: %v", err) + return + } + + for _, s := range servers { + go p.pollServer(ctx, s) + } +} + +func (p *ServerPoller) pollServer(ctx context.Context, server *models.GameServer) { + resp, delay, err := net.PingAndListTimeout(server.Address, 5*time.Second) + if err != nil { + log.Printf("Poller: failed to ping %s (%s): %v", server.Name, server.Address, err) + return + } + + var status basic.ServerList + if err := json.Unmarshal(resp, &status); err != nil { + log.Printf("Poller: failed to unmarshal status for %s: %v", server.Name, err) + return + } + + // MOTD может быть сложным объектом, извлекаем текст + var motdText string + if s, ok := status.Description.(string); ok { + motdText = s + } else { + if m, ok := status.Description.(map[string]interface{}); ok { + if t, ok := m["text"].(string); ok { + motdText = t + } + } + } + + updateData := &models.ServerStatus{ + StatusJSON: string(resp), + Motd: motdText, + PlayerCount: status.Players.Online, + MaxPlayers: status.Players.Max, + VersionName: status.Version.Name, + Ping: delay.Milliseconds(), + } + + if err := p.Repo.UpdateServerStatus(ctx, server.ID, updateData); err != nil { + log.Printf("Poller: failed to update status for %s: %v", server.Name, err) + } else { + log.Printf("Poller: successfully polled %s", server.Name) + } +} diff --git a/internal/database/server_repository.go b/internal/database/server_repository.go new file mode 100644 index 0000000..567668f --- /dev/null +++ b/internal/database/server_repository.go @@ -0,0 +1,68 @@ +package database + +import ( + "context" + "database/sql" + + "gitea.mrixs.me/minecraft-platform/backend/internal/models" +) + +type ServerRepository struct { + DB *sql.DB +} + +// GetAllEnabledServers возвращает все активные серверы для опроса. +func (r *ServerRepository) GetAllEnabledServers(ctx context.Context) ([]*models.GameServer, error) { + rows, err := r.DB.QueryContext(ctx, "SELECT id, name, address FROM game_servers WHERE is_enabled = TRUE") + if err != nil { + return nil, err + } + defer rows.Close() + + var servers []*models.GameServer + for rows.Next() { + s := &models.GameServer{} + if err := rows.Scan(&s.ID, &s.Name, &s.Address); err != nil { + return nil, err + } + servers = append(servers, s) + } + return servers, nil +} + +// UpdateServerStatus обновляет данные о статусе сервера. +func (r *ServerRepository) UpdateServerStatus(ctx context.Context, id int, status *models.ServerStatus) error { + query := ` + UPDATE game_servers SET + status_json = $1, last_polled_at = NOW(), motd = $2, player_count = $3, + max_players = $4, version_name = $5, ping_backend_server = $6 + WHERE id = $7` + _, err := r.DB.ExecContext(ctx, query, + status.StatusJSON, status.Motd, status.PlayerCount, status.MaxPlayers, + status.VersionName, status.Ping, id) + return err +} + +// GetAllWithStatus возвращает все активные серверы с их текущим статусом. +func (r *ServerRepository) GetAllWithStatus(ctx context.Context) ([]*models.GameServer, error) { + query := ` + SELECT id, name, address, is_enabled, last_polled_at, motd, + player_count, max_players, version_name, ping_backend_server + FROM game_servers WHERE is_enabled = TRUE ORDER BY name` + rows, err := r.DB.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var servers []*models.GameServer + for rows.Next() { + s := &models.GameServer{} + if err := rows.Scan(&s.ID, &s.Name, &s.Address, &s.IsEnabled, &s.LastPolledAt, + &s.Motd, &s.PlayerCount, &s.MaxPlayers, &s.VersionName, &s.PingBackendServer); err != nil { + return nil, err + } + servers = append(servers, s) + } + return servers, nil +} diff --git a/internal/models/server.go b/internal/models/server.go new file mode 100644 index 0000000..e6f1143 --- /dev/null +++ b/internal/models/server.go @@ -0,0 +1,26 @@ +package models + +import "time" + +type GameServer struct { + ID int `json:"id"` + Name string `json:"name"` + Address string `json:"address"` + IsEnabled bool `json:"is_enabled"` + StatusJSON *string `json:"-"` + LastPolledAt *time.Time `json:"last_polled_at"` + Motd *string `json:"motd"` + PlayerCount *int `json:"player_count"` + MaxPlayers *int `json:"max_players"` + VersionName *string `json:"version_name"` + PingBackendServer *int `json:"ping_proxy_server"` +} + +type ServerStatus struct { + StatusJSON string + Motd string + PlayerCount int + MaxPlayers int + VersionName string + Ping int64 +}