nsfwapp/backend/chaturbate_biocontext.go
2026-01-13 14:00:05 +01:00

244 lines
6.5 KiB
Go

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), "<!doctype") ||
strings.Contains(lowerBody, "<html") ||
strings.Contains(lowerBody, "just a moment") ||
strings.Contains(lowerBody, "cloudflare")
errMsg := ""
if htmlLike {
errMsg = fmt.Sprintf("upstream returned HTML (likely blocked/age-gate) (ct=%s): %s", resp.Header.Get("Content-Type"), snip)
} else {
errMsg = fmt.Sprintf("invalid json from upstream (ct=%s): %v; body: %s", resp.Header.Get("Content-Type"), err, snip)
}
// 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: 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
}
fetchedAt := time.Now().UTC()
// 5) Persistieren
if cbModelStore != nil {
_ = cbModelStore.SetBioContext("chaturbate.com", model, string(raw), fetchedAt.Format(time.RFC3339Nano))
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(BioContextResp{
Enabled: enabled,
FetchedAt: fetchedAt,
LastError: "",
Model: model,
Bio: bio,
})
}