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 := 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, }) } 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) } }