1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-14 00:07:14 +03:00

Merge pull request #1773 from aussig/feature/847/capi-fleetcarrier

Feature/847/capi fleetcarrier
This commit is contained in:
Athanasius 2022-12-30 16:15:37 +00:00 committed by GitHub
commit 74b070ffe9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 254 additions and 35 deletions

View File

@ -476,6 +476,8 @@ class AppWindow(object):
def __init__(self, master: tk.Tk): # noqa: C901, CCR001 # TODO - can possibly factor something out
self.capi_query_holdoff_time = config.get_int('querytime', default=0) + companion.capi_query_cooldown
self.capi_fleetcarrier_query_holdoff_time = config.get_int('fleetcarrierquerytime', default=0) \
+ companion.capi_fleetcarrier_query_cooldown
self.w = master
self.w.title(applongname)
@ -916,6 +918,9 @@ class AppWindow(object):
def login(self):
"""Initiate CAPI/Frontier login and set other necessary state."""
should_return: bool
new_data: dict[str, Any]
should_return, new_data = killswitch.check_killswitch('capi.auth', {})
if should_return:
logger.warning('capi.auth has been disabled via killswitch. Returning.')
@ -1086,6 +1091,54 @@ class AppWindow(object):
play_sound=play_sound
)
def capi_request_fleetcarrier_data(self, event=None) -> None:
"""
Perform CAPI fleetcarrier data retrieval and associated actions.
This is triggered by certain FleetCarrier journal events
:param event: Tk generated event details.
"""
logger.trace_if('capi.worker', 'Begin')
should_return: bool
new_data: dict[str, Any]
should_return, new_data = killswitch.check_killswitch('capi.request.fleetcarrier', {})
if should_return:
logger.warning('capi.fleetcarrier has been disabled via killswitch. Returning.')
self.status['text'] = 'CAPI fleetcarrier disabled by killswitch'
hotkeymgr.play_bad()
return
if not monitor.cmdr:
logger.trace_if('capi.worker', 'Aborting Query: Cmdr unknown')
# LANG: CAPI queries aborted because Cmdr name is unknown
self.status['text'] = _('CAPI query aborted: Cmdr name 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 companion.session.retrying:
if time() < self.capi_fleetcarrier_query_holdoff_time: # Was invoked while in cooldown
logger.debug('CAPI fleetcarrier query aborted, too soon since last request')
return
# LANG: Status - Attempting to retrieve data from Frontier CAPI
self.status['text'] = _('Fetching data...')
self.w.update_idletasks()
query_time = int(time())
logger.trace_if('capi.worker', 'Requesting fleetcarrier data')
config.set('fleetcarrierquerytime', query_time)
logger.trace_if('capi.worker', 'Calling companion.session.fleetcarrier')
companion.session.fleetcarrier(
query_time=query_time, tk_response_event=self._CAPI_RESPONSE_TK_EVENT_NAME
)
def capi_handle_response(self, event=None): # noqa: C901, CCR001
"""Handle the resulting data from a CAPI query."""
logger.trace_if('capi.worker', 'Handling response')
@ -1110,8 +1163,32 @@ class AppWindow(object):
logger.error(msg)
raise ValueError(msg)
if capi_response.capi_data.source_endpoint == companion.session.FRONTIER_CAPI_PATH_FLEETCARRIER:
# Fleetcarrier CAPI response
# Validation
if 'name' not in capi_response.capi_data:
# LANG: No data was returned for the fleetcarrier from the Frontier CAPI
err = self.status['text'] = _('CAPI: No fleetcarrier data returned')
elif not capi_response.capi_data.get('name', {}).get('callsign'):
# LANG: We didn't have the fleetcarrier callsign when we should have
err = self.status['text'] = _("CAPI: Fleetcarrier data incomplete") # Shouldn't happen
else:
if __debug__: # Recording
companion.session.dump_capi_data(capi_response.capi_data)
err = plug.notify_capi_fleetcarrierdata(capi_response.capi_data)
self.status['text'] = err and err or ''
if err:
play_bad = True
self.capi_fleetcarrier_query_holdoff_time = capi_response.query_time \
+ companion.capi_fleetcarrier_query_cooldown
# Other CAPI response
# Validation
if 'commander' not in capi_response.capi_data:
elif 'commander' not in capi_response.capi_data:
# This can happen with EGS Auth if no commander created yet
# LANG: No data was returned for the commander from the Frontier CAPI
err = self.status['text'] = _('CAPI: No commander data returned')
@ -1234,6 +1311,9 @@ class AppWindow(object):
if err:
play_bad = True
should_return: bool
new_data: dict[str, Any]
should_return, new_data = killswitch.check_killswitch('capi.request./market', {})
if should_return:
logger.warning("capi.request./market has been disabled by killswitch. Returning.")
@ -1494,11 +1574,19 @@ class AppWindow(object):
elif entry['event'] == 'Disembark' and entry.get('Taxi') and entry.get('OnStation'):
auto_update = True
should_return: bool
new_data: dict[str, Any]
if auto_update:
should_return, new_data = killswitch.check_killswitch('capi.auth', {})
if not should_return:
self.w.after(int(SERVER_RETRY * 1000), self.capi_request_data)
if entry['event'] in ('CarrierBuy', 'CarrierStats') and config.get_bool('capi_fleetcarrier'):
should_return, new_data = killswitch.check_killswitch('capi.request.fleetcarrier', {})
if not should_return:
self.w.after(int(SERVER_RETRY * 1000), self.capi_request_fleetcarrier_data)
if entry['event'] == 'ShutDown':
# Enable WinSparkle automatic update checks
# NB: Do this blindly, in case option got changed whilst in-game

View File

@ -99,8 +99,8 @@ liable to change without notice.
`from prefs import prefsVersion` - to allow for versioned preferences.
`from companion import CAPIData, SERVER_LIVE, SERVER_LEGACY, SERVER_BETA` -
`CAPIData` is the actual type of `data` as passed into `cmdr_data()` and
`cmdr_data_legacy()`.
`CAPIData` is the actual type of `data` as passed into `cmdr_data()`,
`cmdr_data_legacy()` and `capi_fleetcarrier()`.
See [Commander Data from Frontier CAPI](#commander-data-from-frontier-capi))
for further information.
@ -561,9 +561,9 @@ See [Avoiding potential pitfalls](#avoiding-potential-pitfalls).
### Events
Once you have created your plugin and EDMarketConnector has loaded it there
are four other functions you can define to be notified by EDMarketConnector
are five other functions you can define to be notified by EDMarketConnector
when something happens: `journal_entry()`, `journal_entry_cqc()`,
`dashboard_entry()` and `cmdr_data()`.
`dashboard_entry()`, `cmdr_data()` and `capi_fleetcarrier()`.
Your events all get called on the main Tkinter loop so be sure not to block for
very long or the app will appear to freeze. If you have a long running
@ -908,11 +908,11 @@ constants.
---
### Commander Data from Frontier CAPI
If a plugin has a `cmdr_data()` function it gets called when the application
has just fetched fresh Cmdr and station data from Frontier's CAPI servers,
**but not for the Legacy galaxy**. See `cmdr_data_legacy()` below for Legacy
data handling.
### Data from Frontier CAPI
#### Commander, Market and Shipyard Data
If a plugin has a `cmdr_data()` function it gets called when the application has just fetched fresh CMDR, station and shipyard data from Frontier's CAPI servers, **but not for the Legacy galaxy**. See `cmdr_data_legacy()` below for Legacy data handling.
```python
from companion import CAPIData, SERVER_LIVE, SERVER_LEGACY, SERVER_BETA
@ -940,29 +940,59 @@ def cmdr_data(data, is_beta):
| :-------- | :--------------: | :------------------------------------------------------------------------------------------------------- |
| `data` | `CAPIData` | `/profile` API response, with `/market` and `/shipyard` added under the keys `marketdata` and `shipdata` |
| `is_beta` | `bool` | If the game is currently in beta |
`CAPIData` is a class, which you can `from companion import CAPIDATA`, and is
based on `UserDict`. The actual data from CAPI queries is thus accessible
via python's normal `data['key']` syntax. However, being a class, it can also
have extra properties, such as `source_host`, as shown above. Plugin authors
are free to use *that* property, **but MUST NOT rely on any other extra
properties present in `CAPIData`, they are for internal use only.**
The contents of `data` will always have at least the data returned by a CAPI
`/profile` query. If the player is docked at a station, and the relevant
services are available then the `lastStarport` key's value will have been
augmented with `/market` and/or `/shipyard` data. **But do not assume this
will always be the case**.
#### Fleet Carrier Data
If a plugin has a `capi_fleetcarrier()` function it gets called when the application has just fetched fresh Fleetcarrier data from Frontier's CAPI servers. This is done when `CarrierBuy`or `CarrierStats` events are detected in the Player Journal. To avoid flooding Frontier's CAPI server, a throttle is applied to ensure a significant interval between requests (currently 15 mins). Also be aware that calls to the `/fleetcarrier` CAPI endpoint have been reported to take a very long time to return, potentially up to 20 minutes. Delays in responses from this endpoint could delay other CAPI queries.
```python
from companion import CAPIData, SERVER_LIVE, SERVER_LEGACY, SERVER_BETA
def capi_fleetcarrier(data):
"""
We have new data on our Fleet Carrier
"""
if data.get('name') is None or data['name'].get('callsign') is None:
raise ValueError("this isn't possible")
logger.info(data['name']['callsign'])
# Determining source galaxy for the data
if data.source_host == SERVER_LIVE:
...
elif data.source_host == SERVER_BETA:
...
elif data.source_host == SERVER_LEGACY:
...
```
| Parameter | Type | Description |
| :-------- | :--------------: | :------------------------------------------------------------------------------------------------------- |
| `data` | `CAPIData` | `/fleetcarrier` API response |
#### CAPIData and Available Properties
`CAPIData` is a class, which you can `from companion import CAPIDATA`, and is based on `UserDict`. The actual data from CAPI queries is thus accessible via python's normal `data['key']` syntax. However, being a class, it can also have extra properties, such as `source_host`, as shown in the code example above.
Plugin authors are free to use the following properties of `CAPIData`, **but MUST NOT rely on any other extra properties, they are for internal use only.**
| Property | Type | Description |
| :------------- | :--------------: | :------------------------------------------------------------------------------------------------------- |
| `data` | `Dict` | The data returned by the CAPI query. For the `cmdr_data()` callback, if the player is docked at a station, and the relevant services are available then the `lastStarport` key's value will have been augmented with `/market` and/or `/shipyard` data. **Do not assume this will always be the case**. |
| `source_host` | `str` | `SERVER_LIVE` \| `SERVER_BETA` \| `SERVER_LEGACY` the current galaxy mode. |
| `request_cmdr` | `str` | The name of the active CMDR _at the point the request was made_. In the case of a CAPI request taking a long time to return, the user may have switched CMDR during the request, so this may be different to the current CMDR. |
See [this documentation](https://github.com/Athanasius/fd-api/blob/main/docs/FrontierDevelopments-CAPI-endpoints.md) for details of the expected content structure and data for CAPI queries.
If there is a killswitch in effect for some of the CAPI endpoints, then the
data passed to this function might not be as complete as you expect. Code
defensively.
#### CAPI data for Legacy
When CAPI data has been retrieved from the separate CAPI host for the Legacy
galaxy, because the Journal gameversion indicated the player is playing/last
played in that galaxy, a different function will be called,
`cmdr_data_legacy()`.
When CAPI data has been retrieved from the separate CAPI host for the Legacy galaxy, because the Journal gameversion indicated the player is playing / last played in that galaxy, a different function will be called, `cmdr_data_legacy()`. Note that there is no legacy equivalent to `capi_fleetcarrier()`, so always use the `source_host` property to determine the user's galaxy.
```python
def cmdr_data_legacy(data, is_beta):
@ -1082,7 +1112,7 @@ time after the corresponding `journal_entry()` event.
## Error messages
You can display an error in EDMarketConnector's status area by returning a
string from your `journal_entry()`, `dashboard_entry()` or `cmdr_data()`
string from your `journal_entry()`, `dashboard_entry()`, `cmdr_data()` or `capi_fleetcarrier()`
function, or asynchronously (e.g. from a "worker" thread that is performing a
long-running operation) by calling `plug.show_error()`. Either method will
cause the "bad" sound to be played (unless the user has muted sound).
@ -1311,7 +1341,7 @@ versions of EDMarketConnector:
`Settings` > `Plugins` tab.
- Check that callback functions `plugin_prefs`, `prefs_changed`,
`journal_entry`, `dashboard_entry` and `cmdr_data`, if used, are declared
`journal_entry`, `dashboard_entry`, `cmdr_data` and `capi_fleetcarrier`, if used, are declared
with the correct number of arguments. Older versions of this app were
tolerant of missing arguments in these function declarations.

View File

@ -46,7 +46,9 @@ else:
capi_query_cooldown = 60 # Minimum time between (sets of) CAPI queries
capi_fleetcarrier_query_cooldown = 60 * 15 # Minimum time between CAPI fleetcarrier queries
capi_default_requests_timeout = 10
capi_fleetcarrier_requests_timeout = 60
auth_timeout = 30 # timeout for initial auth
# Used by both class Auth and Session
@ -66,7 +68,8 @@ class CAPIData(UserDict):
self,
data: Union[str, Dict[str, Any], 'CAPIData', None] = None,
source_host: Optional[str] = None,
source_endpoint: Optional[str] = None
source_endpoint: Optional[str] = None,
request_cmdr: Optional[str] = None
) -> None:
if data is None:
super().__init__()
@ -81,6 +84,7 @@ class CAPIData(UserDict):
self.source_host = source_host
self.source_endpoint = source_endpoint
self.request_cmdr = request_cmdr
if source_endpoint is None:
return
@ -326,7 +330,10 @@ class Auth(object):
"""
logger.debug(f'Trying for "{self.cmdr}"')
should_return, _ = killswitch.check_killswitch('capi.auth', {})
should_return: bool
new_data: dict[str, Any]
should_return, new_data = killswitch.check_killswitch('capi.auth', {})
if should_return:
logger.warning('capi.auth has been disabled via killswitch. Returning.')
return None
@ -612,6 +619,8 @@ class Session(object):
FRONTIER_CAPI_PATH_PROFILE = '/profile'
FRONTIER_CAPI_PATH_MARKET = '/market'
FRONTIER_CAPI_PATH_SHIPYARD = '/shipyard'
FRONTIER_CAPI_PATH_FLEETCARRIER = '/fleetcarrier'
# This is a dummy value, to signal to Session.capi_query_worker that we
# the 'station' triplet of queries.
_CAPI_PATH_STATION = '_edmc_station'
@ -663,7 +672,10 @@ class Session(object):
:return: True if login succeeded, False if re-authorization initiated.
"""
should_return, _ = killswitch.check_killswitch('capi.auth', {})
should_return: bool
new_data: dict[str, Any]
should_return, new_data = killswitch.check_killswitch('capi.auth', {})
if should_return:
logger.warning('capi.auth has been disabled via killswitch. Returning.')
return False
@ -781,7 +793,10 @@ class Session(object):
:return: The resulting CAPI data, of type CAPIData.
"""
capi_data: CAPIData = CAPIData()
should_return, _ = killswitch.check_killswitch('capi.request.' + capi_endpoint, {})
should_return: bool
new_data: dict[str, Any]
should_return, new_data = killswitch.check_killswitch('capi.request.' + capi_endpoint, {})
if should_return:
logger.warning(f"capi.request.{capi_endpoint} has been disabled by killswitch. Returning.")
return capi_data
@ -804,7 +819,7 @@ class Session(object):
# r.status_code = 401
# raise requests.HTTPError
capi_json = r.json()
capi_data = CAPIData(capi_json, capi_host, capi_endpoint)
capi_data = CAPIData(capi_json, capi_host, capi_endpoint, monitor.cmdr)
self.capi_raw_data.record_endpoint(
capi_endpoint, r.content.decode(encoding='utf-8'),
datetime.datetime.utcnow()
@ -956,6 +971,10 @@ class Session(object):
if query.endpoint == self._CAPI_PATH_STATION:
capi_data = capi_station_queries(query.capi_host)
elif query.endpoint == self.FRONTIER_CAPI_PATH_FLEETCARRIER:
capi_data = capi_single_query(query.capi_host, self.FRONTIER_CAPI_PATH_FLEETCARRIER,
timeout=capi_fleetcarrier_requests_timeout)
else:
capi_data = capi_single_query(query.capi_host, self.FRONTIER_CAPI_PATH_PROFILE)
@ -1027,6 +1046,35 @@ class Session(object):
auto_update=auto_update
)
)
def fleetcarrier(
self, query_time: int, tk_response_event: Optional[str] = None,
play_sound: bool = False, auto_update: bool = False
) -> None:
"""
Perform CAPI query for fleetcarrier data.
:param query_time: When this query was initiated.
:param tk_response_event: Name of tk event to generate when response queued.
: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 a fleetcarrier query
logger.trace_if('capi.worker', 'Enqueueing fleetcarrier request')
self.capi_request_queue.put(
EDMCCAPIRequest(
capi_host=capi_host,
endpoint=self.FRONTIER_CAPI_PATH_FLEETCARRIER,
tk_response_event=tk_response_event,
query_time=query_time,
play_sound=play_sound,
auto_update=auto_update
)
)
######################################################################
######################################################################

24
plug.py
View File

@ -405,6 +405,30 @@ def notify_capidata(
return error
def notify_capi_fleetcarrierdata(
data: companion.CAPIData
) -> Optional[str]:
"""
Send the latest CAPI Fleetcarrier data from the FD servers to each plugin.
:param data: The CAPIData returned in the CAPI response
:returns: Error message from the first plugin that returns one (if any)
"""
error = None
for plugin in PLUGINS:
fc_callback = plugin._get_func('capi_fleetcarrier')
if fc_callback is not None and callable(fc_callback):
try:
# Pass a copy of the CAPIData in case the callee modifies it
newerror = fc_callback(copy.deepcopy(data))
error = error or newerror
except Exception:
logger.exception(f'Plugin "{plugin.name}" failed on receiving Fleetcarrier data')
return error
def show_error(err: str) -> None:
"""
Display an error message in the status line of the main window.

View File

@ -42,7 +42,7 @@
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
import tkinter
from typing import TYPE_CHECKING, Any, Mapping, MutableMapping, Optional
from typing import TYPE_CHECKING, Any, Dict, Mapping, MutableMapping, Optional
import requests
@ -168,6 +168,9 @@ def journal_entry( # noqa: CCR001
:param state: `monitor.state`
:return: None if no error, else an error string.
"""
should_return: bool
new_entry: Dict[str, Any] = {}
should_return, new_entry = killswitch.check_killswitch('plugins.eddb.journal', entry)
if should_return:
# LANG: Journal Processing disabled due to an active killswitch

View File

@ -402,6 +402,9 @@ class EDDNSender:
:return: `True` for "now remove this message from the queue"
"""
logger.trace_if("plugin.eddn.send", "Sending message")
should_return: bool
new_data: dict[str, Any]
should_return, new_data = killswitch.check_killswitch('plugins.eddn.send', json.loads(msg))
if should_return:
logger.warning('eddn.send has been disabled via killswitch. Returning.')

View File

@ -386,6 +386,9 @@ def journal_entry( # noqa: C901, CCR001
return ''
should_return: bool
new_entry: Dict[str, Any] = {}
should_return, new_entry = killswitch.check_killswitch('plugins.inara.journal', entry, logger)
if should_return:
plug.show_error(_('Inara disabled. See Log.')) # LANG: INARA support disabled via killswitch
@ -1537,7 +1540,6 @@ def new_add_event(
def clean_event_list(event_list: List[Event]) -> List[Event]:
"""Check for killswitched events and remove or modify them as requested."""
out = []
for e in event_list:
bad, new_event = killswitch.check_killswitch(f'plugins.inara.worker.{e.name}', e.data, logger)
if bad:

View File

@ -470,6 +470,25 @@ class PreferencesDialog(tk.Toplevel):
state=tk.NORMAL if config.get_str('journaldir') else tk.DISABLED
).grid(column=2, pady=self.PADY, sticky=tk.EW, row=row.get())
# CAPI settings
self.capi_fleetcarrier = tk.BooleanVar(value=config.get_bool('capi_fleetcarrier'))
ttk.Separator(config_frame, orient=tk.HORIZONTAL).grid(
columnspan=4, padx=self.PADX, pady=self.PADY*4, sticky=tk.EW, row=row.get()
)
nb.Label(
config_frame,
text=_('CAPI Settings') # LANG: Settings > Configuration - Label for CAPI section
).grid(padx=self.PADX, sticky=tk.W, row=row.get())
nb.Checkbutton(
config_frame,
# LANG: Configuration - Enable or disable the Fleet Carrier CAPI calls
text=_('Enable Fleetcarrier CAPI Queries'),
variable=self.capi_fleetcarrier
).grid(columnspan=4, padx=self.PADX, pady=(5, 0), sticky=tk.W, row=row.get())
if sys.platform in ('darwin', 'win32'):
ttk.Separator(config_frame, orient=tk.HORIZONTAL).grid(
columnspan=4, padx=self.PADX, pady=self.PADY*4, sticky=tk.EW, row=row.get()
@ -1244,6 +1263,8 @@ class PreferencesDialog(tk.Toplevel):
else:
config.set('journaldir', logdir)
config.set('capi_fleetcarrier', self.capi_fleetcarrier.get())
if sys.platform in ('darwin', 'win32'):
config.set('hotkey_code', self.hotkey_code)
config.set('hotkey_mods', self.hotkey_mods)