Skip to content

Commit

Permalink
Merge branch 'feature/issue-288' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
aussig committed Dec 31, 2024
2 parents afc88b2 + 47aef92 commit d867726
Show file tree
Hide file tree
Showing 13 changed files with 267 additions and 79 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
### API Changes ([v1.6](https://studio-ws.apicur.io/sharing/xxxxxxxxxxxxxxxxxxxxxxxxxxxx)):

* New `/objectives` endpoint.
* `/events` endpoint: Synthetic events added for certain activities in game that the game itself doesn't log in the journal:
- `SyntheticCZ`: Sent when a Space CZ is won.
- `SyntheticCZObjective`: Sent when an objective is completed in a Space CZ (cap ship / spec ops / enemy captain / enemy correspondent).
- `SyntheticGroundCZ`: Sent when a Ground CZ is won.
- `SyntheticScenario`: Sent when a scenario is won (only Megaship scenarios for the moment, Installation scenarios cannot be tracked).


## v4.2.0 - 2024-12-22
Expand Down
178 changes: 145 additions & 33 deletions bgstally/activity.py

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion bgstally/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
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': {}}
'MissionFailed': {}, 'MultiSellExplorationData': {}, 'RedeemVoucher': {}, 'SellExplorationData': {}, 'StartUp': {},
'SyntheticCZ': {}, 'SyntheticGroundCZ': {}, 'SyntheticCZObjective': {}, 'SyntheticScenario': {}}

HEADER_APIKEY = "apikey"
HEADER_APIVERSION = "apiversion"
Expand Down
22 changes: 14 additions & 8 deletions bgstally/apimanager.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
from datetime import datetime
from datetime import UTC, datetime
from enum import Enum
from os import path

from bgstally.activity import Activity
Expand All @@ -10,7 +11,6 @@

FILENAME = "apis.json"


class APIManager:
"""
Handles a list of API objects.
Expand Down Expand Up @@ -69,11 +69,16 @@ def send_activity(self, activity:Activity, cmdr:str):
api.send_activity(api_activity)


def send_event(self, event:dict, activity:Activity, cmdr:str, mission:dict):
"""
Event has been received. Add it to the events queue.
def send_event(self, event: dict, activity: Activity, cmdr: str, mission: dict = {}):
"""Event has been received. Add it to the events queue.
Args:
event (dict): A dict containing all the event fields
activity (Activity): The activity object
cmdr (str): The CMDR name
mission (dict, optional): Information about the mission, if applicable. Defaults to {}.
"""
api_event:dict = self._build_api_event(event, activity, cmdr, mission)
api_event: dict = self._build_api_event(event, activity, cmdr, mission)
for api in self.apis:
api.send_event(api_event)

Expand All @@ -86,7 +91,7 @@ def _build_api_activity(self, activity:Activity, cmdr:str):
'cmdr': cmdr,
'tickid': activity.tick_id,
'ticktime': activity.tick_time.strftime(DATETIME_FORMAT_API),
'timestamp': datetime.utcnow().strftime(DATETIME_FORMAT_API),
'timestamp': datetime.now(UTC).strftime(DATETIME_FORMAT_API),
'systems': []
}

Expand Down Expand Up @@ -253,7 +258,7 @@ def _build_api_activity(self, activity:Activity, cmdr:str):
return api_activity


