nsfwapp/backend/chaturbate_biocontext.go
2026-04-04 13:27:52 +02:00

340 lines
9.0 KiB
Go

// backend\chaturbate_biocontext.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"`
// neu
RoomStatus string `json:"roomStatus,omitempty"` // public/private/hidden/away/offline/unknown
IsOnline bool `json:"isOnline,omitempty"`
ChatRoomURL string `json:"chatRoomUrl,omitempty"`
ImageURL string `json:"imageUrl,omitempty"`
}
func normalizeCBShow(v string) string {
s := strings.ToLower(strings.TrimSpace(v))
switch s {
case "public", "private", "hidden", "away", "offline":
return s
case "online", "live":
return "public"
default:
return "unknown"
}
}
func asMap(v any) map[string]any {
m, _ := v.(map[string]any)
return m
}
func firstString(m map[string]any, keys ...string) string {
for _, k := range keys {
if v, ok := m[k]; ok {
if s, ok := v.(string); ok && strings.TrimSpace(s) != "" {
return strings.TrimSpace(s)
}
}
}
return ""
}
func inferRoomStatusFromBio(bio any) (show string, chatRoomURL string, imageURL string, ok bool) {
root := asMap(bio)
if root == nil {
return "unknown", "", "", false
}
// häufige Kandidaten
candidates := []map[string]any{
root,
asMap(root["room_status"]),
asMap(root["room"]),
asMap(root["status"]),
asMap(root["user"]),
asMap(root["broadcaster"]),
}
for _, m := range candidates {
if m == nil {
continue
}
raw := firstString(
m,
"current_show",
"room_status",
"status",
"show",
"chat_status",
)
if raw != "" {
show = normalizeCBShow(raw)
chatRoomURL = firstString(m, "chat_room_url", "room_url", "url")
imageURL = firstString(m, "image_url", "image_url_360x270", "avatar_url", "profile_image_url")
return show, chatRoomURL, imageURL, true
}
}
// Fallback: rekursiv ein paar bekannte Unterobjekte durchsuchen
for _, key := range []string{"room", "user", "broadcaster", "data"} {
if m := asMap(root[key]); m != nil {
raw := firstString(m, "current_show", "room_status", "status", "show", "chat_status")
if raw != "" {
show = normalizeCBShow(raw)
chatRoomURL = firstString(m, "chat_room_url", "room_url", "url")
imageURL = firstString(m, "image_url", "image_url_360x270", "avatar_url", "profile_image_url")
return show, chatRoomURL, imageURL, true
}
}
}
return "unknown", "", "", false
}
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 {
roomStatus, chatRoomURL, imageURL, hasStatus := inferRoomStatusFromBio(cachedBio)
isOnline := hasStatus && roomStatus != "offline" && roomStatus != "unknown"
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,
RoomStatus: roomStatus,
IsOnline: isOnline,
ChatRoomURL: chatRoomURL,
ImageURL: imageURL,
})
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()
roomStatus, chatRoomURL, imageURL, hasStatus := inferRoomStatusFromBio(bio)
isOnline := hasStatus && roomStatus != "offline" && roomStatus != "unknown"
// 5) Persistieren
if cbModelStore != nil {
_ = cbModelStore.SetBioContext("chaturbate.com", model, string(raw), fetchedAt.Format(time.RFC3339Nano))
if hasStatus {
_ = cbModelStore.SetChaturbateRoomState(
"chaturbate.com",
model,
roomStatus,
isOnline,
chatRoomURL,
imageURL,
fetchedAt,
)
}
}
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,
RoomStatus: roomStatus,
IsOnline: isOnline,
ChatRoomURL: chatRoomURL,
ImageURL: imageURL,
})
}