diff --git a/cmd/main.go b/cmd/main.go index 4dccd501..a45edb9f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -17,7 +17,9 @@ import ( "github.com/rs/zerolog/log" "go.codycody31.dev/squad-aegis/core" "go.codycody31.dev/squad-aegis/db" + "go.codycody31.dev/squad-aegis/internal/chat_processor" "go.codycody31.dev/squad-aegis/internal/models" + "go.codycody31.dev/squad-aegis/internal/rcon_manager" "go.codycody31.dev/squad-aegis/internal/server" "go.codycody31.dev/squad-aegis/shared/config" "go.codycody31.dev/squad-aegis/shared/logger" @@ -111,19 +113,52 @@ func run(ctx context.Context) error { return fmt.Errorf("failed to commit transaction: %v", err) } + // Initialize RCON manager + rconManager := rcon_manager.NewRconManager(ctx) + + // Initialize chat processor + chatProcessor := chat_processor.NewChatProcessor(ctx, rconManager, database) + // Initialize services waitingGroup := errgroup.Group{} - // // Start job worker - // waitingGroup.Go(func() error { - // log.Info().Msg("starting job worker service...") - // // if err := worker.Run(ctx, database, rdb); err != nil { - // // go stopServerFunc(err) - // // return err - // // } - // log.Info().Msg("job worker service stopped") - // return nil - // }) + waitingGroup.Go(func() error { + log.Info().Msg("Starting RCON connection manager service...") + go rconManager.StartConnectionManager() + + // Connect to all servers with RCON enabled + log.Info().Msg("Connecting to all servers with RCON enabled...") + rconManager.ConnectToAllServers(ctx, database) + + log.Info().Msg("RCON connection manager service started") + + // Block until context is done + <-ctx.Done() + + // Stop RCON connection manager + rconManager.Shutdown() + + log.Info().Msg("RCON connection manager service stopped") + + return nil + }) + + // Start chat processor in a goroutine + waitingGroup.Go(func() error { + log.Info().Msg("Starting chat processor service...") + + // Start chat processor + chatProcessor.Start() + + // Block until context is done + <-ctx.Done() + + // Stop chat processor + chatProcessor.Stop() + + log.Info().Msg("Chat processor service stopped") + return nil + }) // Start HTTP server waitingGroup.Go(func() error { @@ -135,7 +170,9 @@ func run(ctx context.Context) error { } deps := &server.Dependencies{ - DB: database, + DB: database, + RconManager: rconManager, + ChatProcessor: chatProcessor, } // Initialize router diff --git a/internal/chat_processor/chat_processor.go b/internal/chat_processor/chat_processor.go new file mode 100644 index 00000000..5e773228 --- /dev/null +++ b/internal/chat_processor/chat_processor.go @@ -0,0 +1,233 @@ +package chat_processor + +import ( + "context" + "database/sql" + "strings" + "sync" + + "github.com/google/uuid" + "github.com/rs/zerolog/log" + "go.codycody31.dev/squad-aegis/internal/rcon" + "go.codycody31.dev/squad-aegis/internal/rcon_manager" +) + +// Command represents a chat command +type Command struct { + Name string + Description string + Usage string + Handler func(ctx context.Context, serverID uuid.UUID, message rcon.Message, args []string) (string, error) + AdminOnly bool +} + +// ChatProcessor processes chat messages and handles commands +type ChatProcessor struct { + rconManager *rcon_manager.RconManager + db *sql.DB + commands map[string]Command + mu sync.RWMutex + ctx context.Context + cancel context.CancelFunc +} + +// NewChatProcessor creates a new chat processor +func NewChatProcessor(ctx context.Context, rconManager *rcon_manager.RconManager, db *sql.DB) *ChatProcessor { + ctx, cancel := context.WithCancel(ctx) + return &ChatProcessor{ + rconManager: rconManager, + db: db, + commands: make(map[string]Command), + ctx: ctx, + cancel: cancel, + } +} + +// RegisterCommand registers a command +func (p *ChatProcessor) RegisterCommand(command Command) { + p.mu.Lock() + defer p.mu.Unlock() + p.commands[strings.ToLower(command.Name)] = command +} + +// Start starts the chat processor +func (p *ChatProcessor) Start() { + // Register default commands + p.registerDefaultCommands() + + // Start processing chat messages + p.rconManager.ProcessChatMessages(p.ctx, p.handleChatMessage) + + log.Info().Msg("Chat processor started") +} + +// Stop stops the chat processor +func (p *ChatProcessor) Stop() { + p.cancel() + log.Info().Msg("Chat processor stopped") +} + +// handleChatMessage handles a chat message +func (p *ChatProcessor) handleChatMessage(serverID uuid.UUID, message rcon.Message) { + // Log the message + log.Debug(). + Str("serverID", serverID.String()). + Str("chatType", message.ChatType). + Str("playerName", message.PlayerName). + Str("message", message.Message). + Msg("Chat message received") + + // Check if the message is a command + if !strings.HasPrefix(message.Message, "!") { + return + } + + // Parse the command + parts := strings.Fields(message.Message[1:]) + if len(parts) == 0 { + return + } + + commandName := strings.ToLower(parts[0]) + args := parts[1:] + + // Get the command + p.mu.RLock() + command, exists := p.commands[commandName] + p.mu.RUnlock() + + if !exists { + return + } + + // Check if the command is admin-only + if command.AdminOnly { + isAdmin, err := p.isPlayerAdmin(p.ctx, serverID, message.SteamID) + if err != nil { + log.Error(). + Err(err). + Str("serverID", serverID.String()). + Str("steamID", message.SteamID). + Msg("Failed to check if player is admin") + return + } + + if !isAdmin { + // Send a message to the player + p.sendDirectMessage(serverID, "You don't have permission to use this command.", message.SteamID) + return + } + } + + // Execute the command + response, err := command.Handler(p.ctx, serverID, message, args) + if err != nil { + log.Error(). + Err(err). + Str("serverID", serverID.String()). + Str("command", commandName). + Msg("Failed to execute command") + + // Send error message to the player + p.sendDirectMessage(serverID, "Something went wrong, please try again later.", message.SteamID) + return + } + + // Send the response to the player if there is one + if response != "" { + p.sendDirectMessage(serverID, response, message.SteamID) + } +} + +// isPlayerAdmin checks if a player is an admin +func (p *ChatProcessor) isPlayerAdmin(ctx context.Context, serverID uuid.UUID, steamID string) (bool, error) { + // Query the database to check if the player is an admin + var count int + err := p.db.QueryRowContext(ctx, ` + SELECT COUNT(*) FROM server_admins sa + JOIN users u ON sa.user_id = u.id + WHERE sa.server_id = $1 AND u.steam_id = $2 + `, serverID, steamID).Scan(&count) + + if err != nil { + return false, err + } + + return count > 0, nil +} + +// registerDefaultCommands registers the default commands +func (p *ChatProcessor) registerDefaultCommands() { + p.RegisterCommand(Command{ + Name: "admin", + Description: "Calls an admin", + Usage: "!admin [message]", + Handler: p.handleAdminCommand, + AdminOnly: false, + }) +} + +// handleAdminCommand handles the admin command +func (p *ChatProcessor) handleAdminCommand(ctx context.Context, serverID uuid.UUID, message rcon.Message, args []string) (string, error) { + // Get all admins for the server + rows, err := p.db.QueryContext(ctx, ` + SELECT u.username, u.steam_id + FROM server_admins sa + JOIN users u ON sa.user_id = u.id + WHERE sa.server_id = $1 + `, serverID) + if err != nil { + return "", err + } + defer rows.Close() + + // Build the admin message + adminMessage := message.PlayerName + " is requesting admin assistance" + if len(args) > 0 { + adminMessage += ": " + strings.Join(args, " ") + } + + // TODO: Discord integration: ie: send message to discord channel + + // Send a message to all admins + for rows.Next() { + var username string + var steamID string + if err := rows.Scan(&username, &steamID); err != nil { + log.Error(). + Err(err). + Str("serverID", serverID.String()). + Msg("Failed to scan admin row") + continue + } + + // Send a direct message to the admin + p.sendDirectMessage(serverID, adminMessage, steamID) + } + + if err := rows.Err(); err != nil { + log.Error(). + Err(err). + Str("serverID", serverID.String()). + Msg("Error iterating admin rows") + } + + return "Your request has been sent to the admins.", nil +} + +// sendDirectMessage sends a direct message to a player +func (p *ChatProcessor) sendDirectMessage(serverID uuid.UUID, message string, steamID string) { + // Format the command to send a direct message to the player + command := "AdminWarn " + steamID + " " + message + + // Execute the command + _, err := p.rconManager.ExecuteCommand(serverID, command) + if err != nil { + log.Error(). + Err(err). + Str("serverID", serverID.String()). + Str("steamID", steamID). + Str("message", message). + Msg("Failed to send direct message") + } +} diff --git a/internal/rcon_manager/rcon_manager.go b/internal/rcon_manager/rcon_manager.go new file mode 100644 index 00000000..55fadf9b --- /dev/null +++ b/internal/rcon_manager/rcon_manager.go @@ -0,0 +1,848 @@ +package rcon_manager + +import ( + "context" + "database/sql" + "errors" + "fmt" + "strconv" + "sync" + "time" + + "github.com/google/uuid" + "github.com/rs/zerolog/log" + "go.codycody31.dev/squad-aegis/internal/rcon" + squadRcon "go.codycody31.dev/squad-aegis/internal/squad-rcon" +) + +// RconEvent represents an event from the RCON server +type RconEvent struct { + ServerID uuid.UUID + Type string + Data interface{} + Time time.Time +} + +// RconCommand represents a command to be executed on the RCON server +type RconCommand struct { + Command string + Response chan CommandResponse +} + +// CommandResponse represents the response from an RCON command +type CommandResponse struct { + Response string + Error error +} + +// ServerConnection represents a connection to an RCON server +type ServerConnection struct { + ServerID uuid.UUID + CommandRcon *squadRcon.SquadRcon // Connection for executing commands + EventRcon *squadRcon.SquadRcon // Connection for listening to events + CommandChan chan RconCommand + EventChan chan RconEvent + Disconnected bool + LastUsed time.Time + mu sync.Mutex + cmdSemaphore chan struct{} + // Connection details for reconnection + host string + port string + password string +} + +// RconManager manages RCON connections to multiple servers +type RconManager struct { + connections map[uuid.UUID]*ServerConnection + eventSubscribers []chan<- RconEvent + mu sync.RWMutex + ctx context.Context + cancel context.CancelFunc +} + +// NewRconManager creates a new RCON manager +func NewRconManager(ctx context.Context) *RconManager { + ctx, cancel := context.WithCancel(ctx) + return &RconManager{ + connections: make(map[uuid.UUID]*ServerConnection), + eventSubscribers: []chan<- RconEvent{}, + ctx: ctx, + cancel: cancel, + } +} + +// SubscribeToEvents subscribes to RCON events +func (m *RconManager) SubscribeToEvents() chan RconEvent { + m.mu.Lock() + defer m.mu.Unlock() + + eventChan := make(chan RconEvent, 100) + m.eventSubscribers = append(m.eventSubscribers, eventChan) + return eventChan +} + +// UnsubscribeFromEvents unsubscribes from RCON events +func (m *RconManager) UnsubscribeFromEvents(eventChan chan RconEvent) { + m.mu.Lock() + defer m.mu.Unlock() + + for i, subscriber := range m.eventSubscribers { + if subscriber == eventChan { + m.eventSubscribers = append(m.eventSubscribers[:i], m.eventSubscribers[i+1:]...) + close(eventChan) + return + } + } +} + +// broadcastEvent broadcasts an event to all subscribers +func (m *RconManager) broadcastEvent(event RconEvent) { + m.mu.RLock() + defer m.mu.RUnlock() + + for _, subscriber := range m.eventSubscribers { + select { + case subscriber <- event: + default: + // If channel is full, log and continue + log.Warn(). + Str("serverID", event.ServerID.String()). + Str("eventType", event.Type). + Msg("Event channel full, dropping event") + } + } +} + +// ConnectToServer connects to an RCON server +func (m *RconManager) ConnectToServer(serverID uuid.UUID, host string, port int, password string) error { + m.mu.Lock() + defer m.mu.Unlock() + + log.Trace(). + Str("serverID", serverID.String()). + Str("host", host). + Int("port", port). + Msg("Attempting to connect to RCON server") + + portStr := strconv.Itoa(port) + + // Check if connection already exists + if conn, exists := m.connections[serverID]; exists { + conn.mu.Lock() + defer conn.mu.Unlock() + + // If connection is disconnected, reconnect + if conn.Disconnected { + log.Trace(). + Str("serverID", serverID.String()). + Msg("Reconnecting to disconnected RCON server") + + // Create command connection + log.Trace(). + Str("serverID", serverID.String()). + Msg("Creating command RCON connection") + commandRcon, err := squadRcon.NewSquadRcon(rcon.RconConfig{ + Host: host, + Port: portStr, + Password: password, + AutoReconnect: true, + AutoReconnectDelay: 5, + }) + if err != nil { + log.Error(). + Str("serverID", serverID.String()). + Err(err). + Msg("Failed to connect to command RCON") + return fmt.Errorf("failed to connect to command RCON: %w", err) + } + + // Create event connection + log.Trace(). + Str("serverID", serverID.String()). + Msg("Creating event RCON connection") + eventRcon, err := squadRcon.NewSquadRcon(rcon.RconConfig{ + Host: host, + Port: portStr, + Password: password, + AutoReconnect: true, + AutoReconnectDelay: 5, + }) + if err != nil { + commandRcon.Close() // Close the command connection if event connection fails + log.Error(). + Str("serverID", serverID.String()). + Err(err). + Msg("Failed to connect to event RCON") + return fmt.Errorf("failed to connect to event RCON: %w", err) + } + + conn.CommandRcon = commandRcon + conn.EventRcon = eventRcon + conn.Disconnected = false + conn.LastUsed = time.Now() + // Store connection details + conn.host = host + conn.port = portStr + conn.password = password + + // Start listening for events + log.Trace(). + Str("serverID", serverID.String()). + Msg("Starting event listener") + go m.listenForEvents(serverID, eventRcon) + // Start command processor + log.Trace(). + Str("serverID", serverID.String()). + Msg("Starting command processor") + go m.processCommands(serverID, conn) + + log.Info(). + Str("serverID", serverID.String()). + Msg("Successfully reconnected to RCON server") + + return nil + } + + // Connection already exists and is connected + conn.LastUsed = time.Now() + + log.Trace(). + Str("serverID", serverID.String()). + Msg("RCON connection already exists and is connected") + + return nil + } + + // Create command connection + log.Trace(). + Str("serverID", serverID.String()). + Msg("Creating new command RCON connection") + commandRcon, err := squadRcon.NewSquadRcon(rcon.RconConfig{ + Host: host, + Port: portStr, + Password: password, + AutoReconnect: true, + AutoReconnectDelay: 5, + }) + if err != nil { + log.Error(). + Str("serverID", serverID.String()). + Err(err). + Msg("Failed to connect to command RCON") + return fmt.Errorf("failed to connect to command RCON: %w", err) + } + + // Create event connection + log.Trace(). + Str("serverID", serverID.String()). + Msg("Creating new event RCON connection") + eventRcon, err := squadRcon.NewSquadRcon(rcon.RconConfig{ + Host: host, + Port: portStr, + Password: password, + AutoReconnect: true, + AutoReconnectDelay: 5, + }) + if err != nil { + commandRcon.Close() // Close the command connection if event connection fails + log.Error(). + Str("serverID", serverID.String()). + Err(err). + Msg("Failed to connect to event RCON") + return fmt.Errorf("failed to connect to event RCON: %w", err) + } + + // Create a semaphore to ensure only one command executes at a time + cmdSemaphore := make(chan struct{}, 1) + + conn := &ServerConnection{ + ServerID: serverID, + CommandRcon: commandRcon, + EventRcon: eventRcon, + CommandChan: make(chan RconCommand, 100), + EventChan: make(chan RconEvent, 100), + Disconnected: false, + LastUsed: time.Now(), + cmdSemaphore: cmdSemaphore, + host: host, + port: portStr, + password: password, + } + + m.connections[serverID] = conn + + // Start listening for events + log.Trace(). + Str("serverID", serverID.String()). + Msg("Starting event listener") + go m.listenForEvents(serverID, eventRcon) + // Start command processor + log.Trace(). + Str("serverID", serverID.String()). + Msg("Starting command processor") + go m.processCommands(serverID, conn) + + log.Info(). + Str("serverID", serverID.String()). + Msg("RCON connection created and started") + + return nil +} + +// DisconnectFromServer disconnects from an RCON server +func (m *RconManager) DisconnectFromServer(serverID uuid.UUID) error { + m.mu.Lock() + defer m.mu.Unlock() + + conn, exists := m.connections[serverID] + if !exists { + return errors.New("server not connected") + } + + conn.mu.Lock() + defer conn.mu.Unlock() + + if conn.Disconnected { + return errors.New("server already disconnected") + } + + // Close both connections + conn.CommandRcon.Close() + conn.EventRcon.Close() + conn.Disconnected = true + + return nil +} + +// ExecuteCommand executes a command on an RCON server +func (m *RconManager) ExecuteCommand(serverID uuid.UUID, command string) (string, error) { + m.mu.RLock() + conn, exists := m.connections[serverID] + m.mu.RUnlock() + + if !exists { + log.Error(). + Str("serverID", serverID.String()). + Str("command", command). + Msg("Server not connected") + return "", errors.New("server not connected") + } + + conn.mu.Lock() + if conn.Disconnected { + conn.mu.Unlock() + log.Error(). + Str("serverID", serverID.String()). + Str("command", command). + Msg("Server disconnected") + return "", errors.New("server disconnected") + } + conn.LastUsed = time.Now() + conn.mu.Unlock() + + log.Trace(). + Str("serverID", serverID.String()). + Str("command", command). + Msg("Queueing RCON command") + + // Create response channel + responseChan := make(chan CommandResponse, 1) + + // Send command to command processor + select { + case conn.CommandChan <- RconCommand{Command: command, Response: responseChan}: + log.Trace(). + Str("serverID", serverID.String()). + Str("command", command). + Msg("Command queued successfully") + case <-time.After(5 * time.Second): + log.Error(). + Str("serverID", serverID.String()). + Str("command", command). + Msg("Command queue full, try again later") + return "", errors.New("command queue full, try again later") + } + + log.Trace(). + Str("serverID", serverID.String()). + Str("command", command). + Msg("Waiting for command response") + + // Wait for response + select { + case response := <-responseChan: + if response.Error != nil { + log.Debug(). + Str("serverID", serverID.String()). + Str("command", command). + Err(response.Error). + Msg("Command execution failed") + } else { + log.Trace(). + Str("serverID", serverID.String()). + Str("command", command). + Int("responseLength", len(response.Response)). + Msg("Command executed successfully") + } + return response.Response, response.Error + case <-time.After(30 * time.Second): + log.Error(). + Str("serverID", serverID.String()). + Str("command", command). + Msg("Command timed out") + return "", errors.New("command timed out") + } +} + +// processCommands processes commands for a server +func (m *RconManager) processCommands(serverID uuid.UUID, conn *ServerConnection) { + log.Trace(). + Str("serverID", serverID.String()). + Msg("Starting command processor") + + for { + select { + case cmd := <-conn.CommandChan: + // Process the command directly in the main goroutine + // This ensures sequential processing without potential deadlocks + + log.Trace(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Msg("Received command from queue") + + // Acquire the semaphore + log.Trace(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Msg("Acquiring command semaphore") + conn.cmdSemaphore <- struct{}{} + log.Trace(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Msg("Acquired command semaphore") + + // Check connection status + conn.mu.Lock() + if conn.Disconnected { + conn.mu.Unlock() + log.Debug(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Msg("Server disconnected, cannot execute command") + cmd.Response <- CommandResponse{ + Response: "", + Error: errors.New("server disconnected"), + } + // Release the semaphore + <-conn.cmdSemaphore + log.Trace(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Msg("Released command semaphore after disconnection") + continue + } + + // FIXME: Workaround for Buffer Overflow causing the connection to crash + // Check if we need to recreate the command connection + // This ensures each command gets a fresh connection state + if cmd.Command != "PING_CONNECTION" { + log.Trace(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Msg("Recreating command connection for fresh state") + + // Close the old connection + conn.CommandRcon.Close() + + // Create a new connection using stored connection details + newCommandRcon, err := squadRcon.NewSquadRcon(rcon.RconConfig{ + Host: conn.host, + Port: conn.port, + Password: conn.password, + AutoReconnect: true, + AutoReconnectDelay: 5, + }) + + if err != nil { + conn.mu.Unlock() + log.Error(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Err(err). + Msg("Failed to recreate command connection") + + cmd.Response <- CommandResponse{ + Response: "", + Error: fmt.Errorf("failed to recreate command connection: %w", err), + } + + // Release the semaphore + <-conn.cmdSemaphore + continue + } + + // Update the connection + conn.CommandRcon = newCommandRcon + log.Trace(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Msg("Command connection recreated successfully") + } + + conn.LastUsed = time.Now() + conn.mu.Unlock() + + // Execute command with timeout + responseChan := make(chan CommandResponse, 1) + + log.Trace(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Msg("Executing command") + + startTime := time.Now() + go func() { + // Execute command using the command connection + log.Trace(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Msg("Sending command to RCON server") + response, err := conn.CommandRcon.Rcon.Execute(cmd.Command) + execTime := time.Since(startTime) + if err != nil { + log.Debug(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Err(err). + Dur("execTime", execTime). + Msg("Command execution returned error") + } else { + log.Trace(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Int("responseLength", len(response)). + Dur("execTime", execTime). + Msg("Command execution completed") + } + select { + case responseChan <- CommandResponse{ + Response: response, + Error: err, + }: + log.Trace(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Msg("Response sent to channel") + default: + // Channel might be closed if timeout occurred + log.Debug(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Msg("Could not send response to channel, might be closed") + } + }() + + // Wait for response with timeout + log.Trace(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Msg("Waiting for command response with timeout") + var cmdResponse CommandResponse + select { + case response := <-responseChan: + cmdResponse = response + log.Trace(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Msg("Received command response") + case <-time.After(25 * time.Second): // Slightly less than the client timeout + cmdResponse = CommandResponse{ + Response: "", + Error: errors.New("command execution timed out"), + } + log.Debug(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Msg("Command execution timed out internally") + } + + // Send response back to caller + log.Trace(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Msg("Sending response back to caller") + cmd.Response <- cmdResponse + + // Release the semaphore + log.Trace(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Msg("Releasing command semaphore") + <-conn.cmdSemaphore + log.Trace(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Msg("Released command semaphore") + + // Log command execution + if cmdResponse.Error != nil { + log.Debug(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Err(cmdResponse.Error). + Msg("RCON command execution failed") + } else { + log.Trace(). + Str("serverID", serverID.String()). + Str("command", cmd.Command). + Int("responseLength", len(cmdResponse.Response)). + Msg("RCON command executed successfully") + } + + case <-m.ctx.Done(): + log.Trace(). + Str("serverID", serverID.String()). + Msg("Stopping command processor due to context cancellation") + return + } + } +} + +// listenForEvents listens for events from an RCON server +func (m *RconManager) listenForEvents(serverID uuid.UUID, sr *squadRcon.SquadRcon) { + // Setup event listeners + sr.Rcon.Emitter.On("CHAT_MESSAGE", func(data interface{}) { + event := RconEvent{ + ServerID: serverID, + Type: "CHAT_MESSAGE", + Data: data, + Time: time.Now(), + } + + m.broadcastEvent(event) + }) + + sr.Rcon.Emitter.On("PLAYER_WARNED", func(data interface{}) { + event := RconEvent{ + ServerID: serverID, + Type: "PLAYER_WARNED", + Data: data, + Time: time.Now(), + } + + m.broadcastEvent(event) + }) + + sr.Rcon.Emitter.On("PLAYER_KICKED", func(data interface{}) { + event := RconEvent{ + ServerID: serverID, + Type: "PLAYER_KICKED", + Data: data, + Time: time.Now(), + } + + m.broadcastEvent(event) + }) + + sr.Rcon.Emitter.On("POSSESSED_ADMIN_CAMERA", func(data interface{}) { + event := RconEvent{ + ServerID: serverID, + Type: "POSSESSED_ADMIN_CAMERA", + Data: data, + Time: time.Now(), + } + + m.broadcastEvent(event) + }) + + sr.Rcon.Emitter.On("UNPOSSESSED_ADMIN_CAMERA", func(data interface{}) { + event := RconEvent{ + ServerID: serverID, + Type: "UNPOSSESSED_ADMIN_CAMERA", + Data: data, + Time: time.Now(), + } + + m.broadcastEvent(event) + }) + + sr.Rcon.Emitter.On("SQUAD_CREATED", func(data interface{}) { + event := RconEvent{ + ServerID: serverID, + Type: "SQUAD_CREATED", + Data: data, + Time: time.Now(), + } + + m.broadcastEvent(event) + }) + + // Listen for connection events + sr.Rcon.Emitter.On("close", func(data interface{}) { + event := RconEvent{ + ServerID: serverID, + Type: "CONNECTION_CLOSED", + Data: nil, + Time: time.Now(), + } + + m.broadcastEvent(event) + }) + + sr.Rcon.Emitter.On("error", func(data interface{}) { + event := RconEvent{ + ServerID: serverID, + Type: "CONNECTION_ERROR", + Data: data, + Time: time.Now(), + } + + m.broadcastEvent(event) + }) + + // Block until context is done + <-m.ctx.Done() +} + +// StartConnectionManager starts the connection manager +func (m *RconManager) StartConnectionManager() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + m.cleanupIdleConnections() + case <-m.ctx.Done(): + m.cleanupAllConnections() + return + } + } +} + +// cleanupIdleConnections closes idle connections +func (m *RconManager) cleanupIdleConnections() { + m.mu.Lock() + defer m.mu.Unlock() + + now := time.Now() + idleTimeout := 30 * time.Minute + + for serverID, conn := range m.connections { + conn.mu.Lock() + if !conn.Disconnected && now.Sub(conn.LastUsed) > idleTimeout { + log.Trace(). + Str("serverID", serverID.String()). + Msg("Closing idle RCON connection") + + conn.CommandRcon.Close() + conn.EventRcon.Close() + conn.Disconnected = true + } + conn.mu.Unlock() + } +} + +// cleanupAllConnections closes all connections +func (m *RconManager) cleanupAllConnections() { + m.mu.Lock() + defer m.mu.Unlock() + + for serverID, conn := range m.connections { + conn.mu.Lock() + if !conn.Disconnected { + log.Trace(). + Str("serverID", serverID.String()). + Msg("Closing RCON connections during shutdown") + + conn.CommandRcon.Close() + conn.EventRcon.Close() + conn.Disconnected = true + } + conn.mu.Unlock() + } +} + +// Shutdown shuts down the RCON manager +func (m *RconManager) Shutdown() { + m.cancel() +} + +// ConnectToAllServers connects to all servers in the database +func (m *RconManager) ConnectToAllServers(ctx context.Context, db *sql.DB) { + // Get all servers from the database + rows, err := db.QueryContext(ctx, ` + SELECT id, ip_address, rcon_port, rcon_password + FROM servers + WHERE rcon_port > 0 AND rcon_password != '' + `) + if err != nil { + log.Error().Err(err).Msg("Failed to query servers for RCON connections") + return + } + defer rows.Close() + + // Connect to each server + for rows.Next() { + var id uuid.UUID + var ipAddress string + var rconPort int + var rconPassword string + + if err := rows.Scan(&id, &ipAddress, &rconPort, &rconPassword); err != nil { + log.Error().Err(err).Msg("Failed to scan server row") + continue + } + + // Try to connect to the server + err := m.ConnectToServer(id, ipAddress, rconPort, rconPassword) + if err != nil { + log.Warn(). + Err(err). + Str("serverID", id.String()). + Str("ipAddress", ipAddress). + Int("rconPort", rconPort). + Msg("Failed to connect to server RCON") + continue + } + + log.Info(). + Str("serverID", id.String()). + Str("ipAddress", ipAddress). + Int("rconPort", rconPort). + Msg("Connected to server RCON") + } + + if err := rows.Err(); err != nil { + log.Error().Err(err).Msg("Error iterating server rows") + } +} + +// ProcessChatMessages starts processing chat messages for all connected servers +func (m *RconManager) ProcessChatMessages(ctx context.Context, messageHandler func(serverID uuid.UUID, message rcon.Message)) { + // Create a channel to receive chat events + eventChan := m.SubscribeToEvents() + + // Start a goroutine to process events + go func() { + defer m.UnsubscribeFromEvents(eventChan) + + for { + select { + case <-ctx.Done(): + log.Info().Msg("Stopping chat message processor") + return + case event := <-eventChan: + // Only process chat messages + if event.Type == "CHAT_MESSAGE" { + if message, ok := event.Data.(rcon.Message); ok { + // Call the message handler + messageHandler(event.ServerID, message) + } + } + } + } + }() +} diff --git a/internal/server/server.go b/internal/server/server.go index 66b81835..0d23236e 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -7,6 +7,8 @@ import ( "net/url" "strings" + "go.codycody31.dev/squad-aegis/internal/chat_processor" + "go.codycody31.dev/squad-aegis/internal/rcon_manager" "go.codycody31.dev/squad-aegis/shared/config" "github.com/gin-gonic/gin" @@ -17,7 +19,9 @@ type Server struct { } type Dependencies struct { - DB *sql.DB + DB *sql.DB + RconManager *rcon_manager.RconManager + ChatProcessor *chat_processor.ChatProcessor } func NewRouter(serverDependencies *Dependencies) *gin.Engine { @@ -131,6 +135,7 @@ func NewRouter(serverDependencies *Dependencies) *gin.Engine { serverGroup.POST("/rcon/execute", server.AuthHasServerPermission("manageserver"), server.ServerRconExecute) serverGroup.GET("/rcon/server-population", server.ServerRconServerPopulation) serverGroup.GET("/rcon/available-layers", server.ServerRconAvailableLayers) + serverGroup.GET("/rcon/events", server.AuthHasServerPermission("manageserver"), server.ServerRconEvents) serverGroup.GET("/roles", server.ServerRolesList) serverGroup.POST("/roles", server.AuthIsSuperAdmin(), server.ServerRolesAdd) diff --git a/internal/server/servers_rcon.go b/internal/server/servers_rcon.go index 31894d3f..007d4136 100644 --- a/internal/server/servers_rcon.go +++ b/internal/server/servers_rcon.go @@ -1,6 +1,7 @@ package server import ( + "regexp" "strconv" "strings" @@ -84,17 +85,15 @@ func (s *Server) ServerRconExecute(c *gin.Context) { return } - // TODO: RCON Connection should be handled in a separate goroutine and not in the main thread - // Then we just use a channel to send the response back to the client - - r, err := rcon.NewRcon(rcon.RconConfig{Host: server.IpAddress, Password: server.RconPassword, Port: strconv.Itoa(server.RconPort), AutoReconnect: true, AutoReconnectDelay: 5}) + // Ensure server is connected to RCON manager + err = s.Dependencies.RconManager.ConnectToServer(serverId, server.IpAddress, server.RconPort, server.RconPassword) if err != nil { responses.BadRequest(c, "Failed to connect to RCON", &gin.H{"error": err.Error()}) return } - defer r.Close() - response, err := r.Execute(request.Command) + // Execute command using RCON manager + response, err := s.Dependencies.RconManager.ExecuteCommand(serverId, request.Command) if err != nil { responses.BadRequest(c, "Failed to execute RCON command", &gin.H{"error": err.Error()}) return @@ -126,25 +125,49 @@ func (s *Server) ServerRconServerPopulation(c *gin.Context) { return } - r, err := squadRcon.NewSquadRcon(rcon.RconConfig{Host: server.IpAddress, Password: server.RconPassword, Port: strconv.Itoa(server.RconPort), AutoReconnect: true, AutoReconnectDelay: 5}) + // Ensure server is connected to RCON manager + err = s.Dependencies.RconManager.ConnectToServer(serverId, server.IpAddress, server.RconPort, server.RconPassword) if err != nil { responses.BadRequest(c, "Failed to connect to RCON", &gin.H{"error": err.Error()}) return } - defer r.Close() - squads, teamNames, err := r.GetServerSquads() + // Execute ListSquads command + squadsResponse, err := s.Dependencies.RconManager.ExecuteCommand(serverId, "ListSquads") if err != nil { responses.BadRequest(c, "Failed to get server squads", &gin.H{"error": err.Error()}) return } - players, err := r.GetServerPlayers() + // Execute ListPlayers command + playersResponse, err := s.Dependencies.RconManager.ExecuteCommand(serverId, "ListPlayers") if err != nil { responses.BadRequest(c, "Failed to get server players", &gin.H{"error": err.Error()}) return } + // Parse responses + r, err := squadRcon.NewSquadRcon(rcon.RconConfig{Host: server.IpAddress, Password: server.RconPassword, Port: strconv.Itoa(server.RconPort), AutoReconnect: true, AutoReconnectDelay: 5}) + if err != nil { + responses.BadRequest(c, "Failed to create RCON parser", &gin.H{"error": err.Error()}) + return + } + defer r.Close() + + // Parse squads response + squads, teamNames, err := parseSquadsResponse(squadsResponse) + if err != nil { + responses.BadRequest(c, "Failed to parse squads response", &gin.H{"error": err.Error()}) + return + } + + // Parse players response + players, err := parsePlayersResponse(playersResponse) + if err != nil { + responses.BadRequest(c, "Failed to parse players response", &gin.H{"error": err.Error()}) + return + } + teams, err := squadRcon.ParseTeamsAndSquads(squads, teamNames, players) if err != nil { responses.BadRequest(c, "Failed to parse teams and squads", &gin.H{"error": err.Error()}) @@ -157,6 +180,103 @@ func (s *Server) ServerRconServerPopulation(c *gin.Context) { }) } +// Helper functions to parse RCON responses +func parseSquadsResponse(response string) ([]squadRcon.Squad, []string, error) { + lines := strings.Split(response, "\n") + squads := []squadRcon.Squad{} + teamNames := []string{} + var currentTeamID int + + // First pass: Extract teams and squads + for _, line := range lines { + // Match team information + matchesTeam := regexp.MustCompile(`^Team ID: ([1|2]) \((.*)\)`) + matchTeam := matchesTeam.FindStringSubmatch(line) + + if len(matchTeam) > 0 { + teamId, _ := strconv.Atoi(matchTeam[1]) + currentTeamID = teamId + teamNames = append(teamNames, matchTeam[2]) + continue + } + + // Match squad information + matchesSquad := regexp.MustCompile(`^ID: (\d{1,}) \| Name: (.*?) \| Size: (\d) \| Locked: (True|False)`) + matchSquad := matchesSquad.FindStringSubmatch(line) + + if len(matchSquad) > 0 { + squadId, _ := strconv.Atoi(matchSquad[1]) + size, _ := strconv.Atoi(matchSquad[3]) + + squad := squadRcon.Squad{ + ID: squadId, + TeamId: currentTeamID, + Name: matchSquad[2], + Size: size, + Locked: matchSquad[4] == "True", + Players: []squadRcon.Player{}, + } + + squads = append(squads, squad) + } + } + + return squads, teamNames, nil +} + +func parsePlayersResponse(response string) (squadRcon.PlayersData, error) { + onlinePlayers := []squadRcon.Player{} + disconnectedPlayers := []squadRcon.Player{} + + lines := strings.Split(response, "\n") + for _, line := range lines { + matchesOnline := regexp.MustCompile(`ID: ([0-9]+) \| Online IDs: EOS: (\w{32}) steam: (\d{17}) \| Name: (.+) \| Team ID: ([0-9]+) \| Squad ID: ([0-9]+|N\/A) \| Is Leader: (True|False) \| Role: (.*)`) + matchesDisconnected := regexp.MustCompile(`^ID: (\d{1,}) \| Online IDs: EOS: (\w{32}) steam: (\d{17}) \| Since Disconnect: (\d{2,})m.(\d{2})s \| Name: (.*?)$`) + + matchOnline := matchesOnline.FindStringSubmatch(line) + matchDisconnected := matchesDisconnected.FindStringSubmatch(line) + + if len(matchOnline) > 0 { + playerId, _ := strconv.Atoi(matchOnline[1]) + teamId, _ := strconv.Atoi(matchOnline[5]) + squadId := 0 + if matchOnline[6] != "N/A" { + squadId, _ = strconv.Atoi(matchOnline[6]) + } + + player := squadRcon.Player{ + Id: playerId, + EosId: matchOnline[2], + SteamId: matchOnline[3], + Name: matchOnline[4], + TeamId: teamId, + SquadId: squadId, + IsSquadLeader: matchOnline[7] == "True", + Role: matchOnline[8], + } + + onlinePlayers = append(onlinePlayers, player) + } else if len(matchDisconnected) > 0 { + playerId, _ := strconv.Atoi(matchDisconnected[1]) + + player := squadRcon.Player{ + Id: playerId, + EosId: matchDisconnected[2], + SteamId: matchDisconnected[3], + SinceDisconnect: matchDisconnected[4] + "m" + matchDisconnected[5] + "s", + Name: matchDisconnected[6], + } + + disconnectedPlayers = append(disconnectedPlayers, player) + } + } + + return squadRcon.PlayersData{ + OnlinePlayers: onlinePlayers, + DisconnectedPlayers: disconnectedPlayers, + }, nil +} + func (s *Server) ServerRconAvailableLayers(c *gin.Context) { user := s.getUserFromSession(c) @@ -173,19 +293,46 @@ func (s *Server) ServerRconAvailableLayers(c *gin.Context) { return } - r, err := squadRcon.NewSquadRcon(rcon.RconConfig{Host: server.IpAddress, Password: server.RconPassword, Port: strconv.Itoa(server.RconPort), AutoReconnect: true, AutoReconnectDelay: 5}) + // Ensure server is connected to RCON manager + err = s.Dependencies.RconManager.ConnectToServer(serverId, server.IpAddress, server.RconPort, server.RconPassword) if err != nil { responses.BadRequest(c, "Failed to connect to RCON", &gin.H{"error": err.Error()}) return } - defer r.Close() - availableLayers, err := r.GetAvailableLayers() + // Execute ListLayers command + layersResponse, err := s.Dependencies.RconManager.ExecuteCommand(serverId, "ListLayers") if err != nil { responses.BadRequest(c, "Failed to get available layers", &gin.H{"error": err.Error()}) return } + // Parse layers response + availableLayers := []squadRcon.Layer{} + layersResponse = strings.Replace(layersResponse, "List of available layers :\n", "", 1) + + for _, line := range strings.Split(layersResponse, "\n") { + if line == "" { + continue + } + + if strings.Contains(line, "(") { + mod := strings.Split(line, "(")[1] + mod = strings.Trim(mod, ")") + availableLayers = append(availableLayers, squadRcon.Layer{ + Name: strings.Split(line, "(")[0], + Mod: mod, + IsVanilla: false, + }) + } else { + availableLayers = append(availableLayers, squadRcon.Layer{ + Name: line, + Mod: "", + IsVanilla: true, + }) + } + } + responses.Success(c, "Available layers fetched successfully", &gin.H{"layers": availableLayers}) } @@ -212,12 +359,12 @@ func (s *Server) ServerRconKickPlayer(c *gin.Context) { return } - r, err := rcon.NewRcon(rcon.RconConfig{Host: server.IpAddress, Password: server.RconPassword, Port: strconv.Itoa(server.RconPort), AutoReconnect: true, AutoReconnectDelay: 5}) + // Ensure server is connected to RCON manager + err = s.Dependencies.RconManager.ConnectToServer(serverId, server.IpAddress, server.RconPort, server.RconPassword) if err != nil { responses.BadRequest(c, "Failed to connect to RCON", &gin.H{"error": err.Error()}) return } - defer r.Close() // Format the kick command kickCommand := "AdminKick " + request.SteamId @@ -225,7 +372,8 @@ func (s *Server) ServerRconKickPlayer(c *gin.Context) { kickCommand += " " + request.Reason } - response, err := r.Execute(kickCommand) + // Execute kick command + response, err := s.Dependencies.RconManager.ExecuteCommand(serverId, kickCommand) if err != nil { responses.BadRequest(c, "Failed to kick player", &gin.H{"error": err.Error()}) return @@ -265,21 +413,23 @@ func (s *Server) ServerRconWarnPlayer(c *gin.Context) { return } - r, err := rcon.NewRcon(rcon.RconConfig{Host: server.IpAddress, Password: server.RconPassword, Port: strconv.Itoa(server.RconPort), AutoReconnect: true, AutoReconnectDelay: 5}) + // Ensure server is connected to RCON manager + err = s.Dependencies.RconManager.ConnectToServer(serverId, server.IpAddress, server.RconPort, server.RconPassword) if err != nil { responses.BadRequest(c, "Failed to connect to RCON", &gin.H{"error": err.Error()}) return } - defer r.Close() - // Format the warning command + // Format the warn command warnCommand := "AdminWarn " + request.SteamId + " " + request.Message - response, err := r.Execute(warnCommand) + // Execute warn command + response, err := s.Dependencies.RconManager.ExecuteCommand(serverId, warnCommand) if err != nil { responses.BadRequest(c, "Failed to warn player", &gin.H{"error": err.Error()}) return } + // Create detailed audit log auditData := map[string]interface{}{ "steamId": request.SteamId, @@ -291,7 +441,7 @@ func (s *Server) ServerRconWarnPlayer(c *gin.Context) { responses.Success(c, "Player warned successfully", &gin.H{"response": response}) } -// ServerRconMovePlayer handles moving a player to a different team +// ServerRconMovePlayer handles moving a player to another team func (s *Server) ServerRconMovePlayer(c *gin.Context) { user := s.getUserFromSession(c) @@ -314,17 +464,18 @@ func (s *Server) ServerRconMovePlayer(c *gin.Context) { return } - r, err := rcon.NewRcon(rcon.RconConfig{Host: server.IpAddress, Password: server.RconPassword, Port: strconv.Itoa(server.RconPort), AutoReconnect: true, AutoReconnectDelay: 5}) + // Ensure server is connected to RCON manager + err = s.Dependencies.RconManager.ConnectToServer(serverId, server.IpAddress, server.RconPort, server.RconPassword) if err != nil { responses.BadRequest(c, "Failed to connect to RCON", &gin.H{"error": err.Error()}) return } - defer r.Close() // Format the move command moveCommand := "AdminForceTeamChange " + request.SteamId - response, err := r.Execute(moveCommand) + // Execute move command + response, err := s.Dependencies.RconManager.ExecuteCommand(serverId, moveCommand) if err != nil { responses.BadRequest(c, "Failed to move player", &gin.H{"error": err.Error()}) return @@ -340,6 +491,7 @@ func (s *Server) ServerRconMovePlayer(c *gin.Context) { responses.Success(c, "Player moved successfully", &gin.H{"response": response}) } +// ServerRconServerInfo gets the server info from the server func (s *Server) ServerRconServerInfo(c *gin.Context) { user := s.getUserFromSession(c) @@ -356,18 +508,26 @@ func (s *Server) ServerRconServerInfo(c *gin.Context) { return } - r, err := squadRcon.NewSquadRcon(rcon.RconConfig{Host: server.IpAddress, Password: server.RconPassword, Port: strconv.Itoa(server.RconPort), AutoReconnect: true, AutoReconnectDelay: 5}) + // Ensure server is connected to RCON manager + err = s.Dependencies.RconManager.ConnectToServer(serverId, server.IpAddress, server.RconPort, server.RconPassword) if err != nil { responses.BadRequest(c, "Failed to connect to RCON", &gin.H{"error": err.Error()}) return } - defer r.Close() - serverInfo, err := r.GetServerInfo() + // Execute ShowServerInfo command + serverInfoResponse, err := s.Dependencies.RconManager.ExecuteCommand(serverId, "ShowServerInfo") if err != nil { responses.BadRequest(c, "Failed to get server info", &gin.H{"error": err.Error()}) return } + // Parse server info response + serverInfo, err := squadRcon.MarshalServerInfo(serverInfoResponse) + if err != nil { + responses.BadRequest(c, "Failed to parse server info", &gin.H{"error": err.Error()}) + return + } + responses.Success(c, "Server info fetched successfully", &gin.H{"serverInfo": serverInfo}) } diff --git a/internal/server/servers_rcon_events.go b/internal/server/servers_rcon_events.go new file mode 100644 index 00000000..262a2ae2 --- /dev/null +++ b/internal/server/servers_rcon_events.go @@ -0,0 +1,95 @@ +package server + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "go.codycody31.dev/squad-aegis/core" + "go.codycody31.dev/squad-aegis/internal/server/responses" +) + +// ServerRconEvents handles subscribing to RCON events using Server-Sent Events (SSE) +func (s *Server) ServerRconEvents(c *gin.Context) { + user := s.getUserFromSession(c) + + serverIdString := c.Param("serverId") + serverId, err := uuid.Parse(serverIdString) + if err != nil { + responses.BadRequest(c, "Invalid server ID", &gin.H{"error": err.Error()}) + return + } + + server, err := core.GetServerById(c.Request.Context(), s.Dependencies.DB, serverId, user) + if err != nil { + responses.BadRequest(c, "Failed to get server", &gin.H{"error": err.Error()}) + return + } + + // Ensure server is connected to RCON manager + err = s.Dependencies.RconManager.ConnectToServer(serverId, server.IpAddress, server.RconPort, server.RconPassword) + if err != nil { + responses.BadRequest(c, "Failed to connect to RCON", &gin.H{"error": err.Error()}) + return + } + + // Subscribe to RCON events + eventChan := s.Dependencies.RconManager.SubscribeToEvents() + defer s.Dependencies.RconManager.UnsubscribeFromEvents(eventChan) + + // Set headers for SSE + c.Writer.Header().Set("Content-Type", "text/event-stream") + c.Writer.Header().Set("Cache-Control", "no-cache") + c.Writer.Header().Set("Connection", "keep-alive") + c.Writer.Header().Set("Transfer-Encoding", "chunked") + c.Writer.Flush() + + // Create a context that is canceled when the client disconnects + ctx, cancel := context.WithCancel(c.Request.Context()) + defer cancel() + + // Create a goroutine to detect client disconnection + go func() { + <-c.Request.Context().Done() + cancel() + }() + + // Send a ping event every 30 seconds to keep the connection alive + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + // Create audit log for connection + s.CreateAuditLog(c.Request.Context(), &serverId, &user.Id, "server:rcon:events:connect", nil) + + // Send events to client + for { + select { + case <-ctx.Done(): + // Client disconnected + s.CreateAuditLog(c.Request.Context(), &serverId, &user.Id, "server:rcon:events:disconnect", nil) + return + case event := <-eventChan: + // Only send events for this server + if event.ServerID != serverId { + continue + } + + // Convert event to JSON + eventJSON, err := json.Marshal(event) + if err != nil { + continue + } + + // Send event to client + fmt.Fprintf(c.Writer, "data: %s\n\n", eventJSON) + c.Writer.Flush() + case <-ticker.C: + // Send ping event + fmt.Fprintf(c.Writer, "event: ping\ndata: %d\n\n", time.Now().Unix()) + c.Writer.Flush() + } + } +} diff --git a/internal/squad-rcon/squad-rcon.go b/internal/squad-rcon/squad-rcon.go index 357a3782..dc2408ff 100644 --- a/internal/squad-rcon/squad-rcon.go +++ b/internal/squad-rcon/squad-rcon.go @@ -17,7 +17,7 @@ var ( ) type SquadRcon struct { - rcon *rcon.Rcon + Rcon *rcon.Rcon } // Player represents a player in the game @@ -297,18 +297,18 @@ func NewSquadRcon(rconConfig rcon.RconConfig) (*SquadRcon, error) { return nil, err } - return &SquadRcon{rcon: rcon}, nil + return &SquadRcon{Rcon: rcon}, nil } // BanPlayer bans a player from the server func (s *SquadRcon) BanPlayer(steamId string, duration int, reason string) error { - _, err := s.rcon.Execute(fmt.Sprintf("AdminBan %s %dd %s", steamId, duration, reason)) + _, err := s.Rcon.Execute(fmt.Sprintf("AdminBan %s %dd %s", steamId, duration, reason)) return err } // GetServerPlayers gets the online and disconnected players from the server func (s *SquadRcon) GetServerPlayers() (PlayersData, error) { - playersResponse, err := s.rcon.Execute("ListPlayers") + playersResponse, err := s.Rcon.Execute("ListPlayers") if err != nil { return PlayersData{}, err } @@ -363,7 +363,7 @@ func (s *SquadRcon) GetServerPlayers() (PlayersData, error) { } func (s *SquadRcon) GetServerSquads() ([]Squad, []string, error) { - squadsResponse, err := s.rcon.Execute("ListSquads") + squadsResponse, err := s.Rcon.Execute("ListSquads") if err != nil { return []Squad{}, []string{}, err } @@ -410,7 +410,7 @@ func (s *SquadRcon) GetServerSquads() ([]Squad, []string, error) { } func (s *SquadRcon) GetCurrentMap() (Map, error) { - currentMap, err := s.rcon.Execute("ShowCurrentMap") + currentMap, err := s.Rcon.Execute("ShowCurrentMap") if err != nil { return Map{}, err } @@ -429,7 +429,7 @@ func (s *SquadRcon) GetCurrentMap() (Map, error) { } func (s *SquadRcon) GetNextMap() (Map, error) { - nextMap, err := s.rcon.Execute("ShowNextMap") + nextMap, err := s.Rcon.Execute("ShowNextMap") if err != nil { if nextMap == "Next level is not defined" { return Map{}, ErrNoNextMap @@ -453,7 +453,7 @@ func (s *SquadRcon) GetNextMap() (Map, error) { // GetAvailableMaps gets the available maps from the server func (s *SquadRcon) GetAvailableLayers() ([]Layer, error) { - availableLayers, err := s.rcon.Execute("ListLayers") + availableLayers, err := s.Rcon.Execute("ListLayers") if err != nil { return []Layer{}, err } @@ -483,7 +483,7 @@ func (s *SquadRcon) GetAvailableLayers() ([]Layer, error) { // GetServerInfo gets the server info from the server func (s *SquadRcon) GetServerInfo() (ServerInfo, error) { - serverInfo, err := s.rcon.Execute("ShowServerInfo") + serverInfo, err := s.Rcon.Execute("ShowServerInfo") if err != nil { return ServerInfo{}, err } @@ -576,5 +576,5 @@ func (s *SquadRcon) GetTeamsAndSquads() ([]Team, error) { } func (s *SquadRcon) Close() { - s.rcon.Close() + s.Rcon.Close() }