def _build_api_event(self, event:dict, activity:Activity, cmdr:str, mission:dict):
def _build_api_event(self, event:dict, activity:Activity, cmdr:str, mission:dict = {}):
"""
Build an API-ready event ready for sending. This just involves enhancing the event with some
additional data
Expand All @@ -271,6 +276,7 @@ def _build_api_event(self, event:dict, activity:Activity, cmdr:str, mission:dict
if 'StationFaction' not in event: event['StationFaction'] = {'Name': self.bgstally.state.station_faction}
if 'StarSystem' not in event: event['StarSystem'] = get_by_path(activity.systems, [self.bgstally.state.current_system_id, 'System'], "")
if 'SystemAddress' not in event: event['SystemAddress'] = self.bgstally.state.current_system_id
if 'timestamp' not in event: event['timestamp'] = datetime.now(UTC).strftime(DATETIME_FORMAT_API)

# Event-specific enhancements
match event.get('event'):
Expand Down
12 changes: 9 additions & 3 deletions bgstally/bgstally.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ def journal_entry(self, cmdr, is_beta, system, station, entry, state):
return

activity: Activity = self.activity_manager.get_current_activity()

# Total hack for now. We need cmdr in Activity to allow us to send it to the API when the user changes values in the UI.
# What **should** happen is each Activity object should be associated with a single CMDR, and then all reporting
# kept separate per CMDR.
activity.cmdr = cmdr

dirty: bool = False

if entry.get('event') in ['StartUp', 'Location', 'FSDJump', 'CarrierJump']:
Expand All @@ -132,11 +138,11 @@ def journal_entry(self, cmdr, is_beta, system, station, entry, state):
dirty = True

case 'Bounty':
activity.bv_received(entry, self.state)
activity.bv_received(entry, self.state, cmdr)
dirty = True

case 'CapShipBond':
activity.cap_ship_bond_received(entry)
activity.cap_ship_bond_received(entry, cmdr)
dirty = True

case 'Cargo':
Expand Down Expand Up @@ -175,7 +181,7 @@ def journal_entry(self, cmdr, is_beta, system, station, entry, state):
dirty = True

case 'FactionKillBond' if state['Odyssey']:
activity.cb_received(entry, self.state)
activity.cb_received(entry, self.state, cmdr)
dirty = True

case 'Friends' if entry.get('Status') == "Requested":
Expand Down
21 changes: 21 additions & 0 deletions bgstally/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,27 @@ class CmdrInteractionReason(int, Enum):
TEAM_INVITE_RECEIVED = 5
FRIEND_ADDED = 6

class ApiSyntheticEvent(str, Enum):
CZ = 'SyntheticCZ'
GROUNDCZ = 'SyntheticGroundCZ'
CZOBJECTIVE = 'SyntheticCZObjective'
SCENARIO = 'SyntheticScenario'

class ApiSyntheticCZObjectiveType(str, Enum):
CAPSHIP = 'CapShip'
SPECOPS = 'SpecOps'
GENERAL = 'WarzoneGeneral'
CORRESPONDENT = 'WarzoneCorrespondent'

class ApiSyntheticScenarioType(str, Enum):
MEGASHIP = 'Megaship'
INSTALLATION = 'Installation'

ApiSizeLookup: dict = {
'l': 'low',
'm': 'medium',
'h': 'high'
}

DATETIME_FORMAT_JOURNAL: str = "%Y-%m-%dT%H:%M:%SZ"
DATETIME_FORMAT_API: str = "%Y-%m-%dT%H:%M:%SZ"
Expand Down
6 changes: 3 additions & 3 deletions bgstally/discord.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from copy import deepcopy
from datetime import datetime
from datetime import UTC, datetime

from requests import Response

Expand Down Expand Up @@ -53,7 +53,7 @@ def post_plaintext(self, discord_text:str, webhooks_data:dict|None, channel:Disc
# Get the previous state for this webhook's uuid from the passed in data, if it exists. Default to the state from the webhook manager
specific_webhook_data:dict = {} if webhooks_data is None else webhooks_data.get(webhook.get('uuid', ""), webhook)

utc_time_now:str = datetime.utcnow().strftime(DATETIME_FORMAT) + " " + __("game", lang=self.bgstally.state.discord_lang) # LANG: Discord date/time suffix for game time
utc_time_now:str = datetime.now(UTC).strftime(DATETIME_FORMAT) + " " + __("game", lang=self.bgstally.state.discord_lang) # LANG: Discord date/time suffix for game time
data:dict = {'channel': channel, 'callback': callback, 'webhookdata': specific_webhook_data} # Data that's carried through the request queue and back to the callback

# Fetch the previous post ID, if present, from the webhook data for the channel we're posting in. May be the default True / False value
Expand Down Expand Up @@ -184,7 +184,7 @@ def _get_embed(self, title: str | None = None, description: str | None = None, f
Returns:
dict[str, any]: The post structure, for converting to JSON
"""
footer_timestamp: str = (__("Updated at {date_time} (game)", lang=self.bgstally.state.discord_lang) if update else __("Posted at {date_time} (game)", lang=self.bgstally.state.discord_lang)).format(date_time=datetime.utcnow().strftime(DATETIME_FORMAT)) # LANG: Discord footer message, modern embed mode
footer_timestamp: str = (__("Updated at {date_time} (game)", lang=self.bgstally.state.discord_lang) if update else __("Posted at {date_time} (game)", lang=self.bgstally.state.discord_lang)).format(date_time=datetime.now(UTC).strftime(DATETIME_FORMAT)) # LANG: Discord footer message, modern embed mode
footer_version: str = f"v{str(self.bgstally.version)}"
footer_pad: int = 108 - len(footer_version)

