commit 35ad75cd7c4c3393754479112ecbdbe26b043f71
parent 62b20cf4abeb10aac566f718d299039ad8b1ca43
Author: Emily Doherty <git@emily.sh>
Date: Wed, 28 Jan 2026 17:49:28 -0800
Bug fix
- Validate and enforce positive price for SPECIFIC_PRICE alerts
- improve error handling and alert squelching logic.
Diffstat:
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
@@ -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
@@ -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
@@ -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
@@ -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):