package main
import (
"encoding/json"
"html"
"log"
"math/rand"
"net/http"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
)
func handleConfig(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(defaultConfig)
}
func handleStart(w http.ResponseWriter, r *http.Request) {
sessionID := uuid.New().String()
rawSeed := time.Now().UnixNano()
seed32 := uint32(rawSeed)
emptyObs, _ := json.Marshal([]ActiveObstacle{})
err := rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{
"seed": seed32,
"rng_state": seed32,
"score": 0,
"is_dead": 0,
"pos_y": PlayerYBase,
"vel_y": 0.0,
"obstacles": string(emptyObs),
}).Err()
if err != nil {
http.Error(w, "DB Error", 500)
return
}
rdb.Expire(ctx, "session:"+sessionID, 4000*time.Hour)
json.NewEncoder(w).Encode(StartResponse{SessionID: sessionID, Seed: seed32})
}
func handleValidate(w http.ResponseWriter, r *http.Request) {
var req ValidateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Bad Request", 400)
return
}
key := "session:" + req.SessionID
vals, err := rdb.HGetAll(ctx, key).Result()
if err != nil || len(vals) == 0 {
http.Error(w, "Session invalid", 401)
return
}
if vals["is_dead"] == "1" {
json.NewEncoder(w).Encode(ValidateResponse{Status: "dead", VerifiedScore: 0})
return
}
// ---> HIER RUFEN WIR JETZT DIE SIMULATION AUF <---
isDead, score, obstacles, powerUpState, serverTick, nextSpawnTick := simulateChunk(req.SessionID, req.Inputs, req.TotalTicks, vals)
status := "alive"
if isDead {
status = "dead"
rdb.HSet(ctx, key, "is_dead", 1)
}
rdb.Expire(ctx, key, 1*time.Hour)
json.NewEncoder(w).Encode(ValidateResponse{
Status: status,
VerifiedScore: score,
ServerObs: obstacles,
PowerUps: powerUpState,
ServerTick: serverTick,
NextSpawnTick: nextSpawnTick,
})
}
func handleSubmitName(w http.ResponseWriter, r *http.Request) {
var req SubmitNameRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Bad Request", 400)
return
}
// Validierung
if len(req.Name) > 4 {
http.Error(w, "Zu lang", 400)
return
}
if containsBadWord(req.Name) {
http.Error(w, "Name verboten", 400)
return
}
safeName := html.EscapeString(req.Name)
sessionKey := "session:" + req.SessionID
scoreVal, err := rdb.HGet(ctx, sessionKey, "score").Result()
if err != nil {
http.Error(w, "Session expired", 404)
return
}
scoreInt, _ := strconv.Atoi(scoreVal)
claimCode := generateClaimCode()
timestamp := time.Now().Format("02.01.2006 15:04")
rdb.HSet(ctx, sessionKey, map[string]interface{}{
"name": safeName,
"claim_code": claimCode,
"created_at": timestamp,
})
// Leaderboard Eintrag
rdb.ZAdd(ctx, "leaderboard:unverified", redis.Z{Score: float64(scoreInt), Member: req.SessionID})
rdb.ZAdd(ctx, "leaderboard:public", redis.Z{Score: float64(scoreInt), Member: req.SessionID})
rdb.Persist(ctx, sessionKey)
rdb.HDel(ctx, sessionKey, "obstacles", "rng_state", "pos_y", "vel_y", "p_god_lives", "p_has_bat", "p_boot_ticks")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(SubmitResponse{ClaimCode: claimCode})
}
func handleLeaderboard(w http.ResponseWriter, r *http.Request) {
mySessionID := r.URL.Query().Get("sessionId")
targetKey := "leaderboard:public"
var entries []LeaderboardEntry
top3, _ := rdb.ZRevRangeWithScores(ctx, targetKey, 0, 2).Result()
for i, z := range top3 {
rank := int64(i + 1)
sid := z.Member.(string)
name, _ := rdb.HGet(ctx, "session:"+sid, "name").Result()
if name == "" {
name = "Unbekannt"
}
entries = append(entries, LeaderboardEntry{
Rank: rank, Name: name, Score: int(z.Score), IsMe: (sid == mySessionID),
})
}
if mySessionID != "" {
myRank, err := rdb.ZRevRank(ctx, targetKey, mySessionID).Result()
if err == nil && myRank > 2 {
start := myRank - 1
stop := myRank + 1
neighbors, _ := rdb.ZRevRangeWithScores(ctx, targetKey, start, stop).Result()
for i, z := range neighbors {
rank := start + int64(i) + 1
sid := z.Member.(string)
name, _ := rdb.HGet(ctx, "session:"+sid, "name").Result()
if name == "" {
name = "Unbekannt"
}
entries = append(entries, LeaderboardEntry{
Rank: rank, Name: name, Score: int(z.Score), IsMe: (sid == mySessionID),
})
}
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(entries)
}
func handleAdminPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./secure/admin.html")
}
func handleAdminList(w http.ResponseWriter, r *http.Request) {
listType := r.URL.Query().Get("type")
redisKey := "leaderboard:unverified"
if listType == "public" {
redisKey = "leaderboard:public"
}
vals, _ := rdb.ZRevRangeWithScores(ctx, redisKey, 0, -1).Result()
var adminList []AdminEntry
for _, z := range vals {
sid := z.Member.(string)
info, _ := rdb.HGetAll(ctx, "session:"+sid).Result()
name := info["name"]
if name == "" {
name = "Unbekannt"
}
adminList = append(adminList, AdminEntry{
SessionID: sid, Name: name, Score: int(z.Score),
Code: info["claim_code"], Time: info["created_at"],
})
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(adminList)
}
func handleAdminAction(w http.ResponseWriter, r *http.Request) {
var req AdminActionRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Bad Request", 400)
return
}
log.Printf("👮 ADMIN ACTION: %s on %s", req.Action, req.SessionID)
if req.Action == "approve" {
score, err := rdb.ZScore(ctx, "leaderboard:unverified", req.SessionID).Result()
if err == nil {
rdb.ZAdd(ctx, "leaderboard:public", redis.Z{Score: score, Member: req.SessionID})
rdb.ZRem(ctx, "leaderboard:unverified", req.SessionID)
}
} else if req.Action == "delete" {
rdb.ZRem(ctx, "leaderboard:unverified", req.SessionID)
rdb.ZRem(ctx, "leaderboard:public", req.SessionID)
rdb.Del(ctx, "session:"+req.SessionID)
}
w.WriteHeader(http.StatusOK)
}
func handleClaimDelete(w http.ResponseWriter, r *http.Request) {
var req ClaimDeleteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Bad Request", 400)
return
}
sessionKey := "session:" + req.SessionID
realCode, err := rdb.HGet(ctx, sessionKey, "claim_code").Result()
if err != nil || realCode == "" {
http.Error(w, "Not found", 404)
return
}
if realCode != req.ClaimCode {
http.Error(w, "Wrong Code", 403)
return
}
log.Printf("🗑️ USER DELETE: Session %s deleted via code", req.SessionID)
// Aus Listen entfernen
rdb.ZRem(ctx, "leaderboard:unverified", req.SessionID)
rdb.ZRem(ctx, "leaderboard:public", req.SessionID)
rdb.Del(ctx, sessionKey)
w.WriteHeader(http.StatusOK)
}
func generateClaimCode() string {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, 8)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}
func handleAdminBadwords(w http.ResponseWriter, r *http.Request) {
key := "config:badwords"
// GET: Liste abrufen
if r.Method == http.MethodGet {
words, _ := rdb.SMembers(ctx, key).Result()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(words)
return
}
// POST: Hinzufügen oder Löschen
if r.Method == http.MethodPost {
// Wir nutzen ein einfaches Struct für den Request
type WordReq struct {
Word string `json:"word"`
Action string `json:"action"` // "add" oder "remove"
}
var req WordReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Bad Request", 400)
return
}
if req.Action == "add" && req.Word != "" {
rdb.SAdd(ctx, key, strings.ToLower(req.Word))
} else if req.Action == "remove" && req.Word != "" {
rdb.SRem(ctx, key, strings.ToLower(req.Word))
}
w.WriteHeader(http.StatusOK)
}
}