338 lines
7.3 KiB
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(<).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(<).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")
|
|
}
|