package client import ( "context" "encoding/json" "fmt" "io" "net/http" "gitea.libretechconsulting.com/rmcguire/wingbits/pkg/types/readsb" "gitea.libretechconsulting.com/rmcguire/wingbits/pkg/types/wingbits" ) // Aircraft fetches and decodes aircraft.json. When one or more filters are // supplied, only matching aircraft are retained in the returned report. func (c *Client) Aircraft(ctx context.Context, filters ...readsb.AircraftFilter) (*readsb.AircraftReport, error) { ep, port := c.readsbEndpoint() report, err := getJSON[readsb.AircraftReport](ctx, c, ep.url(port, true, "aircraft.json")) if err != nil { return nil, err } report.Aircraft = report.Filter(filters...) return report, nil } // Receiver fetches and decodes receiver.json. func (c *Client) Receiver(ctx context.Context) (*readsb.Receiver, error) { ep, port := c.readsbEndpoint() return getJSON[readsb.Receiver](ctx, c, ep.url(port, true, "receiver.json")) } // Stats fetches and decodes stats.json. func (c *Client) Stats(ctx context.Context) (*readsb.Stats, error) { ep, port := c.readsbEndpoint() return getJSON[readsb.Stats](ctx, c, ep.url(port, true, "stats.json")) } // Outline fetches and decodes outline.json (the receiver range polygon). It is // only served by the tar1090 root, so a tar1090 endpoint must be configured. func (c *Client) Outline(ctx context.Context) (*readsb.Outline, error) { if c.tar1090 == nil { return nil, fmt.Errorf("wingbits: outline.json requires a tar1090 endpoint") } return getJSON[readsb.Outline](ctx, c, c.tar1090.url(DefaultTar1090Port, true, "outline.json")) } // Diagnostics fetches and decodes /network/diagnostics from the Wingbits root. func (c *Client) Diagnostics(ctx context.Context) (*wingbits.Diagnostics, error) { return getJSON[wingbits.Diagnostics](ctx, c, c.wingbits.url(DefaultWingbitsPort, false, "network", "diagnostics")) } // Status fetches and decodes the Tailscale status from the Wingbits root. func (c *Client) Status(ctx context.Context) (*wingbits.TailscaleStatus, error) { return getJSON[wingbits.TailscaleStatus](ctx, c, c.wingbits.url(DefaultWingbitsPort, false, "tailscale", "status")) } // Metrics fetches /metrics from the Wingbits root and parses the Prometheus // text exposition format into structured metric families. func (c *Client) Metrics(ctx context.Context) (*wingbits.Metrics, error) { body, err := c.get(ctx, c.wingbits.url(DefaultWingbitsPort, false, "metrics")) if err != nil { return nil, err } defer body.Close() return wingbits.ParseMetrics(body) } // getJSON fetches a URL and decodes its JSON body into a freshly allocated T. func getJSON[T any](ctx context.Context, c *Client, url string) (*T, error) { body, err := c.get(ctx, url) if err != nil { return nil, err } defer body.Close() out := new(T) if err := json.NewDecoder(body).Decode(out); err != nil { return nil, fmt.Errorf("wingbits: decoding %s: %w", url, err) } return out, nil } // get performs a GET and returns the response body, which the caller must close. func (c *Client) get(ctx context.Context, url string) (io.ReadCloser, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("wingbits: building request: %w", err) } req.Header.Set("User-Agent", c.userAgent) resp, err := c.http.Do(req) if err != nil { return nil, fmt.Errorf("wingbits: GET %s: %w", url, err) } if resp.StatusCode != http.StatusOK { resp.Body.Close() return nil, fmt.Errorf("wingbits: GET %s: unexpected status %s", url, resp.Status) } return resp.Body, nil }