469 lines
14 KiB
Go
469 lines
14 KiB
Go
package users
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"dk/internal/database"
|
|
|
|
"zombiezen.com/go/sqlite"
|
|
)
|
|
|
|
// User represents a user in the database
|
|
type User struct {
|
|
ID int `json:"id"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
Email string `json:"email"`
|
|
Verified int `json:"verified"`
|
|
Token string `json:"token"`
|
|
Registered int64 `json:"registered"`
|
|
LastOnline int64 `json:"last_online"`
|
|
Auth int `json:"auth"`
|
|
X int `json:"x"`
|
|
Y int `json:"y"`
|
|
ClassID int `json:"class_id"`
|
|
Currently string `json:"currently"`
|
|
Fighting int `json:"fighting"`
|
|
MonsterID int `json:"monster_id"`
|
|
MonsterHP int `json:"monster_hp"`
|
|
MonsterSleep int `json:"monster_sleep"`
|
|
MonsterImmune int `json:"monster_immune"`
|
|
UberDamage int `json:"uber_damage"`
|
|
UberDefense int `json:"uber_defense"`
|
|
HP int `json:"hp"`
|
|
MP int `json:"mp"`
|
|
TP int `json:"tp"`
|
|
MaxHP int `json:"max_hp"`
|
|
MaxMP int `json:"max_mp"`
|
|
MaxTP int `json:"max_tp"`
|
|
Level int `json:"level"`
|
|
Gold int `json:"gold"`
|
|
Exp int `json:"exp"`
|
|
GoldBonus int `json:"gold_bonus"`
|
|
ExpBonus int `json:"exp_bonus"`
|
|
Strength int `json:"strength"`
|
|
Dexterity int `json:"dexterity"`
|
|
Attack int `json:"attack"`
|
|
Defense int `json:"defense"`
|
|
WeaponID int `json:"weapon_id"`
|
|
ArmorID int `json:"armor_id"`
|
|
ShieldID int `json:"shield_id"`
|
|
Slot1ID int `json:"slot_1_id"`
|
|
Slot2ID int `json:"slot_2_id"`
|
|
Slot3ID int `json:"slot_3_id"`
|
|
WeaponName string `json:"weapon_name"`
|
|
ArmorName string `json:"armor_name"`
|
|
ShieldName string `json:"shield_name"`
|
|
Slot1Name string `json:"slot_1_name"`
|
|
Slot2Name string `json:"slot_2_name"`
|
|
Slot3Name string `json:"slot_3_name"`
|
|
DropCode int `json:"drop_code"`
|
|
Spells string `json:"spells"`
|
|
Towns string `json:"towns"`
|
|
|
|
db *database.DB
|
|
}
|
|
|
|
// userColumns returns the column list for user queries (excluding id for inserts)
|
|
func userColumns() string {
|
|
return `id, username, password, email, verified, token, registered, last_online, auth,
|
|
x, y, class_id, currently, fighting, monster_id, monster_hp, monster_sleep, monster_immune,
|
|
uber_damage, uber_defense, hp, mp, tp, max_hp, max_mp, max_tp, level, gold, exp,
|
|
gold_bonus, exp_bonus, strength, dexterity, attack, defense, weapon_id, armor_id, shield_id,
|
|
slot_1_id, slot_2_id, slot_3_id, weapon_name, armor_name, shield_name,
|
|
slot_1_name, slot_2_name, slot_3_name, drop_code, spells, towns`
|
|
}
|
|
|
|
// scanUser populates a User struct from a sqlite.Stmt
|
|
func scanUser(stmt *sqlite.Stmt, db *database.DB) *User {
|
|
return &User{
|
|
ID: stmt.ColumnInt(0),
|
|
Username: stmt.ColumnText(1),
|
|
Password: stmt.ColumnText(2),
|
|
Email: stmt.ColumnText(3),
|
|
Verified: stmt.ColumnInt(4),
|
|
Token: stmt.ColumnText(5),
|
|
Registered: stmt.ColumnInt64(6),
|
|
LastOnline: stmt.ColumnInt64(7),
|
|
Auth: stmt.ColumnInt(8),
|
|
X: stmt.ColumnInt(9),
|
|
Y: stmt.ColumnInt(10),
|
|
ClassID: stmt.ColumnInt(11),
|
|
Currently: stmt.ColumnText(12),
|
|
Fighting: stmt.ColumnInt(13),
|
|
MonsterID: stmt.ColumnInt(14),
|
|
MonsterHP: stmt.ColumnInt(15),
|
|
MonsterSleep: stmt.ColumnInt(16),
|
|
MonsterImmune: stmt.ColumnInt(17),
|
|
UberDamage: stmt.ColumnInt(18),
|
|
UberDefense: stmt.ColumnInt(19),
|
|
HP: stmt.ColumnInt(20),
|
|
MP: stmt.ColumnInt(21),
|
|
TP: stmt.ColumnInt(22),
|
|
MaxHP: stmt.ColumnInt(23),
|
|
MaxMP: stmt.ColumnInt(24),
|
|
MaxTP: stmt.ColumnInt(25),
|
|
Level: stmt.ColumnInt(26),
|
|
Gold: stmt.ColumnInt(27),
|
|
Exp: stmt.ColumnInt(28),
|
|
GoldBonus: stmt.ColumnInt(29),
|
|
ExpBonus: stmt.ColumnInt(30),
|
|
Strength: stmt.ColumnInt(31),
|
|
Dexterity: stmt.ColumnInt(32),
|
|
Attack: stmt.ColumnInt(33),
|
|
Defense: stmt.ColumnInt(34),
|
|
WeaponID: stmt.ColumnInt(35),
|
|
ArmorID: stmt.ColumnInt(36),
|
|
ShieldID: stmt.ColumnInt(37),
|
|
Slot1ID: stmt.ColumnInt(38),
|
|
Slot2ID: stmt.ColumnInt(39),
|
|
Slot3ID: stmt.ColumnInt(40),
|
|
WeaponName: stmt.ColumnText(41),
|
|
ArmorName: stmt.ColumnText(42),
|
|
ShieldName: stmt.ColumnText(43),
|
|
Slot1Name: stmt.ColumnText(44),
|
|
Slot2Name: stmt.ColumnText(45),
|
|
Slot3Name: stmt.ColumnText(46),
|
|
DropCode: stmt.ColumnInt(47),
|
|
Spells: stmt.ColumnText(48),
|
|
Towns: stmt.ColumnText(49),
|
|
db: db,
|
|
}
|
|
}
|
|
|
|
// Find retrieves a user by ID
|
|
func Find(db *database.DB, id int) (*User, error) {
|
|
var user *User
|
|
|
|
query := `SELECT ` + userColumns() + ` FROM users WHERE id = ?`
|
|
|
|
err := db.Query(query, func(stmt *sqlite.Stmt) error {
|
|
user = scanUser(stmt, db)
|
|
return nil
|
|
}, id)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find user: %w", err)
|
|
}
|
|
|
|
if user == nil {
|
|
return nil, fmt.Errorf("user with ID %d not found", id)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// All retrieves all users ordered by registration date (newest first)
|
|
func All(db *database.DB) ([]*User, error) {
|
|
var users []*User
|
|
|
|
query := `SELECT ` + userColumns() + ` FROM users ORDER BY registered DESC, id DESC`
|
|
|
|
err := db.Query(query, func(stmt *sqlite.Stmt) error {
|
|
user := scanUser(stmt, db)
|
|
users = append(users, user)
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve all users: %w", err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
// ByUsername retrieves a user by username (case-insensitive)
|
|
func ByUsername(db *database.DB, username string) (*User, error) {
|
|
var user *User
|
|
|
|
query := `SELECT ` + userColumns() + ` FROM users WHERE LOWER(username) = LOWER(?) LIMIT 1`
|
|
|
|
err := db.Query(query, func(stmt *sqlite.Stmt) error {
|
|
user = scanUser(stmt, db)
|
|
return nil
|
|
}, username)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find user by username: %w", err)
|
|
}
|
|
|
|
if user == nil {
|
|
return nil, fmt.Errorf("user with username '%s' not found", username)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// ByEmail retrieves a user by email address
|
|
func ByEmail(db *database.DB, email string) (*User, error) {
|
|
var user *User
|
|
|
|
query := `SELECT ` + userColumns() + ` FROM users WHERE email = ? LIMIT 1`
|
|
|
|
err := db.Query(query, func(stmt *sqlite.Stmt) error {
|
|
user = scanUser(stmt, db)
|
|
return nil
|
|
}, email)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find user by email: %w", err)
|
|
}
|
|
|
|
if user == nil {
|
|
return nil, fmt.Errorf("user with email '%s' not found", email)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// ByLevel retrieves users at a specific level
|
|
func ByLevel(db *database.DB, level int) ([]*User, error) {
|
|
var users []*User
|
|
|
|
query := `SELECT ` + userColumns() + ` FROM users WHERE level = ? ORDER BY exp DESC, id ASC`
|
|
|
|
err := db.Query(query, func(stmt *sqlite.Stmt) error {
|
|
user := scanUser(stmt, db)
|
|
users = append(users, user)
|
|
return nil
|
|
}, level)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve users by level: %w", err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
// Online retrieves users who have been online within the specified duration
|
|
func Online(db *database.DB, within time.Duration) ([]*User, error) {
|
|
var users []*User
|
|
cutoff := time.Now().Add(-within).Unix()
|
|
|
|
query := `SELECT ` + userColumns() + ` FROM users WHERE last_online >= ? ORDER BY last_online DESC, id ASC`
|
|
|
|
err := db.Query(query, func(stmt *sqlite.Stmt) error {
|
|
user := scanUser(stmt, db)
|
|
users = append(users, user)
|
|
return nil
|
|
}, cutoff)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve online users: %w", err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
// Save updates an existing user in the database
|
|
func (u *User) Save() error {
|
|
if u.ID == 0 {
|
|
return fmt.Errorf("cannot save user without ID")
|
|
}
|
|
|
|
query := `UPDATE users SET username = ?, password = ?, email = ?, verified = ?, token = ?,
|
|
registered = ?, last_online = ?, auth = ?, x = ?, y = ?, class_id = ?, currently = ?,
|
|
fighting = ?, monster_id = ?, monster_hp = ?, monster_sleep = ?, monster_immune = ?,
|
|
uber_damage = ?, uber_defense = ?, hp = ?, mp = ?, tp = ?, max_hp = ?, max_mp = ?, max_tp = ?,
|
|
level = ?, gold = ?, exp = ?, gold_bonus = ?, exp_bonus = ?, strength = ?, dexterity = ?,
|
|
attack = ?, defense = ?, weapon_id = ?, armor_id = ?, shield_id = ?, slot_1_id = ?,
|
|
slot_2_id = ?, slot_3_id = ?, weapon_name = ?, armor_name = ?, shield_name = ?,
|
|
slot_1_name = ?, slot_2_name = ?, slot_3_name = ?, drop_code = ?, spells = ?, towns = ?
|
|
WHERE id = ?`
|
|
|
|
return u.db.Exec(query, u.Username, u.Password, u.Email, u.Verified, u.Token,
|
|
u.Registered, u.LastOnline, u.Auth, u.X, u.Y, u.ClassID, u.Currently,
|
|
u.Fighting, u.MonsterID, u.MonsterHP, u.MonsterSleep, u.MonsterImmune,
|
|
u.UberDamage, u.UberDefense, u.HP, u.MP, u.TP, u.MaxHP, u.MaxMP, u.MaxTP,
|
|
u.Level, u.Gold, u.Exp, u.GoldBonus, u.ExpBonus, u.Strength, u.Dexterity,
|
|
u.Attack, u.Defense, u.WeaponID, u.ArmorID, u.ShieldID, u.Slot1ID,
|
|
u.Slot2ID, u.Slot3ID, u.WeaponName, u.ArmorName, u.ShieldName,
|
|
u.Slot1Name, u.Slot2Name, u.Slot3Name, u.DropCode, u.Spells, u.Towns, u.ID)
|
|
}
|
|
|
|
// Delete removes the user from the database
|
|
func (u *User) Delete() error {
|
|
if u.ID == 0 {
|
|
return fmt.Errorf("cannot delete user without ID")
|
|
}
|
|
|
|
query := "DELETE FROM users WHERE id = ?"
|
|
return u.db.Exec(query, u.ID)
|
|
}
|
|
|
|
// RegisteredTime returns the registration timestamp as a time.Time
|
|
func (u *User) RegisteredTime() time.Time {
|
|
return time.Unix(u.Registered, 0)
|
|
}
|
|
|
|
// LastOnlineTime returns the last online timestamp as a time.Time
|
|
func (u *User) LastOnlineTime() time.Time {
|
|
return time.Unix(u.LastOnline, 0)
|
|
}
|
|
|
|
// UpdateLastOnline sets the last online timestamp to current time
|
|
func (u *User) UpdateLastOnline() {
|
|
u.LastOnline = time.Now().Unix()
|
|
}
|
|
|
|
// IsVerified returns true if the user's email is verified
|
|
func (u *User) IsVerified() bool {
|
|
return u.Verified == 1
|
|
}
|
|
|
|
// IsAdmin returns true if the user has admin privileges (auth >= 4)
|
|
func (u *User) IsAdmin() bool {
|
|
return u.Auth >= 4
|
|
}
|
|
|
|
// IsModerator returns true if the user has moderator privileges (auth >= 2)
|
|
func (u *User) IsModerator() bool {
|
|
return u.Auth >= 2
|
|
}
|
|
|
|
// IsFighting returns true if the user is currently fighting
|
|
func (u *User) IsFighting() bool {
|
|
return u.Fighting == 1
|
|
}
|
|
|
|
// IsAlive returns true if the user has HP > 0
|
|
func (u *User) IsAlive() bool {
|
|
return u.HP > 0
|
|
}
|
|
|
|
// GetSpellIDs returns spell IDs as a slice of strings
|
|
func (u *User) GetSpellIDs() []string {
|
|
if u.Spells == "" {
|
|
return []string{}
|
|
}
|
|
return strings.Split(u.Spells, ",")
|
|
}
|
|
|
|
// SetSpellIDs sets spell IDs from a slice of strings
|
|
func (u *User) SetSpellIDs(spells []string) {
|
|
u.Spells = strings.Join(spells, ",")
|
|
}
|
|
|
|
// HasSpell returns true if the user knows the specified spell ID
|
|
func (u *User) HasSpell(spellID string) bool {
|
|
spells := u.GetSpellIDs()
|
|
for _, spell := range spells {
|
|
if strings.TrimSpace(spell) == spellID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetTownIDs returns town IDs as a slice of strings
|
|
func (u *User) GetTownIDs() []string {
|
|
if u.Towns == "" {
|
|
return []string{}
|
|
}
|
|
return strings.Split(u.Towns, ",")
|
|
}
|
|
|
|
// SetTownIDs sets town IDs from a slice of strings
|
|
func (u *User) SetTownIDs(towns []string) {
|
|
u.Towns = strings.Join(towns, ",")
|
|
}
|
|
|
|
// HasVisitedTown returns true if the user has visited the specified town ID
|
|
func (u *User) HasVisitedTown(townID string) bool {
|
|
towns := u.GetTownIDs()
|
|
for _, town := range towns {
|
|
if strings.TrimSpace(town) == townID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetEquipment returns all equipped item information
|
|
func (u *User) GetEquipment() map[string]any {
|
|
return map[string]any{
|
|
"weapon": map[string]any{"id": u.WeaponID, "name": u.WeaponName},
|
|
"armor": map[string]any{"id": u.ArmorID, "name": u.ArmorName},
|
|
"shield": map[string]any{"id": u.ShieldID, "name": u.ShieldName},
|
|
"slot1": map[string]any{"id": u.Slot1ID, "name": u.Slot1Name},
|
|
"slot2": map[string]any{"id": u.Slot2ID, "name": u.Slot2Name},
|
|
"slot3": map[string]any{"id": u.Slot3ID, "name": u.Slot3Name},
|
|
}
|
|
}
|
|
|
|
// GetStats returns combat-relevant stats
|
|
func (u *User) GetStats() map[string]int {
|
|
return map[string]int{
|
|
"level": u.Level,
|
|
"hp": u.HP,
|
|
"mp": u.MP,
|
|
"tp": u.TP,
|
|
"max_hp": u.MaxHP,
|
|
"max_mp": u.MaxMP,
|
|
"max_tp": u.MaxTP,
|
|
"strength": u.Strength,
|
|
"dexterity": u.Dexterity,
|
|
"attack": u.Attack,
|
|
"defense": u.Defense,
|
|
"uber_damage": u.UberDamage,
|
|
"uber_defense": u.UberDefense,
|
|
}
|
|
}
|
|
|
|
// GetPosition returns the user's coordinates
|
|
func (u *User) GetPosition() (int, int) {
|
|
return u.X, u.Y
|
|
}
|
|
|
|
// SetPosition sets the user's coordinates
|
|
func (u *User) SetPosition(x, y int) {
|
|
u.X = x
|
|
u.Y = y
|
|
}
|
|
|
|
// GetByUsername retrieves a user by username
|
|
func GetByUsername(db *database.DB, username string) (*User, error) {
|
|
var user *User
|
|
|
|
query := `SELECT ` + userColumns() + ` FROM users WHERE LOWER(username) = LOWER(?) LIMIT 1`
|
|
|
|
err := db.Query(query, func(stmt *sqlite.Stmt) error {
|
|
user = scanUser(stmt, db)
|
|
return nil
|
|
}, username)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query failed: %w", err)
|
|
}
|
|
|
|
if user == nil {
|
|
return nil, fmt.Errorf("user not found: %s", username)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// GetByEmail retrieves a user by email
|
|
func GetByEmail(db *database.DB, email string) (*User, error) {
|
|
var user *User
|
|
|
|
query := `SELECT ` + userColumns() + ` FROM users WHERE LOWER(email) = LOWER(?) LIMIT 1`
|
|
|
|
err := db.Query(query, func(stmt *sqlite.Stmt) error {
|
|
user = scanUser(stmt, db)
|
|
return nil
|
|
}, email)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query failed: %w", err)
|
|
}
|
|
|
|
if user == nil {
|
|
return nil, fmt.Errorf("user not found: %s", email)
|
|
}
|
|
|
|
return user, nil
|
|
}
|