559 lines
17 KiB
Go
559 lines
17 KiB
Go
package users
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"dk/internal/database"
|
|
"dk/internal/utils/scanner"
|
|
|
|
"zombiezen.com/go/sqlite"
|
|
)
|
|
|
|
// User represents a user in the database
|
|
type User struct {
|
|
ID int `db:"id" json:"id"`
|
|
Username string `db:"username" json:"username"`
|
|
Password string `db:"password" json:"password"`
|
|
Email string `db:"email" json:"email"`
|
|
Verified int `db:"verified" json:"verified"`
|
|
Token string `db:"token" json:"token"`
|
|
Registered int64 `db:"registered" json:"registered"`
|
|
LastOnline int64 `db:"last_online" json:"last_online"`
|
|
Auth int `db:"auth" json:"auth"`
|
|
X int `db:"x" json:"x"`
|
|
Y int `db:"y" json:"y"`
|
|
ClassID int `db:"class_id" json:"class_id"`
|
|
Currently string `db:"currently" json:"currently"`
|
|
Fighting int `db:"fighting" json:"fighting"`
|
|
MonsterID int `db:"monster_id" json:"monster_id"`
|
|
MonsterHP int `db:"monster_hp" json:"monster_hp"`
|
|
MonsterSleep int `db:"monster_sleep" json:"monster_sleep"`
|
|
MonsterImmune int `db:"monster_immune" json:"monster_immune"`
|
|
UberDamage int `db:"uber_damage" json:"uber_damage"`
|
|
UberDefense int `db:"uber_defense" json:"uber_defense"`
|
|
HP int `db:"hp" json:"hp"`
|
|
MP int `db:"mp" json:"mp"`
|
|
TP int `db:"tp" json:"tp"`
|
|
MaxHP int `db:"max_hp" json:"max_hp"`
|
|
MaxMP int `db:"max_mp" json:"max_mp"`
|
|
MaxTP int `db:"max_tp" json:"max_tp"`
|
|
Level int `db:"level" json:"level"`
|
|
Gold int `db:"gold" json:"gold"`
|
|
Exp int `db:"exp" json:"exp"`
|
|
GoldBonus int `db:"gold_bonus" json:"gold_bonus"`
|
|
ExpBonus int `db:"exp_bonus" json:"exp_bonus"`
|
|
Strength int `db:"strength" json:"strength"`
|
|
Dexterity int `db:"dexterity" json:"dexterity"`
|
|
Attack int `db:"attack" json:"attack"`
|
|
Defense int `db:"defense" json:"defense"`
|
|
WeaponID int `db:"weapon_id" json:"weapon_id"`
|
|
ArmorID int `db:"armor_id" json:"armor_id"`
|
|
ShieldID int `db:"shield_id" json:"shield_id"`
|
|
Slot1ID int `db:"slot_1_id" json:"slot_1_id"`
|
|
Slot2ID int `db:"slot_2_id" json:"slot_2_id"`
|
|
Slot3ID int `db:"slot_3_id" json:"slot_3_id"`
|
|
WeaponName string `db:"weapon_name" json:"weapon_name"`
|
|
ArmorName string `db:"armor_name" json:"armor_name"`
|
|
ShieldName string `db:"shield_name" json:"shield_name"`
|
|
Slot1Name string `db:"slot_1_name" json:"slot_1_name"`
|
|
Slot2Name string `db:"slot_2_name" json:"slot_2_name"`
|
|
Slot3Name string `db:"slot_3_name" json:"slot_3_name"`
|
|
DropCode int `db:"drop_code" json:"drop_code"`
|
|
Spells string `db:"spells" json:"spells"`
|
|
Towns string `db:"towns" json:"towns"`
|
|
}
|
|
|
|
// New creates a new User with sensible defaults
|
|
func New() *User {
|
|
now := time.Now().Unix()
|
|
return &User{
|
|
Verified: 0, // Default unverified
|
|
Token: "", // Empty verification token
|
|
Registered: now, // Current time
|
|
LastOnline: now, // Current time
|
|
Auth: 0, // Default no special permissions
|
|
X: 0, // Default starting position
|
|
Y: 0, // Default starting position
|
|
ClassID: 1, // Default to class 1
|
|
Currently: "In Town", // Default status
|
|
Fighting: 0, // Default not fighting
|
|
HP: 15, // Default starting HP
|
|
MP: 0, // Default starting MP
|
|
TP: 10, // Default starting TP
|
|
MaxHP: 15, // Default starting max HP
|
|
MaxMP: 0, // Default starting max MP
|
|
MaxTP: 10, // Default starting max TP
|
|
Level: 1, // Default starting level
|
|
Gold: 100, // Default starting gold
|
|
Exp: 0, // Default starting exp
|
|
Strength: 5, // Default starting strength
|
|
Dexterity: 5, // Default starting dexterity
|
|
Attack: 5, // Default starting attack
|
|
Defense: 5, // Default starting defense
|
|
Spells: "", // No spells initially
|
|
Towns: "", // No towns visited initially
|
|
}
|
|
}
|
|
|
|
var userScanner = scanner.New[User]()
|
|
|
|
// userColumns returns the column list for user queries
|
|
func userColumns() string {
|
|
return userScanner.Columns()
|
|
}
|
|
|
|
// scanUser populates a User struct using the fast scanner
|
|
func scanUser(stmt *sqlite.Stmt) *User {
|
|
user := &User{}
|
|
userScanner.Scan(stmt, user)
|
|
return user
|
|
}
|
|
|
|
// Find retrieves a user by ID
|
|
func Find(id int) (*User, error) {
|
|
var user *User
|
|
|
|
query := `SELECT ` + userColumns() + ` FROM users WHERE id = ?`
|
|
|
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
|
user = scanUser(stmt)
|
|
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() ([]*User, error) {
|
|
var users []*User
|
|
|
|
query := `SELECT ` + userColumns() + ` FROM users ORDER BY registered DESC, id DESC`
|
|
|
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
|
user := scanUser(stmt)
|
|
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(username string) (*User, error) {
|
|
var user *User
|
|
|
|
query := `SELECT ` + userColumns() + ` FROM users WHERE LOWER(username) = LOWER(?) LIMIT 1`
|
|
|
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
|
user = scanUser(stmt)
|
|
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(email string) (*User, error) {
|
|
var user *User
|
|
|
|
query := `SELECT ` + userColumns() + ` FROM users WHERE email = ? LIMIT 1`
|
|
|
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
|
user = scanUser(stmt)
|
|
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(level int) ([]*User, error) {
|
|
var users []*User
|
|
|
|
query := `SELECT ` + userColumns() + ` FROM users WHERE level = ? ORDER BY exp DESC, id ASC`
|
|
|
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
|
user := scanUser(stmt)
|
|
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(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 := database.Query(query, func(stmt *sqlite.Stmt) error {
|
|
user := scanUser(stmt)
|
|
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 database.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)
|
|
}
|
|
|
|
// Insert saves a new user to the database and sets the ID
|
|
func (u *User) Insert() error {
|
|
if u.ID != 0 {
|
|
return fmt.Errorf("user already has ID %d, use Save() to update", u.ID)
|
|
}
|
|
|
|
// Use a transaction to ensure we can get the ID
|
|
err := database.Transaction(func(tx *database.Tx) error {
|
|
query := `INSERT INTO users (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)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
|
|
if err := tx.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); err != nil {
|
|
return fmt.Errorf("failed to insert user: %w", err)
|
|
}
|
|
|
|
// Get the last insert ID
|
|
var id int
|
|
err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error {
|
|
id = stmt.ColumnInt(0)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get insert ID: %w", err)
|
|
}
|
|
|
|
u.ID = id
|
|
return nil
|
|
})
|
|
|
|
return err
|
|
}
|
|
|
|
// Delete removes the user from the database
|
|
func (u *User) Delete() error {
|
|
if u.ID == 0 {
|
|
return fmt.Errorf("cannot delete user without ID")
|
|
}
|
|
|
|
return database.Exec("DELETE FROM users WHERE id = ?", 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(username string) (*User, error) {
|
|
var user *User
|
|
|
|
query := `SELECT ` + userColumns() + ` FROM users WHERE LOWER(username) = LOWER(?) LIMIT 1`
|
|
|
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
|
user = scanUser(stmt)
|
|
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(email string) (*User, error) {
|
|
var user *User
|
|
|
|
query := `SELECT ` + userColumns() + ` FROM users WHERE LOWER(email) = LOWER(?) LIMIT 1`
|
|
|
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
|
user = scanUser(stmt)
|
|
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
|
|
}
|
|
|
|
// ToMap converts the user to a map for efficient template rendering
|
|
func (u *User) ToMap() map[string]any {
|
|
return map[string]any{
|
|
"ID": u.ID,
|
|
"Username": u.Username,
|
|
"Email": u.Email,
|
|
"Verified": u.Verified,
|
|
"Token": u.Token,
|
|
"Registered": u.Registered,
|
|
"LastOnline": u.LastOnline,
|
|
"Auth": u.Auth,
|
|
"X": u.X,
|
|
"Y": u.Y,
|
|
"ClassID": u.ClassID,
|
|
"Currently": u.Currently,
|
|
"Fighting": u.Fighting,
|
|
"MonsterID": u.MonsterID,
|
|
"MonsterHP": u.MonsterHP,
|
|
"MonsterSleep": u.MonsterSleep,
|
|
"MonsterImmune": u.MonsterImmune,
|
|
"UberDamage": u.UberDamage,
|
|
"UberDefense": u.UberDefense,
|
|
"HP": u.HP,
|
|
"MP": u.MP,
|
|
"TP": u.TP,
|
|
"MaxHP": u.MaxHP,
|
|
"MaxMP": u.MaxMP,
|
|
"MaxTP": u.MaxTP,
|
|
"Level": u.Level,
|
|
"Gold": u.Gold,
|
|
"Exp": u.Exp,
|
|
"GoldBonus": u.GoldBonus,
|
|
"ExpBonus": u.ExpBonus,
|
|
"Strength": u.Strength,
|
|
"Dexterity": u.Dexterity,
|
|
"Attack": u.Attack,
|
|
"Defense": u.Defense,
|
|
"WeaponID": u.WeaponID,
|
|
"ArmorID": u.ArmorID,
|
|
"ShieldID": u.ShieldID,
|
|
"Slot1ID": u.Slot1ID,
|
|
"Slot2ID": u.Slot2ID,
|
|
"Slot3ID": u.Slot3ID,
|
|
"WeaponName": u.WeaponName,
|
|
"ArmorName": u.ArmorName,
|
|
"ShieldName": u.ShieldName,
|
|
"Slot1Name": u.Slot1Name,
|
|
"Slot2Name": u.Slot2Name,
|
|
"Slot3Name": u.Slot3Name,
|
|
"DropCode": u.DropCode,
|
|
"Spells": u.Spells,
|
|
"Towns": u.Towns,
|
|
|
|
// Computed values
|
|
"IsVerified": u.IsVerified(),
|
|
"IsAdmin": u.IsAdmin(),
|
|
"IsModerator": u.IsModerator(),
|
|
"IsFighting": u.IsFighting(),
|
|
"IsAlive": u.IsAlive(),
|
|
"RegisteredTime": u.RegisteredTime(),
|
|
"LastOnlineTime": u.LastOnlineTime(),
|
|
"Equipment": u.GetEquipment(),
|
|
"Stats": u.GetStats(),
|
|
"Position": map[string]int{"X": u.X, "Y": u.Y},
|
|
"SpellIDs": u.GetSpellIDs(),
|
|
"TownIDs": u.GetTownIDs(),
|
|
}
|
|
}
|