Compare commits
35 Commits
3a06464c29
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 509a7d8a74 | |||
| 28fbdab904 | |||
| 4cb9d4ab70 | |||
| 35ad75cd7c | |||
| 62b20cf4ab | |||
| ed79f4b65c | |||
| 19eb0a4e24 | |||
| faab0d5f7e | |||
| 7e387b6cd7 | |||
| f18b129907 | |||
| 6a4b1f117b | |||
| fc57a7d172 | |||
| 0d0fe8e685 | |||
| c5bb53c0e7 | |||
| 2164e98730 | |||
| f20a8c6476 | |||
| 3fffcf86c9 | |||
| 84561948da | |||
| 621bd16d71 | |||
| 7c6b66660e | |||
| dfa5637a42 | |||
| 9ddba0d9b1 | |||
| ceaabe3dac | |||
| a9e50c2c8e | |||
| ab885fccff | |||
| aa58f775ab | |||
| 4cd7e6284b | |||
| 719842c20e | |||
| df0cd8b40e | |||
| 5a34e6f3ed | |||
| eef31b5631 | |||
| e7a9466092 | |||
| dd97d9b1f9 | |||
| cd23c8e350 | |||
| 34badf17eb |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.env
|
||||||
5
.pre-commit-config.yaml
Normal file
5
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 24.10.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.12-bookworm AS base
|
FROM python:3.12-trixie AS base
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -1,3 +1,15 @@
|
|||||||
# wow-token-app-bot
|
# wow-token-app-bot
|
||||||
|
|
||||||
An interactions.py Discord bot for getting and alerting on the WoW Token Price
|
An interactions.py Discord bot for getting and alerting on the WoW Token Price
|
||||||
|
|
||||||
|
|
||||||
|
# Help
|
||||||
|
If you are a user of this bot and are looking for help on how to use it, you can find a small tutorial [here](https://blog.emily.sh/token-bot/)
|
||||||
|
|
||||||
|
|
||||||
|
# Contributing
|
||||||
|
1. Clone the project
|
||||||
|
2. Install the requirements `pip install -r requirements.txt`
|
||||||
|
3. Install pre-commit hooks `pre-commit install`
|
||||||
|
4. To run this application you will need to have created DynamoDB tables and have a discord app token to use
|
||||||
|
- You will need to specify at least ALERTS_TABLE, USERS_TABLE, DISCORD_TOKEN, and AWS_REGION
|
||||||
|
|||||||
7
compose.yaml
Normal file
7
compose.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
services:
|
||||||
|
token-bot:
|
||||||
|
image: wowtoken-app/token-bot
|
||||||
|
build: ./
|
||||||
|
restart: always
|
||||||
|
env_file:
|
||||||
|
.env
|
||||||
2
main.py
2
main.py
@@ -1,5 +1,5 @@
|
|||||||
import token_bot.token_bot as token_bot
|
import token_bot.token_bot as token_bot
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
bot = token_bot.TokenBot()
|
bot = token_bot.TokenBot()
|
||||||
bot.run()
|
bot.run()
|
||||||
|
|||||||
@@ -1,25 +1,42 @@
|
|||||||
aiodynamo==24.1
|
aiodynamo==24.7
|
||||||
aiohttp==3.9.5
|
aiohappyeyeballs==2.6.1
|
||||||
aiosignal==1.3.1
|
aiohttp==3.13.2
|
||||||
anyio==4.4.0
|
aiosignal==1.4.0
|
||||||
attrs==23.2.0
|
anyio==4.11.0
|
||||||
certifi==2024.7.4
|
attrs==25.4.0
|
||||||
croniter==2.0.5
|
black==25.9.0
|
||||||
discord-py-interactions==5.12.1
|
certifi==2025.10.5
|
||||||
|
cfgv==3.4.0
|
||||||
|
click==8.3.0
|
||||||
|
croniter==6.0.0
|
||||||
|
discord-py-interactions==5.15.0
|
||||||
discord-typings==0.9.0
|
discord-typings==0.9.0
|
||||||
emoji==2.12.1
|
distlib==0.4.0
|
||||||
frozenlist==1.4.1
|
emoji==2.15.0
|
||||||
h11==0.14.0
|
filelock==3.20.0
|
||||||
httpcore==1.0.5
|
frozenlist==1.8.0
|
||||||
httpx==0.27.0
|
h11==0.16.0
|
||||||
idna==3.7
|
httpcore==1.0.9
|
||||||
multidict==6.0.5
|
httpx==0.28.1
|
||||||
|
identify==2.6.15
|
||||||
|
idna==3.11
|
||||||
|
multidict==6.7.0
|
||||||
|
mypy_extensions==1.1.0
|
||||||
|
nodeenv==1.9.1
|
||||||
|
packaging==25.0
|
||||||
|
pathspec==0.12.1
|
||||||
|
platformdirs==4.5.0
|
||||||
|
pre_commit==4.3.0
|
||||||
|
propcache==0.4.1
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.2.1
|
||||||
pytz==2024.1
|
pytokens==0.2.0
|
||||||
six==1.16.0
|
pytz==2025.2
|
||||||
|
PyYAML==6.0.3
|
||||||
|
six==1.17.0
|
||||||
sniffio==1.3.1
|
sniffio==1.3.1
|
||||||
tomli==2.0.1
|
tomli==2.3.0
|
||||||
typing_extensions==4.12.2
|
typing_extensions==4.15.0
|
||||||
uvloop==0.21.0
|
uvloop==0.22.1
|
||||||
yarl==1.9.4
|
virtualenv==20.35.4
|
||||||
|
yarl==1.22.0
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ from token_bot.persistant_database import database as pdb
|
|||||||
class AlertsController:
|
class AlertsController:
|
||||||
def __init__(self, session: aiohttp.ClientSession):
|
def __init__(self, session: aiohttp.ClientSession):
|
||||||
self._pdb: pdb.Database = pdb.Database(session)
|
self._pdb: pdb.Database = pdb.Database(session)
|
||||||
self.table = aiodynamo.client.Table = self._pdb.client.table(os.getenv('ALERTS_TABLE'))
|
self.table: aiodynamo.client.Table = self._pdb.client.table(
|
||||||
|
os.getenv("ALERTS_TABLE")
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _user_to_obj(user: int | User) -> User:
|
def _user_to_obj(user: int | User) -> User:
|
||||||
@@ -40,4 +42,17 @@ class AlertsController:
|
|||||||
user = self._user_to_obj(user)
|
user = self._user_to_obj(user)
|
||||||
await alert.remove_user(self.table, user)
|
await alert.remove_user(self.table, user)
|
||||||
|
|
||||||
|
async def get_all_by_type(self, alert_type: int) -> List[Alert]:
|
||||||
|
"""Query all alerts of a specific type from the database."""
|
||||||
|
from aiodynamo.expressions import F
|
||||||
|
alerts = []
|
||||||
|
async for item in self.table.query(
|
||||||
|
key_condition=F("alert").equals(alert_type)
|
||||||
|
):
|
||||||
|
alert = Alert.from_item(
|
||||||
|
primary_key=item["alert"],
|
||||||
|
sort_key=item["flavor-region-price"],
|
||||||
|
users=item.get("users", [])
|
||||||
|
)
|
||||||
|
alerts.append(alert)
|
||||||
|
return alerts
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ from token_bot.persistant_database.user_schema import User
|
|||||||
class UsersController:
|
class UsersController:
|
||||||
def __init__(self, session: aiohttp.ClientSession):
|
def __init__(self, session: aiohttp.ClientSession):
|
||||||
self._pdb: pdb.Database = pdb.Database(session)
|
self._pdb: pdb.Database = pdb.Database(session)
|
||||||
self.table: aiodynamo.client.Table = self._pdb.client.table(os.getenv('USERS_TABLE'))
|
self.table: aiodynamo.client.Table = self._pdb.client.table(
|
||||||
|
os.getenv("USERS_TABLE")
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _user_to_obj(user: int | User) -> User:
|
def _user_to_obj(user: int | User) -> User:
|
||||||
@@ -67,4 +68,3 @@ class UsersController:
|
|||||||
await user.get(self.table)
|
await user.get(self.table)
|
||||||
user.subscribed_alerts.remove(alert)
|
user.subscribed_alerts.remove(alert)
|
||||||
await user.put(self.table)
|
await user.put(self.table)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from interactions.api.events import Startup
|
|||||||
from token_bot.token_database import database as pdb
|
from token_bot.token_database import database as pdb
|
||||||
from token_bot.token_database import database as tdb
|
from token_bot.token_database import database as tdb
|
||||||
|
|
||||||
VERSION = "0.9.0"
|
VERSION = "0.9.12"
|
||||||
|
|
||||||
|
|
||||||
class Core(Extension):
|
class Core(Extension):
|
||||||
@@ -18,15 +18,14 @@ class Core(Extension):
|
|||||||
|
|
||||||
@listen(Startup)
|
@listen(Startup)
|
||||||
async def on_start(self):
|
async def on_start(self):
|
||||||
self.bot.logger.log(logging.INFO,"TokenBot Core ready")
|
self.bot.logger.log(logging.INFO, "TokenBot Core ready")
|
||||||
self.bot.logger.log(logging.INFO,f"This is bot version {VERSION}")
|
self.bot.logger.log(logging.INFO, f"This is bot version {VERSION}")
|
||||||
self._tdb = tdb.Database(aiohttp.ClientSession())
|
self._tdb = tdb.Database(aiohttp.ClientSession())
|
||||||
|
|
||||||
@slash_command()
|
|
||||||
@check(is_owner())
|
|
||||||
async def version(self, ctx):
|
|
||||||
await ctx.send(f"This is bot version {VERSION}", ephemeral=True)
|
|
||||||
|
|
||||||
@slash_command()
|
@slash_command()
|
||||||
async def help(self, ctx):
|
async def help(self, ctx):
|
||||||
await ctx.send(f"This is bot help command", ephemeral=True)
|
await ctx.send(
|
||||||
|
f"For help on using GoblinBot, please visit the help page found "
|
||||||
|
f"[here](https://blog.emily.sh/token-bot/#getting-started)",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
|||||||
@@ -9,14 +9,19 @@ from token_bot.token_database.region import Region
|
|||||||
|
|
||||||
class History:
|
class History:
|
||||||
def __init__(self, flavor: Flavor, region: Region):
|
def __init__(self, flavor: Flavor, region: Region):
|
||||||
self._flavor : Flavor = flavor
|
self._flavor: Flavor = flavor
|
||||||
self._region : Region = region
|
self._region: Region = region
|
||||||
self._history : List[Tuple[datetime, int]] = []
|
self._history: List[Tuple[datetime, int]] = []
|
||||||
self._last_price_movement : int = 0
|
self._last_price_movement: int = 0
|
||||||
self._latest_price_datum : Tuple[datetime.datetime, int] | None = None
|
self._latest_price_datum: Tuple[datetime.datetime, int] | None = None
|
||||||
self._update_triggers : List[UpdateTrigger] = []
|
self._update_triggers: List[UpdateTrigger] = []
|
||||||
|
# Create triggers for all non-custom alert types
|
||||||
for alert_type in AlertType:
|
for alert_type in AlertType:
|
||||||
self._update_triggers.append(UpdateTrigger(Alert(alert_type, flavor, self._region)))
|
if alert_type != AlertType.SPECIFIC_PRICE:
|
||||||
|
self._update_triggers.append(
|
||||||
|
UpdateTrigger(Alert(alert_type, flavor, self._region))
|
||||||
|
)
|
||||||
|
# SPECIFIC_PRICE triggers are created on-demand as they have unique prices
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def flavor(self) -> Flavor:
|
def flavor(self) -> Flavor:
|
||||||
@@ -53,10 +58,21 @@ class History:
|
|||||||
self._history.append(datum)
|
self._history.append(datum)
|
||||||
return await self._process_update_triggers()
|
return await self._process_update_triggers()
|
||||||
|
|
||||||
async def find_update_trigger_from_alert(self, alert: Alert) -> UpdateTrigger:
|
async def find_update_trigger_from_alert(self, alert: Alert, initial_import: bool = False) -> UpdateTrigger:
|
||||||
for trigger in self._update_triggers:
|
for trigger in self._update_triggers:
|
||||||
if trigger.alert == alert:
|
if trigger.alert == alert:
|
||||||
return trigger
|
return trigger
|
||||||
|
|
||||||
|
# If not found and it's a SPECIFIC_PRICE alert, create it on-demand
|
||||||
|
if alert.alert_type == AlertType.SPECIFIC_PRICE:
|
||||||
|
new_trigger = UpdateTrigger(alert)
|
||||||
|
if initial_import:
|
||||||
|
new_trigger.squelched = True
|
||||||
|
self._update_triggers.append(new_trigger)
|
||||||
|
# Initialize the trigger with current history
|
||||||
|
if self._latest_price_datum is not None:
|
||||||
|
new_trigger.check_and_update(self._latest_price_datum, self._history)
|
||||||
|
new_trigger.squelched = False
|
||||||
|
return new_trigger
|
||||||
|
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,23 +9,31 @@ from token_bot.token_database.region import Region
|
|||||||
|
|
||||||
|
|
||||||
class HistoryManager:
|
class HistoryManager:
|
||||||
HIGH_FIDELITY_PERIOD = '72h'
|
HIGH_FIDELITY_PERIOD = "72h"
|
||||||
|
|
||||||
def __init__(self, token_db: tdb.Database):
|
def __init__(self, token_db: tdb.Database):
|
||||||
self._history : Dict[Flavor, Dict[Region, History]] = {}
|
self._history: Dict[Flavor, Dict[Region, History]] = {}
|
||||||
self._tdb : tdb.Database = token_db
|
self._tdb: tdb.Database = token_db
|
||||||
for flavor in Flavor:
|
for flavor in Flavor:
|
||||||
self._history[flavor] = {}
|
self._history[flavor] = {}
|
||||||
for region in Region:
|
for region in Region:
|
||||||
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) -> List[Tuple[datetime.datetime, int]]:
|
self, flavor: Flavor, region: Region
|
||||||
high_fidelity_time = datetime.datetime.now(tz=datetime.UTC) - datetime.timedelta(hours=72)
|
) -> List[Tuple[datetime.datetime, int]]:
|
||||||
|
high_fidelity_time = datetime.datetime.now(
|
||||||
|
tz=datetime.UTC
|
||||||
|
) - datetime.timedelta(hours=72)
|
||||||
all_history = await self._tdb.history(flavor, region)
|
all_history = await self._tdb.history(flavor, region)
|
||||||
high_fidelity_history = await self._tdb.history(flavor, region, self.HIGH_FIDELITY_PERIOD)
|
high_fidelity_history = await self._tdb.history(
|
||||||
|
flavor, region, self.HIGH_FIDELITY_PERIOD
|
||||||
|
)
|
||||||
final_response = []
|
final_response = []
|
||||||
|
|
||||||
def _convert_to_datetime(data: Tuple[str, int]) -> Tuple[datetime.datetime, int]:
|
def _convert_to_datetime(
|
||||||
|
data: Tuple[str, int]
|
||||||
|
) -> Tuple[datetime.datetime, int]:
|
||||||
return datetime.datetime.fromisoformat(data[0]), data[1]
|
return datetime.datetime.fromisoformat(data[0]), data[1]
|
||||||
|
|
||||||
for data_point in all_history:
|
for data_point in all_history:
|
||||||
@@ -33,11 +41,12 @@ class HistoryManager:
|
|||||||
if datetime_tuple[0] < high_fidelity_time:
|
if datetime_tuple[0] < high_fidelity_time:
|
||||||
final_response.append(datetime_tuple)
|
final_response.append(datetime_tuple)
|
||||||
|
|
||||||
final_response.extend(_convert_to_datetime(data_point) for data_point in high_fidelity_history)
|
final_response.extend(
|
||||||
|
_convert_to_datetime(data_point) for data_point in high_fidelity_history
|
||||||
|
)
|
||||||
|
|
||||||
return final_response
|
return final_response
|
||||||
|
|
||||||
|
|
||||||
async def load_data(self):
|
async def load_data(self):
|
||||||
for flavor in Flavor:
|
for flavor in Flavor:
|
||||||
for r in Region:
|
for r in Region:
|
||||||
@@ -48,6 +57,15 @@ class HistoryManager:
|
|||||||
await history.add_price(item)
|
await history.add_price(item)
|
||||||
self._history[flavor][region] = history
|
self._history[flavor][region] = history
|
||||||
|
|
||||||
|
async def load_custom_alerts(self, custom_alerts: List[Alert]):
|
||||||
|
"""Load custom price alerts and initialize their triggers with historical data."""
|
||||||
|
for alert in custom_alerts:
|
||||||
|
history = self._history[alert.flavor][alert.region]
|
||||||
|
# This will create the trigger on-demand via find_update_trigger_from_alert
|
||||||
|
trigger = await history.find_update_trigger_from_alert(alert)
|
||||||
|
# Process all historical data through this trigger to initialize its state
|
||||||
|
for datum in history.history:
|
||||||
|
trigger.check_and_update(datum, history.history)
|
||||||
|
|
||||||
async def update_data(self, flavor: Flavor, region: Region) -> List[Alert]:
|
async def update_data(self, flavor: Flavor, region: Region) -> List[Alert]:
|
||||||
history = self._history[flavor][region]
|
history = self._history[flavor][region]
|
||||||
@@ -55,12 +73,11 @@ class HistoryManager:
|
|||||||
current_region_data = current_price_data[region.value.lower()]
|
current_region_data = current_price_data[region.value.lower()]
|
||||||
datum = (
|
datum = (
|
||||||
datetime.datetime.fromisoformat(current_region_data[0]),
|
datetime.datetime.fromisoformat(current_region_data[0]),
|
||||||
current_region_data[1]
|
current_region_data[1],
|
||||||
)
|
)
|
||||||
if datum != history.last_price_datum:
|
if datum != history.last_price_datum:
|
||||||
return await history.add_price(datum)
|
return await history.add_price(datum)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def get_history(self, flavor, region) -> History:
|
def get_history(self, flavor, region) -> History:
|
||||||
return self._history[flavor][region]
|
return self._history[flavor][region]
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ from token_bot.token_database.flavor import Flavor
|
|||||||
|
|
||||||
class UpdateTrigger:
|
class UpdateTrigger:
|
||||||
def __init__(self, alert: Alert):
|
def __init__(self, alert: Alert):
|
||||||
self._alert : Alert = alert
|
self._alert: Alert = alert
|
||||||
self._last_trigger : Tuple[datetime.datetime, int] | None = None
|
self._last_trigger: Tuple[datetime.datetime, int] | None = None
|
||||||
self._last_alerting: Tuple[datetime.datetime, int] | None = None
|
self._last_alerting: Tuple[datetime.datetime, int] | None = None
|
||||||
self._squelched : bool = False
|
self._squelched: bool = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def alert(self) -> Alert:
|
def alert(self) -> Alert:
|
||||||
@@ -29,20 +29,39 @@ class UpdateTrigger:
|
|||||||
def squelched(self):
|
def squelched(self):
|
||||||
return self._squelched
|
return self._squelched
|
||||||
|
|
||||||
def _find_next_trigger(self, comparison_operator: Callable, starting_point: datetime.datetime, history: List[Tuple[datetime.datetime, int]]):
|
@squelched.setter
|
||||||
candidate_datum : Tuple[datetime.datetime, int] | None = None
|
def squelched(self, value):
|
||||||
|
self._squelched = value
|
||||||
|
|
||||||
|
def _find_next_trigger(
|
||||||
|
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:
|
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(datum[1], candidate_datum[1]):
|
if candidate_datum is None or comparison_operator(
|
||||||
|
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(self, new_datum: Tuple[datetime.datetime, int], history: List[Tuple[datetime.datetime, int]]) -> bool:
|
def check_and_update(
|
||||||
|
self,
|
||||||
|
new_datum: Tuple[datetime.datetime, int],
|
||||||
|
history: List[Tuple[datetime.datetime, int]],
|
||||||
|
) -> bool:
|
||||||
match self.alert.flavor:
|
match self.alert.flavor:
|
||||||
case Flavor.RETAIL:
|
case Flavor.RETAIL:
|
||||||
start_time = datetime.datetime.fromisoformat('2020-11-15 00:00:01.000000000+00:00')
|
start_time = datetime.datetime.fromisoformat(
|
||||||
|
"2020-11-15 00:00:01.000000000+00:00"
|
||||||
|
)
|
||||||
case Flavor.CLASSIC:
|
case Flavor.CLASSIC:
|
||||||
start_time = datetime.datetime.fromisoformat('2023-05-23 00:00:01.000000000+00:00')
|
start_time = datetime.datetime.fromisoformat(
|
||||||
|
"2023-05-23 00:00:01.000000000+00:00"
|
||||||
|
)
|
||||||
case _:
|
case _:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -78,12 +97,49 @@ class UpdateTrigger:
|
|||||||
case AlertType.ALL_TIME_HIGH:
|
case AlertType.ALL_TIME_HIGH:
|
||||||
time_range = now - start_time
|
time_range = now - start_time
|
||||||
comparison_operator = operator.gt
|
comparison_operator = operator.gt
|
||||||
|
case AlertType.SPECIFIC_PRICE:
|
||||||
|
# For custom price alerts, check if the price crosses the threshold
|
||||||
|
# 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 - initialize tracking
|
||||||
|
self._last_trigger = new_datum
|
||||||
|
if new_datum[1] >= target_price:
|
||||||
|
# Price already at/above target - alert and squelch
|
||||||
|
self._last_alerting = new_datum
|
||||||
|
self._squelched = True
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# Check if we crossed the threshold
|
||||||
|
old_price = self._last_trigger[1]
|
||||||
|
new_price = new_datum[1]
|
||||||
|
|
||||||
|
# Alert if we cross the threshold in either direction
|
||||||
|
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:
|
||||||
|
# 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
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# Not squelched - send alert and squelch
|
||||||
|
self._last_alerting = new_datum
|
||||||
|
self._squelched = True
|
||||||
|
return True
|
||||||
|
return False
|
||||||
case _:
|
case _:
|
||||||
# TODO: The logic here is certainly wrong for Custom
|
|
||||||
time_range = datetime.timedelta(days=int(365.25 * 6))
|
time_range = datetime.timedelta(days=int(365.25 * 6))
|
||||||
comparison_operator = operator.eq
|
comparison_operator = operator.eq
|
||||||
|
|
||||||
if new_datum[0] > now - time_range:
|
if new_datum[0] > now - time_range and self._alert.alert_type != AlertType.SPECIFIC_PRICE:
|
||||||
if self._last_trigger is None:
|
if self._last_trigger is None:
|
||||||
self._last_trigger = new_datum
|
self._last_trigger = new_datum
|
||||||
self._last_alerting = new_datum
|
self._last_alerting = new_datum
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from .alert_type import AlertType
|
from .alert_type import AlertType
|
||||||
from .user_schema import User
|
from .user_schema import User
|
||||||
from .alert_schema import Alert
|
from .alert_schema import Alert
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ class AlertCategory(Enum):
|
|||||||
CUSTOM = 3
|
CUSTOM = 3
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_str(category: str): # It gets mad when I use the Type[AlertCategory] as a type hint
|
def from_str(
|
||||||
|
category: str,
|
||||||
|
): # It gets mad when I use the Type[AlertCategory] as a type hint
|
||||||
match category:
|
match category:
|
||||||
case "high_alert_button":
|
case "high_alert_button":
|
||||||
return AlertCategory.HIGH
|
return AlertCategory.HIGH
|
||||||
|
|||||||
@@ -11,29 +11,36 @@ import token_bot.persistant_database as pdb
|
|||||||
|
|
||||||
|
|
||||||
class Alert:
|
class Alert:
|
||||||
def __init__(self, alert: pdb.AlertType, flavor: Flavor, region: Region, price: int = 0) -> None:
|
def __init__(
|
||||||
|
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
|
# 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
|
||||||
self.flavor: Flavor = flavor
|
self._flavor: Flavor = flavor
|
||||||
self.region: Region = region
|
self._region: Region = region
|
||||||
self.price: int = price
|
self._price: int = price
|
||||||
self._loaded: bool = False
|
self._loaded: bool = False
|
||||||
self.users: List[pdb.User] = []
|
self._users: List[pdb.User] = []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_item(cls, primary_key: int, sort_key: str, users: List[int]) -> 'Alert':
|
def from_item(cls, primary_key: int, sort_key: str, users: List[int]) -> "Alert":
|
||||||
alert_type = pdb.AlertType(primary_key)
|
alert_type = pdb.AlertType(primary_key)
|
||||||
flavor_repr, region_repr, price_repr = sort_key.split('-')
|
flavor_repr, region_repr, price_repr = sort_key.split("-")
|
||||||
flavor = Flavor(int(flavor_repr))
|
flavor = Flavor(int(flavor_repr))
|
||||||
region = Region(region_repr)
|
region = Region(region_repr)
|
||||||
price = int(price_repr)
|
price = int(price_repr)
|
||||||
return cls(alert_type, flavor, region, price)
|
return cls(alert_type, flavor, region, price)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_str(cls, string_trinity: str) -> 'Alert':
|
def from_str(cls, string_trinity: str) -> "Alert":
|
||||||
alert_repr, flavor_repr, region_repr, price_repr = string_trinity.split('-')
|
alert_repr, flavor_repr, region_repr, price_repr = string_trinity.split("-")
|
||||||
if len(string_trinity.split('-')) != 4:
|
if len(string_trinity.split("-")) != 4:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
alert = pdb.AlertType(int(alert_repr))
|
alert = pdb.AlertType(int(alert_repr))
|
||||||
flavor = Flavor(int(flavor_repr))
|
flavor = Flavor(int(flavor_repr))
|
||||||
@@ -61,20 +68,46 @@ class Alert:
|
|||||||
def key(self) -> dict[str, str | int]:
|
def key(self) -> dict[str, str | int]:
|
||||||
return {
|
return {
|
||||||
self.primary_key_name: self.primary_key,
|
self.primary_key_name: self.primary_key,
|
||||||
self.sort_key_name: self.sort_key
|
self.sort_key_name: self.sort_key,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def alert_type(self) -> pdb.AlertType:
|
||||||
|
return self._alert_type
|
||||||
|
|
||||||
|
@property
|
||||||
|
def flavor(self) -> Flavor:
|
||||||
|
return self._flavor
|
||||||
|
|
||||||
|
@property
|
||||||
|
def region(self) -> Region:
|
||||||
|
return self._region
|
||||||
|
|
||||||
|
@property
|
||||||
|
def price(self) -> int:
|
||||||
|
return self._price
|
||||||
|
|
||||||
|
@property
|
||||||
|
def users(self) -> List[pdb.User]:
|
||||||
|
return self._users
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.alert_type.value}-{self.flavor.value}-{self.region.value}-{self.price}"
|
return f"{self.alert_type.value}-{self.flavor.value}-{self.region.value}-{self.price}"
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.alert_type == other.alert_type and self.flavor == other.flavor and self.price == other.price
|
return (
|
||||||
|
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):
|
def to_human_string(self):
|
||||||
if self.alert_type == AlertType.SPECIFIC_PRICE:
|
if self.alert_type == AlertType.SPECIFIC_PRICE:
|
||||||
raise NotImplementedError
|
price_gold = self.price
|
||||||
|
return f"Custom Price: {format(price_gold, ',')}g"
|
||||||
else:
|
else:
|
||||||
alert_type_str = ' '.join(self.alert_type.name.split("_"))
|
alert_type_str = " ".join(self.alert_type.name.split("_"))
|
||||||
return f"{alert_type_str.title()}"
|
return f"{alert_type_str.title()}"
|
||||||
|
|
||||||
async def _lazy_load(self, table: Table, consistent: bool = False) -> None:
|
async def _lazy_load(self, table: Table, consistent: bool = False) -> None:
|
||||||
@@ -95,19 +128,16 @@ class Alert:
|
|||||||
item={
|
item={
|
||||||
self.primary_key_name: self.primary_key,
|
self.primary_key_name: self.primary_key,
|
||||||
self.sort_key_name: self.sort_key,
|
self.sort_key_name: self.sort_key,
|
||||||
'users': user_ids
|
"users": user_ids,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get(self, table: Table, consistent: bool = False) -> bool:
|
async def get(self, table: Table, consistent: bool = False) -> bool:
|
||||||
try:
|
try:
|
||||||
response = await table.get_item(
|
response = await table.get_item(key=self.key, consistent_read=consistent)
|
||||||
key=self.key,
|
|
||||||
consistent_read=consistent
|
|
||||||
)
|
|
||||||
except ItemNotFound:
|
except ItemNotFound:
|
||||||
return False
|
return False
|
||||||
self.users = [pdb.User(int(user_id)) for user_id in response['users']]
|
self._users = [pdb.User(int(user_id)) for user_id in response["users"]]
|
||||||
self._loaded = True
|
self._loaded = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -116,13 +146,17 @@ class Alert:
|
|||||||
|
|
||||||
return self.users
|
return self.users
|
||||||
|
|
||||||
async def add_user(self, table: Table, user: pdb.User, consistent: bool = False) -> None:
|
async def add_user(
|
||||||
|
self, table: Table, user: pdb.User, consistent: bool = False
|
||||||
|
) -> None:
|
||||||
await self._lazy_load(table, consistent=consistent)
|
await self._lazy_load(table, consistent=consistent)
|
||||||
|
|
||||||
if user not in self.users:
|
if user not in self.users:
|
||||||
await self._append_user(table=table, user=user)
|
await self._append_user(table=table, user=user)
|
||||||
|
|
||||||
async def remove_user(self, table: Table, user: pdb.User, consistent: bool = True) -> None:
|
async def remove_user(
|
||||||
|
self, table: Table, user: pdb.User, consistent: bool = True
|
||||||
|
) -> None:
|
||||||
await self._lazy_load(table, consistent=consistent)
|
await self._lazy_load(table, consistent=consistent)
|
||||||
|
|
||||||
if user in self.users:
|
if user in self.users:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
class AlertType(Enum):
|
class AlertType(Enum):
|
||||||
ALL_TIME_HIGH = 1
|
ALL_TIME_HIGH = 1
|
||||||
ALL_TIME_LOW = 2
|
ALL_TIME_LOW = 2
|
||||||
@@ -37,4 +38,7 @@ class AlertType(Enum):
|
|||||||
case "All Time Low":
|
case "All Time Low":
|
||||||
return AlertType.ALL_TIME_LOW
|
return AlertType.ALL_TIME_LOW
|
||||||
case _:
|
case _:
|
||||||
|
# Check if it's a custom price format like "Custom Price: 250,000g"
|
||||||
|
if category.startswith("Custom Price"):
|
||||||
|
return AlertType.SPECIFIC_PRICE
|
||||||
return AlertType.SPECIFIC_PRICE
|
return AlertType.SPECIFIC_PRICE
|
||||||
@@ -8,5 +8,6 @@ from aiodynamo.http.aiohttp import AIOHTTP
|
|||||||
|
|
||||||
class Database:
|
class Database:
|
||||||
def __init__(self, session: aiohttp.ClientSession):
|
def __init__(self, session: aiohttp.ClientSession):
|
||||||
self.client = Client(AIOHTTP(session), Credentials.auto(), os.getenv('AWS_REGION'))
|
self.client = Client(
|
||||||
|
AIOHTTP(session), Credentials.auto(), os.getenv("AWS_REGION")
|
||||||
|
)
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ from token_bot.token_database.region import Region
|
|||||||
|
|
||||||
|
|
||||||
class User:
|
class User:
|
||||||
def __init__(self, user_id: int, region: Region = None, subscribed_alerts: List['pdb.Alert'] = None) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
user_id: int,
|
||||||
|
region: Region = None,
|
||||||
|
subscribed_alerts: List["pdb.Alert"] = None,
|
||||||
|
) -> None:
|
||||||
self.user_id: int = user_id
|
self.user_id: int = user_id
|
||||||
self._loaded: bool = False
|
self._loaded: bool = False
|
||||||
self.region: Region = region
|
self.region: Region = region
|
||||||
@@ -21,7 +26,9 @@ class User:
|
|||||||
return hash(self.user_id)
|
return hash(self.user_id)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_item(cls, primary_key: int, region: Region, subscribed_alerts: List[str]) -> 'User':
|
def from_item(
|
||||||
|
cls, primary_key: int, region: Region, subscribed_alerts: List[str]
|
||||||
|
) -> "User":
|
||||||
alerts = [pdb.Alert.from_str(alert_str) for alert_str in subscribed_alerts]
|
alerts = [pdb.Alert.from_str(alert_str) for alert_str in subscribed_alerts]
|
||||||
return cls(primary_key, region, alerts)
|
return cls(primary_key, region, alerts)
|
||||||
|
|
||||||
@@ -31,17 +38,18 @@ class User:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def primary_key_name(self) -> str:
|
def primary_key_name(self) -> str:
|
||||||
return 'user_id'
|
return "user_id"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def key(self) -> Dict[str, str]:
|
def key(self) -> Dict[str, str]:
|
||||||
return {
|
return {self.primary_key_name: self.primary_key}
|
||||||
self.primary_key_name: self.primary_key
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _subscribed_alerts_as_trinity_list(self) -> List[str]:
|
def _subscribed_alerts_as_trinity_list(self) -> List[str]:
|
||||||
return [str(alert) for alert in self.subscribed_alerts] if self.subscribed_alerts else []
|
return (
|
||||||
|
[str(alert) for alert in self.subscribed_alerts]
|
||||||
|
if self.subscribed_alerts
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
|
||||||
async def _lazy_load(self, table: Table, consistent: bool = False) -> None:
|
async def _lazy_load(self, table: Table, consistent: bool = False) -> None:
|
||||||
if consistent or not self._loaded:
|
if consistent or not self._loaded:
|
||||||
@@ -51,8 +59,8 @@ class User:
|
|||||||
await table.put_item(
|
await table.put_item(
|
||||||
item={
|
item={
|
||||||
self.primary_key_name: self.primary_key,
|
self.primary_key_name: self.primary_key,
|
||||||
'region': self.region,
|
"region": self.region,
|
||||||
'subscribed_alerts': self._subscribed_alerts_as_trinity_list()
|
"subscribed_alerts": self._subscribed_alerts_as_trinity_list(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -65,26 +73,27 @@ class User:
|
|||||||
|
|
||||||
async def get(self, table: Table, consistent: bool = False) -> bool:
|
async def get(self, table: Table, consistent: bool = False) -> bool:
|
||||||
try:
|
try:
|
||||||
response = await table.get_item(
|
response = await table.get_item(key=self.key, consistent_read=consistent)
|
||||||
key=self.key,
|
|
||||||
consistent_read=consistent
|
|
||||||
)
|
|
||||||
except ItemNotFound:
|
except ItemNotFound:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.subscribed_alerts = []
|
self.subscribed_alerts = []
|
||||||
for string_trinity in response['subscribed_alerts']:
|
for string_trinity in response["subscribed_alerts"]:
|
||||||
self.subscribed_alerts.append(pdb.Alert.from_str(string_trinity))
|
self.subscribed_alerts.append(pdb.Alert.from_str(string_trinity))
|
||||||
self.region = Region(response['region'])
|
self.region = Region(response["region"])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def add_alert(self, table: Table, alert: 'pdb.Alert', consistent: bool = False) -> None:
|
async def add_alert(
|
||||||
|
self, table: Table, alert: "pdb.Alert", consistent: bool = False
|
||||||
|
) -> None:
|
||||||
await self._lazy_load(table, consistent=consistent)
|
await self._lazy_load(table, consistent=consistent)
|
||||||
if alert not in self.subscribed_alerts:
|
if alert not in self.subscribed_alerts:
|
||||||
self.subscribed_alerts.append(alert)
|
self.subscribed_alerts.append(alert)
|
||||||
await self.put(table)
|
await self.put(table)
|
||||||
|
|
||||||
async def remove_alert(self, table: Table, alert: 'pdb.Alert', consistent: bool = True) -> None:
|
async def remove_alert(
|
||||||
|
self, table: Table, alert: "pdb.Alert", consistent: bool = True
|
||||||
|
) -> None:
|
||||||
await self._lazy_load(table, consistent=consistent)
|
await self._lazy_load(table, consistent=consistent)
|
||||||
if alert in self.subscribed_alerts:
|
if alert in self.subscribed_alerts:
|
||||||
self.subscribed_alerts.remove(alert)
|
self.subscribed_alerts.remove(alert)
|
||||||
|
|||||||
@@ -10,15 +10,17 @@ class TokenBot:
|
|||||||
load_dotenv()
|
load_dotenv()
|
||||||
print("#### WoW Token Bot Startup ####")
|
print("#### WoW Token Bot Startup ####")
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
format='%(asctime)s.%(msecs)03d %(levelname)s %(module)s - %(funcName)s: %(message)s',
|
format="%(asctime)s.%(msecs)03d %(levelname)s %(module)s - %(funcName)s: %(message)s",
|
||||||
datefmt='%Y-%m-%d %H:%M:%S',
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
)
|
)
|
||||||
log = logging.getLogger("TokenBotLogger")
|
log = logging.getLogger("TokenBotLogger")
|
||||||
log.setLevel(logging.INFO)
|
log.setLevel(logging.INFO)
|
||||||
|
is_debug = os.getenv("ENV") == "DEBUG"
|
||||||
self.bot = Client(
|
self.bot = Client(
|
||||||
intents=Intents.DEFAULT,
|
intents=Intents.DEFAULT,
|
||||||
asyncio_debug=True,
|
asyncio_debug=is_debug,
|
||||||
logger=log
|
send_command_tracebacks=is_debug,
|
||||||
|
logger=log,
|
||||||
)
|
)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import asyncio
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@@ -17,13 +18,16 @@ class Database:
|
|||||||
|
|
||||||
success = False
|
success = False
|
||||||
tries = 0
|
tries = 0
|
||||||
|
backoff = 0.1
|
||||||
|
|
||||||
while not success and tries < 3:
|
while not success and tries <= 5:
|
||||||
|
if tries > 1:
|
||||||
|
await asyncio.sleep(backoff * tries)
|
||||||
try:
|
try:
|
||||||
return await self._external_call(url)
|
return await self._external_call(url)
|
||||||
except TokenHttpException:
|
except TokenHttpException:
|
||||||
tries += 1
|
tries += 1
|
||||||
return {}
|
raise TokenHttpException
|
||||||
|
|
||||||
async def _external_call(self, url) -> Dict | List:
|
async def _external_call(self, url) -> Dict | List:
|
||||||
async with self.session.get(url) as resp:
|
async with self.session.get(url) as resp:
|
||||||
@@ -34,7 +38,9 @@ class Database:
|
|||||||
raise TokenHttpException(resp.status)
|
raise TokenHttpException(resp.status)
|
||||||
|
|
||||||
async def current(self, flavor: Flavor) -> dict:
|
async def current(self, flavor: Flavor) -> dict:
|
||||||
return await self._get_data(f'current/{flavor.name.lower()}.json')
|
return await self._get_data(f"current/{flavor.name.lower()}.json")
|
||||||
|
|
||||||
async def history(self, flavor: Flavor, region: Region, relative_time: str = 'all'):
|
async def history(self, flavor: Flavor, region: Region, relative_time: str = "all"):
|
||||||
return await self._get_data(f'relative/{flavor.name.lower()}/{region.value.lower()}/{relative_time}.json')
|
return await self._get_data(
|
||||||
|
f"relative/{flavor.name.lower()}/{region.value.lower()}/{relative_time}.json"
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,4 +4,3 @@ from enum import Enum
|
|||||||
class Flavor(Enum):
|
class Flavor(Enum):
|
||||||
RETAIL = 1
|
RETAIL = 1
|
||||||
CLASSIC = 2
|
CLASSIC = 2
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
class Region(str, Enum):
|
class Region(str, Enum):
|
||||||
US = 'us'
|
US = "us"
|
||||||
EU = 'eu'
|
EU = "eu"
|
||||||
KR = 'kr'
|
KR = "kr"
|
||||||
TW = 'tw'
|
TW = "tw"
|
||||||
|
|||||||
@@ -2,15 +2,32 @@ import asyncio
|
|||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from typing import Type, Dict, List
|
from typing import Type, Dict, List
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from interactions import Extension, SlashContext, component_callback, \
|
from interactions import (
|
||||||
ComponentContext, StringSelectMenu, Message, Embed, EmbedField, is_owner, check, StringSelectOption
|
Extension,
|
||||||
|
SlashContext,
|
||||||
|
component_callback,
|
||||||
|
ComponentContext,
|
||||||
|
StringSelectMenu,
|
||||||
|
Message,
|
||||||
|
Embed,
|
||||||
|
EmbedField,
|
||||||
|
is_owner,
|
||||||
|
check,
|
||||||
|
StringSelectOption, integration_types,
|
||||||
|
Modal,
|
||||||
|
ShortText,
|
||||||
|
modal_callback,
|
||||||
|
ModalContext,
|
||||||
|
)
|
||||||
from interactions import Task, IntervalTrigger
|
from interactions import Task, IntervalTrigger
|
||||||
from interactions import slash_command, listen
|
from interactions import slash_command, listen
|
||||||
from interactions.api.events import Component
|
from interactions.api.events import Component
|
||||||
from interactions.api.events import Startup
|
from interactions.api.events import Startup
|
||||||
|
from interactions.client.errors import Forbidden
|
||||||
|
|
||||||
from token_bot.controller.alerts import AlertsController
|
from token_bot.controller.alerts import AlertsController
|
||||||
from token_bot.controller.users import UsersController
|
from token_bot.controller.users import UsersController
|
||||||
@@ -27,8 +44,10 @@ from token_bot.ui.select_menus.alert_menu import HIGH_ALERT_MENU, LOW_ALERT_MENU
|
|||||||
from token_bot.ui.select_menus.flavor_menu import FLAVOR_MENU
|
from token_bot.ui.select_menus.flavor_menu import FLAVOR_MENU
|
||||||
from token_bot.ui.select_menus.region_menu import REGION_MENU
|
from token_bot.ui.select_menus.region_menu import REGION_MENU
|
||||||
|
|
||||||
|
|
||||||
#### Static Helper Functions
|
#### Static Helper Functions
|
||||||
|
|
||||||
|
|
||||||
async def gather_alerts_by_flavor(alerts: List[Alert]) -> Dict[Flavor, List[Alert]]:
|
async def gather_alerts_by_flavor(alerts: List[Alert]) -> Dict[Flavor, List[Alert]]:
|
||||||
alerts_by_flavor = {}
|
alerts_by_flavor = {}
|
||||||
for alert in alerts:
|
for alert in alerts:
|
||||||
@@ -39,14 +58,13 @@ async def gather_alerts_by_flavor(alerts: List[Alert]) -> Dict[Flavor, List[Aler
|
|||||||
return alerts_by_flavor
|
return alerts_by_flavor
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Tracker(Extension):
|
class Tracker(Extension):
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self._users: UsersController | None = None
|
self._users: UsersController | None = None
|
||||||
self._alerts: AlertsController | None = None
|
self._alerts: AlertsController | None = None
|
||||||
self._tdb: tdb.Database | None = None
|
self._tdb: tdb.Database | None = None
|
||||||
self._history_manager: HistoryManager | None = None
|
self._history_manager: HistoryManager | None = None
|
||||||
|
self._session: aiohttp.ClientSession | None = None
|
||||||
|
|
||||||
###################################
|
###################################
|
||||||
# Task Functions #
|
# Task Functions #
|
||||||
@@ -66,22 +84,48 @@ class Tracker(Extension):
|
|||||||
users_alerts[user] = [alert]
|
users_alerts[user] = [alert]
|
||||||
else:
|
else:
|
||||||
users_alerts[user].append(alert)
|
users_alerts[user].append(alert)
|
||||||
for user in users_alerts:
|
if users_alerts:
|
||||||
discord_user = await self.bot.fetch_user(user.user_id)
|
self.bot.logger.log(
|
||||||
embeds = [Embed(
|
logging.INFO, "TokenBot Tracker: Processing User Alerts"
|
||||||
title="TokenBot Tracker Alert Triggered",
|
)
|
||||||
color=0xb10000,
|
for user in users_alerts:
|
||||||
description=f"Hello, you requested to be sent an alert when the price of the World of Warcraft "
|
discord_user = await self.bot.fetch_user(user.user_id)
|
||||||
f"token reaches a certain value.\n\n"
|
alerts_by_flavor = await gather_alerts_by_flavor(users_alerts[user])
|
||||||
f"As a reminder, you can remove an alert via ```/remove-alert```\n"
|
alert_tally = 0
|
||||||
f"or you can remove all registrations via ```/remove-registration```\n\n"
|
for flavor in alerts_by_flavor:
|
||||||
)]
|
for _ in alerts_by_flavor[flavor]:
|
||||||
alerts_by_flavor = await gather_alerts_by_flavor(users_alerts[user])
|
alert_tally += 1
|
||||||
for flavor in alerts_by_flavor:
|
alert_word = "alert" if alert_tally == 1 else "alerts"
|
||||||
embeds.append(await self.render_alert_flavor(alerts_by_flavor[flavor], user=user))
|
embeds = [
|
||||||
|
Embed(
|
||||||
await discord_user.send(embeds=embeds)
|
title="GoblinBot Tracker Alert Triggered",
|
||||||
|
color=0xB10000,
|
||||||
|
description=f"You requested to be alerted on the WoW token price. You have {alert_tally} {alert_word}\n\n",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
for flavor in alerts_by_flavor:
|
||||||
|
embeds.append(
|
||||||
|
await self._render_alert_flavor(
|
||||||
|
alerts_by_flavor[flavor], user=user
|
||||||
|
)
|
||||||
|
)
|
||||||
|
embeds.append(
|
||||||
|
Embed(
|
||||||
|
title="",
|
||||||
|
color=0xB10000,
|
||||||
|
description=f"You can remove an alert via ```/remove-alert```\n"
|
||||||
|
f"or you can remove all alerts and user data via ```/remove-registration```\n",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await discord_user.send(embeds=embeds)
|
||||||
|
except Forbidden:
|
||||||
|
self.bot.logger.log(
|
||||||
|
logging.ERROR, f"User: {discord_user.id} has no permissions to send alerts, skipping")
|
||||||
|
|
||||||
|
self.bot.logger.log(
|
||||||
|
logging.INFO, "TokenBot Tracker: Done Processing User Alerts"
|
||||||
|
)
|
||||||
|
|
||||||
###################################
|
###################################
|
||||||
# Slash Commands #
|
# Slash Commands #
|
||||||
@@ -90,36 +134,55 @@ class Tracker(Extension):
|
|||||||
@listen(Startup)
|
@listen(Startup)
|
||||||
async def on_start(self):
|
async def on_start(self):
|
||||||
self.bot.logger.log(logging.INFO, "TokenBot Tracker: Initializing")
|
self.bot.logger.log(logging.INFO, "TokenBot Tracker: Initializing")
|
||||||
self._users = UsersController(aiohttp.ClientSession())
|
# Create a single shared ClientSession for all components
|
||||||
self._alerts = AlertsController(aiohttp.ClientSession())
|
self._session = aiohttp.ClientSession()
|
||||||
self._tdb = tdb.Database(aiohttp.ClientSession())
|
self._users = UsersController(self._session)
|
||||||
|
self._alerts = AlertsController(self._session)
|
||||||
|
self._tdb = tdb.Database(self._session)
|
||||||
self._history_manager = HistoryManager(self._tdb)
|
self._history_manager = HistoryManager(self._tdb)
|
||||||
self.bot.logger.log(logging.INFO, "TokenBot Tracker: Initialized")
|
self.bot.logger.log(logging.INFO, "TokenBot Tracker: Initialized")
|
||||||
self.bot.logger.log(logging.INFO, "TokenBot Tracker: Loading Historical Data")
|
self.bot.logger.log(logging.INFO, "TokenBot Tracker: Loading Historical Data")
|
||||||
await self._history_manager.load_data()
|
await self._history_manager.load_data()
|
||||||
self.bot.logger.log(logging.INFO, "TokenBot Tracker: Loading Historical Data Finished")
|
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.bot.logger.log(logging.INFO, "TokenBot Tracker: Started")
|
||||||
self.update_data.start()
|
self.update_data.start()
|
||||||
|
|
||||||
|
def extension_unload(self):
|
||||||
|
"""Clean up resources when the extension is unloaded"""
|
||||||
|
if self._session and not self._session.closed:
|
||||||
|
asyncio.create_task(self._session.close())
|
||||||
|
self.bot.logger.log(logging.INFO, "TokenBot Tracker: ClientSession closed")
|
||||||
|
|
||||||
@slash_command(
|
@slash_command(
|
||||||
name="register",
|
name="register",
|
||||||
description="Register with TokenBot for alerts on token price changes."
|
description="Register with a new GoblinBot Region for alerts on token price changes.",
|
||||||
)
|
)
|
||||||
|
@integration_types(guild=True, user=True)
|
||||||
async def register(self, ctx: SlashContext):
|
async def register(self, ctx: SlashContext):
|
||||||
text = ("## Select a region to register with \n\n"
|
text = (
|
||||||
"Please note: \n"
|
"## Select a region to register with \n\n"
|
||||||
"* You can only be registered with one region at a time \n"
|
"Please note: \n"
|
||||||
"* Changing your region will remove all previous alerts you have signed up for \n"
|
"* You can only be registered with one region at a time \n"
|
||||||
"* You can remove all alerts and registration using ```/remove-registration```")
|
"* Changing your region will remove all previous alerts you have signed up for \n"
|
||||||
|
"* You can remove all alerts and user data using ```/remove-registration```"
|
||||||
|
)
|
||||||
menu = copy.deepcopy(REGION_MENU)
|
menu = copy.deepcopy(REGION_MENU)
|
||||||
await ctx.send(text, components=menu, ephemeral=True)
|
await ctx.send(text, components=menu, ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
@slash_command(
|
@slash_command(
|
||||||
name="remove-registration",
|
name="remove-registration",
|
||||||
description="Remove all alerts and registration from TokenBot"
|
description="Remove all alerts and registration from GoblinBot",
|
||||||
)
|
)
|
||||||
|
@integration_types(guild=True, user=True)
|
||||||
async def remove_registration(self, ctx: SlashContext):
|
async def remove_registration(self, ctx: SlashContext):
|
||||||
if await self._users.exists(ctx.user.id):
|
if await self._users.exists(ctx.user.id):
|
||||||
user = await self._users.get(ctx.user.id)
|
user = await self._users.get(ctx.user.id)
|
||||||
@@ -127,82 +190,65 @@ class Tracker(Extension):
|
|||||||
await self._alerts.remove_user(alert, user)
|
await self._alerts.remove_user(alert, user)
|
||||||
await self._users.delete(ctx.user.id)
|
await self._users.delete(ctx.user.id)
|
||||||
|
|
||||||
await ctx.send("All alert subscriptions and user registration deleted", ephemeral=True)
|
await ctx.send("All alert subscriptions and user data deleted", ephemeral=True)
|
||||||
|
|
||||||
|
@slash_command(description="The current retail token cost")
|
||||||
@slash_command(
|
@integration_types(guild=True, user=True)
|
||||||
name="exists",
|
|
||||||
description="Check if you are registered with TokenBot"
|
|
||||||
)
|
|
||||||
@check(is_owner())
|
|
||||||
async def exists(self, ctx: SlashContext):
|
|
||||||
await ctx.send(str(await self._users.exists(ctx.user.id)), ephemeral=True)
|
|
||||||
|
|
||||||
|
|
||||||
@slash_command(
|
|
||||||
description="The current retail token cost"
|
|
||||||
)
|
|
||||||
async def current(self, ctx: SlashContext):
|
async def current(self, ctx: SlashContext):
|
||||||
current_str = await self.get_current_token(ctx, tdb.Flavor.RETAIL)
|
current_str = await self.get_current_token(ctx, tdb.Flavor.RETAIL)
|
||||||
await ctx.send(current_str, ephemeral=True)
|
await ctx.send(current_str, ephemeral=True)
|
||||||
|
|
||||||
|
@slash_command(description="The current classic token cost")
|
||||||
@slash_command(
|
@integration_types(guild=True, user=True)
|
||||||
description="The current classic token cost"
|
|
||||||
)
|
|
||||||
async def current_classic(self, ctx: SlashContext):
|
async def current_classic(self, ctx: SlashContext):
|
||||||
current_str = await self.get_current_token(ctx, tdb.Flavor.CLASSIC)
|
current_str = await self.get_current_token(ctx, tdb.Flavor.CLASSIC)
|
||||||
await ctx.send(current_str, ephemeral=True)
|
await ctx.send(current_str, ephemeral=True)
|
||||||
|
|
||||||
|
@slash_command(name="add-alert", description="Add an alert listener")
|
||||||
@slash_command(
|
@integration_types(guild=True, user=True)
|
||||||
name="add-alert",
|
|
||||||
description="Add an alert listener"
|
|
||||||
)
|
|
||||||
async def add_alert(self, ctx: SlashContext):
|
async def add_alert(self, ctx: SlashContext):
|
||||||
if not await self._users.exists(ctx.user.id):
|
if not await self._users.exists(ctx.user.id):
|
||||||
await ctx.send("You are not registered with any region\n"
|
try:
|
||||||
"Please register with /register before adding alerts",
|
await self.region_select_menu(ctx)
|
||||||
ephemeral=True)
|
except TimeoutError:
|
||||||
return
|
return
|
||||||
|
|
||||||
user = await self._users.get(ctx.user.id)
|
user = await self._users.get(ctx.user.id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
flavor = await self.flavor_select_menu(ctx)
|
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:
|
match alert_category:
|
||||||
case AlertCategory.LOW:
|
case AlertCategory.LOW:
|
||||||
alert_type = await self.low_alert_select_menu(ctx)
|
alert_type = await self.low_alert_select_menu(ctx)
|
||||||
case AlertCategory.HIGH:
|
case AlertCategory.HIGH:
|
||||||
alert_type = await self.high_alert_select_menu(ctx)
|
alert_type = await self.high_alert_select_menu(ctx)
|
||||||
case _:
|
case AlertCategory.CUSTOM:
|
||||||
raise NotImplementedError
|
alert_type = AlertType.SPECIFIC_PRICE
|
||||||
|
|
||||||
except TimeoutError:
|
except (TimeoutError, ValueError):
|
||||||
return
|
return
|
||||||
|
|
||||||
else:
|
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):
|
if not await self._users.is_subscribed(user, alert):
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
self._users.add_alert(user, alert),
|
self._users.add_alert(user, alert),
|
||||||
self._alerts.add_user(alert, user)
|
self._alerts.add_user(alert, user),
|
||||||
)
|
)
|
||||||
await ctx.send("Successfully added alert", ephemeral=True)
|
await ctx.send("Successfully added alert", ephemeral=True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
await ctx.send("You are already subscribed to this alert", ephemeral=True)
|
await ctx.send(
|
||||||
|
"You are already subscribed to this alert", ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
@slash_command(
|
@slash_command(
|
||||||
name="remove-alert",
|
name="remove-alert", description="Remove an alert you have signed up for"
|
||||||
description="Remove an alert you have signed up for"
|
|
||||||
)
|
)
|
||||||
|
@integration_types(guild=True, user=True)
|
||||||
async def remove_alert(self, ctx: SlashContext):
|
async def remove_alert(self, ctx: SlashContext):
|
||||||
if not await self._users.exists(ctx.user.id):
|
if not await self._user_is_registered(ctx):
|
||||||
await ctx.send("You are not registered with any region\n"
|
|
||||||
"Please register with /register before adding alerts",
|
|
||||||
ephemeral=True)
|
|
||||||
return
|
return
|
||||||
user = await self._users.get(ctx.user.id)
|
user = await self._users.get(ctx.user.id)
|
||||||
alerts = await self._users.list_alerts(user)
|
alerts = await self._users.list_alerts(user)
|
||||||
@@ -217,20 +263,16 @@ class Tracker(Extension):
|
|||||||
else:
|
else:
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
self._users.remove_alert(user, alert),
|
self._users.remove_alert(user, alert),
|
||||||
self._alerts.remove_user(alert, user)
|
self._alerts.remove_user(alert, user),
|
||||||
)
|
)
|
||||||
await ctx.send("Successfully removed alert", ephemeral=True)
|
await ctx.send("Successfully removed alert", ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
@slash_command(
|
@slash_command(
|
||||||
name="list-alerts",
|
name="list-alerts", description="List all alerts you have signed up for"
|
||||||
description="List all alerts you have signed up for"
|
|
||||||
)
|
)
|
||||||
|
@integration_types(guild=True, user=True)
|
||||||
async def list_alerts(self, ctx: SlashContext):
|
async def list_alerts(self, ctx: SlashContext):
|
||||||
if not await self._users.exists(ctx.user.id):
|
if not await self._user_is_registered(ctx):
|
||||||
await ctx.send("You are not registered with any region\n"
|
|
||||||
"Please register with /register before adding alerts",
|
|
||||||
ephemeral=True)
|
|
||||||
return
|
return
|
||||||
user = await self._users.get(ctx.user.id)
|
user = await self._users.get(ctx.user.id)
|
||||||
alerts = await self._users.list_alerts(user)
|
alerts = await self._users.list_alerts(user)
|
||||||
@@ -238,57 +280,36 @@ class Tracker(Extension):
|
|||||||
await ctx.send("You do not have any alerts registered", ephemeral=True)
|
await ctx.send("You do not have any alerts registered", ephemeral=True)
|
||||||
return
|
return
|
||||||
alerts_str = f"You have {len(alerts)} out of 25 maximum alerts registered"
|
alerts_str = f"You have {len(alerts)} out of 25 maximum alerts registered"
|
||||||
embeds = [Embed(
|
embeds = [
|
||||||
title="List of TokenBot Tracker Alerts",
|
Embed(
|
||||||
color=0x0000b1,
|
title="List of GoblinBot Tracker Alerts",
|
||||||
description=alerts_str
|
color=0x0000B1,
|
||||||
)]
|
description=alerts_str,
|
||||||
|
)
|
||||||
|
]
|
||||||
alerts_by_flavor = await gather_alerts_by_flavor(alerts)
|
alerts_by_flavor = await gather_alerts_by_flavor(alerts)
|
||||||
for flavor in alerts_by_flavor:
|
for flavor in alerts_by_flavor:
|
||||||
embeds.append(await self.render_alert_flavor(alerts_by_flavor[flavor], user=user))
|
embeds.append(
|
||||||
|
await self._render_alert_flavor(alerts_by_flavor[flavor], user=user)
|
||||||
|
)
|
||||||
await ctx.send(embeds=embeds, ephemeral=True)
|
await ctx.send(embeds=embeds, ephemeral=True)
|
||||||
|
|
||||||
###################################
|
###################################
|
||||||
# Callbacks Commands #
|
# Callbacks Commands #
|
||||||
###################################
|
###################################
|
||||||
|
|
||||||
@component_callback('flavor_menu')
|
# Note: Callbacks for flavor_menu, high_alert_menu, low_alert_menu, and alert buttons
|
||||||
async def flavor_menu(self, ctx: ComponentContext):
|
# are disabled because they interfere with wait_for_component manual handling
|
||||||
await ctx.send(f"Selected Flavor: {ctx.values[0]}", ephemeral=True)
|
|
||||||
|
|
||||||
@component_callback('high_alert_menu')
|
@component_callback("region_menu")
|
||||||
async def alert_menu(self, ctx: ComponentContext):
|
async def region_menu_cb(self, ctx: ComponentContext):
|
||||||
await ctx.send(f"Selected Alert: {ctx.values[0]}", ephemeral=True)
|
discord_user = await self.bot.fetch_user(ctx.user.id)
|
||||||
|
await discord_user.send(
|
||||||
@component_callback('low_alert_menu')
|
"You have successfully registered your region with GoblinBot!\n"
|
||||||
async def alert_menu(self, ctx: ComponentContext):
|
"Most interactions will happen in direct messages with GoblinBot here.\n"
|
||||||
await ctx.send(f"Selected Alert: {ctx.values[0]}", ephemeral=True)
|
"You can remove your user data and alerts at any time using ```/remove-registration```\n"
|
||||||
|
)
|
||||||
@component_callback('remove_alert_menu')
|
await ctx.defer(edit_origin=True, suppress_error=True)
|
||||||
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)
|
|
||||||
|
|
||||||
@component_callback('region_menu')
|
|
||||||
async def region_menu(self, ctx: ComponentContext):
|
|
||||||
user = User(ctx.user.id, Region(ctx.values[0].lower()), subscribed_alerts=[])
|
|
||||||
await self._users.add(user)
|
|
||||||
discord_user = await self.bot.fetch_user(user.user_id)
|
|
||||||
await discord_user.send("You have successfully registered with TokenBot!\n"
|
|
||||||
"Most interactions will happen in direct messages with TokenBot here.\n"
|
|
||||||
"You can remove your registration and alerts at any time using ```/remove-registration```\n")
|
|
||||||
await ctx.send(f"Successfully registered with the {ctx.values[0]} region", ephemeral=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 #
|
# Helper Functions #
|
||||||
@@ -296,36 +317,47 @@ class Tracker(Extension):
|
|||||||
|
|
||||||
async def get_current_token(self, ctx: SlashContext, flavor: Flavor) -> str:
|
async def get_current_token(self, ctx: SlashContext, flavor: Flavor) -> str:
|
||||||
user: User = await self._users.get(ctx.user.id)
|
user: User = await self._users.get(ctx.user.id)
|
||||||
|
if user.region is None:
|
||||||
|
return (
|
||||||
|
f"Please register with a region before attempting to list alerts using\n"
|
||||||
|
"```/register```"
|
||||||
|
)
|
||||||
region = user.region.name
|
region = user.region.name
|
||||||
region_history = self._history_manager.get_history(flavor, user.region)
|
region_history = self._history_manager.get_history(flavor, user.region)
|
||||||
price_movement_str = format(region_history.last_price_movement, ',')
|
price_movement_str = format(region_history.last_price_movement, ",")
|
||||||
if region_history.last_price_movement > 0:
|
if region_history.last_price_movement > 0:
|
||||||
price_movement_str = f"+{price_movement_str}"
|
price_movement_str = f"+{price_movement_str}"
|
||||||
|
|
||||||
return (f"Last Price Value for {region}: {format(region_history.last_price_datum[1], ",")}\n"
|
return (
|
||||||
f"Last Update Time: {region_history.last_price_datum[0].strftime('%Y-%m-%d %H:%M:%S UTC')}\n"
|
f"Last Price Value for {region}: {format(region_history.last_price_datum[1], ",")}\n"
|
||||||
f"Last Price Movement: {price_movement_str}")
|
f"Last Update Time: <t:{int(region_history.last_price_datum[0].timestamp())}:F> local time\n"
|
||||||
|
f"Last Price Movement: {price_movement_str}"
|
||||||
|
)
|
||||||
|
|
||||||
async def remove_alert_select_menu(self, ctx: SlashContext, user: User):
|
async def remove_alert_select_menu(self, ctx: SlashContext, user: User):
|
||||||
alerts_by_flavor = await gather_alerts_by_flavor(user.subscribed_alerts)
|
alerts_by_flavor = await gather_alerts_by_flavor(user.subscribed_alerts)
|
||||||
select_options: List[StringSelectOption] = []
|
select_options: List[StringSelectOption] = []
|
||||||
for flavor in alerts_by_flavor:
|
for flavor in alerts_by_flavor:
|
||||||
for alert in alerts_by_flavor[flavor]:
|
for alert in alerts_by_flavor[flavor]:
|
||||||
select_options.append(StringSelectOption(
|
select_options.append(
|
||||||
label=f"{alert.flavor.name.lower().title()} {alert.to_human_string()}",
|
StringSelectOption(
|
||||||
value=f"{alert.flavor.name.lower()} {alert.to_human_string()}"
|
label=f"{alert.flavor.name.lower().title()} {alert.to_human_string()}",
|
||||||
))
|
value=f"{alert.flavor.name.lower()} {alert.to_human_string()}",
|
||||||
|
)
|
||||||
|
)
|
||||||
menu = StringSelectMenu(
|
menu = StringSelectMenu(
|
||||||
select_options,
|
select_options,
|
||||||
placeholder="Select an alert to remove",
|
placeholder="Select an alert to remove",
|
||||||
custom_id="remove_alert_menu"
|
custom_id="remove_alert_menu",
|
||||||
|
)
|
||||||
|
message = await ctx.send(
|
||||||
|
"Select an alert to remove", components=menu, ephemeral=True
|
||||||
)
|
)
|
||||||
message = await ctx.send("Select an alert to remove", components=menu, ephemeral=True)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
alert_component: Component = await self.bot.wait_for_component(messages=message,
|
alert_component: Component = await self.bot.wait_for_component(
|
||||||
components=menu, timeout=30)
|
messages=message, components=menu, timeout=30
|
||||||
|
)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
menu.disabled = True
|
menu.disabled = True
|
||||||
await message.edit(context=ctx, components=menu, content="Timed out")
|
await message.edit(context=ctx, components=menu, content="Timed out")
|
||||||
@@ -335,115 +367,274 @@ class Tracker(Extension):
|
|||||||
await message.edit(context=ctx, components=menu)
|
await message.edit(context=ctx, components=menu)
|
||||||
selection_split = alert_component.ctx.values[0].split(" ")
|
selection_split = alert_component.ctx.values[0].split(" ")
|
||||||
flavor = Flavor[selection_split[0].upper()]
|
flavor = Flavor[selection_split[0].upper()]
|
||||||
alert_type = AlertType.from_str(' '.join(selection_split[1:]))
|
alert_type_str = " ".join(selection_split[1:])
|
||||||
return Alert(alert_type, flavor, user.region)
|
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)
|
||||||
|
region_menu_str = str()
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
region_menu_str += "You are not currently registered with a region, please select a region to register with.\n"
|
||||||
|
region_menu_str += (
|
||||||
|
"* You can only be registered with one region at a time.\n"
|
||||||
|
"* Registering for a new region will remove your old region's registration.\n"
|
||||||
|
)
|
||||||
|
region_message = await ctx.send(
|
||||||
|
region_menu_str,
|
||||||
|
components=region_menu,
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
region_component = await self.bot.wait_for_component(
|
||||||
|
messages=region_message, components=region_menu, timeout=30
|
||||||
|
)
|
||||||
|
except TimeoutError:
|
||||||
|
region_menu.disabled = True
|
||||||
|
await region_message.edit(
|
||||||
|
context=ctx, components=region_menu, content="Timed out"
|
||||||
|
)
|
||||||
|
raise TimeoutError
|
||||||
|
else:
|
||||||
|
# Acknowledge the component interaction to avoid 404 Unknown Interaction
|
||||||
|
await region_component.ctx.defer(edit_origin=True, suppress_error=True)
|
||||||
|
region_menu.disabled = True
|
||||||
|
region = Region(region_component.ctx.values[0].lower())
|
||||||
|
user = User(ctx.user.id, region, subscribed_alerts=[])
|
||||||
|
await asyncio.gather(
|
||||||
|
self._users.add(user),
|
||||||
|
region_message.edit(context=ctx, components=region_menu),
|
||||||
|
)
|
||||||
|
return region
|
||||||
|
|
||||||
async def flavor_select_menu(self, ctx: SlashContext) -> Type[Flavor]:
|
async def flavor_select_menu(self, ctx: SlashContext) -> Type[Flavor]:
|
||||||
flavor_menu = copy.deepcopy(FLAVOR_MENU)
|
flavor_menu = copy.deepcopy(FLAVOR_MENU)
|
||||||
|
|
||||||
flavor_message = await ctx.send(
|
flavor_message = await ctx.send(
|
||||||
"Select a flavor to add alerts for",
|
"Select a flavor to add alerts for", components=flavor_menu, ephemeral=True
|
||||||
components=flavor_menu,
|
)
|
||||||
ephemeral=True)
|
|
||||||
try:
|
try:
|
||||||
flavor_component: Component = await self.bot.wait_for_component(messages=flavor_message,
|
flavor_component: Component = await self.bot.wait_for_component(
|
||||||
components=flavor_menu, timeout=30)
|
messages=flavor_message, components=flavor_menu, timeout=30
|
||||||
|
)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
flavor_menu.disabled = True
|
flavor_menu.disabled = True
|
||||||
await flavor_message.edit(context=ctx, components=flavor_menu, content="Timed out")
|
await flavor_message.edit(
|
||||||
|
context=ctx, components=flavor_menu, content="Timed out"
|
||||||
|
)
|
||||||
raise TimeoutError
|
raise TimeoutError
|
||||||
else:
|
else:
|
||||||
|
# Acknowledge the component interaction to avoid 404 Unknown Interaction
|
||||||
|
await flavor_component.ctx.defer(edit_origin=True, suppress_error=True)
|
||||||
flavor = Flavor[flavor_component.ctx.values[0].upper()]
|
flavor = Flavor[flavor_component.ctx.values[0].upper()]
|
||||||
flavor_menu.disabled = True
|
flavor_menu.disabled = True
|
||||||
await flavor_message.edit(context=ctx, components=flavor_menu)
|
await flavor_message.edit(context=ctx, components=flavor_menu)
|
||||||
return flavor
|
return flavor
|
||||||
|
|
||||||
|
async def alert_category_select_menu(self, ctx: SlashContext) -> tuple[AlertCategory, int]:
|
||||||
async def alert_category_select_menu(self, ctx: SlashContext) -> AlertCategory:
|
|
||||||
alert_type_button = copy.deepcopy(ALERT_TYPE_ROW)
|
alert_type_button = copy.deepcopy(ALERT_TYPE_ROW)
|
||||||
alert_type_message = await ctx.send(
|
alert_type_message = await ctx.send(
|
||||||
"Select an alert type to add",
|
"Select an alert type to add", components=alert_type_button, ephemeral=True
|
||||||
components=alert_type_button,
|
)
|
||||||
ephemeral=True)
|
|
||||||
try:
|
try:
|
||||||
alert_type_component: Component = await self.bot.wait_for_component(messages=alert_type_message,
|
alert_type_component: Component = await self.bot.wait_for_component(
|
||||||
components=alert_type_button,
|
messages=alert_type_message, components=alert_type_button, timeout=30
|
||||||
timeout=30)
|
)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
for button in alert_type_button[0].components:
|
for button in alert_type_button[0].components:
|
||||||
button.disabled = True
|
button.disabled = True
|
||||||
await alert_type_message.edit(context=ctx, components=alert_type_button, content="Timed out")
|
await alert_type_message.edit(
|
||||||
|
context=ctx, components=alert_type_button, content="Timed out"
|
||||||
|
)
|
||||||
raise TimeoutError
|
raise TimeoutError
|
||||||
else:
|
else:
|
||||||
alert_type = AlertCategory.from_str(alert_type_component.ctx.custom_id)
|
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:
|
for button in alert_type_button[0].components:
|
||||||
button.disabled = True
|
button.disabled = True
|
||||||
await alert_type_message.edit(context=ctx, components=alert_type_button)
|
await alert_type_message.edit(context=ctx, components=alert_type_button)
|
||||||
return alert_type
|
return alert_type, price
|
||||||
|
|
||||||
|
async def _alert_select_menu_handler(
|
||||||
async def _alert_select_menu_handler(self, ctx: SlashContext, menu: StringSelectMenu, message: Message) -> AlertType:
|
self, ctx: SlashContext, menu: StringSelectMenu, message: Message
|
||||||
|
) -> AlertType:
|
||||||
try:
|
try:
|
||||||
component: Component = await self.bot.wait_for_component(messages=message, components=menu, timeout=30)
|
component: Component = await self.bot.wait_for_component(
|
||||||
|
messages=message, components=menu, timeout=30
|
||||||
|
)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
menu.disabled = True
|
menu.disabled = True
|
||||||
await message.edit(context=ctx, components=menu, content="Timed out")
|
await message.edit(context=ctx, components=menu, content="Timed out")
|
||||||
raise TimeoutError
|
raise TimeoutError
|
||||||
else:
|
else:
|
||||||
menu.disabled = True
|
menu.disabled = True
|
||||||
|
await component.ctx.defer(edit_origin=True, suppress_error=True)
|
||||||
await message.edit(context=ctx, components=menu)
|
await message.edit(context=ctx, components=menu)
|
||||||
return AlertType.from_str(component.ctx.values[0])
|
return AlertType.from_str(component.ctx.values[0])
|
||||||
|
|
||||||
|
|
||||||
async def high_alert_select_menu(self, ctx: SlashContext) -> AlertType:
|
async def high_alert_select_menu(self, ctx: SlashContext) -> AlertType:
|
||||||
high_menu = copy.deepcopy(HIGH_ALERT_MENU)
|
high_menu = copy.deepcopy(HIGH_ALERT_MENU)
|
||||||
high_message = await ctx.send(
|
high_message = await ctx.send(
|
||||||
"Select a time range to add a High Alert for",
|
"Select a time range to add a High Alert for",
|
||||||
components=high_menu,
|
components=high_menu,
|
||||||
ephemeral=True)
|
ephemeral=True,
|
||||||
|
)
|
||||||
return await self._alert_select_menu_handler(ctx, high_menu, high_message)
|
return await self._alert_select_menu_handler(ctx, high_menu, high_message)
|
||||||
|
|
||||||
|
|
||||||
async def low_alert_select_menu(self, ctx: SlashContext) -> AlertType:
|
async def low_alert_select_menu(self, ctx: SlashContext) -> AlertType:
|
||||||
low_menu = copy.deepcopy(LOW_ALERT_MENU)
|
low_menu = copy.deepcopy(LOW_ALERT_MENU)
|
||||||
low_message = await ctx.send(
|
low_message = await ctx.send(
|
||||||
"Select a time range to add a Low Alert for",
|
"Select a time range to add a Low Alert for",
|
||||||
components=low_menu,
|
components=low_menu,
|
||||||
ephemeral=True)
|
ephemeral=True,
|
||||||
|
)
|
||||||
return await self._alert_select_menu_handler(ctx, low_menu, low_message)
|
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)
|
||||||
|
|
||||||
async def render_alert_flavor(self, alerts: List[Alert], user: User | None = None) -> Embed:
|
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", ""))
|
||||||
|
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):
|
||||||
|
await ctx.send(
|
||||||
|
"You are not registered with any region\n"
|
||||||
|
"Please add an alert to get started ```/add-alert```",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def _render_alert_flavor(
|
||||||
|
self, alerts: List[Alert], user: User | None = None
|
||||||
|
) -> Embed:
|
||||||
region = alerts[0].region
|
region = alerts[0].region
|
||||||
flavor = alerts[0].flavor
|
flavor = alerts[0].flavor
|
||||||
fields: List[EmbedField] = []
|
fields: List[EmbedField] = []
|
||||||
for alert in alerts:
|
for alert in alerts:
|
||||||
history = self._history_manager.get_history(alert.flavor, alert.region)
|
history = self._history_manager.get_history(alert.flavor, alert.region)
|
||||||
trigger = await history.find_update_trigger_from_alert(alert)
|
trigger = await history.find_update_trigger_from_alert(alert)
|
||||||
if trigger.last_trigger is not None:
|
if trigger.last_alerting is not None:
|
||||||
alert_str = (f"Last Alerting Price Value: {format(trigger.last_alerting[1], ",")}\n"
|
alert_str = (
|
||||||
f"Last Alerting Time: {trigger.last_alerting[0].strftime('%Y-%m-%d %H:%M:%S UTC')}\n")
|
f"Last Alerting Price Value: {format(trigger.last_alerting[1], ",")}\n"
|
||||||
if user is not None and user.user_id == 265678699435655169:
|
f"Last Alerting Time: <t:{int(trigger.last_alerting[0].timestamp())}:F> local time\n"
|
||||||
alert_str += (f"\nShowing you some internals since you are the bot owner:\n"
|
f"[Link to this Chart]({self._render_token_url(alert)})\n"
|
||||||
f"```history.last_price_datum:\n"
|
)
|
||||||
f"\t{history.last_price_datum[0].strftime('%Y-%m-%d %H:%M:%S UTC')}\n"
|
if os.getenv("ENV") == "DEBUG":
|
||||||
f"\t{history.last_price_datum[1]}\n"
|
alert_str += (
|
||||||
f"trigger.last_alerting:\n"
|
f"\nShowing you some internals since this is a DEBUG build:\n"
|
||||||
f"\t{trigger.last_alerting[0].strftime('%Y-%m-%d %H:%M:%S UTC')}\n"
|
f"```history.last_price_datum:\n"
|
||||||
f"\t{trigger.last_alerting[1]}\n"
|
f"\t{history.last_price_datum[0].strftime('%Y-%m-%d %H:%M:%S UTC')}\n"
|
||||||
f"trigger.last_trigger:\n"
|
f"\t{history.last_price_datum[1]}\n"
|
||||||
f"\t{trigger.last_trigger[0].strftime('%Y-%m-%d %H:%M:%S UTC')}\n"
|
f"trigger.last_alerting:\n"
|
||||||
f"\t{trigger.last_trigger[1]}\n"
|
f"\t{trigger.last_alerting[0].strftime('%Y-%m-%d %H:%M:%S UTC')}\n"
|
||||||
f"trigger.squelched:\n\t{trigger.squelched}```")
|
f"\t{trigger.last_alerting[1]}\n"
|
||||||
|
f"trigger.last_trigger:\n"
|
||||||
|
f"\t{trigger.last_trigger[0].strftime('%Y-%m-%d %H:%M:%S UTC')}\n"
|
||||||
|
f"\t{trigger.last_trigger[1]}\n"
|
||||||
|
f"trigger.squelched:\n\t{trigger.squelched}```"
|
||||||
|
)
|
||||||
else:
|
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(
|
fields.append(
|
||||||
EmbedField(
|
EmbedField(
|
||||||
name=f"{alert.to_human_string()} Alert", value=alert_str, inline=False))
|
name=f"{alert.to_human_string()} Alert",
|
||||||
|
value=alert_str,
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
embed = Embed(
|
embed = Embed(
|
||||||
title=f"Alerts for {region.name} {flavor.name.lower().title()}",
|
title=f"Alerts for {region.name} {flavor.name.lower().title()}",
|
||||||
color=0xb10000,
|
color=0xB10000,
|
||||||
fields=fields
|
fields=fields,
|
||||||
)
|
)
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
|
def _render_token_url(self, alert: Alert, time_range: str | None = None) -> str:
|
||||||
|
match alert.flavor:
|
||||||
|
case Flavor.CLASSIC:
|
||||||
|
url = "https://classic.wowtoken.app/?"
|
||||||
|
case Flavor.RETAIL:
|
||||||
|
url = "https://wowtoken.app/?"
|
||||||
|
case _:
|
||||||
|
raise NotImplementedError
|
||||||
|
url += f"region={alert.region.value}&"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
from interactions import ActionRow
|
from interactions import ActionRow
|
||||||
|
|
||||||
from token_bot.ui.buttons.tracker.alert_category import HIGH_ALERT_BUTTON, LOW_ALERT_BUTTON
|
from token_bot.ui.buttons.tracker.alert_category import (
|
||||||
|
HIGH_ALERT_BUTTON,
|
||||||
|
LOW_ALERT_BUTTON,
|
||||||
|
CUSTOM_ALERT_BUTTON,
|
||||||
|
)
|
||||||
|
|
||||||
ALERT_TYPE_ROW: list[ActionRow] = [
|
ALERT_TYPE_ROW: list[ActionRow] = [
|
||||||
ActionRow(
|
ActionRow(
|
||||||
HIGH_ALERT_BUTTON,
|
HIGH_ALERT_BUTTON,
|
||||||
LOW_ALERT_BUTTON,
|
LOW_ALERT_BUTTON,
|
||||||
|
CUSTOM_ALERT_BUTTON,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,67 +1,45 @@
|
|||||||
from interactions import Button, ButtonStyle
|
from interactions import Button, ButtonStyle
|
||||||
|
|
||||||
ATH_ADD_BUTTON = Button(
|
ATH_ADD_BUTTON = Button(
|
||||||
custom_id='ath_add_button',
|
custom_id="ath_add_button", style=ButtonStyle.GREEN, label="All Time High"
|
||||||
style=ButtonStyle.GREEN,
|
|
||||||
label="All Time High"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ATL_ADD_BUTTON = Button(
|
ATL_ADD_BUTTON = Button(
|
||||||
custom_id='atl_add_button',
|
custom_id="atl_add_button", style=ButtonStyle.RED, label="All Time Low"
|
||||||
style=ButtonStyle.RED,
|
|
||||||
label="All Time Low"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
DH_ADD_BUTTON = Button(
|
DH_ADD_BUTTON = Button(
|
||||||
custom_id='dh_add_button',
|
custom_id="dh_add_button", style=ButtonStyle.GREEN, label="Daily High"
|
||||||
style=ButtonStyle.GREEN,
|
|
||||||
label="Daily High"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
DL_ADD_BUTTON = Button(
|
DL_ADD_BUTTON = Button(
|
||||||
custom_id='dl_add_button',
|
custom_id="dl_add_button", style=ButtonStyle.RED, label="Daily Low"
|
||||||
style=ButtonStyle.RED,
|
|
||||||
label="Daily Low"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
WH_ADD_BUTTON = Button(
|
WH_ADD_BUTTON = Button(
|
||||||
custom_id='wh_add_button',
|
custom_id="wh_add_button", style=ButtonStyle.GREEN, label="Weekly High"
|
||||||
style=ButtonStyle.GREEN,
|
|
||||||
label="Weekly High"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
WL_ADD_BUTTON = Button(
|
WL_ADD_BUTTON = Button(
|
||||||
custom_id='wl_add_button',
|
custom_id="wl_add_button", style=ButtonStyle.RED, label="Weekly Low"
|
||||||
style=ButtonStyle.RED,
|
|
||||||
label="Weekly Low"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
MH_ADD_BUTTON = Button(
|
MH_ADD_BUTTON = Button(
|
||||||
custom_id='mh_add_button',
|
custom_id="mh_add_button", style=ButtonStyle.GREEN, label="Monthly High"
|
||||||
style=ButtonStyle.GREEN,
|
|
||||||
label="Monthly High"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ML_ADD_BUTTON = Button(
|
ML_ADD_BUTTON = Button(
|
||||||
custom_id='ml_add_button',
|
custom_id="ml_add_button", style=ButtonStyle.RED, label="Monthly Low"
|
||||||
style=ButtonStyle.RED,
|
|
||||||
label="Monthly Low"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
YH_ADD_BUTTON = Button(
|
YH_ADD_BUTTON = Button(
|
||||||
custom_id='yh_add_button',
|
custom_id="yh_add_button", style=ButtonStyle.GREEN, label="Yearly High"
|
||||||
style=ButtonStyle.GREEN,
|
|
||||||
label="Yearly High"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
YL_ADD_BUTTON = Button(
|
YL_ADD_BUTTON = Button(
|
||||||
custom_id='yl_add_button',
|
custom_id="yl_add_button", style=ButtonStyle.RED, label="Yearly Low"
|
||||||
style=ButtonStyle.RED,
|
|
||||||
label="Yearly Low"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
SP_ADD_BUTTON = Button(
|
SP_ADD_BUTTON = Button(
|
||||||
custom_id='sp_add_button',
|
custom_id="sp_add_button", style=ButtonStyle.GRAY, label="Custom Limit Price"
|
||||||
style=ButtonStyle.GRAY,
|
|
||||||
label="Custom Limit Price"
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
from interactions import Button, ButtonStyle
|
from interactions import Button, ButtonStyle
|
||||||
|
|
||||||
HIGH_ALERT_BUTTON = Button(
|
HIGH_ALERT_BUTTON = Button(
|
||||||
custom_id='high_alert_button',
|
custom_id="high_alert_button", style=ButtonStyle.GREEN, label="High Price Alert"
|
||||||
style=ButtonStyle.GREEN,
|
|
||||||
label="High Price Alert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
LOW_ALERT_BUTTON = Button(
|
LOW_ALERT_BUTTON = Button(
|
||||||
custom_id='low_alert_button',
|
custom_id="low_alert_button", style=ButtonStyle.RED, label="Low Price Alert"
|
||||||
style=ButtonStyle.RED,
|
|
||||||
label="Low Price Alert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
CUSTOM_ALERT_BUTTON = Button(
|
CUSTOM_ALERT_BUTTON = Button(
|
||||||
custom_id='sp_add_button',
|
custom_id="sp_add_button", style=ButtonStyle.GRAY, label="Custom Price Alert"
|
||||||
style=ButtonStyle.GRAY,
|
|
||||||
label="Custom Price Alert"
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
from interactions import Button, ButtonStyle
|
from interactions import Button, ButtonStyle
|
||||||
|
|
||||||
HIGH_ALERT = Button(
|
HIGH_ALERT = Button(
|
||||||
custom_id='high_alert_button',
|
custom_id="high_alert_button", style=ButtonStyle.GREEN, label="Add High Alert"
|
||||||
style=ButtonStyle.GREEN,
|
|
||||||
label="Add High Alert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
LOW_ALERT = Button(
|
LOW_ALERT = Button(
|
||||||
custom_id='low_alert_button',
|
custom_id="low_alert_button", style=ButtonStyle.RED, label="Add Low Alert"
|
||||||
style=ButtonStyle.RED,
|
|
||||||
label="Add Low Alert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
CUSTOM_ALERT = Button(
|
CUSTOM_ALERT = Button(
|
||||||
custom_id='custom_alert_button',
|
custom_id="custom_alert_button", style=ButtonStyle.GRAY, label="Add Custom Alert"
|
||||||
style=ButtonStyle.GRAY,
|
|
||||||
label="Add Custom Alert"
|
|
||||||
)
|
)
|
||||||
@@ -1,15 +1,25 @@
|
|||||||
from interactions import StringSelectMenu
|
from interactions import StringSelectMenu
|
||||||
|
|
||||||
HIGH_ALERT_MENU = StringSelectMenu(
|
HIGH_ALERT_MENU = StringSelectMenu(
|
||||||
"Daily High", "Weekly High", "Monthly High", "Yearly High", "All Time High",
|
"Daily High",
|
||||||
|
"Weekly High",
|
||||||
|
"Monthly High",
|
||||||
|
"Yearly High",
|
||||||
|
"All Time High",
|
||||||
placeholder="Select a time period",
|
placeholder="Select a time period",
|
||||||
min_values=1, max_values=1,
|
min_values=1,
|
||||||
custom_id='high_alert_menu'
|
max_values=1,
|
||||||
|
custom_id="high_alert_menu",
|
||||||
)
|
)
|
||||||
|
|
||||||
LOW_ALERT_MENU = StringSelectMenu(
|
LOW_ALERT_MENU = StringSelectMenu(
|
||||||
"Daily Low", "Weekly Low", "Monthly Low", "Yearly Low", "All Time Low",
|
"Daily Low",
|
||||||
|
"Weekly Low",
|
||||||
|
"Monthly Low",
|
||||||
|
"Yearly Low",
|
||||||
|
"All Time Low",
|
||||||
placeholder="Select a time period",
|
placeholder="Select a time period",
|
||||||
min_values=1, max_values=1,
|
min_values=1,
|
||||||
custom_id='low_alert_menu'
|
max_values=1,
|
||||||
|
custom_id="low_alert_menu",
|
||||||
)
|
)
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
from interactions import StringSelectMenu
|
from interactions import StringSelectMenu
|
||||||
|
|
||||||
FLAVOR_MENU = StringSelectMenu(
|
FLAVOR_MENU = StringSelectMenu(
|
||||||
"Retail", "Classic",
|
"Retail",
|
||||||
|
"Classic",
|
||||||
placeholder="Select version of WoW",
|
placeholder="Select version of WoW",
|
||||||
min_values=1, max_values=1,
|
min_values=1,
|
||||||
custom_id='flavor_menu'
|
max_values=1,
|
||||||
|
custom_id="flavor_menu",
|
||||||
)
|
)
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
from interactions import StringSelectMenu
|
from interactions import StringSelectMenu
|
||||||
|
|
||||||
REGION_MENU = StringSelectMenu(
|
REGION_MENU = StringSelectMenu(
|
||||||
"US", "EU", "KR", "TW",
|
"US",
|
||||||
|
"EU",
|
||||||
|
"KR",
|
||||||
|
"TW",
|
||||||
placeholder="Select a region",
|
placeholder="Select a region",
|
||||||
min_values=1, max_values=1,
|
min_values=1,
|
||||||
custom_id='region_menu'
|
max_values=1,
|
||||||
|
custom_id="region_menu",
|
||||||
)
|
)
|
||||||
Reference in New Issue
Block a user