Expand Down
10 changes: 7 additions & 3 deletions bgstally/missionlog.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
from datetime import UTC, datetime, timedelta
from os import path, remove
from datetime import datetime, timedelta

from bgstally.constants import DATETIME_FORMAT_JOURNAL, FOLDER_OTHER_DATA
from bgstally.debug import Debug
Expand Down Expand Up @@ -118,9 +118,13 @@ def _expire_old_missions(self):
"""
for mission in reversed(self.missionlog):
# Old missions pre v1.11.0 and missions with missing expiry dates don't have Expiry stored. Set to 7 days ahead for safety
if not 'Expiry' in mission or mission['Expiry'] == "": mission['Expiry'] = (datetime.utcnow() + timedelta(days = TIME_MISSION_EXPIRY_D)).strftime(DATETIME_FORMAT_JOURNAL)
if not 'Expiry' in mission or mission['Expiry'] == "": mission['Expiry'] = (datetime.now(UTC) + timedelta(days = TIME_MISSION_EXPIRY_D)).strftime(DATETIME_FORMAT_JOURNAL)

timedifference = datetime.utcnow() - datetime.strptime(mission['Expiry'], DATETIME_FORMAT_JOURNAL)
# Need to do this shenanegans to parse a tz-aware timestamp from a string
expiry_timestamp: datetime = datetime.strptime(mission['Expiry'], DATETIME_FORMAT_JOURNAL)
expiry_timestamp = expiry_timestamp.replace(tzinfo=UTC)

timedifference = datetime.now(UTC) - expiry_timestamp
if timedifference > timedelta(days = TIME_MISSION_EXPIRY_D):
# Keep missions for a while after they have expired, so we can log failed missions correctly
self.missionlog.remove(mission)
4 changes: 3 additions & 1 deletion bgstally/objectivesmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ def get_human_readable_objectives(self) -> str:
mission_system: str|None = mission.get('system')
mission_faction: str|None = mission.get('faction')
mission_startdate: datetime = datetime.strptime(mission.get('startdate', datetime.now(UTC).strftime(DATETIME_FORMAT_API)), DATETIME_FORMAT_API)
mission_enddate: datetime|None = datetime.strptime(mission.get('enddate', None), DATETIME_FORMAT_API)
mission_startdate = mission_startdate.replace(tzinfo=UTC)
mission_enddate: datetime = datetime.strptime(mission.get('enddate', datetime(3999, 12, 31, 23, 59, 59, 0, UTC).strftime(DATETIME_FORMAT_API)), DATETIME_FORMAT_API)
mission_enddate = mission_enddate.replace(tzinfo=UTC)
if mission_enddate < datetime.now(UTC): continue
mission_activity: Activity = self.bgstally.activity_manager.query_activity(mission_startdate)

Expand Down
10 changes: 7 additions & 3 deletions bgstally/targetmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os.path
import re
from copy import copy
from datetime import datetime, timedelta
from datetime import UTC, datetime, timedelta

from requests import Response

Expand Down Expand Up @@ -336,7 +336,7 @@ def _fetch_cmdr_info(self, cmdr_name:str, cmdr_data:dict):
'events': [
{
'eventName': "getCommanderProfile",
'eventTimestamp': datetime.utcnow().strftime(DATETIME_FORMAT_INARA),
'eventTimestamp': datetime.now(UTC).strftime(DATETIME_FORMAT_INARA),
'eventData': {
'searchName': cmdr_name
}
Expand Down Expand Up @@ -377,6 +377,10 @@ def _expire_old_targets(self):
Clear out all old targets from the target log
"""
for target in reversed(self.targetlog):
timedifference = datetime.utcnow() - datetime.strptime(target['Timestamp'], DATETIME_FORMAT_JOURNAL)
# Need to do this shenanegans to parse a tz-aware timestamp from a string
target_timestamp: datetime = datetime.strptime(target['Timestamp'], DATETIME_FORMAT_JOURNAL)
target_timestamp = target_timestamp.replace(tzinfo=UTC)

