package remnawave import ( "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) type Client struct { baseURL string token string caddyToken string http *http.Client } type PanelStatus struct { OK bool StatusCode int Users int Nodes int Detail string } func NewClient(baseURL, apiToken, caddyToken string) *Client { return &Client{ baseURL: strings.TrimRight(baseURL, "/"), token: apiToken, caddyToken: caddyToken, http: &http.Client{ Timeout: 15 * time.Second, }, } } func (c *Client) Check(ctx context.Context) (PanelStatus, error) { st := PanelStatus{} resp, body, err := c.get(ctx, "/api/system/stats/recap") if err != nil { return st, err } st.StatusCode = resp.StatusCode switch resp.StatusCode { case http.StatusOK: st.OK = true st.Detail = "API панели отвечает" case http.StatusUnauthorized, http.StatusForbidden: return st, fmt.Errorf("доступ запрещён (HTTP %d): проверьте REMNAWAVE_API_TOKEN", resp.StatusCode) default: return st, fmt.Errorf("панель вернула HTTP %d: %s", resp.StatusCode, trimBody(body, 200)) } users, err := c.countFromEndpoint(ctx, "/api/users", "users") if err == nil { st.Users = users } nodes, err := c.countFromEndpoint(ctx, "/api/nodes", "nodes") if err == nil { st.Nodes = nodes } return st, nil } func (c *Client) countFromEndpoint(ctx context.Context, path, arrayKey string) (int, error) { resp, body, err := c.get(ctx, path) if err != nil { return 0, err } if resp.StatusCode != http.StatusOK { return 0, fmt.Errorf("HTTP %d", resp.StatusCode) } return parseCount(body, arrayKey), nil } func (c *Client) get(ctx context.Context, path string) (*http.Response, []byte, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil) if err != nil { return nil, nil, err } req.Header.Set("Authorization", "Bearer "+c.token) req.Header.Set("Accept", "application/json") if c.caddyToken != "" { req.Header.Set("X-Api-Key", c.caddyToken) } resp, err := c.http.Do(req) if err != nil { return nil, nil, fmt.Errorf("нет связи с панелью: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return resp, nil, err } return resp, body, nil } func parseCount(body []byte, arrayKey string) int { var raw map[string]json.RawMessage if err := json.Unmarshal(body, &raw); err != nil { return 0 } if n := countInRaw(raw["response"], arrayKey); n > 0 { return n } return countInRaw(json.RawMessage(body), arrayKey) } func countInRaw(data json.RawMessage, arrayKey string) int { if len(data) == 0 { return 0 } var obj map[string]json.RawMessage if err := json.Unmarshal(data, &obj); err != nil { var arr []json.RawMessage if err := json.Unmarshal(data, &arr); err == nil { return len(arr) } return 0 } if totalRaw, ok := obj["total"]; ok { var total int if err := json.Unmarshal(totalRaw, &total); err == nil && total > 0 { return total } } if items, ok := obj[arrayKey]; ok { var arr []json.RawMessage if err := json.Unmarshal(items, &arr); err == nil { return len(arr) } } for _, v := range obj { var arr []json.RawMessage if err := json.Unmarshal(v, &arr); err == nil && len(arr) > 0 { return len(arr) } } return 0 } func trimBody(b []byte, max int) string { s := strings.TrimSpace(string(b)) if len(s) > max { return s[:max] + "…" } return s }