244 lines
6.5 KiB
Go
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,
|
|
})
|
|
}
|