From 35ad75cd7c4c3393754479112ecbdbe26b043f71 Mon Sep 17 00:00:00 2001 From: Emily Doherty Date: Wed, 28 Jan 2026 17:49:28 -0800 Subject: [PATCH] Bug fix - Validate and enforce positive price for SPECIFIC_PRICE alerts - improve error handling and alert squelching logic. --- token_bot/history_manager/history_manager.py | 4 +- token_bot/history_manager/update_trigger.py | 48 ++++++++++--------- token_bot/persistant_database/alert_schema.py | 19 +++++--- token_bot/tracker.py | 16 ++++--- 4 files changed, 48 insertions(+), 39 deletions(-) diff --git a/token_bot/history_manager/history_manager.py b/token_bot/history_manager/history_manager.py index 305e8f2..3997733 100644 --- a/token_bot/history_manager/history_manager.py +++ b/token_bot/history_manager/history_manager.py @@ -20,7 +20,7 @@ class HistoryManager: self._history[flavor][Region(region)] = History(flavor, Region(region)) async def _retrieve_data( - self, flavor: Flavor, region: Region + self, flavor: Flavor, region: Region ) -> List[Tuple[datetime.datetime, int]]: high_fidelity_time = datetime.datetime.now( tz=datetime.UTC @@ -32,7 +32,7 @@ class HistoryManager: final_response = [] def _convert_to_datetime( - data: Tuple[str, int] + data: Tuple[str, int] ) -> Tuple[datetime.datetime, int]: return datetime.datetime.fromisoformat(data[0]), data[1] diff --git a/token_bot/history_manager/update_trigger.py b/token_bot/history_manager/update_trigger.py index 96d0058..d01dc0d 100644 --- a/token_bot/history_manager/update_trigger.py +++ b/token_bot/history_manager/update_trigger.py @@ -34,24 +34,24 @@ class UpdateTrigger: self._squelched = value def _find_next_trigger( - self, - comparison_operator: Callable, - starting_point: datetime.datetime, - history: List[Tuple[datetime.datetime, int]], + self, + comparison_operator: Callable, + starting_point: datetime.datetime, + history: List[Tuple[datetime.datetime, int]], ): candidate_datum: Tuple[datetime.datetime, int] | None = None for datum in history: if datum[0] > starting_point and datum != history[-1]: if candidate_datum is None or comparison_operator( - datum[1], candidate_datum[1] + datum[1], candidate_datum[1] ): candidate_datum = datum self._last_trigger = candidate_datum def check_and_update( - self, - new_datum: Tuple[datetime.datetime, int], - history: List[Tuple[datetime.datetime, int]], + self, + new_datum: Tuple[datetime.datetime, int], + history: List[Tuple[datetime.datetime, int]], ) -> bool: match self.alert.flavor: case Flavor.RETAIL: @@ -102,10 +102,12 @@ class UpdateTrigger: # We alert when price moves from below to above (or vice versa) target_price = self._alert.price if self._last_trigger is None: - # First time - check if current price crossed the threshold + # First time - initialize tracking + self._last_trigger = new_datum if new_datum[1] >= target_price: - self._last_trigger = new_datum + # Price already at/above target - alert and squelch self._last_alerting = new_datum + self._squelched = True return True return False else: @@ -117,21 +119,21 @@ class UpdateTrigger: crossed_up = old_price < target_price <= new_price crossed_down = old_price >= target_price > new_price + # Always update last_trigger for tracking + self._last_trigger = new_datum + if crossed_up or crossed_down: - self._last_trigger = new_datum - self._last_alerting = new_datum - was_squelched = self._squelched - self._squelched = True - return not was_squelched - elif self._squelched: - # Update last_trigger but don't alert (we're tracking current price) - self._last_trigger = new_datum - # If we moved away from threshold, allow next crossing to alert - if (crossed_up and new_price < target_price) or (crossed_down and new_price >= target_price): + # We're crossing the threshold + if self._squelched: + # Currently squelched - this crossing unsquelches us + # but doesn't alert (prevents rapid-fire alerts on oscillation) self._squelched = False - else: - # Just update the last trigger for tracking - self._last_trigger = new_datum + return False + else: + # Not squelched - send alert and squelch + self._last_alerting = new_datum + self._squelched = True + return True return False case _: time_range = datetime.timedelta(days=int(365.25 * 6)) diff --git a/token_bot/persistant_database/alert_schema.py b/token_bot/persistant_database/alert_schema.py index 9ec9e00..79a3033 100644 --- a/token_bot/persistant_database/alert_schema.py +++ b/token_bot/persistant_database/alert_schema.py @@ -12,8 +12,13 @@ import token_bot.persistant_database as pdb class Alert: def __init__( - self, alert: pdb.AlertType, flavor: Flavor, region: Region, price: int = 0 + self, alert: pdb.AlertType, flavor: Flavor, region: Region, price: int = 0 ) -> None: + # Validate price for SPECIFIC_PRICE alerts + if alert == AlertType.SPECIFIC_PRICE: + if price is None or price <= 0: + raise ValueError("SPECIFIC_PRICE alerts require a positive price value") + # AlertType is the Primary Key self._alert_type: pdb.AlertType = alert # Flavor (Retail, Classic) is the Sort Key @@ -91,10 +96,10 @@ class Alert: def __eq__(self, other): return ( - self.alert_type == other.alert_type - and self.flavor == other.flavor - and self.region == other.region - and self.price == other.price + self.alert_type == other.alert_type + and self.flavor == other.flavor + and self.region == other.region + and self.price == other.price ) def to_human_string(self): @@ -142,7 +147,7 @@ class Alert: return self.users async def add_user( - self, table: Table, user: pdb.User, consistent: bool = False + self, table: Table, user: pdb.User, consistent: bool = False ) -> None: await self._lazy_load(table, consistent=consistent) @@ -150,7 +155,7 @@ class Alert: await self._append_user(table=table, user=user) async def remove_user( - self, table: Table, user: pdb.User, consistent: bool = True + self, table: Table, user: pdb.User, consistent: bool = True ) -> None: await self._lazy_load(table, consistent=consistent) diff --git a/token_bot/tracker.py b/token_bot/tracker.py index 994c971..5072b72 100644 --- a/token_bot/tracker.py +++ b/token_bot/tracker.py @@ -226,7 +226,7 @@ class Tracker(Extension): case AlertCategory.CUSTOM: alert_type = AlertType.SPECIFIC_PRICE - except TimeoutError: + except (TimeoutError, ValueError): return else: @@ -531,14 +531,16 @@ class Tracker(Extension): price_str = modal_ctx.responses["price_input"] try: price_gold = int(price_str.replace(",", "").replace(" ", "").replace("g", "")) - if price_gold <= 0: - await modal_ctx.send("Price must be greater than 0", ephemeral=True) - # Convert gold to copper (1 gold = 10000 copper) - price_copper = price_gold - await modal_ctx.send(f"Custom price alert set for {format(price_gold, ',')}g", ephemeral=True) - return price_copper except ValueError: await modal_ctx.send("Invalid price. Please enter a valid number.", ephemeral=True) + raise + + if price_gold <= 0: + await modal_ctx.send("Price must be greater than 0", ephemeral=True) + raise ValueError("Price must be greater than 0") + + await modal_ctx.send(f"Custom price alert set for {format(price_gold, ',')}g", ephemeral=True) + return price_gold async def _user_is_registered(self, ctx: SlashContext) -> bool: if not await self._users.exists(ctx.user.id):