Skip to content

Commit

Permalink
Merge branch 'feature/issue-287' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
aussig committed Dec 31, 2024
2 parents b3b5d69 + 752d9ab commit afc88b2
Show file tree
Hide file tree
Showing 14 changed files with 454 additions and 30 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@

### New Features:

* Objectives! If you use the API to connect to a server that supports them (API ≥ v1.6.0) then your squadron or group can define shared missions that multiple CMDRs can work towards. Missions can be of various types (for example - `win a war` or `boost a faction`) and each mission can have one or more targets (for example - `win xx space CZs` or `generate yyy CR in trade profit`).
* Highlight conflict states in the on-screen activity window: Elections in orange and wars in red.

### Bug Fixes:

* The tick time was not being sent in /event API calls.

### API Changes ([v1.6](https://studio-ws.apicur.io/sharing/xxxxxxxxxxxxxxxxxxxxxxxxxxxx)):

* New `/objectives` endpoint.


## v4.2.0 - 2024-12-22

Expand Down
17 changes: 16 additions & 1 deletion bgstally/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from bgstally.missionlog import MissionLog
from bgstally.state import State
from bgstally.tick import Tick
from bgstally.utils import _, __
from bgstally.utils import _, __, add_dicts
from thirdparty.colors import *

DATETIME_FORMAT_ACTIVITY = "%Y-%m-%dT%H:%M:%S.%fZ"
Expand Down Expand Up @@ -107,6 +107,17 @@
}


class SystemActivity(dict):
"""
Utility class for working with system activity. Adds some accessor methods for ease of use.
"""
def get_trade_profit_total(self):
try:
return sum(int(d['profit']) for d in dict.__getitem__(self, 'TradeSell'))
except KeyError:
return 0


class Activity:
"""
User activity for a single tick
Expand Down Expand Up @@ -1390,6 +1401,10 @@ def __repr__(self):
return f"{self.tick_id} ({self.tick_time}): {self._as_dict()}"


def __add__(self, other):
self.systems = add_dicts(self.systems, other.systems)
return self

# Deep copy override function - we don't deep copy any class references, just data

def __deepcopy__(self, memo):
Expand Down
28 changes: 26 additions & 2 deletions bgstally/activitymanager.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from copy import deepcopy
from datetime import datetime, timedelta
from os import listdir, mkdir, path, remove, rename

from config import config

from bgstally.activity import Activity
from bgstally.constants import FILE_SUFFIX
from bgstally.debug import Debug
Expand Down Expand Up @@ -60,6 +59,31 @@ def get_previous_activities(self) -> list[Activity]:
return self.activity_data[1:]


def query_activity(self, start_date: datetime) -> Activity:
"""Aggregate all activity back to and including the tick encompassing a given start date
Args:
start_date (datetime): The start date
Returns:
Activity: A new Activity object containing the aggregated data.
"""
result: Activity = Activity(self.bgstally)

# Iterate activities (already kept sorted by date, newest first)
for activity in self.activity_data:

result = result + activity

if activity.tick_time <= start_date:
# Once we reach an activity that is older than our start date, stop. Note that we have INCLUDED the
# activity which overlaps with the start_date
break

return result



def new_tick(self, tick: Tick, forced: bool) -> bool:
"""
New tick detected, duplicate the current Activity object or ignore if it's older than current tick.
Expand Down
101 changes: 83 additions & 18 deletions bgstally/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@
from bgstally.requestmanager import BGSTallyRequest
from bgstally.utils import get_by_path, string_to_alphanumeric

API_VERSION = "1.5.0"
API_VERSION = "1.6.0"

ENDPOINT_ACTIVITIES = "activities" # Used as both the dict key and default path
ENDPOINT_DISCOVERY = "discovery" # Used as the path
ENDPOINT_EVENTS = "events" # Used as both the dict key and default path
ENDPOINT_ACTIVITIES = "activities" # Used as both the dict key and default path
ENDPOINT_DISCOVERY = "discovery" # Used as the path
ENDPOINT_EVENTS = "events" # Used as both the dict key and default path
ENDPOINT_OBJECTIVES = "objectives" # Used as both the dict key and default path

