340 lines
9.0 KiB
Go
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,
|
|
})
|
|
}
|