package main import ( "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) const chaturbateBioContextURLFmt = "https://chaturbate.com/api/biocontext/%s/" // wie lange wir Biocontext ohne Refresh aus der DB zurückgeben const bioCacheMaxAge = 10 * time.Minute type BioContextResp struct { Enabled bool `json:"enabled"` FetchedAt time.Time `json:"fetchedAt"` LastError string `json:"lastError,omitempty"` Model string `json:"model"` Bio any `json:"bio,omitempty"` } func sanitizeModelKey(s string) string { s = strings.TrimSpace(strings.TrimPrefix(s, "@")) // erlaubte chars: a-z A-Z 0-9 _ - . s = strings.Map(func(r rune) rune { switch { case r >= 'a' && r <= 'z': return r case r >= 'A' && r <= 'Z': return r case r >= '0' && r <= '9': return r case r == '_' || r == '-' || r == '.': return r default: return -1 } }, s) return strings.TrimSpace(s) } func chaturbateBioContextHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Nur GET erlaubt", http.StatusMethodNotAllowed) return } // ✅ Option A: Biocontext ist IMMER aktiv (unabhängig von UseChaturbateAPI) enabled := true model := sanitizeModelKey(r.URL.Query().Get("model")) if model == "" { http.Error(w, "model fehlt", http.StatusBadRequest) return } refresh := strings.TrimSpace(r.URL.Query().Get("refresh")) forceRefresh := refresh == "1" || strings.EqualFold(refresh, "true") // 1) Cache aus ModelStore lesen (persistiert über Neustarts) var ( cachedBio any cachedAt time.Time hasCache bool ) if cbModelStore != nil { raw, ts, ok, err := cbModelStore.GetBioContext("chaturbate.com", model) if err == nil && ok { var tmp any if e := json.Unmarshal([]byte(raw), &tmp); e == nil { cachedBio = tmp hasCache = true if t, e2 := time.Parse(time.RFC3339Nano, strings.TrimSpace(ts)); e2 == nil { cachedAt = t } } } } // 2) Wenn Cache frisch ist und kein Force-Refresh, geben wir ihn sofort zurück if hasCache && !forceRefresh && !cachedAt.IsZero() && time.Since(cachedAt) < bioCacheMaxAge { w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") _ = json.NewEncoder(w).Encode(BioContextResp{ Enabled: enabled, FetchedAt: cachedAt, LastError: "", Model: model, Bio: cachedBio, }) return } // 3) Upstream abrufen ctx, cancel := context.WithTimeout(r.Context(), 12*time.Second) defer cancel() u := fmt.Sprintf(chaturbateBioContextURLFmt, model) req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) if err != nil { http.Error(w, "request build failed: "+err.Error(), http.StatusInternalServerError) return } req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)") req.Header.Set("Accept", "application/json, text/plain, */*") req.Header.Set("Referer", "https://chaturbate.com/"+model+"/") // ✅ optional: Cookies vom Frontend (für Cloudflare / session) if ck := strings.TrimSpace(r.Header.Get("X-Chaturbate-Cookie")); ck != "" { req.Header.Set("Cookie", ck) } resp, err := cbHTTP.Do(req) // cbHTTP existiert schon in chaturbate_online.go if err != nil { // Fallback: Cache liefern (wenn vorhanden) if hasCache { w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") _ = json.NewEncoder(w).Encode(BioContextResp{ Enabled: enabled, FetchedAt: cachedAt, LastError: err.Error(), Model: model, Bio: cachedBio, }) return } w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") _ = json.NewEncoder(w).Encode(BioContextResp{ Enabled: enabled, FetchedAt: time.Now().UTC(), LastError: err.Error(), Model: model, }) return } defer resp.Body.Close() // max 1MB raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) // Helper: Body-Snippet für Debug snip := strings.TrimSpace(string(raw)) if len(snip) > 400 { snip = snip[:400] + "…" } if resp.StatusCode != 200 { errMsg := fmt.Sprintf("HTTP %d (ct=%s): %s", resp.StatusCode, resp.Header.Get("Content-Type"), snip) if hasCache { w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") _ = json.NewEncoder(w).Encode(BioContextResp{ Enabled: enabled, FetchedAt: cachedAt, LastError: errMsg, Model: model, Bio: cachedBio, }) return } w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") _ = json.NewEncoder(w).Encode(BioContextResp{ Enabled: enabled, FetchedAt: time.Now().UTC(), LastError: errMsg, Model: model, }) return } // 4) JSON parse var bio any if err := json.Unmarshal(raw, &bio); err != nil { ct := strings.ToLower(resp.Header.Get("Content-Type")) lowerBody := strings.ToLower(snip) // sehr häufig: HTML statt JSON (Cloudflare/Age-Gate/etc.) htmlLike := strings.Contains(ct, "text/html") || strings.HasPrefix(strings.TrimSpace(lowerBody), "