NAME_DEFAULT = "This server has not supplied a name."
VERSION_DEFAULT = API_VERSION
DESCRIPTION_DEFAULT = "This server has not supplied a description."
ENDPOINTS_DEFAULT = {ENDPOINT_ACTIVITIES: {'path': ENDPOINT_ACTIVITIES}, ENDPOINT_EVENTS: {'path': ENDPOINT_EVENTS}}
ENDPOINTS_DEFAULT = {ENDPOINT_ACTIVITIES: {'path': ENDPOINT_ACTIVITIES},
ENDPOINT_EVENTS: {'path': ENDPOINT_EVENTS},
ENDPOINT_OBJECTIVES: {'path': ENDPOINT_OBJECTIVES}}
EVENTS_FILTER_DEFAULTS = {'ApproachSettlement': {}, 'CarrierJump': {}, 'CommitCrime': {}, 'Died': {}, 'Docked': {}, 'FactionKillBond': {},
'FSDJump': {}, 'Location': {}, 'MarketBuy': {}, 'MarketSell': {}, 'MissionAbandoned': {}, 'MissionAccepted': {}, 'MissionCompleted': {},
'MissionFailed': {}, 'MultiSellExplorationData': {}, 'RedeemVoucher': {}, 'SellExplorationData': {}, 'StartUp': {}}
Expand All @@ -31,6 +34,7 @@
HEADER_APIVERSION = "apiversion"
TIME_ACTIVITIES_WORKER_PERIOD_S = 60
TIME_EVENTS_WORKER_PERIOD_S = 5
TIME_OBJECTIVES_WORKER_PERIOD_S = 5
BATCH_EVENTS_MAX_SIZE = 10


Expand All @@ -39,7 +43,7 @@ class API:
Handles data for an API.
"""

def __init__(self, bgstally, data:list = None):
def __init__(self, bgstally, data: list = None):
"""
Instantiate
"""
Expand All @@ -50,20 +54,21 @@ def __init__(self, bgstally, data:list = None):
self.from_dict(data)
else:
# Default user state
self.url:str = ""
self.key:str = ""
self.activities_enabled:bool = True
self.events_enabled:bool = True
self.user_approved:bool = False
self.url: str = ""
self.key: str = ""
self.activities_enabled: bool = True
self.events_enabled: bool = True
self.objectives_enabled: bool = True
self.user_approved: bool = False

# Default API discovery state. Overridden by response from /discovery endpoint if it exists
self._revert_discovery_to_defaults()

# Used to store a single dict containing BGS activity when it's been updated.
self.activity:dict = None
self.activity: dict = None

# Events queue is used to batch up events API messages. All batched messages are sent when the worker works.
self.events_queue:Queue = Queue()
self.events_queue: Queue = Queue()

self.activities_thread: Thread = Thread(target=self._activities_worker, name=f"BGSTally Activities API Worker ({self.url})")
self.activities_thread.daemon = True
Expand All @@ -73,6 +78,10 @@ def __init__(self, bgstally, data:list = None):
self.events_thread.daemon = True
self.events_thread.start()

self.objectives_thread: Thread = Thread(target=self._objectives_worker, name=f"BGSTally Objectives API Worker ({self.url})")
self.objectives_thread.daemon = True
self.objectives_thread.start()

self.discover(self.discovery_received)


Expand All @@ -86,6 +95,7 @@ def as_dict(self) -> dict:
'key': self.key,
'activities_enabled': self.activities_enabled,
'events_enabled': self.events_enabled,
'objectives_enabled': self.objectives_enabled,
'user_approved': self.user_approved,

# Discovery state
Expand All @@ -102,11 +112,12 @@ def from_dict(self, data: dict):
Populate our user and discovery state from a dict
"""
# User state
self.url: str = data['url']
self.key: str = string_to_alphanumeric(data['key'])[:128]
self.activities_enabled: bool = data['activities_enabled']
self.events_enabled: bool = data['events_enabled']
self.user_approved: bool = data['user_approved']
self.url: str = data.get('url', "")
self.key: str = string_to_alphanumeric(data.get('key', ""))[:128]
self.activities_enabled: bool = data.get('activities_enabled', True)
self.events_enabled: bool = data.get('events_enabled', True)
self.objectives_enabled: bool = data.get('objectives_enabled', True)
self.user_approved: bool = data.get('user_approved', False)

# Discovery state
self.name: str = data['name']
Expand Down Expand Up @@ -191,6 +202,13 @@ def send_event(self, event:dict):
self.events_queue.put(event)


def get_objectives(self) -> dict:
"""Fetch objectives from the server
Returns:
dict: _description_
"""

