package remnawave import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) // Client обращается к Remnawave Panel API: // - Base: REMNAWAVE_PANEL_URL (например https://panel.example.com) // - Auth: Authorization: Bearer REMNAWAVE_API_TOKEN // - Опционально: X-Api-Key: CADDY_AUTH_API_TOKEN // Документация: https://docs.rw/docs/install/subscription-page/bundled type Client struct { panelURL string token string caddyToken string http *http.Client } func NewClient(panelURL, apiToken, caddyAuthToken string) *Client { return &Client{ panelURL: strings.TrimRight(panelURL, "/"), token: apiToken, caddyToken: caddyAuthToken, http: &http.Client{ Timeout: 15 * time.Second, }, } } func (c *Client) PanelURL() string { return c.panelURL } 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) { return c.doRequest(ctx, http.MethodGet, c.panelURL+path, nil, true) } func (c *Client) getPublic(ctx context.Context, path string) (*http.Response, []byte, error) { return c.doRequest(ctx, http.MethodGet, c.panelURL+path, nil, false) } func (c *Client) post(ctx context.Context, path string, body any) (*http.Response, []byte, error) { return c.doRequest(ctx, http.MethodPost, c.panelURL+path, body, true) } func (c *Client) patch(ctx context.Context, path string, body any) (*http.Response, []byte, error) { return c.doRequest(ctx, http.MethodPatch, c.panelURL+path, body, true) } func (c *Client) doRequest(ctx context.Context, method, url string, body any, withBearer bool) (*http.Response, []byte, error) { var bodyReader io.Reader if body != nil { raw, err := json.Marshal(body) if err != nil { return nil, nil, err } bodyReader = bytes.NewReader(raw) } req, err := http.NewRequestWithContext(ctx, method, url, bodyReader) if err != nil { return nil, nil, err } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") if withBearer { req.Header.Set("Authorization", "Bearer "+c.token) } 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() respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return resp, nil, err } return resp, respBody, nil } func apiError(status int, body []byte) error { msg := trimBody(body, 300) if msg == "" { return fmt.Errorf("HTTP %d", status) } return fmt.Errorf("HTTP %d: %s", status, msg) } 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 }