diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 041f7331..ef97b3a2 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -944,7 +944,7 @@ class AppWindow(object): return True - def capi_request_data(self, event=None) -> None: + def capi_request_data(self, event=None) -> None: # noqa: CCR001 """ Perform CAPI data retrieval and associated actions. @@ -969,6 +969,17 @@ class AppWindow(object): self.status['text'] = _('CAPI query aborted: Game mode unknown') return + if monitor.state['GameVersion'] is None: + logger.trace_if('capi.worker', 'Aborting Query: GameVersion unknown') + # LANG: CAPI queries aborted because GameVersion unknown + self.status['text'] = _('CAPI query aborted: GameVersion unknown') + return + + if not monitor.is_live_galaxy(): + logger.warning("Dropping CAPI request because this is the Legacy galaxy, which is not yet supported") + self.status['text'] = 'CAPI for Legacy not yet supported' + return + if not monitor.system: logger.trace_if('capi.worker', 'Aborting Query: Current star system unknown') # LANG: CAPI queries aborted because current star system name unknown @@ -1161,6 +1172,7 @@ class AppWindow(object): monitor.state['Loan'] = capi_response.capi_data['commander'].get('debt', 0) # stuff we can do when not docked + # TODO: Use plug.notify_capi_legacy if Legacy host err = plug.notify_newdata(capi_response.capi_data, monitor.is_beta) self.status['text'] = err and err or '' if err: diff --git a/L10n/en.template b/L10n/en.template index 8bbfad96..6ed81d22 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -214,6 +214,9 @@ /* EDMarketConnector.py: CAPI queries aborted because game mode unknown; In files: EDMarketConnector.py:967; */ "CAPI query aborted: Game mode unknown" = "CAPI query aborted: Game mode unknown"; +/* EDMarketConnector.py: CAPI queries aborted because GameVersion unknown; In files: EDMarketConnector.py:974; */ +"CAPI query aborted: GameVersion unknown" = "CAPI query aborted: GameVersion unknown"; + /* EDMarketConnector.py: CAPI queries aborted because current star system name unknown; In files: EDMarketConnector.py:973; */ "CAPI query aborted: Current system unknown" = "CAPI query aborted: Current system unknown"; diff --git a/PLUGINS.md b/PLUGINS.md index 4b175397..011f1224 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -896,6 +896,9 @@ data from Frontier's servers. | :-------- | :--------------: | :------------------------------------------------------------------------------------------------------- | | `data` | `Dict[str, Any]` | `/profile` API response, with `/market` and `/shipyard` added under the keys `marketdata` and `shipdata` | | `is_beta` | `bool` | If the game is currently in beta | +NB: Actually `data` is a custom type, based on `UserDict`, called `CAPIData`, +and has some extra properties. However, these are for **internal use only** +at this time, especially as there are some caveats about at least one of them. --- diff --git a/companion.py b/companion.py index 91b306e3..4aff101a 100644 --- a/companion.py +++ b/companion.py @@ -55,6 +55,7 @@ auth_timeout = 30 # timeout for initial auth FRONTIER_AUTH_SERVER = 'https://auth.frontierstore.net' SERVER_LIVE = 'https://companion.orerve.net' +SERVER_LEGACY = 'https://legacy-companion.orerve.net' SERVER_BETA = 'https://pts-companion.orerve.net' commodity_map: Dict = {} @@ -65,24 +66,29 @@ class CAPIData(UserDict): def __init__( self, - data: Union[str, Dict[str, Any], 'CAPIData', None] = None, source_endpoint: str = None + data: Union[str, Dict[str, Any], 'CAPIData', None] = None, + source_host: str = None, + source_endpoint: str = None ) -> None: if data is None: super().__init__() + elif isinstance(data, str): super().__init__(json.loads(data)) + else: super().__init__(data) self.original_data = self.data.copy() # Just in case + self.source_host = source_host self.source_endpoint = source_endpoint if source_endpoint is None: return if source_endpoint == Session.FRONTIER_CAPI_PATH_SHIPYARD and self.data.get('lastStarport'): - # All the other endpoints may or may not have a lastStarport, but definitely wont have valid data + # All the other endpoints may or may not have a lastStarport, but definitely won't have valid data # for this check, which means it'll just make noise for no reason while we're working on other things self.check_modules_ships() @@ -556,7 +562,8 @@ class EDMCCAPIRequest(EDMCCAPIReturn): REQUEST_WORKER_SHUTDOWN = '__EDMC_WORKER_SHUTDOWN' def __init__( - self, endpoint: str, query_time: int, + self, capi_host: str, endpoint: str, + query_time: int, tk_response_event: Optional[str] = None, play_sound: bool = False, auto_update: bool = False ): @@ -564,6 +571,7 @@ class EDMCCAPIRequest(EDMCCAPIReturn): query_time=query_time, tk_response_event=tk_response_event, play_sound=play_sound, auto_update=auto_update ) + self.capi_host: str = capi_host # The CAPI host to use. self.endpoint: str = endpoint # The CAPI query to perform. @@ -605,7 +613,6 @@ class Session(object): def __init__(self) -> None: self.state = Session.STATE_INIT - self.server: Optional[str] = None self.credentials: Optional[Dict[str, Any]] = None self.requests_session: Optional[requests.Session] = None self.auth: Optional[Auth] = None @@ -679,7 +686,6 @@ class Session(object): self.close() self.credentials = credentials - self.server = self.credentials['beta'] and SERVER_BETA or SERVER_LIVE self.state = Session.STATE_INIT self.auth = Auth(self.credentials['cmdr']) @@ -743,17 +749,24 @@ class Session(object): """Worker thread that performs actual CAPI queries.""" logger.debug('CAPI worker thread starting') - def capi_single_query( # noqa: CCR001 - capi_endpoint: str, timeout: int = capi_default_requests_timeout + def capi_single_query( + capi_host: str, + capi_endpoint: str, + timeout: int = capi_default_requests_timeout ) -> CAPIData: """ Perform a *single* CAPI endpoint query within the thread worker. + :param capi_host: CAPI host to query. :param capi_endpoint: An actual Frontier CAPI endpoint to query. :param timeout: requests query timeout to use. :return: The resulting CAPI data, of type CAPIData. """ capi_data: CAPIData + if capi_host == SERVER_LEGACY: + logger.warning("Dropping CAPI request because this is the Legacy galaxy") + return capi_data + try: logger.trace_if('capi.worker', 'Sending HTTP request...') if conf_module.capi_pretend_down: @@ -764,7 +777,7 @@ class Session(object): # This is one-shot conf_module.capi_debug_access_token = None - r = self.requests_session.get(self.server + capi_endpoint, timeout=timeout) # type: ignore + r = self.requests_session.get(capi_host + capi_endpoint, timeout=timeout) # type: ignore logger.trace_if('capi.worker', '... got result...') r.raise_for_status() # Typically 403 "Forbidden" on token expiry @@ -772,7 +785,7 @@ class Session(object): # r.status_code = 401 # raise requests.HTTPError capi_json = r.json() - capi_data = CAPIData(capi_json, capi_endpoint) + capi_data = CAPIData(capi_json, capi_host, capi_endpoint) self.capi_raw_data.record_endpoint( capi_endpoint, r.content.decode(encoding='utf-8'), datetime.datetime.utcnow() @@ -818,7 +831,9 @@ class Session(object): return capi_data - def capi_station_queries(timeout: int = capi_default_requests_timeout) -> CAPIData: # noqa: CCR001 + def capi_station_queries( # noqa: CCR001 + capi_host: str, timeout: int = capi_default_requests_timeout + ) -> CAPIData: """ Perform all 'station' queries for the caller. @@ -831,7 +846,7 @@ class Session(object): :param timeout: requests timeout to use. :return: CAPIData instance with what we retrieved. """ - station_data = capi_single_query(self.FRONTIER_CAPI_PATH_PROFILE, timeout=timeout) + station_data = capi_single_query(capi_host, self.FRONTIER_CAPI_PATH_PROFILE, timeout=timeout) if not station_data['commander'].get('docked') and not monitor.state['OnFoot']: return station_data @@ -872,7 +887,7 @@ class Session(object): last_starport_id = int(last_starport.get('id')) if services.get('commodities'): - market_data = capi_single_query(self.FRONTIER_CAPI_PATH_MARKET, timeout=timeout) + market_data = capi_single_query(capi_host, self.FRONTIER_CAPI_PATH_MARKET, timeout=timeout) if last_starport_id != int(market_data['id']): logger.warning(f"{last_starport_id!r} != {int(market_data['id'])!r}") raise ServerLagging() @@ -882,7 +897,7 @@ class Session(object): station_data['lastStarport'].update(market_data) if services.get('outfitting') or services.get('shipyard'): - shipyard_data = capi_single_query(self.FRONTIER_CAPI_PATH_SHIPYARD, timeout=timeout) + shipyard_data = capi_single_query(capi_host, self.FRONTIER_CAPI_PATH_SHIPYARD, timeout=timeout) if last_starport_id != int(shipyard_data['id']): logger.warning(f"{last_starport_id!r} != {int(shipyard_data['id'])!r}") raise ServerLagging() @@ -909,10 +924,10 @@ class Session(object): capi_data: CAPIData try: if query.endpoint == self._CAPI_PATH_STATION: - capi_data = capi_station_queries() + capi_data = capi_station_queries(query.capi_host) else: - capi_data = capi_single_query(self.FRONTIER_CAPI_PATH_PROFILE) + capi_data = capi_single_query(query.capi_host, self.FRONTIER_CAPI_PATH_PROFILE) except Exception as e: self.capi_response_queue.put( @@ -936,7 +951,7 @@ class Session(object): ) # If the query came from EDMC.(py|exe) there's no tk to send an - # event too, so assume it will be polling there response queue. + # event too, so assume it will be polling the response queue. if query.tk_response_event is not None: logger.trace_if('capi.worker', 'Sending <>') self.tk_master.event_generate('<>') @@ -947,6 +962,7 @@ class Session(object): """Ask the CAPI query thread to finish.""" self.capi_request_queue.put( EDMCCAPIRequest( + capi_host='', endpoint=EDMCCAPIRequest.REQUEST_WORKER_SHUTDOWN, query_time=int(time.time()) ) @@ -964,10 +980,15 @@ class Session(object): :param play_sound: Whether the app should play a sound on error. :param auto_update: Whether this request was triggered automatically. """ + capi_host = self.capi_host_for_galaxy() + if not capi_host: + return + # Ask the thread worker to perform all three queries logger.trace_if('capi.worker', 'Enqueueing request') self.capi_request_queue.put( EDMCCAPIRequest( + capi_host=capi_host, endpoint=self._CAPI_PATH_STATION, tk_response_event=tk_response_event, query_time=query_time, @@ -1048,6 +1069,28 @@ class Session(object): indent=2, sort_keys=True, separators=(',', ': ')).encode('utf-8')) + + def capi_host_for_galaxy(self) -> str: + """ + Determine the correct CAPI host. + + This is based on the current state of beta and game galaxy. + + :return: The required CAPI host. + """ + if self.credentials is None: + # Can't tell if beta or not + return '' + + if self.credentials['beta']: + return SERVER_BETA + + if monitor.is_live_galaxy(): + return SERVER_LIVE + + # return SERVER_LEGACY # Not Yet + logger.warning("Dropping CAPI request because this is the Legacy galaxy, which is not yet supported") + return "" ###################################################################### diff --git a/plugins/eddn.py b/plugins/eddn.py index 5af9b6e6..c7e36756 100644 --- a/plugins/eddn.py +++ b/plugins/eddn.py @@ -41,6 +41,7 @@ from typing import Tuple, Union import requests +import companion import edmc_data import killswitch import myNotebook as nb # noqa: N813 @@ -610,7 +611,7 @@ class EDDN: logger.debug('Done.') - def export_commodities(self, data: Mapping[str, Any], is_beta: bool) -> None: # noqa: CCR001 + def export_commodities(self, data: CAPIData, is_beta: bool) -> None: # noqa: CCR001 """ Update EDDN with the commodities on the current (lastStarport) station. @@ -678,7 +679,12 @@ class EDDN: self.send_message(data['commander']['name'], { '$schemaRef': f'https://eddn.edcd.io/schemas/commodity/3{"/test" if is_beta else ""}', 'message': message, - 'header': self.standard_header(game_version='CAPI-market', game_build=''), + 'header': self.standard_header( + game_version=self.capi_gameversion_from_host_endpoint( + data.source_host, companion.Session.FRONTIER_CAPI_PATH_MARKET + ), + game_build='' + ), }) this.commodities = commodities @@ -772,7 +778,12 @@ class EDDN: ('modules', outfitting), ('odyssey', this.odyssey), ]), - 'header': self.standard_header(game_version='CAPI-shipyard', game_build=''), + 'header': self.standard_header( + game_version=self.capi_gameversion_from_host_endpoint( + data.source_host, companion.Session.FRONTIER_CAPI_PATH_SHIPYARD + ), + game_build='' + ), }) this.outfitting = (horizons, outfitting) @@ -817,7 +828,12 @@ class EDDN: ('ships', shipyard), ('odyssey', this.odyssey), ]), - 'header': self.standard_header(game_version='CAPI-shipyard', game_build=''), + 'header': self.standard_header( + game_version=self.capi_gameversion_from_host_endpoint( + data.source_host, companion.Session.FRONTIER_CAPI_PATH_SHIPYARD + ), + game_build='' + ), }) this.shipyard = (horizons, shipyard) @@ -1467,7 +1483,7 @@ class EDDN: return None def export_capi_fcmaterials( - self, data: Mapping[str, Any], is_beta: bool, horizons: bool + self, data: CAPIData, is_beta: bool, horizons: bool ) -> Optional[str]: """ Send CAPI-sourced 'onfootmicroresources' data on `fcmaterials/1` schema. @@ -1519,7 +1535,11 @@ class EDDN: msg = { '$schemaRef': f'https://eddn.edcd.io/schemas/fcmaterials_capi/1{"/test" if is_beta else ""}', 'message': entry, - 'header': self.standard_header(game_version='CAPI-market', game_build=''), + 'header': self.standard_header( + game_version=self.capi_gameversion_from_host_endpoint( + data.source_host, companion.Session.FRONTIER_CAPI_PATH_MARKET + ), game_build='' + ), } this.eddn.send_message(data['commander']['name'], msg) @@ -1825,9 +1845,47 @@ class EDDN: match = self.CANONICALISE_RE.match(item) return match and match.group(1) or item + def capi_gameversion_from_host_endpoint(self, capi_host: str, capi_endpoint: str) -> str: + """ + Return the correct CAPI gameversion string for the given host/endpoint. + + :param capi_host: CAPI host used. + :param capi_endpoint: CAPI endpoint queried. + :return: CAPI gameversion string. + """ + gv = '' + ####################################################################### + # Base string + if capi_host == companion.SERVER_LIVE or capi_host == companion.SERVER_BETA: + gv = 'CAPI-Live-' + + elif capi_host == companion.SERVER_LEGACY: + gv = 'CAPI-Legacy-' + + else: + # Technically incorrect, but it will inform Listeners + logger.error(f"{capi_host=} lead to bad gameversion") + gv = 'CAPI-UNKNOWN-' + ####################################################################### + + ####################################################################### + # endpoint + if capi_endpoint == companion.Session.FRONTIER_CAPI_PATH_MARKET: + gv += 'market' + + elif capi_endpoint == companion.Session.FRONTIER_CAPI_PATH_SHIPYARD: + gv += 'shipyard' + + else: + # Technically incorrect, but it will inform Listeners + logger.error(f"{capi_endpoint=} lead to bad gameversion") + gv += 'UNKNOWN' + ####################################################################### + + return gv + # Plugin callbacks - def plugin_start3(plugin_dir: str) -> str: """ Start this plugin.