timedifference: datetime = datetime.now(UTC) - target_timestamp
if timedifference > timedelta(days = TIME_TARGET_LOG_EXPIRY_D):
self.targetlog.remove(target)
7 changes: 4 additions & 3 deletions bgstally/tick.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import hashlib
from datetime import datetime, timedelta
from secrets import token_hex
from datetime import UTC, datetime, timedelta

import plug
import requests
Expand All @@ -23,7 +22,7 @@ class Tick:
def __init__(self, bgstally, load: bool = False):
self.bgstally = bgstally
self.tick_id: str = TICKID_UNKNOWN
self.tick_time: datetime = (datetime.utcnow() - timedelta(days = 30)) # Default to a tick a month old
self.tick_time: datetime = (datetime.now(UTC) - timedelta(days = 30)) # Default to a tick a month old
if load: self.load()


Expand All @@ -48,6 +47,7 @@ def fetch_tick(self):
return None

tick_time: datetime = datetime.strptime(tick_time_raw, DATETIME_FORMAT_TICK_DETECTOR)
tick_time = tick_time.replace(tzinfo=UTC)

if tick_time > self.tick_time:
# There is a newer tick
Expand Down Expand Up @@ -76,6 +76,7 @@ def load(self):
"""
self.tick_id = config.get_str("XLastTick")
self.tick_time = datetime.strptime(config.get_str("XTickTime", default=self.tick_time.strftime(DATETIME_FORMAT_TICK_DETECTOR)), DATETIME_FORMAT_TICK_DETECTOR)
self.tick_time = self.tick_time.replace(tzinfo=UTC)


def save(self):
Expand Down
11 changes: 5 additions & 6 deletions bgstally/ui.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import tkinter as tk
from datetime import datetime, timedelta
from datetime import UTC, datetime, timedelta
from functools import partial
from os import path
from threading import Thread
Expand All @@ -11,7 +11,6 @@
import myNotebook as nb
from ttkHyperlinkLabel import HyperlinkLabel

import l10n
from bgstally.activity import Activity
from bgstally.constants import FOLDER_ASSETS, FONT_HEADING_2, FONT_SMALL, CheckStates, DiscordActivity, DiscordPostStyle, UpdateUIPolicy
from bgstally.debug import Debug
Expand Down Expand Up @@ -368,13 +367,13 @@ def _worker(self) -> None:
self.bgstally.overlay.display_message("tick", _("Curr Tick:") + " " + self.bgstally.tick.get_formatted(DATETIME_FORMAT_OVERLAY), True) # Overlay tick message

# Tick Warning
minutes_delta:int = int((datetime.utcnow() - self.bgstally.tick.next_predicted()) / timedelta(minutes=1))
minutes_delta:int = int((datetime.now(UTC) - self.bgstally.tick.next_predicted()) / timedelta(minutes=1))
if self.bgstally.state.enable_overlay_current_tick:
if datetime.utcnow() > self.bgstally.tick.next_predicted() + timedelta(minutes = TIME_TICK_ALERT_M):
if datetime.now(UTC) > self.bgstally.tick.next_predicted() + timedelta(minutes = TIME_TICK_ALERT_M):
self.bgstally.overlay.display_message("tickwarn", _("Tick {minutes_delta}m Overdue (Estimated)").format(minutes_delta=minutes_delta), True) # Overlay tick message
elif datetime.utcnow() > self.bgstally.tick.next_predicted():
elif datetime.now(UTC) > self.bgstally.tick.next_predicted():
self.bgstally.overlay.display_message("tickwarn", _("Past Estimated Tick Time"), True, text_colour_override="#FFA500") # Overlay tick message
elif datetime.utcnow() > self.bgstally.tick.next_predicted() - timedelta(minutes = TIME_TICK_ALERT_M):
elif datetime.now(UTC) > self.bgstally.tick.next_predicted() - timedelta(minutes = TIME_TICK_ALERT_M):
self.bgstally.overlay.display_message("tickwarn", _("Within {minutes_to_tick}m of Next Tick (Estimated)").format(minutes_to_tick=TIME_TICK_ALERT_M), True, text_colour_override="yellow") # Overlay tick message

# Activity Indicator
Expand Down
57 changes: 42 additions & 15 deletions bgstally/windows/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

from ttkHyperlinkLabel import HyperlinkLabel

from bgstally.activity import STATES_WAR, STATES_ELECTION, Activity
from bgstally.constants import (COLOUR_HEADING_1, FOLDER_ASSETS, FONT_HEADING_1, FONT_HEADING_2, FONT_TEXT, CheckStates, CZs, DiscordActivity, DiscordChannel,
DiscordPostStyle)
from bgstally.activity import STATES_ELECTION, STATES_WAR, Activity
from bgstally.constants import (COLOUR_HEADING_1, FOLDER_ASSETS, FONT_HEADING_1, FONT_HEADING_2, FONT_TEXT, ApiSizeLookup, ApiSyntheticEvent, CheckStates, CZs,
DiscordActivity, DiscordChannel, DiscordPostStyle)
from bgstally.debug import Debug
from bgstally.formatters.base import BaseActivityFormatterInterface
from bgstally.utils import _, __, human_format
Expand Down Expand Up @@ -560,18 +560,45 @@ def _cz_change(self, notebook: ScrollableNotebook, tab_index: int, CZVar: tk.Int
"""
Callback (set as a variable trace) for when a CZ Variable is changed
"""
if cz_type == CZs.SPACE_LOW:
faction['SpaceCZ']['l'] = CZVar.get()
elif cz_type == CZs.SPACE_MED:
faction['SpaceCZ']['m'] = CZVar.get()
elif cz_type == CZs.SPACE_HIGH:
faction['SpaceCZ']['h'] = CZVar.get()
elif cz_type == CZs.GROUND_LOW:
faction['GroundCZ']['l'] = CZVar.get()
elif cz_type == CZs.GROUND_MED:
faction['GroundCZ']['m'] = CZVar.get()
elif cz_type == CZs.GROUND_HIGH:
faction['GroundCZ']['h'] = CZVar.get()
match cz_type:
case CZs.SPACE_LOW:
event_type: ApiSyntheticEvent = ApiSyntheticEvent.CZ
event_size: str = ApiSizeLookup['l']
event_diff: int = int(CZVar.get()) - int(faction['SpaceCZ']['l'])
faction['SpaceCZ']['l'] = CZVar.get()
case CZs.SPACE_MED:
event_type: ApiSyntheticEvent = ApiSyntheticEvent.CZ
event_size: str = ApiSizeLookup['m']
event_diff: int = int(CZVar.get()) - int(faction['SpaceCZ']['m'])
faction['SpaceCZ']['m'] = CZVar.get()
case CZs.SPACE_HIGH:
event_type: ApiSyntheticEvent = ApiSyntheticEvent.CZ
event_size: str = ApiSizeLookup['h']
event_diff: int = int(CZVar.get()) - int(faction['SpaceCZ']['h'])
faction['SpaceCZ']['h'] = CZVar.get()
case CZs.GROUND_LOW:
event_type: ApiSyntheticEvent = ApiSyntheticEvent.GROUNDCZ
event_size: str = ApiSizeLookup['l']
event_diff: int = int(CZVar.get()) - int(faction['GroundCZ']['l'])
faction['GroundCZ']['l'] = CZVar.get()
case CZs.GROUND_MED:
event_type: ApiSyntheticEvent = ApiSyntheticEvent.GROUNDCZ
event_size: str = ApiSizeLookup['m']
event_diff: int = int(CZVar.get()) - int(faction['GroundCZ']['m'])
faction['GroundCZ']['m'] = CZVar.get()
case CZs.GROUND_HIGH:
event_type: ApiSyntheticEvent = ApiSyntheticEvent.GROUNDCZ
event_size: str = ApiSizeLookup['h']
event_diff: int = int(CZVar.get()) - int(faction['GroundCZ']['h'])
faction['GroundCZ']['h'] = CZVar.get()

# Send to API
event: dict = {
'event': event_type,
event_size: event_diff,
'Faction': faction['Faction']
}
if activity.cmdr is not None: self.bgstally.api_manager.send_event(event, activity, activity.cmdr)

activity.recalculate_zero_activity()
self._update_tab_image(notebook, tab_index, EnableAllCheckbutton, system)
Expand Down

0 comments on commit d867726

Please sign in to comment.