Initial implementation of custom price triggers
- likely to have some bugs, but this is good enough for a preview release.
This commit is contained in:
@@ -18,6 +18,10 @@ from interactions import (
|
||||
is_owner,
|
||||
check,
|
||||
StringSelectOption, integration_types,
|
||||
Modal,
|
||||
ShortText,
|
||||
modal_callback,
|
||||
ModalContext,
|
||||
)
|
||||
from interactions import Task, IntervalTrigger
|
||||
from interactions import slash_command, listen
|
||||
@@ -142,6 +146,13 @@ class Tracker(Extension):
|
||||
self.bot.logger.log(
|
||||
logging.INFO, "TokenBot Tracker: Loading Historical Data Finished"
|
||||
)
|
||||
self.bot.logger.log(logging.INFO, "TokenBot Tracker: Loading Custom Price Alerts")
|
||||
# Load all SPECIFIC_PRICE alerts from database (AlertType.SPECIFIC_PRICE = 11)
|
||||
custom_alerts = await self._alerts.get_all_by_type(AlertType.SPECIFIC_PRICE.value)
|
||||
await self._history_manager.load_custom_alerts(custom_alerts)
|
||||
self.bot.logger.log(
|
||||
logging.INFO, f"TokenBot Tracker: Loaded {len(custom_alerts)} Custom Price Alerts"
|
||||
)
|
||||
self.bot.logger.log(logging.INFO, "TokenBot Tracker: Started")
|
||||
self.update_data.start()
|
||||
|
||||
@@ -206,20 +217,20 @@ class Tracker(Extension):
|
||||
|
||||
try:
|
||||
flavor = await self.flavor_select_menu(ctx)
|
||||
alert_category = await self.alert_category_select_menu(ctx)
|
||||
alert_category, price = await self.alert_category_select_menu(ctx)
|
||||
match alert_category:
|
||||
case AlertCategory.LOW:
|
||||
alert_type = await self.low_alert_select_menu(ctx)
|
||||
case AlertCategory.HIGH:
|
||||
alert_type = await self.high_alert_select_menu(ctx)
|
||||
case _:
|
||||
raise NotImplementedError
|
||||
case AlertCategory.CUSTOM:
|
||||
alert_type = AlertType.SPECIFIC_PRICE
|
||||
|
||||
except TimeoutError:
|
||||
return
|
||||
|
||||
else:
|
||||
alert = Alert(alert_type, flavor, user.region)
|
||||
alert = Alert(alert_type, flavor, user.region, price)
|
||||
if not await self._users.is_subscribed(user, alert):
|
||||
await asyncio.gather(
|
||||
self._users.add_alert(user, alert),
|
||||
@@ -287,24 +298,8 @@ class Tracker(Extension):
|
||||
# Callbacks Commands #
|
||||
###################################
|
||||
|
||||
@component_callback("flavor_menu")
|
||||
async def flavor_menu(self, ctx: ComponentContext):
|
||||
await ctx.send(f"Selected Flavor: {ctx.values[0]}", ephemeral=True)
|
||||
|
||||
@component_callback("high_alert_menu")
|
||||
async def high_alert_menu(self, ctx: ComponentContext):
|
||||
await ctx.send(f"Selected Alert: {ctx.values[0]}", ephemeral=True)
|
||||
|
||||
@component_callback("low_alert_menu")
|
||||
async def low_alert_menu(self, ctx: ComponentContext):
|
||||
await ctx.send(f"Selected Alert: {ctx.values[0]}", ephemeral=True)
|
||||
|
||||
@component_callback("remove_alert_menu")
|
||||
async def remove_alert_menu(self, ctx: ComponentContext):
|
||||
await ctx.send(
|
||||
f"You have selected to remove the following alert: {ctx.values[0].title()}",
|
||||
ephemeral=True,
|
||||
)
|
||||
# Note: Callbacks for flavor_menu, high_alert_menu, low_alert_menu, and alert buttons
|
||||
# are disabled because they interfere with wait_for_component manual handling
|
||||
|
||||
@component_callback("region_menu")
|
||||
async def region_menu_cb(self, ctx: ComponentContext):
|
||||
@@ -316,18 +311,6 @@ class Tracker(Extension):
|
||||
)
|
||||
await ctx.defer(edit_origin=True, suppress_error=True)
|
||||
|
||||
@component_callback("high_alert_button")
|
||||
async def high_alert_button(self, ctx: ComponentContext):
|
||||
await ctx.send("You selected to add a High Price Alert", ephemeral=True)
|
||||
|
||||
@component_callback("low_alert_button")
|
||||
async def low_alert_button(self, ctx: ComponentContext):
|
||||
await ctx.send("You selected to add a Low Price Alert", ephemeral=True)
|
||||
|
||||
@component_callback("custom_alert_button")
|
||||
async def custom_alert_button(self, ctx: ComponentContext):
|
||||
await ctx.send("You selected to add a Custom Price Alert", ephemeral=True)
|
||||
|
||||
###################################
|
||||
# Helper Functions #
|
||||
###################################
|
||||
@@ -384,8 +367,19 @@ class Tracker(Extension):
|
||||
await message.edit(context=ctx, components=menu)
|
||||
selection_split = alert_component.ctx.values[0].split(" ")
|
||||
flavor = Flavor[selection_split[0].upper()]
|
||||
alert_type = AlertType.from_str(" ".join(selection_split[1:]))
|
||||
return Alert(alert_type, flavor, user.region)
|
||||
alert_type_str = " ".join(selection_split[1:])
|
||||
alert_type = AlertType.from_str(alert_type_str)
|
||||
|
||||
# Parse price for custom alerts
|
||||
price = 0
|
||||
if alert_type == AlertType.SPECIFIC_PRICE:
|
||||
# Extract price from "Custom Price: 250,000g"
|
||||
price_part = alert_type_str.split(": ")[1].rstrip("g").replace(",", "")
|
||||
price_gold = int(price_part)
|
||||
# Convert gold to copper
|
||||
price = price_gold
|
||||
|
||||
return Alert(alert_type, flavor, user.region, price)
|
||||
|
||||
async def region_select_menu(self, ctx: SlashContext, user: User | None = None):
|
||||
region_menu = copy.deepcopy(REGION_MENU)
|
||||
@@ -448,7 +442,7 @@ class Tracker(Extension):
|
||||
await flavor_message.edit(context=ctx, components=flavor_menu)
|
||||
return flavor
|
||||
|
||||
async def alert_category_select_menu(self, ctx: SlashContext) -> AlertCategory:
|
||||
async def alert_category_select_menu(self, ctx: SlashContext) -> tuple[AlertCategory, int]:
|
||||
alert_type_button = copy.deepcopy(ALERT_TYPE_ROW)
|
||||
alert_type_message = await ctx.send(
|
||||
"Select an alert type to add", components=alert_type_button, ephemeral=True
|
||||
@@ -465,13 +459,20 @@ class Tracker(Extension):
|
||||
)
|
||||
raise TimeoutError
|
||||
else:
|
||||
# Acknowledge the component interaction to avoid 404 Unknown Interaction
|
||||
await alert_type_component.ctx.defer(edit_origin=True, suppress_error=True)
|
||||
alert_type = AlertCategory.from_str(alert_type_component.ctx.custom_id)
|
||||
|
||||
# If custom alert, send modal as response to button press
|
||||
if alert_type == AlertCategory.CUSTOM:
|
||||
price = await self.custom_price_modal(alert_type_component.ctx)
|
||||
else:
|
||||
# Acknowledge the component interaction to avoid 404 Unknown Interaction
|
||||
await alert_type_component.ctx.defer(edit_origin=True, suppress_error=True)
|
||||
price = 0
|
||||
|
||||
for button in alert_type_button[0].components:
|
||||
button.disabled = True
|
||||
await alert_type_message.edit(context=ctx, components=alert_type_button)
|
||||
return alert_type
|
||||
return alert_type, price
|
||||
|
||||
async def _alert_select_menu_handler(
|
||||
self, ctx: SlashContext, menu: StringSelectMenu, message: Message
|
||||
@@ -508,6 +509,37 @@ class Tracker(Extension):
|
||||
)
|
||||
return await self._alert_select_menu_handler(ctx, low_menu, low_message)
|
||||
|
||||
async def custom_price_modal(self, ctx: ComponentContext) -> int:
|
||||
modal = Modal(
|
||||
ShortText(
|
||||
label="Price (in gold)",
|
||||
custom_id="price_input",
|
||||
placeholder="e.g., 250000 for 250k gold",
|
||||
required=True,
|
||||
),
|
||||
title="Custom Price Alert",
|
||||
custom_id="custom_price_modal",
|
||||
)
|
||||
await ctx.send_modal(modal)
|
||||
|
||||
try:
|
||||
modal_ctx: ModalContext = await self.bot.wait_for_modal(modal, timeout=300)
|
||||
except TimeoutError:
|
||||
await ctx.send("Modal timed out", ephemeral=True)
|
||||
raise TimeoutError
|
||||
else:
|
||||
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)
|
||||
|
||||
async def _user_is_registered(self, ctx: SlashContext) -> bool:
|
||||
if not await self._users.exists(ctx.user.id):
|
||||
await ctx.send(
|
||||
@@ -548,7 +580,20 @@ class Tracker(Extension):
|
||||
f"trigger.squelched:\n\t{trigger.squelched}```"
|
||||
)
|
||||
else:
|
||||
alert_str = "You should only be seeing this if the bot has not finished importing history at startup."
|
||||
# For custom price alerts, show current status vs threshold
|
||||
if alert.alert_type == AlertType.SPECIFIC_PRICE:
|
||||
current_price = history.last_price_datum[1]
|
||||
target_price_gold = alert.price
|
||||
current_price_gold = current_price
|
||||
|
||||
alert_str = (
|
||||
f"Threshold has never been crossed\n"
|
||||
f"Current Price: {format(current_price_gold, ',')}g\n"
|
||||
f"Target Price: {format(target_price_gold, ',')}g\n"
|
||||
f"[Link to this Chart]({self._render_token_url(alert, time_range='72h')})\n"
|
||||
)
|
||||
else:
|
||||
alert_str = "You should only be seeing this if the bot has not finished importing history at startup."
|
||||
fields.append(
|
||||
EmbedField(
|
||||
name=f"{alert.to_human_string()} Alert",
|
||||
@@ -563,7 +608,7 @@ class Tracker(Extension):
|
||||
)
|
||||
return embed
|
||||
|
||||
def _render_token_url(self, alert: Alert) -> str:
|
||||
def _render_token_url(self, alert: Alert, time_range: str | None = None) -> str:
|
||||
match alert.flavor:
|
||||
case Flavor.CLASSIC:
|
||||
url = "https://classic.wowtoken.app/?"
|
||||
@@ -572,14 +617,22 @@ class Tracker(Extension):
|
||||
case _:
|
||||
raise NotImplementedError
|
||||
url += f"region={alert.region.value}&"
|
||||
match alert.alert_type:
|
||||
case AlertType.WEEKLY_LOW | AlertType.WEEKLY_HIGH:
|
||||
url += "time=168h&"
|
||||
case AlertType.MONTHLY_LOW | AlertType.MONTHLY_HIGH:
|
||||
url += "time=720h&"
|
||||
case AlertType.YEARLY_LOW | AlertType.YEARLY_HIGH:
|
||||
url += "time=1y&"
|
||||
case AlertType.ALL_TIME_LOW | AlertType.ALL_TIME_HIGH:
|
||||
url += "time=all&"
|
||||
|
||||
# If time_range is explicitly provided, use it
|
||||
if time_range:
|
||||
url += f"time={time_range}&"
|
||||
else:
|
||||
# Otherwise, determine time range based on alert type
|
||||
match alert.alert_type:
|
||||
case AlertType.WEEKLY_LOW | AlertType.WEEKLY_HIGH:
|
||||
url += "time=168h&"
|
||||
case AlertType.MONTHLY_LOW | AlertType.MONTHLY_HIGH:
|
||||
url += "time=720h&"
|
||||
case AlertType.YEARLY_LOW | AlertType.YEARLY_HIGH:
|
||||
url += "time=1y&"
|
||||
case AlertType.ALL_TIME_LOW | AlertType.ALL_TIME_HIGH:
|
||||
url += "time=all&"
|
||||
case _:
|
||||
url += "time=72h&"
|
||||
|
||||
return url
|
||||
|
||||
Reference in New Issue
Block a user