// Package esi provides EVE Online ESI API implementations for planetary interaction data. package esi import ( "context" "encoding/json" "fmt" "io" "net/http" "os" "time" "go-eve-pi/options" logger "git.site.quack-lab.dev/dave/cylogger" ) const ( ESIBaseURL = "https://esi.evetech.net" ) type Planet struct { PlanetID int `json:"planet_id"` PlanetType string `json:"planet_type"` SolarSystemID int `json:"solar_system_id"` UpgradeLevel int `json:"upgrade_level"` NumPins int `json:"num_pins"` LastUpdate string `json:"last_update"` OwnerID int `json:"owner_id"` PlanetName string `json:"planet_name"` PlanetTypeID int `json:"planet_type_id"` Position struct { X float64 `json:"x"` Y float64 `json:"y"` Z float64 `json:"z"` } `json:"position"` } type PlanetDetail struct { Links []Link `json:"links"` Pins []Pin `json:"pins"` Routes []Route `json:"routes"` LastUpdate string `json:"last_update"` } type Link struct { DestinationPinID int `json:"destination_pin_id"` LinkLevel int `json:"link_level"` SourcePinID int `json:"source_pin_id"` } type Pin struct { Contents []StorageContent `json:"contents"` ExpiryTime *string `json:"expiry_time"` ExtractorDetails *ExtractorDetails `json:"extractor_details"` FactoryDetails *FactoryDetails `json:"factory_details"` InstallTime *string `json:"install_time"` LastCycleStart *string `json:"last_cycle_start"` Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` PinID int `json:"pin_id"` SchematicID *int `json:"schematic_id"` TypeID int `json:"type_id"` } type StorageContent struct { Amount int `json:"amount"` TypeID int `json:"type_id"` } type ExtractorDetails struct { CycleTime *int `json:"cycle_time"` HeadRadius *float64 `json:"head_radius"` Heads []Head `json:"heads"` ProductTypeID *int `json:"product_type_id"` QtyPerCycle *int `json:"qty_per_cycle"` } type Head struct { HeadID int `json:"head_id"` Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` } type FactoryDetails struct { SchematicID int `json:"schematic_id"` } type Route struct { ContentTypeID int `json:"content_type_id"` DestinationPinID int `json:"destination_pin_id"` Quantity float64 `json:"quantity"` RouteID int `json:"route_id"` SourcePinID int `json:"source_pin_id"` Waypoints []int `json:"waypoints"` } // ESIInterface defines the contract for ESI API interactions type ESIInterface interface { GetCharacterPlanets(ctx context.Context, characterID int, accessToken string) ([]Planet, error) GetPlanetDetails(ctx context.Context, characterID, planetID int, accessToken string) (*PlanetDetail, error) } // DirectESI implements ESIInterface with direct API calls type DirectESI struct { httpClient *http.Client } // NewDirectESI creates a new DirectESI instance func NewDirectESI() *DirectESI { // Parse HTTP timeout ONCE at initialization httpTimeout, err := time.ParseDuration(options.GlobalOptions.HTTPTimeout) if err != nil { logger.Error("Invalid HTTP timeout duration %s: %v", options.GlobalOptions.HTTPTimeout, err) os.Exit(1) } return &DirectESI{ httpClient: &http.Client{ Timeout: httpTimeout, }, } } // GetCharacterPlanets retrieves a list of planets for a character func (d *DirectESI) GetCharacterPlanets(ctx context.Context, characterID int, accessToken string) ([]Planet, error) { logger.Debug("Fetching planets for character ID %d", characterID) url := fmt.Sprintf("%s/v1/characters/%d/planets/", ESIBaseURL, characterID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { logger.Error("Failed to create request for character planets: %v", err) return nil, err } req.Header.Set("Authorization", "Bearer "+accessToken) req.Header.Set("Accept", "application/json") resp, err := d.httpClient.Do(req) if err != nil { logger.Error("Failed to fetch character planets: %v", err) return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) logger.Error("Character planets API returned status %d: %s", resp.StatusCode, string(body)) return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) } var planets []Planet if err := json.NewDecoder(resp.Body).Decode(&planets); err != nil { logger.Error("Failed to decode character planets response: %v", err) return nil, err } logger.Info("Successfully fetched %d planets for character ID %d", len(planets), characterID) return planets, nil } // GetPlanetDetails retrieves detailed information about a specific planet func (d *DirectESI) GetPlanetDetails(ctx context.Context, characterID, planetID int, accessToken string) (*PlanetDetail, error) { logger.Debug("Fetching planet details for character ID %d, planet ID %d", characterID, planetID) url := fmt.Sprintf("%s/v3/characters/%d/planets/%d/", ESIBaseURL, characterID, planetID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { logger.Error("Failed to create request for planet details: %v", err) return nil, err } req.Header.Set("Authorization", "Bearer "+accessToken) req.Header.Set("Accept", "application/json") resp, err := d.httpClient.Do(req) if err != nil { logger.Error("Failed to fetch planet details: %v", err) return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) logger.Error("Planet details API returned status %d: %s", resp.StatusCode, string(body)) return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) } var planetDetail PlanetDetail if err := json.NewDecoder(resp.Body).Decode(&planetDetail); err != nil { logger.Error("Failed to decode planet details response: %v", err) return nil, err } logger.Info("Successfully fetched planet details for character ID %d, planet ID %d", characterID, planetID) return &planetDetail, nil }