676 lines
18 KiB
Go
676 lines
18 KiB
Go
package admin
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/nyoungo/dsfree2api/internal/config"
|
|
"github.com/nyoungo/dsfree2api/internal/httpx"
|
|
"github.com/nyoungo/dsfree2api/internal/openai"
|
|
"github.com/nyoungo/dsfree2api/internal/upstream"
|
|
)
|
|
|
|
func (s *Server) handleActions(w http.ResponseWriter, r *http.Request) {
|
|
body, err := readBody(r, 1<<20)
|
|
if err != nil {
|
|
fail(w, err.Error())
|
|
return
|
|
}
|
|
var req struct {
|
|
Action string `json:"action"`
|
|
}
|
|
if err := json.Unmarshal(body, &req); err != nil || req.Action == "" {
|
|
fail(w, "action is required")
|
|
return
|
|
}
|
|
|
|
switch req.Action {
|
|
case "create_key":
|
|
key := "sk-" + randomToken(24)
|
|
s.cfg.Lock()
|
|
s.cfg.Security.APIKeys = append(s.cfg.Security.APIKeys, key)
|
|
s.cfg.Unlock()
|
|
s.save(w)
|
|
case "delete_key":
|
|
var p struct {
|
|
Key string `json:"key"`
|
|
}
|
|
_ = json.Unmarshal(body, &p)
|
|
s.cfg.Lock()
|
|
out := s.cfg.Security.APIKeys[:0]
|
|
for _, k := range s.cfg.Security.APIKeys {
|
|
if k != p.Key {
|
|
out = append(out, k)
|
|
}
|
|
}
|
|
s.cfg.Security.APIKeys = out
|
|
s.cfg.Unlock()
|
|
s.save(w)
|
|
|
|
case "toggle_model", "toggle_site":
|
|
var p struct {
|
|
ID string `json:"id"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
_ = json.Unmarshal(body, &p)
|
|
s.cfg.Lock()
|
|
var e error
|
|
if req.Action == "toggle_model" {
|
|
if m, ok := s.cfg.Models[p.ID]; ok {
|
|
m.Enabled = p.Enabled
|
|
} else {
|
|
e = fmt.Errorf("model %q not found", p.ID)
|
|
}
|
|
} else {
|
|
if st, ok := s.cfg.Sites[p.ID]; ok {
|
|
st.Enabled = p.Enabled
|
|
} else {
|
|
e = fmt.Errorf("site %q not found", p.ID)
|
|
}
|
|
}
|
|
s.cfg.Unlock()
|
|
if e != nil {
|
|
fail(w, e.Error())
|
|
return
|
|
}
|
|
s.save(w)
|
|
|
|
case "update_model":
|
|
var p struct {
|
|
ID string `json:"id"`
|
|
Label string `json:"label"`
|
|
PagePath string `json:"page_path"`
|
|
BotID int `json:"bot_id"`
|
|
PostID int `json:"post_id"`
|
|
}
|
|
_ = json.Unmarshal(body, &p)
|
|
s.cfg.Lock()
|
|
m, ok := s.cfg.Models[p.ID]
|
|
if ok {
|
|
if p.Label != "" {
|
|
m.Label = p.Label
|
|
}
|
|
if p.PagePath != "" {
|
|
m.PagePath = p.PagePath
|
|
}
|
|
m.BotID, m.PostID = p.BotID, p.PostID
|
|
}
|
|
s.cfg.Unlock()
|
|
if !ok {
|
|
fail(w, "model not found")
|
|
return
|
|
}
|
|
s.up.InvalidateAll()
|
|
s.save(w)
|
|
|
|
case "update_site":
|
|
var p struct {
|
|
Code string `json:"code"`
|
|
BaseURL string `json:"base_url"`
|
|
AJAXURL string `json:"ajax_url"`
|
|
Language string `json:"language"`
|
|
SiteKey string `json:"sitekey"`
|
|
}
|
|
_ = json.Unmarshal(body, &p)
|
|
s.cfg.Lock()
|
|
st, ok := s.cfg.Sites[p.Code]
|
|
if ok {
|
|
if p.BaseURL != "" {
|
|
st.BaseURL = strings.TrimRight(p.BaseURL, "/")
|
|
if p.AJAXURL == "" {
|
|
st.AJAXURL = st.BaseURL + "/wp-admin/admin-ajax.php"
|
|
}
|
|
}
|
|
if p.AJAXURL != "" {
|
|
st.AJAXURL = p.AJAXURL
|
|
}
|
|
if p.Language != "" {
|
|
st.Language = p.Language
|
|
}
|
|
if p.SiteKey != "" {
|
|
st.SiteKey = p.SiteKey
|
|
}
|
|
}
|
|
s.cfg.Unlock()
|
|
if !ok {
|
|
fail(w, "site not found")
|
|
return
|
|
}
|
|
s.up.InvalidateAll()
|
|
s.save(w)
|
|
|
|
case "set_primary":
|
|
var p struct {
|
|
URL string `json:"url"`
|
|
}
|
|
_ = json.Unmarshal(body, &p)
|
|
s.cfg.Lock()
|
|
s.cfg.Proxy.URL = strings.TrimSpace(p.URL)
|
|
s.cfg.Unlock()
|
|
s.save(w)
|
|
|
|
case "add_fallback":
|
|
var p struct {
|
|
URL string `json:"url"`
|
|
}
|
|
_ = json.Unmarshal(body, &p)
|
|
p.URL = strings.TrimSpace(p.URL)
|
|
if p.URL == "" {
|
|
fail(w, "url is required")
|
|
return
|
|
}
|
|
s.cfg.Lock()
|
|
for _, u := range s.cfg.Proxy.FallbackURLs {
|
|
if u == p.URL {
|
|
s.cfg.Unlock()
|
|
fail(w, "fallback already exists")
|
|
return
|
|
}
|
|
}
|
|
s.cfg.Proxy.FallbackURLs = append(s.cfg.Proxy.FallbackURLs, p.URL)
|
|
s.cfg.Unlock()
|
|
s.save(w)
|
|
|
|
case "remove_fallback":
|
|
var p struct {
|
|
URL string `json:"url"`
|
|
}
|
|
_ = json.Unmarshal(body, &p)
|
|
s.cfg.Lock()
|
|
out := s.cfg.Proxy.FallbackURLs[:0]
|
|
for _, u := range s.cfg.Proxy.FallbackURLs {
|
|
if u != p.URL {
|
|
out = append(out, u)
|
|
}
|
|
}
|
|
s.cfg.Proxy.FallbackURLs = out
|
|
s.cfg.Unlock()
|
|
s.save(w)
|
|
|
|
case "set_limits":
|
|
var p struct {
|
|
SlowStart float64 `json:"slow_start"`
|
|
MaxConcurrent int `json:"max_concurrent"`
|
|
RatePerMinute float64 `json:"rate_per_minute"`
|
|
CrossSite *bool `json:"cross_site_failover"`
|
|
AutoRefresh *bool `json:"auto_refresh"`
|
|
RefreshRetries *int `json:"refresh_retries"`
|
|
}
|
|
_ = json.Unmarshal(body, &p)
|
|
s.cfg.Lock()
|
|
if p.SlowStart >= 0 {
|
|
s.cfg.Proxy.SlowStartSeconds = p.SlowStart
|
|
}
|
|
s.cfg.Limits.MaxConcurrentPerSite = p.MaxConcurrent
|
|
s.cfg.Limits.RatePerMinute = p.RatePerMinute
|
|
if p.CrossSite != nil {
|
|
s.cfg.Upstream.CrossSiteFailover = *p.CrossSite
|
|
}
|
|
if p.AutoRefresh != nil {
|
|
s.cfg.Upstream.AutoRefresh = *p.AutoRefresh
|
|
}
|
|
if p.RefreshRetries != nil {
|
|
s.cfg.Upstream.RefreshRetries = *p.RefreshRetries
|
|
}
|
|
s.cfg.Unlock()
|
|
s.save(w)
|
|
|
|
case "set_turnstile":
|
|
var p struct {
|
|
Enabled bool `json:"enabled"`
|
|
Provider *string `json:"provider"`
|
|
APIURL string `json:"api_url"`
|
|
APIKey string `json:"api_key"`
|
|
SiteKey string `json:"sitekey"`
|
|
Challenge string `json:"challenge_action"`
|
|
Legacy string `json:"ts_action"`
|
|
CookieTT int `json:"cookie_ttl_seconds"`
|
|
|
|
BrowserPath *string `json:"browser_path"`
|
|
BrowserHeadless *bool `json:"browser_headless"`
|
|
BrowserUserDataDir *string `json:"browser_user_data_dir"`
|
|
BrowserTimezone *string `json:"browser_timezone"`
|
|
BrowserLocale *string `json:"browser_locale"`
|
|
}
|
|
_ = json.Unmarshal(body, &p)
|
|
// Build the candidate config first: a rejected save must not leave
|
|
// half-applied fields behind in memory.
|
|
s.cfg.Lock()
|
|
tc := s.cfg.Turnstile
|
|
if p.Provider != nil {
|
|
tc.Provider = strings.ToLower(strings.TrimSpace(*p.Provider))
|
|
}
|
|
if p.BrowserPath != nil {
|
|
tc.BrowserPath = strings.TrimSpace(*p.BrowserPath)
|
|
}
|
|
if p.BrowserHeadless != nil {
|
|
tc.BrowserHeadless = *p.BrowserHeadless
|
|
}
|
|
if p.BrowserUserDataDir != nil {
|
|
tc.BrowserUserDataDir = strings.TrimSpace(*p.BrowserUserDataDir)
|
|
}
|
|
if p.BrowserTimezone != nil {
|
|
tc.BrowserTimezone = strings.TrimSpace(*p.BrowserTimezone)
|
|
}
|
|
if p.BrowserLocale != nil {
|
|
tc.BrowserLocale = strings.TrimSpace(*p.BrowserLocale)
|
|
}
|
|
if strings.TrimSpace(p.APIURL) != "" {
|
|
tc.APIURL = strings.TrimSpace(p.APIURL)
|
|
}
|
|
if p.APIKey != "" {
|
|
tc.APIKey = p.APIKey
|
|
}
|
|
if p.SiteKey != "" {
|
|
tc.SiteKey = p.SiteKey
|
|
}
|
|
if act := firstNonEmpty(p.Challenge, p.Legacy); act != "" {
|
|
tc.Action = act
|
|
}
|
|
if p.CookieTT > 0 {
|
|
tc.CookieTTLSeconds = p.CookieTT
|
|
}
|
|
tc.Enabled = p.Enabled
|
|
if tc.Enabled {
|
|
// provider-specific requirements mirror config.Validate()
|
|
switch tc.ProviderValue() {
|
|
case config.ProviderAPI:
|
|
if strings.TrimSpace(tc.APIURL) == "" {
|
|
s.cfg.Unlock()
|
|
fail(w, "turnstile.api_url is empty — enter your solver endpoint, or switch provider to browser/manual")
|
|
return
|
|
}
|
|
case config.ProviderBrowser:
|
|
if tc.BrowserPath == "" {
|
|
s.cfg.Unlock()
|
|
fail(w, "turnstile.browser_path is empty — point it at Chrome/Edge, or switch provider")
|
|
return
|
|
}
|
|
case config.ProviderManual:
|
|
default:
|
|
prov := tc.Provider
|
|
s.cfg.Unlock()
|
|
fail(w, fmt.Sprintf("unknown provider %q (api|browser|manual)", prov))
|
|
return
|
|
}
|
|
}
|
|
s.cfg.Turnstile = tc
|
|
s.cfg.Unlock()
|
|
s.ts.SetConfig(tc)
|
|
s.save(w)
|
|
|
|
case "refresh_cookies":
|
|
var p struct {
|
|
Site string `json:"site"`
|
|
}
|
|
_ = json.Unmarshal(body, &p)
|
|
if !s.ts.Enabled() {
|
|
fail(w, "turnstile solver is disabled — enable it first, or import cookies manually")
|
|
return
|
|
}
|
|
if s.ts.Config().ProviderValue() == config.ProviderManual {
|
|
fail(w, `provider is "manual" — re-import cookies below instead of forcing a refresh`)
|
|
return
|
|
}
|
|
for _, code := range s.siteCodes(p.Site) {
|
|
s.ts.Invalidate(code)
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
|
|
case "clear_cookies":
|
|
var p struct {
|
|
Site string `json:"site"`
|
|
}
|
|
_ = json.Unmarshal(body, &p)
|
|
for _, code := range s.siteCodes(p.Site) {
|
|
s.ts.Invalidate(code)
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
|
|
case "import_cookies":
|
|
var p struct {
|
|
Site string `json:"site"`
|
|
Cookies string `json:"cookies"`
|
|
}
|
|
_ = json.Unmarshal(body, &p)
|
|
s.cfg.RLock()
|
|
site, ok := s.cfg.Sites[p.Site]
|
|
proxy := s.cfg.Proxy.URL
|
|
s.cfg.RUnlock()
|
|
if !ok || site == nil {
|
|
fail(w, "unknown site: "+p.Site)
|
|
return
|
|
}
|
|
exp, n, err := s.ts.ImportCookies(site.Code, p.Cookies, proxy)
|
|
if err != nil {
|
|
fail(w, err.Error())
|
|
return
|
|
}
|
|
s.log.Info("turnstile cookies imported", "site", site.Code, "count", n, "expires_at", exp)
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"ok": true, "site": site.Code, "cookie_count": n, "expires_at": exp.Unix(),
|
|
})
|
|
|
|
case "invalidate":
|
|
s.up.InvalidateAll()
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
|
|
case "reset_metrics":
|
|
s.met.Reset()
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
|
|
case "test_proxy":
|
|
var p struct {
|
|
URL string `json:"url"`
|
|
}
|
|
_ = json.Unmarshal(body, &p)
|
|
result := s.testProxy(p.URL)
|
|
writeJSON(w, http.StatusOK, result)
|
|
|
|
case "probe_site":
|
|
var p struct {
|
|
Site string `json:"site"`
|
|
}
|
|
_ = json.Unmarshal(body, &p)
|
|
writeJSON(w, http.StatusOK, s.probeSite(p.Site))
|
|
|
|
case "site_balance":
|
|
var p struct {
|
|
Site string `json:"site"`
|
|
}
|
|
_ = json.Unmarshal(body, &p)
|
|
writeJSON(w, http.StatusOK, s.siteBalance(p.Site))
|
|
|
|
case "rotate_guest":
|
|
var p struct {
|
|
Site string `json:"site"`
|
|
}
|
|
_ = json.Unmarshal(body, &p)
|
|
writeJSON(w, http.StatusOK, s.rotateGuest(p.Site))
|
|
|
|
default:
|
|
fail(w, "unknown action: "+req.Action)
|
|
}
|
|
}
|
|
|
|
func (s *Server) save(w http.ResponseWriter) {
|
|
if err := s.cfg.Save(); err != nil {
|
|
fail(w, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
func (s *Server) testProxy(url string) map[string]any {
|
|
url = strings.TrimSpace(url)
|
|
target := "https://deepseek.fr/"
|
|
start := time.Now()
|
|
cli, err := httpx.StdClient(url, 20*time.Second)
|
|
if err != nil {
|
|
return map[string]any{"ok": false, "error": err.Error()}
|
|
}
|
|
req, _ := http.NewRequest(http.MethodGet, target, nil)
|
|
req.Header.Set("User-Agent", httpx.DefaultUserAgent)
|
|
resp, err := cli.Do(req)
|
|
if err != nil {
|
|
return map[string]any{"ok": false, "error": err.Error(), "ms": time.Since(start).Milliseconds()}
|
|
}
|
|
defer resp.Body.Close()
|
|
io.Copy(io.Discard, io.LimitReader(resp.Body, 4096))
|
|
return map[string]any{"ok": resp.StatusCode < 400, "status": resp.StatusCode, "ms": time.Since(start).Milliseconds()}
|
|
}
|
|
|
|
func (s *Server) probeSite(code string) map[string]any {
|
|
s.cfg.RLock()
|
|
st, ok := s.cfg.Sites[code]
|
|
proxy := s.cfg.Proxy.URL
|
|
var site config.Site
|
|
if ok {
|
|
site = *st
|
|
}
|
|
s.cfg.RUnlock()
|
|
if !ok {
|
|
return map[string]any{"ok": false, "error": "site not found"}
|
|
}
|
|
|
|
start := time.Now()
|
|
sess, err := httpx.NewSession(httpx.Options{ProxyURL: proxy, Timeout: 20 * time.Second})
|
|
if err != nil {
|
|
return map[string]any{"ok": false, "error": err.Error()}
|
|
}
|
|
defer sess.Close()
|
|
resp, err := sess.Do(nil, httpx.Request{
|
|
Method: "GET", URL: site.BaseURL + "/",
|
|
Header: map[string]string{
|
|
"User-Agent": httpx.DefaultUserAgent,
|
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
"Accept-Language": site.Language,
|
|
},
|
|
Timeout: 20 * time.Second,
|
|
})
|
|
if err != nil {
|
|
return map[string]any{"ok": false, "error": err.Error(), "ms": time.Since(start).Milliseconds()}
|
|
}
|
|
hasContainer := bytes.Contains(resp.Body, []byte("aipkit_chat_container"))
|
|
return map[string]any{
|
|
"ok": resp.Status < 400, "status": resp.Status,
|
|
"ms": time.Since(start).Milliseconds(), "chat_container": hasContainer,
|
|
"bytes": len(resp.Body),
|
|
}
|
|
}
|
|
|
|
// ── playground ───────────────────────────────────────────────────
|
|
|
|
func (s *Server) handlePlayground(w http.ResponseWriter, r *http.Request) {
|
|
body, err := readBody(r, 4<<20)
|
|
if err != nil {
|
|
fail(w, err.Error())
|
|
return
|
|
}
|
|
var req struct {
|
|
Model string `json:"model"`
|
|
Messages []openai.ChatMessage `json:"messages"`
|
|
Stream bool `json:"stream"`
|
|
Tools []openai.ToolDef `json:"tools"`
|
|
}
|
|
if err := json.Unmarshal(body, &req); err != nil {
|
|
fail(w, "invalid request: "+err.Error())
|
|
return
|
|
}
|
|
if req.Model == "" || len(req.Messages) == 0 {
|
|
fail(w, "model and messages are required")
|
|
return
|
|
}
|
|
s.cfg.RLock()
|
|
_, ok := s.cfg.Models[req.Model]
|
|
s.cfg.RUnlock()
|
|
if !ok {
|
|
fail(w, "unknown model")
|
|
return
|
|
}
|
|
prompt := openai.BuildPrompt(req.Messages, openai.PromptOptions{Tools: req.Tools})
|
|
|
|
start := time.Now()
|
|
info := &upstream.ServeInfo{}
|
|
|
|
s.cfg.RLock()
|
|
deadline := time.Duration(s.cfg.Upstream.StreamTimeout * float64(time.Second))
|
|
s.cfg.RUnlock()
|
|
if deadline <= 0 {
|
|
deadline = 5 * time.Minute
|
|
}
|
|
ctx, cancel := context.WithTimeout(r.Context(), deadline)
|
|
defer cancel()
|
|
|
|
if !req.Stream {
|
|
var sb strings.Builder
|
|
err := s.up.Chat(ctx, req.Model, prompt, info, func(ev upstream.Event) error {
|
|
if ev.Kind == upstream.KindDelta {
|
|
sb.WriteString(ev.Value)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadGateway, map[string]any{"error": err.Error()})
|
|
return
|
|
}
|
|
_, site, route := info.Get()
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"text": sb.String(), "ms": time.Since(start).Milliseconds(),
|
|
"site": site, "route": route, "prompt_chars": len(prompt),
|
|
})
|
|
return
|
|
}
|
|
|
|
flusher, _ := w.(http.Flusher)
|
|
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
w.Header().Set("X-Accel-Buffering", "no")
|
|
send := func(v any) {
|
|
raw, _ := json.Marshal(v)
|
|
fmt.Fprintf(w, "data: %s\n\n", raw)
|
|
if flusher != nil {
|
|
flusher.Flush()
|
|
}
|
|
}
|
|
send(map[string]any{"status": "connecting"})
|
|
err = s.up.Chat(ctx, req.Model, prompt, info, func(ev upstream.Event) error {
|
|
if ev.Kind == upstream.KindDelta {
|
|
send(map[string]any{"delta": ev.Value})
|
|
}
|
|
return nil
|
|
})
|
|
_, site, route := info.Get()
|
|
if err != nil {
|
|
send(map[string]any{"error": err.Error()})
|
|
} else {
|
|
send(map[string]any{"done": true, "ms": time.Since(start).Milliseconds(), "site": site, "route": route})
|
|
}
|
|
fmt.Fprint(w, "data: [DONE]\n\n")
|
|
if flusher != nil {
|
|
flusher.Flush()
|
|
}
|
|
}
|
|
|
|
// ── helpers ──────────────────────────────────────────────────────
|
|
|
|
func readBody(r *http.Request, max int64) ([]byte, error) {
|
|
defer r.Body.Close()
|
|
return io.ReadAll(io.LimitReader(r.Body, max))
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, code int, v any) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.WriteHeader(code)
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
func fail(w http.ResponseWriter, msg string) {
|
|
writeJSON(w, http.StatusBadRequest, map[string]any{"error": msg})
|
|
}
|
|
|
|
func firstNonEmpty(vals ...string) string {
|
|
for _, v := range vals {
|
|
if strings.TrimSpace(v) != "" {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// siteCodes resolves a requested site code to a list; an empty request means
|
|
// every configured site.
|
|
// siteBalance reports the free daily tier of the visitor id currently
|
|
// cached for the site (same endpoint the site's own balance badge uses).
|
|
func (s *Server) siteBalance(code string) map[string]any {
|
|
site, ok, proxy, botID := s.balanceContext(code)
|
|
if !ok {
|
|
return map[string]any{"ok": false, "error": "site not found"}
|
|
}
|
|
gid, has := s.ts.GuestID(code, proxy)
|
|
if !has {
|
|
return map[string]any{"ok": false, "error": "no cached session for this site — send a chat request first"}
|
|
}
|
|
return s.balanceJSON(code, site, gid, botID, proxy, false)
|
|
}
|
|
|
|
// rotateGuest swaps the site's visitor cookie for a fresh id — the site
|
|
// hands every new visitor id its own daily free tier, the same effect as a
|
|
// new private window — then reports the new identity's balance.
|
|
func (s *Server) rotateGuest(code string) map[string]any {
|
|
site, ok, proxy, botID := s.balanceContext(code)
|
|
if !ok {
|
|
return map[string]any{"ok": false, "error": "site not found"}
|
|
}
|
|
gid := s.ts.RotateGuest(code)
|
|
if gid == "" {
|
|
return map[string]any{"ok": false, "error": "no cached session for this site — refresh cookies first"}
|
|
}
|
|
return s.balanceJSON(code, site, gid, botID, proxy, true)
|
|
}
|
|
|
|
func (s *Server) balanceContext(code string) (config.Site, bool, string, int) {
|
|
s.cfg.RLock()
|
|
defer s.cfg.RUnlock()
|
|
st, ok := s.cfg.Sites[code]
|
|
if !ok {
|
|
return config.Site{}, false, "", 0
|
|
}
|
|
botID := 0
|
|
for _, m := range s.cfg.Models {
|
|
if m.Site == code && m.Enabled {
|
|
botID = m.BotID
|
|
break
|
|
}
|
|
}
|
|
return *st, true, s.cfg.Proxy.URL, botID
|
|
}
|
|
|
|
func (s *Server) balanceJSON(code string, site config.Site, gid string, botID int, proxy string, rotated bool) map[string]any {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
|
defer cancel()
|
|
bal, err := s.up.FetchBalance(ctx, site, gid, botID, upstream.Route{Name: "balance", Proxy: proxy})
|
|
if err != nil {
|
|
return map[string]any{"ok": false, "error": err.Error()}
|
|
}
|
|
out := map[string]any{
|
|
"ok": true, "site": code, "rotated": rotated,
|
|
"balance": bal.Balance, "limit": bal.Free.Limit,
|
|
"used": bal.Free.Used, "remaining": bal.Free.Remaining,
|
|
"reset_period": bal.Free.ResetPeriod,
|
|
}
|
|
if rotated {
|
|
s.log.Info("visitor identity rotated from console", "site", code, "remaining", bal.Free.Remaining)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *Server) siteCodes(want string) []string {
|
|
s.cfg.RLock()
|
|
defer s.cfg.RUnlock()
|
|
if want != "" {
|
|
return []string{want}
|
|
}
|
|
out := make([]string, 0, len(s.cfg.Sites))
|
|
for code := range s.cfg.Sites {
|
|
out = append(out, code)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func randomToken(n int) string {
|
|
b := make([]byte, n)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return fmt.Sprintf("%x", time.Now().UnixNano())
|
|
}
|
|
return hex.EncodeToString(b)
|
|
}
|