152 lines
4.0 KiB
Go
152 lines
4.0 KiB
Go
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)
|
|
}
|