def _revert_discovery_to_defaults(self):
"""
Revert all API information to default values
Expand Down Expand Up @@ -282,6 +300,53 @@ def _events_worker(self) -> None:
sleep(max(int(get_by_path(self.endpoints, [ENDPOINT_EVENTS, 'min_period'], 0)), TIME_EVENTS_WORKER_PERIOD_S))


def _objectives_worker(self) -> None:
"""
Handle objectives API. Simply fetch the latest objectives
"""
Debug.logger.debug("Starting Objectives API Worker...")

while True:
# Need to check settings every time in case the user has changed them
if self.user_approved \
and self.objectives_enabled \
and ENDPOINT_OBJECTIVES in self.endpoints \
and self.bgstally.request_manager.url_valid(self.url):

# Refresh our local list of objectives
url: str = self.url + get_by_path(self.endpoints, [ENDPOINT_OBJECTIVES, 'path'], ENDPOINT_OBJECTIVES)
self.bgstally.request_manager.queue_request(url, RequestMethod.GET, headers=self._get_headers(), callback=self._objectives_received)

sleep(max(int(get_by_path(self.endpoints, [ENDPOINT_OBJECTIVES, 'min_period'], 0)), TIME_OBJECTIVES_WORKER_PERIOD_S))


def _objectives_received(self, success: bool, response: Response, request: BGSTallyRequest):
"""Received objectives from server
Args:
success (bool): True if the request was successful
response (Response): The Response object
request (BGSTallyRequest): The original BGSTallyRequest object
"""
if not success:
Debug.logger.info(f"Unable to receive objectives")
return

objectives_data: list = None

try:
objectives_data: list = response.json()
except JSONDecodeError:
Debug.logger.warning(f"Objectives data is invalid (not valid JSON)")
return

if not isinstance(objectives_data, list):
Debug.logger.warning(f"Objectives data is invalid (not a list)")
return

self.bgstally.objectives_manager.set_objectives(objectives_data)


def _get_headers(self) -> dict:
"""
Get the API headers
Expand Down
8 changes: 4 additions & 4 deletions bgstally/apimanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from bgstally.activity import Activity
from bgstally.api import API
from bgstally.constants import DATETIME_FORMAT_JOURNAL, FOLDER_OTHER_DATA
from bgstally.constants import DATETIME_FORMAT_API, FOLDER_OTHER_DATA
from bgstally.debug import Debug
from bgstally.utils import get_by_path

Expand Down Expand Up @@ -85,8 +85,8 @@ def _build_api_activity(self, activity:Activity, cmdr:str):
api_activity:dict = {
'cmdr': cmdr,
'tickid': activity.tick_id,
'ticktime': activity.tick_time.strftime(DATETIME_FORMAT_JOURNAL),
'timestamp': datetime.utcnow().strftime(DATETIME_FORMAT_JOURNAL),
'ticktime': activity.tick_time.strftime(DATETIME_FORMAT_API),
'timestamp': datetime.utcnow().strftime(DATETIME_FORMAT_API),
'systems': []
}

Expand Down Expand Up @@ -265,7 +265,7 @@ def _build_api_event(self, event:dict, activity:Activity, cmdr:str, mission:dict
# BGS-Tally specific global enhancements
event['cmdr'] = cmdr
event['tickid'] = activity.tick_id
event['ticktime'] = activity.tick_time.strftime(DATETIME_FORMAT_JOURNAL)
event['ticktime'] = activity.tick_time.strftime(DATETIME_FORMAT_API)

# Other global enhancements
if 'StationFaction' not in event: event['StationFaction'] = {'Name': self.bgstally.state.station_faction}
Expand Down
2 changes: 2 additions & 0 deletions bgstally/bgstally.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from bgstally.formattermanager import ActivityFormatterManager
from bgstally.market import Market
from bgstally.missionlog import MissionLog
from bgstally.objectivesmanager import ObjectivesManager
from bgstally.overlay import Overlay
from bgstally.requestmanager import RequestManager
from bgstally.state import State
Expand Down Expand Up @@ -90,6 +91,7 @@ def plugin_start(self, plugin_dir: str):
self.update_manager: UpdateManager = UpdateManager(self)
self.ui: UI = UI(self)
self.formatter_manager: ActivityFormatterManager = ActivityFormatterManager(self)
self.objectives_manager: ObjectivesManager = ObjectivesManager(self)
self.thread: Thread = Thread(target=self._worker, name="BGSTally Main worker")
self.thread.daemon = True
self.thread.start()
Expand Down
1 change: 1 addition & 0 deletions bgstally/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ class CmdrInteractionReason(int, Enum):


DATETIME_FORMAT_JOURNAL: str = "%Y-%m-%dT%H:%M:%SZ"
DATETIME_FORMAT_API: str = "%Y-%m-%dT%H:%M:%SZ"
FILE_SUFFIX: str = ".json"
FOLDER_ASSETS: str = "assets"
FOLDER_BACKUPS: str = "backups"
Expand Down
Loading

0 comments on commit afc88b2

Please sign in to comment.