Files
qrank/src/routes.go

338 lines
7.3 KiB
Go

package main
import (
"errors"
"fmt"
"log"
"math"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func getEnter(c *gin.Context) {
tm.Render(c, "enter", gin.H{})
}
func postEnter(c *gin.Context) {
current := getSessionUser(c)
if current == nil {
c.Redirect(http.StatusFound, "/login")
return
}
// Parse form
a1h := strings.TrimSpace(c.PostForm("a1"))
a2h := strings.TrimSpace(c.PostForm("a2"))
b1h := strings.TrimSpace(c.PostForm("b1"))
b2h := strings.TrimSpace(c.PostForm("b2"))
scoreA := atoiSafe(c.PostForm("scoreA"))
scoreB := atoiSafe(c.PostForm("scoreB"))
// Require a winner
if scoreA == scoreB {
SaveForm(c)
SetMessage(c, "There must be a winner")
c.Redirect(http.StatusSeeOther, "/enter")
return
}
// Resolve players
resolveUser := func(handle string) (*User, error) {
if handle == "" {
return nil, nil
}
u, err := findUserByHandle(handle)
if err != nil {
SaveForm(c)
SetMessage(c, fmt.Sprintf(`Player "%v" does not exist`, handle))
c.Redirect(http.StatusSeeOther, "/enter")
return nil, err
}
return u, nil
}
var a1, a2, b1, b2 *User
// Always ensure A1 exists, fallback to current if empty
var err error
if a1, err = resolveUser(a1h); err != nil {
return
}
if a2, err = resolveUser(a2h); err != nil {
return
}
if b1, err = resolveUser(b1h); err != nil {
return
}
if b2, err = resolveUser(b2h); err != nil {
return
}
// Check for at least one player on each side
if b1 == nil && b2 == nil || a1 == nil && a2 == nil {
SaveForm(c)
SetMessage(c, "There must be at least one player on each side")
c.Redirect(http.StatusSeeOther, "/enter")
return
}
// Check for duplicate users
seen := map[uint]bool{}
users := []*User{a1, a2, b1, b2}
for i, u := range users {
if u != nil {
if seen[u.ID] {
SaveForm(c)
SetMessage(c, fmt.Sprintf(`Player "%v" cannot play twice in one game`, users[i].Username))
c.Redirect(http.StatusSeeOther, "/enter")
return
}
seen[u.ID] = true
}
}
// Create game
g := Game{
ScoreA: scoreA,
ScoreB: scoreB,
}
if err := db.Create(&g).Error; err != nil {
c.String(http.StatusInternalServerError, "game create error")
return
}
var players []Player
var playerbuf []float64
for i, user := range users {
if user != nil {
var side string
if i > 1 {
side = "B"
} else {
side = "A"
}
players = append(players, Player{u: user, side: side, dElo: 0})
playerbuf = append(playerbuf, user.Elo)
}
}
team1 := NewTeam(players[:len(players)/2], g.ScoreA)
team2 := NewTeam(players[len(players)/2:], g.ScoreB)
// Set new elo for all players
GetNewElo([]*Team{team1, team2})
var winningSide string
if g.ScoreA > g.ScoreB {
winningSide = "A"
} else {
winningSide = "B"
}
for i, p := range players {
db.Save(p.u)
p.dElo = players[i].u.Elo - playerbuf[i]
var updateString string
if p.side == winningSide {
updateString = "win_count"
} else {
updateString = "loss_count"
}
var expr = gorm.Expr(updateString+" + ?", 1)
// Update win or loss
err = db.Model(&User{}).
Where("id = ?", p.u.ID).
UpdateColumn(updateString, expr).UpdateColumn("game_count", gorm.Expr("game_count + ?", 1)).Error
if err != nil {
fmt.Println(err)
}
if err := db.Create(&GameUser{GameID: g.ID, UserID: p.u.ID, Side: p.side, DeltaElo: p.dElo}).Error; err != nil {
c.String(http.StatusInternalServerError, "failed to assign player")
return
}
}
c.Redirect(http.StatusFound, "/history")
}
func getIndex(c *gin.Context) {
if u := getSessionUser(c); u != nil {
c.Redirect(302, "/enter")
return
}
getLogin(c)
}
func roundFloat(val float64, precision uint) float64 {
ratio := math.Pow(10, float64(precision))
return math.Round(val*ratio) / ratio
}
func getHistory(c *gin.Context) {
// Load recent games
var games []Game
db.Order("created_at desc").Find(&games)
type UserElo struct {
User
DeltaElo float64
}
type GRow struct {
Game
PlayersA []UserElo
PlayersB []UserElo
WinnerIsA bool
}
var rows []GRow
for _, g := range games {
var gps []GameUser
db.Model(&GameUser{}).Preload("User").Where("game_id = ?", g.ID).Find(&gps)
var a, b []UserElo
for _, gp := range gps {
if gp.Side == "A" {
a = append(a, UserElo{gp.User, roundFloat(gp.DeltaElo, 2)})
} else {
b = append(b, UserElo{gp.User, roundFloat(gp.DeltaElo, 2)})
}
}
rows = append(rows, GRow{Game: g, PlayersA: a, PlayersB: b, WinnerIsA: g.ScoreA > g.ScoreB})
log.Printf("%+v", rows[0].PlayersA[0].Username)
}
tm.Render(c, "history", gin.H{"Games": rows})
}
func getLeaderboard(c *gin.Context) {
var users []User
if err := db.Order("elo DESC").Find(&users).Error; err != nil {
c.String(http.StatusInternalServerError, "Error: %v", err)
return
}
tm.Render(c, "leaderboard", gin.H{
"Users": users,
})
}
func getUserView(c *gin.Context) {
slug := c.Param("userSlug")
u, err := userBySlug(slug)
if err != nil {
c.String(404, "user not found")
return
}
// Check if own user
var own bool
if slug == u.Slug {
own = true
}
tm.Render(c, "user", gin.H{"User": u, "Own": own})
}
func getMe(c *gin.Context) {
u := getSessionUser(c)
tm.Render(c, "user", gin.H{"User": u, "Own": true})
}
func postMe(c *gin.Context) {
cu := getSessionUser(c)
if cu == nil {
c.Redirect(302, "/login")
return
}
newU := strings.TrimSpace(c.PostForm("username"))
if newU == "" || newU == cu.Username {
c.Redirect(302, "/me")
return
}
// Update username and slug
cu.Username = newU
cu.Slug = slugify(newU)
if err := ensureUniqueUsernameAndSlug(cu); err != nil {
log.Println("unique username error:", err)
}
if err := db.Save(cu).Error; err != nil {
log.Println("save user:", err)
}
c.Redirect(302, "/me")
}
func getLogin(c *gin.Context) {
tm.Render(c, "login", gin.H{})
}
func postLogin(c *gin.Context) {
email := strings.ToLower(strings.TrimSpace(c.PostForm("email")))
if email == "" {
c.Redirect(302, "/login")
return
}
// ensure user exists
var u User
tx := db.Where("email = ?", email).First(&u)
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
u = User{Email: email, Username: defaultUsername()}
if err := ensureUniqueUsernameAndSlug(&u); err != nil {
log.Println(err)
}
if err := db.Create(&u).Error; err != nil {
log.Println("create user:", err)
}
}
// create token valid 30 days
token := mustRandToken(24)
lt := LoginToken{Token: token, Email: email, ExpiresAt: now().Add(30 * 24 * time.Hour)}
if err := db.Create(&lt).Error; err != nil {
log.Println("create token:", err)
}
link := strings.TrimRight(baseURL, "/") + "/magic?token=" + token
log.Printf("[MAGIC LINK] %s for %s (valid until %s)\n", link, email, lt.ExpiresAt.Format(time.RFC3339))
tm.Render(c, "sent", gin.H{"Email": email})
}
func getMagic(c *gin.Context) {
token := c.Query("token")
if token == "" {
c.Redirect(302, "/login")
return
}
var lt LoginToken
if err := db.Where("token = ? AND expires_at > ?", token, now()).First(&lt).Error; err != nil {
c.String(400, "Invalid or expired token")
return
}
// find or create user by email again (in case)
u, err := userByEmail(lt.Email)
if err != nil {
c.String(500, "user lookup error")
return
}
// create session
sessTok := mustRandToken(24)
if err := db.Create(&Session{Token: sessTok, UserID: u.ID}).Error; err != nil {
c.String(500, "session error")
return
}
setSessionCookie(c, sessTok)
c.Redirect(302, "/enter")
}