package prices import ( "encoding/json" "fmt" "io" "net/http" "time" ) const userAgent = "rsw-cli/1.0 (RuneScape Wiki CLI tool; https://github.com/runescape-wiki/rsw)" // Client wraps HTTP requests to the real-time prices API. type Client struct { baseURL string httpClient *http.Client } // NewClient creates a price API client. func NewClient(baseURL string) *Client { return &Client{ baseURL: baseURL, httpClient: &http.Client{ Timeout: 15 * time.Second, }, } } // LatestPrice represents the current GE price for an item. type LatestPrice struct { High *int `json:"high"` HighTime *int64 `json:"highTime"` Low *int `json:"low"` LowTime *int64 `json:"lowTime"` } // LatestResponse maps item IDs to their latest prices. type LatestResponse struct { Data map[string]LatestPrice `json:"data"` } // MappingItem maps an item ID to metadata. type MappingItem struct { ID int `json:"id"` Name string `json:"name"` Examine string `json:"examine"` Members bool `json:"members"` HighAlch *int `json:"highalch"` LowAlch *int `json:"lowalch"` Limit *int `json:"limit"` Value int `json:"value"` Icon string `json:"icon"` } // GetLatest fetches the latest prices for all items. func (c *Client) GetLatest() (*LatestResponse, error) { url := fmt.Sprintf("%s/latest", c.baseURL) var resp LatestResponse if err := c.getJSON(url, &resp); err != nil { return nil, err } return &resp, nil } // GetLatestForItem fetches the latest price for a specific item by ID. func (c *Client) GetLatestForItem(itemID int) (*LatestPrice, error) { url := fmt.Sprintf("%s/latest?id=%d", c.baseURL, itemID) var resp LatestResponse if err := c.getJSON(url, &resp); err != nil { return nil, err } idStr := fmt.Sprintf("%d", itemID) if p, ok := resp.Data[idStr]; ok { return &p, nil } return nil, fmt.Errorf("no price data for item %d", itemID) } // GetMapping fetches the item ID → metadata mapping. func (c *Client) GetMapping() ([]MappingItem, error) { url := fmt.Sprintf("%s/mapping", c.baseURL) var resp []MappingItem if err := c.getJSON(url, &resp); err != nil { return nil, err } return resp, nil } // VolumeEntry represents trade volume data for an item. type VolumeEntry struct { AvgHighPrice *int `json:"avgHighPrice"` HighVolume int `json:"highPriceVolume"` AvgLowPrice *int `json:"avgLowPrice"` LowVolume int `json:"lowPriceVolume"` Timestamp int64 `json:"timestamp"` } // TimeseriesResponse wraps the 5m/1h API responses. // The API returns data as a map keyed by item ID string. type TimeseriesResponse struct { Data map[string]VolumeEntry `json:"data"` } // Get5Min fetches 5-minute average data for an item. func (c *Client) Get5Min(itemID int) (*VolumeEntry, error) { url := fmt.Sprintf("%s/5m?id=%d", c.baseURL, itemID) var resp TimeseriesResponse if err := c.getJSON(url, &resp); err != nil { return nil, err } idStr := fmt.Sprintf("%d", itemID) if entry, ok := resp.Data[idStr]; ok { return &entry, nil } return nil, fmt.Errorf("no 5m data for item %d", itemID) } // Get1Hour fetches 1-hour average data for an item. func (c *Client) Get1Hour(itemID int) (*VolumeEntry, error) { url := fmt.Sprintf("%s/1h?id=%d", c.baseURL, itemID) var resp TimeseriesResponse if err := c.getJSON(url, &resp); err != nil { return nil, err } idStr := fmt.Sprintf("%d", itemID) if entry, ok := resp.Data[idStr]; ok { return &entry, nil } return nil, fmt.Errorf("no 1h data for item %d", itemID) } func (c *Client) getJSON(url string, dest any) error { req, err := http.NewRequest("GET", url, nil) if err != nil { return fmt.Errorf("creating request: %w", err) } req.Header.Set("User-Agent", userAgent) resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("executing request: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body)) } return json.NewDecoder(resp.Body).Decode(dest) }