* @license MIT License * @version GIT: * @link https://spacetraders.io */ require_once __DIR__ . '/spacetraders-api-exception.php'; require_once __DIR__ . '/spacetraders-storage.php'; /** * Spacetraders API Client * * Main client class for interacting with the Spacetraders API. Provides methods * for authentication, managing agents, ships, contracts, systems, and performing * various game actions like navigation, trading, and resource extraction. * * @package SpacetradersAPI */ class SpacetradersApi { /** * The base URL for the Spacetraders API. * * @var string */ private string $baseUrl; /** * The authentication token for API requests. * * @var string|null */ private ?string $token; /** * The timeout for HTTP requests in seconds. * * @var int */ private int $timeout; /** * Optional SQLite storage for settings and cache. * * @var SpacetradersStorage|null */ private ?SpacetradersStorage $storage; /** * Default cache lifetime in seconds. * * @var int */ private int $cacheTtl; /** * Constructor for SpacetradersApi. * * @param string|null $token The authentication token for API requests ( default: null ). * @param string $baseUrl The base URL for the Spacetraders API ( default: 'https://api.spacetraders.io/v2' ). * @param int $timeout The timeout for HTTP requests in seconds ( default: 30 ). * @param SpacetradersStorage|null $storage Optional SQLite storage object for settings and caching. * @param int $cacheTtl API cache lifetime in seconds (default: 600). */ public function __construct( ?string $token = null, string $baseUrl = 'https://api.spacetraders.io/v2', int $timeout = 30, ?SpacetradersStorage $storage = null, int $cacheTtl = 600 ) { $this->baseUrl = rtrim( $baseUrl, '/' ); $this->token = $token; $this->timeout = $timeout; $this->storage = $storage; $this->cacheTtl = max( 1, $cacheTtl ); } /** * Set the authentication token for API requests. * * @param string $token The authentication token to set. * * @return void */ public function setToken( string $token ): void { $this->token = trim( $token ); } /** * Get the current authentication token. * * @return string|null The authentication token or null if not set. */ public function getToken(): ?string { return $this->token; } /** * Attach storage for API caching and persisted settings. * * @param SpacetradersStorage|null $storage Storage handler. * * @return void */ public function setStorage( ?SpacetradersStorage $storage ): void { $this->storage = $storage; } /** * Set cache lifetime for cached API calls in seconds. * * @param int $cacheTtl Cache lifetime in seconds. * * @return void */ public function setCacheTtl( int $cacheTtl ): void { $this->cacheTtl = max( 1, $cacheTtl ); } /** * Make an HTTP request to the Spacetraders API. * * @param string $method The HTTP method (GET, POST, etc.). * @param string $endpoint The API endpoint to call. * @param array $payload The request payload data. * @param array $query The query parameters. * * @return array * @throws SpacetradersApiException When cURL fails to initialize, JSON encoding fails, network errors occur, or API returns an error response. */ public function request( string $method, string $endpoint, array $payload = array(), array $query = array() ): array { $url = $this->_buildUrl( $endpoint, $query ); $method = strtoupper( $method ); $cacheKey = $this->_buildCacheKey( $method, $url, $payload ); if ($method === 'GET' && $this->storage !== null ) { $cachedResponse = $this->storage->getCache( $cacheKey ); if (is_array( $cachedResponse ) ) { return $cachedResponse; } } $ch = curl_init( $url ); if ($ch === false ) { throw new SpacetradersApiException( 'Unable to initialize cURL.' ); } $headers = array( 'Accept: application/json' ); $jsonPayload = null; if (! empty( $this->token ) ) { $headers[] = 'Authorization: Bearer ' . $this->token; } if (! empty( $payload ) ) { $jsonPayload = json_encode( $payload ); if ($jsonPayload === false ) { throw new SpacetradersApiException( 'Unable to encode request payload as JSON.' ); } $headers[] = 'Content-Type: application/json'; } curl_setopt_array( $ch, array( CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => $this->timeout, ) ); if ($jsonPayload !== null ) { curl_setopt( $ch, CURLOPT_POSTFIELDS, $jsonPayload ); } $rawResponse = curl_exec( $ch ); if ($rawResponse === false ) { $error = curl_error( $ch ); curl_close( $ch ); throw new SpacetradersApiException( 'Network error: ' . $error ); } $statusCode = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE ); curl_close( $ch ); $decoded = json_decode( $rawResponse, true ); if (! is_array( $decoded ) ) { throw new SpacetradersApiException( 'API returned an invalid JSON response.', $statusCode ); } if ($statusCode >= 400 ) { $message = $decoded['error']['message'] ?? 'Unknown API error.'; $errorCode = (int) ( $decoded['error']['code'] ?? $statusCode ); throw new SpacetradersApiException( $message, $errorCode, $decoded ); } if ($method === 'GET' && $this->storage !== null ) { $this->storage->setCache( $cacheKey, $decoded, $this->cacheTtl ); $this->storage->purgeExpiredCache(); } return $decoded; } /** * Register a new agent and returns the API response. * * @param array $options Optional registration options: `symbol`, `faction`, and `email`. * * @return array */ public function registerAgent( array $options ): array { return $this->request( 'POST', '/register', $options ); } /** * Get the current agent's information. * * @return array */ public function getMyAgent(): array { return $this->request( 'GET', '/my/agent' ); } /** * Get a list of all ships owned by the current agent. * * @param array $query * * @return array */ public function listMyShips( array $query = array() ): array { return $this->request( 'GET', '/my/ships', array(), $query ); } /** * Get information about a specific ship owned by the current agent. * * @param string $shipSymbol The symbol of the ship to retrieve. * * @return array */ public function getShip( string $shipSymbol ): array { return $this->request( 'GET', '/my/ships/' . rawurlencode( $shipSymbol ) ); } /** * Get a list of all contracts for the current agent. * * @param array $query * * @return array */ public function listMyContracts( array $query = array() ): array { return $this->request( 'GET', '/my/contracts', array(), $query ); } /** * Accept a contract for the current agent. * * @param string $contractId The ID of the contract to accept. * * @return array */ public function acceptContract( string $contractId ): array { return $this->request( 'POST', '/my/contracts/' . rawurlencode( $contractId ) . '/accept' ); } /** * Get a list of all systems in the universe. * * @param array $query * * @return array */ public function listSystems( array $query = array() ): array { return $this->request( 'GET', '/systems', array(), $query ); } /** * Get information about a specific system. * * @param string $systemSymbol The symbol of the system to retrieve. * * @return array */ public function getSystem( string $systemSymbol ): array { return $this->request( 'GET', '/systems/' . rawurlencode( $systemSymbol ) ); } /** * Get a list of waypoints in a specific system. * * @param string $systemSymbol The symbol of the system to retrieve waypoints from. * @param array $query * * @return array */ public function listWaypoints( string $systemSymbol, array $query = array() ): array { return $this->request( 'GET', '/systems/' . rawurlencode( $systemSymbol ) . '/waypoints', array(), $query ); } /** * Get information about a single waypoint. * * @param string $systemSymbol The system symbol. * @param string $waypointSymbol The waypoint symbol. * * @return array */ public function getWaypoint( string $systemSymbol, string $waypointSymbol ): array { return $this->request( 'GET', '/systems/' . rawurlencode( $systemSymbol ) . '/waypoints/' . rawurlencode( $waypointSymbol ) ); } /** * Get market data for a waypoint with a marketplace. * * @param string $systemSymbol The system symbol. * @param string $waypointSymbol The waypoint symbol. * * @return array */ public function getWaypointMarket( string $systemSymbol, string $waypointSymbol ): array { return $this->request( 'GET', '/systems/' . rawurlencode( $systemSymbol ) . '/waypoints/' . rawurlencode( $waypointSymbol ) . '/market' ); } /** * Get shipyard data for a waypoint with a shipyard. * * @param string $systemSymbol The system symbol. * @param string $waypointSymbol The waypoint symbol. * * @return array */ public function getWaypointShipyard( string $systemSymbol, string $waypointSymbol ): array { return $this->request( 'GET', '/systems/' . rawurlencode( $systemSymbol ) . '/waypoints/' . rawurlencode( $waypointSymbol ) . '/shipyard' ); } /** * Navigate a ship to a specific waypoint. * * @param string $shipSymbol The symbol of the ship to navigate. * @param string $waypointSymbol The symbol of the waypoint to navigate to. * * @return array */ public function navigateShip( string $shipSymbol, string $waypointSymbol ): array { return $this->request( 'POST', '/my/ships/' . rawurlencode( $shipSymbol ) . '/navigate', array( 'waypointSymbol' => $waypointSymbol ) ); } /** * Put a ship into orbit around its current waypoint. * * @param string $shipSymbol The symbol of the ship to put into orbit. * * @return array */ public function orbitShip( string $shipSymbol ): array { return $this->request( 'POST', '/my/ships/' . rawurlencode( $shipSymbol ) . '/orbit' ); } /** * Dock a ship at its current waypoint. * * @param string $shipSymbol The symbol of the ship to dock. * * @return array */ public function dockShip( string $shipSymbol ): array { return $this->request( 'POST', '/my/ships/' . rawurlencode( $shipSymbol ) . '/dock' ); } /** * Extract resources from the current waypoint using the specified ship. * * @param string $shipSymbol The symbol of the ship to use for resource extraction. * * @return array */ public function extractResources( string $shipSymbol ): array { return $this->request( 'POST', '/my/ships/' . rawurlencode( $shipSymbol ) . '/extract' ); } /** * Create a survey at the ship's current waypoint. * * @param string $shipSymbol The symbol of the ship creating the survey. * * @return array */ public function surveyWaypoint( string $shipSymbol ): array { return $this->request( 'POST', '/my/ships/' . rawurlencode( $shipSymbol ) . '/survey' ); } /** * Siphon resources at the ship's current waypoint. * * @param string $shipSymbol The symbol of the ship performing siphon. * * @return array */ public function siphonResources( string $shipSymbol ): array { return $this->request( 'POST', '/my/ships/' . rawurlencode( $shipSymbol ) . '/siphon' ); } /** * Refuel a ship. * * @param string $shipSymbol The symbol of the ship to refuel. * @param bool $fromCargo Whether to refuel from cargo instead of market. * @param int|null $units Optional number of fuel units to buy. * * @return array */ public function refuelShip( string $shipSymbol, bool $fromCargo = false, ?int $units = null ): array { $payload = array(); if ($fromCargo ) { $payload['fromCargo'] = true; } if ($units !== null && $units > 0 ) { $payload['units'] = $units; } return $this->request( 'POST', '/my/ships/' . rawurlencode( $shipSymbol ) . '/refuel', $payload ); } /** * Purchase cargo for a specific ship at its current waypoint. * * @param string $shipSymbol The symbol of the ship to purchase cargo for. * @param string $tradeSymbol The symbol of the trade good to purchase. * @param int $units The number of units to purchase. * * @return array */ public function purchaseCargo( string $shipSymbol, string $tradeSymbol, int $units ): array { return $this->request( 'POST', '/my/ships/' . rawurlencode( $shipSymbol ) . '/purchase', array( 'symbol' => $tradeSymbol, 'units' => $units, ) ); } /** * Sell cargo from a specific ship at its current waypoint. * * @param string $shipSymbol The symbol of the ship to sell cargo from. * @param string $tradeSymbol The symbol of the trade good to sell. * @param int $units The number of units to sell. * * @return array */ public function sellCargo( string $shipSymbol, string $tradeSymbol, int $units ): array { return $this->request( 'POST', '/my/ships/' . rawurlencode( $shipSymbol ) . '/sell', array( 'symbol' => $tradeSymbol, 'units' => $units, ) ); } /** * Jettison cargo from a ship. * * @param string $shipSymbol The symbol of the ship. * @param string $tradeSymbol The cargo symbol to jettison. * @param int $units Number of units to jettison. * * @return array */ public function jettisonCargo( string $shipSymbol, string $tradeSymbol, int $units ): array { return $this->request( 'POST', '/my/ships/' . rawurlencode( $shipSymbol ) . '/jettison', array( 'symbol' => $tradeSymbol, 'units' => $units, ) ); } /** * Purchase a new ship from a shipyard. * * @param string $shipType The ship type to purchase. * @param string $waypointSymbol The shipyard waypoint symbol. * * @return array */ public function purchaseShip( string $shipType, string $waypointSymbol ): array { return $this->request( 'POST', '/my/ships', array( 'shipType' => $shipType, 'waypointSymbol' => $waypointSymbol, ) ); } /** * Build the full URL for an API endpoint with optional query parameters. * * @param string $endpoint The API endpoint path. * @param array $query The query parameters to append. * * @return string The complete URL. */ private function _buildUrl( string $endpoint, array $query = array() ): string { $url = $this->baseUrl . '/' . ltrim( $endpoint, '/' ); if (! empty( $query ) ) { $url .= '?' . http_build_query( $query ); } return $url; } /** * Build a deterministic cache key for an API request. * * @param string $method HTTP method. * @param string $url Full URL. * @param array $payload Request payload. * * @return string */ private function _buildCacheKey( string $method, string $url, array $payload ): string { $payloadJson = json_encode( $payload ); if ($payloadJson === false ) { $payloadJson = ''; } return hash( 'sha256', $method . '|' . $url . '|' . $payloadJson ); } }