mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
feat(tools): adds a small tools to monitor players.
This commit is contained in:
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- New `usercheck` CLI tool in `tools/usercheck/` for querying connected users and server status
|
||||
- `list` command: List all currently connected users with character details
|
||||
- `count` command: Show count of connected users per server/channel
|
||||
- `search` command: Search for specific connected users by name
|
||||
- `servers` command: Display server/channel status and player counts
|
||||
- `history` command: Show recent login history for a player
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgraded Go version requirement from 1.19 to 1.25
|
||||
|
||||
2
tools/usercheck/.gitignore
vendored
Normal file
2
tools/usercheck/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
usercheck
|
||||
usercheck.exe
|
||||
54
tools/usercheck/README.md
Normal file
54
tools/usercheck/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# usercheck
|
||||
|
||||
CLI tool to query connected users and server status from the Erupe database.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
go build -o usercheck
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
./usercheck <command> [options]
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `list` | List all currently connected users |
|
||||
| `count` | Show count of connected users per server |
|
||||
| `search` | Search for a connected user by name |
|
||||
| `servers` | Show server/channel status |
|
||||
| `history` | Show login history for a player |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# List connected users
|
||||
./usercheck list -password "dbpass"
|
||||
|
||||
# Verbose output with last login times
|
||||
./usercheck list -v -password "dbpass"
|
||||
|
||||
# Search for a player
|
||||
./usercheck search -name "Hunter" -password "dbpass"
|
||||
|
||||
# Show server status
|
||||
./usercheck servers -password "dbpass"
|
||||
|
||||
# Player login history
|
||||
./usercheck history -name "Hunter" -password "dbpass"
|
||||
```
|
||||
|
||||
### Database Options
|
||||
|
||||
| Flag | Env Variable | Default |
|
||||
|------|--------------|---------|
|
||||
| `-host` | `ERUPE_DB_HOST` | localhost |
|
||||
| `-port` | - | 5432 |
|
||||
| `-user` | `ERUPE_DB_USER` | postgres |
|
||||
| `-password` | `ERUPE_DB_PASSWORD` | (required) |
|
||||
| `-dbname` | `ERUPE_DB_NAME` | erupe |
|
||||
438
tools/usercheck/commands.go
Normal file
438
tools/usercheck/commands.go
Normal file
@@ -0,0 +1,438 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
)
|
||||
|
||||
// runList lists all currently connected users across all channels.
|
||||
//
|
||||
// Queries the sign_sessions table joined with characters and servers
|
||||
// to show all active sessions with character details.
|
||||
//
|
||||
// Options:
|
||||
// - Database connection flags (host, port, user, password, dbname)
|
||||
// - v: Verbose output with additional details
|
||||
func runList(args []string) {
|
||||
fs := flag.NewFlagSet("list", flag.ExitOnError)
|
||||
cfg := &DBConfig{}
|
||||
addDBFlags(fs, cfg)
|
||||
verbose := fs.Bool("v", false, "Verbose output with additional details")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
db, err := connectDB(cfg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
ss.char_id,
|
||||
COALESCE(c.name, '') as char_name,
|
||||
COALESCE(ss.server_id, 0) as server_id,
|
||||
COALESCE(s.world_name, 'Unknown') as world_name,
|
||||
COALESCE(c.user_id, 0) as user_id,
|
||||
COALESCE(u.username, '') as username,
|
||||
c.last_login,
|
||||
COALESCE(c.hrp, 0) as hr,
|
||||
COALESCE(c.gr, 0) as gr
|
||||
FROM sign_sessions ss
|
||||
LEFT JOIN characters c ON ss.char_id = c.id
|
||||
LEFT JOIN servers s ON ss.server_id = s.server_id
|
||||
LEFT JOIN users u ON c.user_id = u.id
|
||||
WHERE ss.char_id IS NOT NULL AND ss.server_id IS NOT NULL
|
||||
ORDER BY s.world_name, c.name
|
||||
`
|
||||
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error querying database: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
var users []ConnectedUser
|
||||
for rows.Next() {
|
||||
var u ConnectedUser
|
||||
err := rows.Scan(&u.CharID, &u.CharName, &u.ServerID, &u.ServerName, &u.UserID, &u.Username, &u.LastLogin, &u.HR, &u.GR)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error scanning row: %v\n", err)
|
||||
continue
|
||||
}
|
||||
users = append(users, u)
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
fmt.Println("No users currently connected.")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("=== Connected Users (%d total) ===\n\n", len(users))
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
if *verbose {
|
||||
_, _ = fmt.Fprintln(w, "CHAR ID\tNAME\tSERVER\tHR\tGR\tUSERNAME\tLAST LOGIN")
|
||||
_, _ = fmt.Fprintln(w, "-------\t----\t------\t--\t--\t--------\t----------")
|
||||
for _, u := range users {
|
||||
lastLogin := "N/A"
|
||||
if u.LastLogin.Valid {
|
||||
lastLogin = u.LastLogin.Time.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%d\t%s\t%s\t%d\t%d\t%s\t%s\n",
|
||||
u.CharID, u.CharName, u.ServerName, u.HR, u.GR, u.Username, lastLogin)
|
||||
}
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(w, "CHAR ID\tNAME\tSERVER\tHR\tGR")
|
||||
_, _ = fmt.Fprintln(w, "-------\t----\t------\t--\t--")
|
||||
for _, u := range users {
|
||||
_, _ = fmt.Fprintf(w, "%d\t%s\t%s\t%d\t%d\n",
|
||||
u.CharID, u.CharName, u.ServerName, u.HR, u.GR)
|
||||
}
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
// runCount shows the count of connected users per server/channel.
|
||||
//
|
||||
// Options:
|
||||
// - Database connection flags (host, port, user, password, dbname)
|
||||
func runCount(args []string) {
|
||||
fs := flag.NewFlagSet("count", flag.ExitOnError)
|
||||
cfg := &DBConfig{}
|
||||
addDBFlags(fs, cfg)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
db, err := connectDB(cfg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
// Get total from sign_sessions
|
||||
var totalConnected int
|
||||
err = db.QueryRow(`
|
||||
SELECT COUNT(*) FROM sign_sessions
|
||||
WHERE char_id IS NOT NULL AND server_id IS NOT NULL
|
||||
`).Scan(&totalConnected)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error querying total: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get count per server
|
||||
query := `
|
||||
SELECT
|
||||
s.server_id,
|
||||
s.world_name,
|
||||
s.current_players,
|
||||
s.land
|
||||
FROM servers s
|
||||
ORDER BY s.world_name, s.land
|
||||
`
|
||||
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error querying servers: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
fmt.Printf("=== Connected Users Summary ===\n\n")
|
||||
fmt.Printf("Total Connected: %d\n\n", totalConnected)
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintln(w, "SERVER ID\tWORLD\tLAND\tPLAYERS")
|
||||
_, _ = fmt.Fprintln(w, "---------\t-----\t----\t-------")
|
||||
|
||||
for rows.Next() {
|
||||
var serverID uint16
|
||||
var worldName string
|
||||
var currentPlayers, land int
|
||||
if err := rows.Scan(&serverID, &worldName, ¤tPlayers, &land); err != nil {
|
||||
continue
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%d\t%s\t%d\t%d\n", serverID, worldName, land, currentPlayers)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
// runSearch searches for a specific connected user by name.
|
||||
//
|
||||
// Options:
|
||||
// - name: Player name to search for (partial match, case-insensitive)
|
||||
// - Database connection flags (host, port, user, password, dbname)
|
||||
func runSearch(args []string) {
|
||||
fs := flag.NewFlagSet("search", flag.ExitOnError)
|
||||
cfg := &DBConfig{}
|
||||
addDBFlags(fs, cfg)
|
||||
name := fs.String("name", "", "Player name to search for (partial match)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *name == "" {
|
||||
fmt.Fprintf(os.Stderr, "Error: -name flag is required\n")
|
||||
fs.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
db, err := connectDB(cfg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
ss.char_id,
|
||||
COALESCE(c.name, '') as char_name,
|
||||
COALESCE(ss.server_id, 0) as server_id,
|
||||
COALESCE(s.world_name, 'Unknown') as world_name,
|
||||
COALESCE(c.user_id, 0) as user_id,
|
||||
COALESCE(u.username, '') as username,
|
||||
c.last_login,
|
||||
COALESCE(c.hrp, 0) as hr,
|
||||
COALESCE(c.gr, 0) as gr
|
||||
FROM sign_sessions ss
|
||||
LEFT JOIN characters c ON ss.char_id = c.id
|
||||
LEFT JOIN servers s ON ss.server_id = s.server_id
|
||||
LEFT JOIN users u ON c.user_id = u.id
|
||||
WHERE ss.char_id IS NOT NULL
|
||||
AND ss.server_id IS NOT NULL
|
||||
AND LOWER(c.name) LIKE LOWER($1)
|
||||
ORDER BY c.name
|
||||
`
|
||||
|
||||
rows, err := db.Query(query, "%"+*name+"%")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error querying database: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
var users []ConnectedUser
|
||||
for rows.Next() {
|
||||
var u ConnectedUser
|
||||
err := rows.Scan(&u.CharID, &u.CharName, &u.ServerID, &u.ServerName, &u.UserID, &u.Username, &u.LastLogin, &u.HR, &u.GR)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
users = append(users, u)
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
fmt.Printf("No connected users found matching '%s'\n", *name)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("=== Search Results for '%s' (%d found) ===\n\n", *name, len(users))
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintln(w, "CHAR ID\tNAME\tSERVER\tHR\tGR\tUSERNAME")
|
||||
_, _ = fmt.Fprintln(w, "-------\t----\t------\t--\t--\t--------")
|
||||
for _, u := range users {
|
||||
_, _ = fmt.Fprintf(w, "%d\t%s\t%s\t%d\t%d\t%s\n",
|
||||
u.CharID, u.CharName, u.ServerName, u.HR, u.GR, u.Username)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
// runServers shows server/channel status and player counts.
|
||||
//
|
||||
// Options:
|
||||
// - Database connection flags (host, port, user, password, dbname)
|
||||
func runServers(args []string) {
|
||||
fs := flag.NewFlagSet("servers", flag.ExitOnError)
|
||||
cfg := &DBConfig{}
|
||||
addDBFlags(fs, cfg)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
db, err := connectDB(cfg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
server_id,
|
||||
world_name,
|
||||
world_description,
|
||||
land,
|
||||
current_players,
|
||||
season
|
||||
FROM servers
|
||||
ORDER BY world_name, land
|
||||
`
|
||||
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error querying servers: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
var servers []ServerStatus
|
||||
var totalPlayers int
|
||||
for rows.Next() {
|
||||
var s ServerStatus
|
||||
if err := rows.Scan(&s.ServerID, &s.WorldName, &s.WorldDesc, &s.Land, &s.CurrentPlayers, &s.Season); err != nil {
|
||||
continue
|
||||
}
|
||||
servers = append(servers, s)
|
||||
totalPlayers += s.CurrentPlayers
|
||||
}
|
||||
|
||||
if len(servers) == 0 {
|
||||
fmt.Println("No servers found. Is the Erupe server running?")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("=== Server Status ===\n\n")
|
||||
fmt.Printf("Total Servers: %d\n", len(servers))
|
||||
fmt.Printf("Total Players: %d\n\n", totalPlayers)
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintln(w, "ID\tWORLD\tLAND\tPLAYERS\tSEASON\tDESCRIPTION")
|
||||
_, _ = fmt.Fprintln(w, "--\t-----\t----\t-------\t------\t-----------")
|
||||
|
||||
seasonNames := map[int]string{0: "Green", 1: "Orange", 2: "Blue"}
|
||||
for _, s := range servers {
|
||||
seasonName := seasonNames[s.Season]
|
||||
if seasonName == "" {
|
||||
seasonName = fmt.Sprintf("%d", s.Season)
|
||||
}
|
||||
// Truncate description if too long
|
||||
desc := s.WorldDesc
|
||||
if len(desc) > 30 {
|
||||
desc = desc[:27] + "..."
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%d\t%s\t%d\t%d\t%s\t%s\n",
|
||||
s.ServerID, s.WorldName, s.Land, s.CurrentPlayers, seasonName, desc)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
// runHistory shows recent login history for a player.
|
||||
//
|
||||
// Options:
|
||||
// - name: Player name to search for (partial match, case-insensitive)
|
||||
// - limit: Maximum number of entries to show (default: 20)
|
||||
// - all: Show all characters, not just with recent logins
|
||||
// - Database connection flags (host, port, user, password, dbname)
|
||||
func runHistory(args []string) {
|
||||
fs := flag.NewFlagSet("history", flag.ExitOnError)
|
||||
cfg := &DBConfig{}
|
||||
addDBFlags(fs, cfg)
|
||||
name := fs.String("name", "", "Player name to search for (partial match)")
|
||||
limit := fs.Int("limit", 20, "Maximum number of entries to show")
|
||||
showAll := fs.Bool("all", false, "Show all characters (including those without recent logins)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *name == "" {
|
||||
fmt.Fprintf(os.Stderr, "Error: -name flag is required\n")
|
||||
fs.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
db, err := connectDB(cfg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
whereClause := "WHERE LOWER(c.name) LIKE LOWER($1)"
|
||||
if !*showAll {
|
||||
whereClause += " AND c.last_login IS NOT NULL"
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
c.id as char_id,
|
||||
c.name as char_name,
|
||||
c.last_login,
|
||||
COALESCE(c.hrp, 0) as hr,
|
||||
COALESCE(c.gr, 0) as gr,
|
||||
COALESCE(u.username, '') as username
|
||||
FROM characters c
|
||||
LEFT JOIN users u ON c.user_id = u.id
|
||||
%s
|
||||
ORDER BY c.last_login DESC NULLS LAST
|
||||
LIMIT $2
|
||||
`, whereClause)
|
||||
|
||||
rows, err := db.Query(query, "%"+*name+"%", *limit)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error querying database: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
var history []LoginHistory
|
||||
for rows.Next() {
|
||||
var h LoginHistory
|
||||
if err := rows.Scan(&h.CharID, &h.CharName, &h.LastLogin, &h.HR, &h.GR, &h.Username); err != nil {
|
||||
continue
|
||||
}
|
||||
history = append(history, h)
|
||||
}
|
||||
|
||||
if len(history) == 0 {
|
||||
fmt.Printf("No characters found matching '%s'\n", *name)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("=== Login History for '%s' (%d entries) ===\n\n", *name, len(history))
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintln(w, "CHAR ID\tNAME\tHR\tGR\tUSERNAME\tLAST LOGIN\tONLINE")
|
||||
_, _ = fmt.Fprintln(w, "-------\t----\t--\t--\t--------\t----------\t------")
|
||||
|
||||
for _, h := range history {
|
||||
lastLogin := "Never"
|
||||
online := "No"
|
||||
if h.LastLogin.Valid {
|
||||
lastLogin = h.LastLogin.Time.Format("2006-01-02 15:04:05")
|
||||
// Check if logged in within last hour (rough online indicator)
|
||||
if time.Since(h.LastLogin.Time) < time.Hour {
|
||||
online = "Possibly"
|
||||
}
|
||||
}
|
||||
|
||||
// Check if actually online by checking sign_sessions
|
||||
var isOnline bool
|
||||
if err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM sign_sessions WHERE char_id = $1 AND server_id IS NOT NULL)", h.CharID).Scan(&isOnline); err == nil && isOnline {
|
||||
online = "Yes"
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(w, "%d\t%s\t%d\t%d\t%s\t%s\t%s\n",
|
||||
h.CharID, h.CharName, h.HR, h.GR, h.Username, lastLogin, online)
|
||||
}
|
||||
_ = w.Flush()
|
||||
|
||||
// Show legend
|
||||
fmt.Println()
|
||||
fmt.Println("ONLINE column: Yes = Currently connected, Possibly = Last login within 1 hour, No = Not connected")
|
||||
}
|
||||
93
tools/usercheck/db.go
Normal file
93
tools/usercheck/db.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
// DBConfig holds database connection configuration.
|
||||
type DBConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
DBName string
|
||||
}
|
||||
|
||||
// addDBFlags adds common database flags to a FlagSet.
|
||||
func addDBFlags(fs *flag.FlagSet, cfg *DBConfig) {
|
||||
fs.StringVar(&cfg.Host, "host", getEnvOrDefault("ERUPE_DB_HOST", "localhost"), "Database host")
|
||||
fs.IntVar(&cfg.Port, "port", 5432, "Database port")
|
||||
fs.StringVar(&cfg.User, "user", getEnvOrDefault("ERUPE_DB_USER", "postgres"), "Database user")
|
||||
fs.StringVar(&cfg.Password, "password", os.Getenv("ERUPE_DB_PASSWORD"), "Database password")
|
||||
fs.StringVar(&cfg.DBName, "dbname", getEnvOrDefault("ERUPE_DB_NAME", "erupe"), "Database name")
|
||||
}
|
||||
|
||||
// getEnvOrDefault returns the environment variable value or a default.
|
||||
func getEnvOrDefault(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// connectDB establishes a connection to the PostgreSQL database.
|
||||
func connectDB(cfg *DBConfig) (*sql.DB, error) {
|
||||
if cfg.Password == "" {
|
||||
return nil, fmt.Errorf("database password is required (use -password flag or ERUPE_DB_PASSWORD env var)")
|
||||
}
|
||||
|
||||
connStr := fmt.Sprintf(
|
||||
"host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
|
||||
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName,
|
||||
)
|
||||
|
||||
db, err := sql.Open("postgres", connStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// ConnectedUser represents a user currently connected to the server.
|
||||
type ConnectedUser struct {
|
||||
CharID uint32
|
||||
CharName string
|
||||
ServerID uint16
|
||||
ServerName string
|
||||
UserID int
|
||||
Username string
|
||||
LastLogin sql.NullTime
|
||||
HR int
|
||||
GR int
|
||||
}
|
||||
|
||||
// ServerStatus represents the status of a channel server.
|
||||
type ServerStatus struct {
|
||||
ServerID uint16
|
||||
WorldName string
|
||||
WorldDesc string
|
||||
Land int
|
||||
CurrentPlayers int
|
||||
Season int
|
||||
}
|
||||
|
||||
// LoginHistory represents a player's login history entry.
|
||||
type LoginHistory struct {
|
||||
CharID uint32
|
||||
CharName string
|
||||
LastLogin sql.NullTime
|
||||
HR int
|
||||
GR int
|
||||
Username string
|
||||
}
|
||||
69
tools/usercheck/main.go
Normal file
69
tools/usercheck/main.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// main is the entry point for the user check CLI tool.
|
||||
//
|
||||
// The tool provides commands to query connected users and server status
|
||||
// by reading from the Erupe database. It's designed to be used while
|
||||
// the server is running to monitor player activity.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// usercheck <command> [options]
|
||||
// usercheck list # List all connected users
|
||||
// usercheck count # Count connected users
|
||||
// usercheck search -name "player" # Search for a specific player
|
||||
// usercheck servers # Show server/channel status
|
||||
// usercheck history -name "player" # Show player login history
|
||||
func main() {
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "Erupe User Check - Tool to query connected users and server status\n\n")
|
||||
fmt.Fprintf(os.Stderr, "Usage:\n")
|
||||
fmt.Fprintf(os.Stderr, " %s <command> [options]\n\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, "Available commands:\n")
|
||||
fmt.Fprintf(os.Stderr, " list List all currently connected users\n")
|
||||
fmt.Fprintf(os.Stderr, " count Show count of connected users per server\n")
|
||||
fmt.Fprintf(os.Stderr, " search Search for a specific connected user by name\n")
|
||||
fmt.Fprintf(os.Stderr, " servers Show server/channel status and player counts\n")
|
||||
fmt.Fprintf(os.Stderr, " history Show recent login history for a player\n")
|
||||
fmt.Fprintf(os.Stderr, "\nDatabase connection options (apply to all commands):\n")
|
||||
fmt.Fprintf(os.Stderr, " -host Database host (default: localhost)\n")
|
||||
fmt.Fprintf(os.Stderr, " -port Database port (default: 5432)\n")
|
||||
fmt.Fprintf(os.Stderr, " -user Database user (default: postgres)\n")
|
||||
fmt.Fprintf(os.Stderr, " -password Database password (required)\n")
|
||||
fmt.Fprintf(os.Stderr, " -dbname Database name (default: erupe)\n")
|
||||
fmt.Fprintf(os.Stderr, "\nUse '%s <command> -h' for more information about a command.\n", os.Args[0])
|
||||
}
|
||||
|
||||
if len(os.Args) < 2 {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
command := os.Args[1]
|
||||
args := os.Args[2:]
|
||||
|
||||
switch command {
|
||||
case "list":
|
||||
runList(args)
|
||||
case "count":
|
||||
runCount(args)
|
||||
case "search":
|
||||
runSearch(args)
|
||||
case "servers":
|
||||
runServers(args)
|
||||
case "history":
|
||||
runHistory(args)
|
||||
case "-h", "--help", "help":
|
||||
flag.Usage()
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "Unknown command: %s\n\n", command)
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user