package main import ( "context" "database/sql" "encoding/hex" "fmt" "github.com/pkg/errors" "math/rand" "time" _ "github.com/lib/pq" ) func init() { rand.Seed(time.Now().UnixNano()) } type dbLayer struct { db *sql.DB } func connectToDataBase(host string, port uint32, user string, password string, dbName string) (*dbLayer, error) { db, err := sql.Open("postgres", fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbName, ), ) if err != nil { return nil, errors.Wrap(err, "Failed to connect to database") } return &dbLayer{ db: db, }, nil } func (db *dbLayer) execInTransaction(ctx context.Context, query string, args ...interface{}) error { tx, err := db.db.BeginTx(ctx, &sql.TxOptions{}) if err != nil { return errors.Wrap(err, "Failed to begin transaction") } getLogger(ctx).Debugf("[Run sql query]: %s, [%v]", query, args) _, err = tx.ExecContext(ctx, query, args...) if err != nil { _ = tx.Rollback() return errors.Wrapf(err, "Failed to run db query [%s] [%v]", query, args) } err = tx.Commit() if err != nil { return errors.Wrap(err, "Failed to commit transaction") } return nil } type Player struct { id string name string lastOnline time.Time onlineDuration time.Duration deaths int level int } func fetchPlayer(r *sql.Rows, p *Player) error { var ( err error lastOnline string onlineDuration int ) err = r.Scan(&p.id, &p.name, &lastOnline, &onlineDuration, &p.deaths, &p.level) if err != nil { return errors.Wrap(err, "Failed to fetch data base") } p.lastOnline, err = time.Parse(time.RFC3339, lastOnline) if err != nil { return errors.Wrapf(err, "Failed to parse date [%s]", lastOnline) } p.onlineDuration = time.Second * time.Duration(onlineDuration) return nil } func (db *dbLayer) getPlayers(ctx context.Context) ([]Player, error) { var players []Player query := "SELECT id, name, last_login, online_duration, deaths, level FROM players" rows, err := db.db.QueryContext(ctx, query) if err != nil { return nil, errors.Wrapf(err, "Failed to run db query [%s]", query) } defer rows.Close() for rows.Next() { p := Player{} err = fetchPlayer(rows, &p) if err != nil { return nil, err } players = append(players, p) } return players, nil } func (db *dbLayer) getPlayerByID(ctx context.Context, id string) (*Player, error) { query := "SELECT id, name, last_login, online_duration, deaths, level FROM players WHERE id = $1" rows, err := db.db.QueryContext(ctx, query, id) if err != nil { return nil, errors.Wrapf(err, "Failed to run db query [%s]", query) } defer rows.Close() if rows.Next() { var p Player err = fetchPlayer(rows, &p) if err != nil { return nil, err } return &p, nil } return nil, nil } func (db *dbLayer) getPlayerByName(ctx context.Context, name string) (*Player, error) { query := "SELECT id, name, last_login, online_duration, deaths, level FROM players WHERE name = $1 LIMIT 1" rows, err := db.db.QueryContext(ctx, query, name) if err != nil { return nil, errors.Wrapf(err, "Failed to run db query [%s]", query) } defer rows.Close() if rows.Next() { var p Player err = fetchPlayer(rows, &p) if err != nil { return nil, err } return &p, nil } return nil, nil } func (db *dbLayer) createPlayer(ctx context.Context, name string, lastOnline time.Time) (*Player, error) { id := uuid() err := db.execInTransaction(ctx, "INSERT INTO players (id, name, last_login) VALUES ($1, $2, $3)", id, name, lastOnline.Format(time.RFC3339)) if err != nil { return nil, err } return &Player{ id: id, name: name, lastOnline: lastOnline, }, nil } func (db *dbLayer) increasePlayerOnlineDuration(ctx context.Context, playerID string, d time.Duration) error { onlineDurationSecond := int(d / time.Second) if onlineDurationSecond == 0 { return nil } return db.execInTransaction(ctx, "UPDATE players SET online_duration = online_duration + $2 WHERE id = $1", playerID, onlineDurationSecond) } func (db *dbLayer) updatePlayerLastOnline(ctx context.Context, playerID string, lastOnline time.Time) error { return db.execInTransaction(ctx, "UPDATE players SET last_login = $2 WHERE id = $1", playerID, lastOnline.Format(time.RFC3339)) } func (db *dbLayer) increasePlayerDeath(ctx context.Context, playerID string, deaths int) error { return db.execInTransaction(ctx, "UPDATE players SET deaths = deaths + $2 WHERE id = $1", playerID, deaths) } func (db *dbLayer) increasePlayerEntryKills(ctx context.Context, playerID string, entity string, count int) error { return db.execInTransaction(ctx, "INSERT INTO killings (player_id, entity, count) VALUES ($1, $2, $3) ON CONFLICT (player_id, entity) DO UPDATE SET count = killings.count + excluded.count", playerID, entity, count, ) } func (db *dbLayer) updatePlayerLevel(ctx context.Context, playerID string, level int) error { return db.execInTransaction(ctx, "UPDATE players SET level = $2 WHERE id = $1", playerID, level) } func uuid() string { token := make([]byte, 18) rand.Read(token) result := make([]byte, 36) hex.Encode(result, token) result[8] = 45 result[13] = 45 result[18] = 45 result[23] = 45 return string(result) }