Bug fix
- Validate and enforce positive price for SPECIFIC_PRICE alerts - improve error handling and alert squelching logic.
This commit is contained in:
@@ -20,7 +20,7 @@ class HistoryManager:
|
|||||||
self._history[flavor][Region(region)] = History(flavor, Region(region))
|
self._history[flavor][Region(region)] = History(flavor, Region(region))
|
||||||
|
|
||||||
async def _retrieve_data(
|
async def _retrieve_data(
|
||||||
self, flavor: Flavor, region: Region
|
self, flavor: Flavor, region: Region
|
||||||
) -> List[Tuple[datetime.datetime, int]]:
|
) -> List[Tuple[datetime.datetime, int]]:
|
||||||
high_fidelity_time = datetime.datetime.now(
|
high_fidelity_time = datetime.datetime.now(
|
||||||
tz=datetime.UTC
|
tz=datetime.UTC
|
||||||
@@ -32,7 +32,7 @@ class HistoryManager:
|
|||||||
final_response = []
|
final_response = []
|
||||||
|
|
||||||
def _convert_to_datetime(
|
def _convert_to_datetime(
|
||||||
data: Tuple[str, int]
|
data: Tuple[str, int]
|
||||||
) -> Tuple[datetime.datetime, int]:
|
) -> Tuple[datetime.datetime, int]:
|
||||||
return datetime.datetime.fromisoformat(data[0]), data[1]
|
return datetime.datetime.fromisoformat(data[0]), data[1]
|
||||||
|
|
||||||
|
|||||||
@@ -34,24 +34,24 @@ class UpdateTrigger:
|
|||||||
self._squelched = value
|
self._squelched = value
|
||||||
|
|
||||||
def _find_next_trigger(
|
def _find_next_trigger(
|
||||||
self,
|
self,
|
||||||
comparison_operator: Callable,
|
comparison_operator: Callable,
|
||||||
starting_point: datetime.datetime,
|
starting_point: datetime.datetime,
|
||||||
history: List[Tuple[datetime.datetime, int]],
|
history: List[Tuple[datetime.datetime, int]],
|
||||||
):
|
):
|
||||||
candidate_datum: Tuple[datetime.datetime, int] | None = None
|
candidate_datum: Tuple[datetime.datetime, int] | None = None
|
||||||
for datum in history:
|
for datum in history:
|
||||||
if datum[0] > starting_point and datum != history[-1]:
|
if datum[0] > starting_point and datum != history[-1]:
|
||||||
if candidate_datum is None or comparison_operator(
|
if candidate_datum is None or comparison_operator(
|
||||||
datum[1], candidate_datum[1]
|
datum[1], candidate_datum[1]
|
||||||
):
|
):
|
||||||
candidate_datum = datum
|
candidate_datum = datum
|
||||||
self._last_trigger = candidate_datum
|
self._last_trigger = candidate_datum
|
||||||
|
|
||||||
def check_and_update(
|
def check_and_update(
|
||||||
self,
|
self,
|
||||||
new_datum: Tuple[datetime.datetime, int],
|
new_datum: Tuple[datetime.datetime, int],
|
||||||
history: List[Tuple[datetime.datetime, int]],
|
history: List[Tuple[datetime.datetime, int]],
|
||||||
) -> bool:
|
) -> bool:
|
||||||
match self.alert.flavor:
|
match self.alert.flavor:
|
||||||
case Flavor.RETAIL:
|
case Flavor.RETAIL:
|
||||||
@@ -102,10 +102,12 @@ class UpdateTrigger:
|
|||||||
# We alert when price moves from below to above (or vice versa)
|
# We alert when price moves from below to above (or vice versa)
|
||||||
target_price = self._alert.price
|
target_price = self._alert.price
|
||||||
if self._last_trigger is None:
|
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:
|
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._last_alerting = new_datum
|
||||||
|
self._squelched = True
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
@@ -117,21 +119,21 @@ class UpdateTrigger:
|
|||||||
crossed_up = old_price < target_price <= new_price
|
crossed_up = old_price < target_price <= new_price
|
||||||
crossed_down = 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:
|
if crossed_up or crossed_down:
|
||||||
self._last_trigger = new_datum
|
# We're crossing the threshold
|
||||||
self._last_alerting = new_datum
|
if self._squelched:
|
||||||
was_squelched = self._squelched
|
# Currently squelched - this crossing unsquelches us
|
||||||
self._squelched = True
|
# but doesn't alert (prevents rapid-fire alerts on oscillation)
|
||||||
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):
|
|
||||||
self._squelched = False
|
self._squelched = False
|
||||||
else:
|
return False
|
||||||
# Just update the last trigger for tracking
|
else:
|
||||||
self._last_trigger = new_datum
|
# Not squelched - send alert and squelch
|
||||||
|
self._last_alerting = new_datum
|
||||||
|
self._squelched = True
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
case _:
|
case _:
|
||||||
time_range = datetime.timedelta(days=int(365.25 * 6))
|
time_range = datetime.timedelta(days=int(365.25 * 6))
|
||||||
|
|||||||
@@ -12,8 +12,13 @@ import token_bot.persistant_database as pdb
|
|||||||
|
|
||||||
class Alert:
|
class Alert:
|
||||||
def __init__(
|
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:
|
) -> 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
|
# AlertType is the Primary Key
|
||||||
self._alert_type: pdb.AlertType = alert
|
self._alert_type: pdb.AlertType = alert
|
||||||
# Flavor (Retail, Classic) is the Sort Key
|
# Flavor (Retail, Classic) is the Sort Key
|
||||||
@@ -91,10 +96,10 @@ class Alert:
|
|||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return (
|
return (
|
||||||
self.alert_type == other.alert_type
|
self.alert_type == other.alert_type
|
||||||
and self.flavor == other.flavor
|
and self.flavor == other.flavor
|
||||||
and self.region == other.region
|
and self.region == other.region
|
||||||
and self.price == other.price
|
and self.price == other.price
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_human_string(self):
|
def to_human_string(self):
|
||||||
@@ -142,7 +147,7 @@ class Alert:
|
|||||||
return self.users
|
return self.users
|
||||||
|
|
||||||
async def add_user(
|
async def add_user(
|
||||||
self, table: Table, user: pdb.User, consistent: bool = False
|
self, table: Table, user: pdb.User, consistent: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
await self._lazy_load(table, consistent=consistent)
|
await self._lazy_load(table, consistent=consistent)
|
||||||
|
|
||||||
@@ -150,7 +155,7 @@ class Alert:
|
|||||||
await self._append_user(table=table, user=user)
|
await self._append_user(table=table, user=user)
|
||||||
|
|
||||||
async def remove_user(
|
async def remove_user(
|
||||||
self, table: Table, user: pdb.User, consistent: bool = True
|
self, table: Table, user: pdb.User, consistent: bool = True
|
||||||
) -> None:
|
) -> None:
|
||||||
await self._lazy_load(table, consistent=consistent)
|
await self._lazy_load(table, consistent=consistent)
|
||||||
|
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ class Tracker(Extension):
|
|||||||
case AlertCategory.CUSTOM:
|
case AlertCategory.CUSTOM:
|
||||||
alert_type = AlertType.SPECIFIC_PRICE
|
alert_type = AlertType.SPECIFIC_PRICE
|
||||||
|
|
||||||
except TimeoutError:
|
except (TimeoutError, ValueError):
|
||||||
return
|
return
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -531,14 +531,16 @@ class Tracker(Extension):
|
|||||||
price_str = modal_ctx.responses["price_input"]
|
price_str = modal_ctx.responses["price_input"]
|
||||||
try:
|
try:
|
||||||
price_gold = int(price_str.replace(",", "").replace(" ", "").replace("g", ""))
|
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:
|
except ValueError:
|
||||||
await modal_ctx.send("Invalid price. Please enter a valid number.", ephemeral=True)
|
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:
|
async def _user_is_registered(self, ctx: SlashContext) -> bool:
|
||||||
if not await self._users.exists(ctx.user.id):
|
if not await self._users.exists(ctx.user.id):
|
||||||
|
|||||||
Reference in New Issue
Block a user