Very Initial MVP
There is so much more to do, but I think it is time to commit this to VCS
This commit is contained in:
parent
c1a3c73c1d
commit
c78ced85ca
1
.gitignore
vendored
1
.gitignore
vendored
@ -160,3 +160,4 @@ cython_debug/
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
/.tool-versions
|
||||
|
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
9
Dockerfile
Normal file
9
Dockerfile
Normal file
@ -0,0 +1,9 @@
|
||||
FROM python:3.11-bookworm as base
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
FROM base as app
|
||||
COPY . .
|
||||
|
||||
CMD ["python", "main.py"]
|
5
main.py
Normal file
5
main.py
Normal file
@ -0,0 +1,5 @@
|
||||
import token_bot.token_bot as token_bot
|
||||
|
||||
if __name__ == '__main__':
|
||||
bot = token_bot.TokenBot()
|
||||
bot.run()
|
24
requirements.txt
Normal file
24
requirements.txt
Normal file
@ -0,0 +1,24 @@
|
||||
aiodynamo==24.1
|
||||
aiohttp==3.9.5
|
||||
aiosignal==1.3.1
|
||||
anyio==4.4.0
|
||||
attrs==23.2.0
|
||||
certifi==2024.6.2
|
||||
croniter==2.0.5
|
||||
discord-py-interactions==5.12.1
|
||||
discord-typings==0.9.0
|
||||
emoji==2.12.1
|
||||
frozenlist==1.4.1
|
||||
h11==0.14.0
|
||||
httpcore==1.0.5
|
||||
httpx==0.27.0
|
||||
idna==3.7
|
||||
multidict==6.0.5
|
||||
python-dateutil==2.9.0.post0
|
||||
python-dotenv==1.0.1
|
||||
pytz==2024.1
|
||||
six==1.16.0
|
||||
sniffio==1.3.1
|
||||
tomli==2.0.1
|
||||
typing_extensions==4.12.2
|
||||
yarl==1.9.4
|
0
token_bot/__init__.py
Normal file
0
token_bot/__init__.py
Normal file
0
token_bot/controller/__init__.py
Normal file
0
token_bot/controller/__init__.py
Normal file
42
token_bot/controller/alerts.py
Normal file
42
token_bot/controller/alerts.py
Normal file
@ -0,0 +1,42 @@
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
import aiodynamo.client
|
||||
import aiohttp
|
||||
|
||||
from token_bot.persistant_database import Alert, User, AlertType
|
||||
from token_bot.persistant_database import database as pdb
|
||||
from token_bot.token_database.flavor import Flavor
|
||||
from token_bot.token_database.region import Region
|
||||
|
||||
|
||||
class AlertsController:
|
||||
def __init__(self, session: aiohttp.ClientSession):
|
||||
self._pdb: pdb.Database = pdb.Database(session)
|
||||
self.table = aiodynamo.client.Table = self._pdb.client.table(os.getenv('ALERTS_TABLE'))
|
||||
|
||||
@staticmethod
|
||||
def _user_to_obj(user: int | User) -> User:
|
||||
if isinstance(user, int):
|
||||
return User(user)
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def _alert_to_obj(alert: str | Alert) -> Alert:
|
||||
if isinstance(alert, str):
|
||||
return Alert.from_str(alert)
|
||||
return alert
|
||||
|
||||
async def add_user(self, user: int | User, alert: str | Alert) -> None:
|
||||
user = self._user_to_obj(user)
|
||||
alert = self._alert_to_obj(alert)
|
||||
await alert.add_user(self.table, user)
|
||||
|
||||
async def delete_user(self, user: int | User, alert: str | Alert):
|
||||
user = self._user_to_obj(user)
|
||||
alert = self._alert_to_obj(alert)
|
||||
await alert.remove_user(self.table, user)
|
||||
|
||||
async def get_users(self, alert: str | Alert, consistent: bool = False) -> List[User]:
|
||||
alert = self._alert_to_obj(alert)
|
||||
return await alert.get_users(self.table, consistent=consistent )
|
62
token_bot/controller/users.py
Normal file
62
token_bot/controller/users.py
Normal file
@ -0,0 +1,62 @@
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
import aiodynamo.client
|
||||
import aiohttp
|
||||
|
||||
from token_bot.persistant_database.user_schema import User
|
||||
from token_bot.persistant_database import database as pdb, Alert
|
||||
from token_bot.controller.alerts import AlertsController as AlertS, AlertsController
|
||||
|
||||
|
||||
class UsersController:
|
||||
def __init__(self, session: aiohttp.ClientSession):
|
||||
self._pdb: pdb.Database = pdb.Database(session)
|
||||
self.table: aiodynamo.client.Table = self._pdb.client.table(os.getenv('USERS_TABLE'))
|
||||
|
||||
@staticmethod
|
||||
def _user_to_obj(user: int | User) -> User:
|
||||
if isinstance(user, int):
|
||||
return User(user)
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def _alert_to_obj(alert: str | Alert) -> Alert:
|
||||
if isinstance(alert, str):
|
||||
return Alert.from_str(alert)
|
||||
return alert
|
||||
|
||||
async def add(self, user: int | User):
|
||||
user = self._user_to_obj(user)
|
||||
await user.put(self.table)
|
||||
|
||||
async def delete(self, user: int | User):
|
||||
user = self._user_to_obj(user)
|
||||
await user.delete(self.table)
|
||||
|
||||
async def exists(self, user: int | User) -> bool:
|
||||
user = self._user_to_obj(user)
|
||||
return await user.get(self.table)
|
||||
|
||||
async def get(self, user: int | User) -> User:
|
||||
user = self._user_to_obj(user)
|
||||
await user.get(self.table)
|
||||
return user
|
||||
|
||||
async def list_alerts(self, user: int | User) -> List[Alert]:
|
||||
user = self._user_to_obj(user)
|
||||
await user.get(self.table)
|
||||
return user.subscribed_alerts
|
||||
|
||||
async def is_subscribed(self, user: int | User, alert: str | Alert) -> bool:
|
||||
user = self._user_to_obj(user)
|
||||
alert = self._alert_to_obj(alert)
|
||||
await user.get(self.table)
|
||||
return alert in user.subscribed_alerts
|
||||
|
||||
async def add_alert(self, user: int | User, alert: str | Alert) -> None:
|
||||
user = self._user_to_obj(user)
|
||||
alert = self._alert_to_obj(alert)
|
||||
await user.get(self.table)
|
||||
user.subscribed_alerts.append(alert)
|
||||
await user.put(self.table)
|
50
token_bot/core.py
Normal file
50
token_bot/core.py
Normal file
@ -0,0 +1,50 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import aiohttp
|
||||
from interactions import Extension, Permissions, SlashContext, OptionType, slash_option
|
||||
from interactions import check, is_owner, slash_command, slash_default_member_permission, listen
|
||||
from interactions.api.events import Startup, MessageCreate
|
||||
from interactions import Task, IntervalTrigger
|
||||
|
||||
|
||||
from token_bot.token_database import database as tdb
|
||||
from token_bot.token_database import database as pdb
|
||||
|
||||
VERSION = "0.1.0"
|
||||
|
||||
|
||||
class Core(Extension):
|
||||
def __init__(self, bot):
|
||||
self._tdb: tdb.Database | None = None
|
||||
self._pdb: pdb.Database | None = None
|
||||
|
||||
@listen(Startup)
|
||||
async def on_start(self):
|
||||
print("TokenBot Core ready")
|
||||
print(f"This is bot version {VERSION}")
|
||||
self._tdb = tdb.Database(aiohttp.ClientSession())
|
||||
|
||||
@slash_command()
|
||||
async def version(self, ctx):
|
||||
await ctx.send(f"This is bot version {VERSION}", ephemeral=True)
|
||||
|
||||
@slash_command()
|
||||
async def help(self, ctx):
|
||||
await ctx.send(f"This is bot help command", ephemeral=True)
|
||||
|
||||
@slash_command(
|
||||
description="The current retail token cost"
|
||||
)
|
||||
async def current(self, ctx):
|
||||
current = await self._tdb.current(tdb.Flavor.RETAIL)
|
||||
await ctx.send(f"us: {current['us']}\neu: {current['eu']}\ntw: {current['tw']}\nkr: {current['kr']}",
|
||||
ephemeral=True)
|
||||
|
||||
@slash_command(
|
||||
description="The current classic token cost"
|
||||
)
|
||||
async def current_classic(self, ctx):
|
||||
current = await self._tdb.current(tdb.Flavor.CLASSIC)
|
||||
await ctx.send(f"us: {current['us']}\neu: {current['eu']}\ntw: {current['tw']}\nkr: {current['kr']}",
|
||||
ephemeral=True)
|
0
token_bot/history_manager/__init__.py
Normal file
0
token_bot/history_manager/__init__.py
Normal file
55
token_bot/history_manager/history.py
Normal file
55
token_bot/history_manager/history.py
Normal file
@ -0,0 +1,55 @@
|
||||
import datetime
|
||||
from typing import LiteralString, Set, List, Tuple
|
||||
|
||||
from token_bot.history_manager.update_trigger import UpdateTrigger
|
||||
from token_bot.persistant_database import AlertType, Alert
|
||||
from token_bot.token_database.flavor import Flavor
|
||||
from token_bot.token_database.region import Region
|
||||
|
||||
|
||||
class History:
|
||||
def __init__(self, flavor: Flavor, region: Region):
|
||||
self._flavor : Flavor = flavor
|
||||
self._region : Region = region
|
||||
self._history : List[Tuple[datetime, int]] = []
|
||||
self._last_price_movement : int = 0
|
||||
self._latest_price_datum : Tuple[datetime.datetime, int] | None = None
|
||||
self._update_triggers : List[UpdateTrigger] = []
|
||||
self._squelched_alerts : List[Alert] = []
|
||||
|
||||
for alert_type in AlertType:
|
||||
self._update_triggers.append(UpdateTrigger(Alert(alert_type, flavor, self._region)))
|
||||
|
||||
@property
|
||||
def flavor(self) -> Flavor:
|
||||
return self._flavor
|
||||
|
||||
@property
|
||||
def region(self) -> Region:
|
||||
return self._region
|
||||
|
||||
@property
|
||||
def last_price_datum(self) -> Tuple[datetime.datetime, int] | None:
|
||||
return self._latest_price_datum
|
||||
|
||||
@property
|
||||
def history(self) -> List[Tuple[datetime.datetime, int]]:
|
||||
return self._history
|
||||
|
||||
async def _process_update_triggers(self) -> List[Alert]:
|
||||
alerts = []
|
||||
for trigger in self._update_triggers:
|
||||
if trigger.check_and_update(self._latest_price_datum, self._history) and trigger.alert not in self._squelched_alerts:
|
||||
alerts.append(trigger.alert)
|
||||
|
||||
return alerts
|
||||
|
||||
async def add_price(self, datum: Tuple[datetime.datetime, int]) -> List[Alert]:
|
||||
if self._latest_price_datum is None:
|
||||
self._latest_price_datum = datum
|
||||
self._last_price_movement = datum[1] - self._latest_price_datum[1]
|
||||
self._latest_price_datum = datum
|
||||
self._history.append(datum)
|
||||
return await self._process_update_triggers()
|
||||
|
||||
|
61
token_bot/history_manager/history_manager.py
Normal file
61
token_bot/history_manager/history_manager.py
Normal file
@ -0,0 +1,61 @@
|
||||
import datetime
|
||||
from typing import Set, List, Dict, LiteralString, Tuple
|
||||
|
||||
from token_bot.history_manager.history import History
|
||||
from token_bot.persistant_database import Alert
|
||||
from token_bot.token_database.flavor import Flavor
|
||||
from token_bot.token_database.region import Region
|
||||
|
||||
from token_bot.token_database import database as tdb
|
||||
|
||||
class HistoryManager:
|
||||
def __init__(self, token_db: tdb.Database):
|
||||
self._history : Dict[Flavor, Dict[Region, History]] = {}
|
||||
self._tdb : tdb.Database = token_db
|
||||
for flavor in Flavor:
|
||||
self._history[flavor] = {}
|
||||
for region in Region:
|
||||
self._history[flavor][Region(region)] = History(flavor, Region(region))
|
||||
|
||||
|
||||
async def _retrieve_data(self, flavor: Flavor, region: Region) -> 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)
|
||||
high_fidelity_history = await self._tdb.history(flavor, region, '72h')
|
||||
final_response = []
|
||||
for data_point in all_history:
|
||||
datum = (datetime.datetime.fromisoformat(data_point[0]), data_point[1])
|
||||
if datum[0] < high_fidelity_time:
|
||||
final_response.append(datum)
|
||||
for data_point in high_fidelity_history:
|
||||
datum = (datetime.datetime.fromisoformat(data_point[0]), data_point[1])
|
||||
final_response.append(datum)
|
||||
return final_response
|
||||
|
||||
|
||||
async def load_data(self):
|
||||
for flavor in Flavor:
|
||||
for r in Region:
|
||||
region = Region(r)
|
||||
history = History(flavor, Region(region))
|
||||
history_response = await self._retrieve_data(flavor, region)
|
||||
for item in history_response:
|
||||
await history.add_price(item)
|
||||
self._history[flavor][region] = history
|
||||
|
||||
|
||||
async def update_data(self, flavor: Flavor, region: Region) -> List[Alert]:
|
||||
history = self._history[flavor][region]
|
||||
current_price_data = await self._tdb.current(flavor)
|
||||
current_region_data = current_price_data[region.value.lower()]
|
||||
datum = (
|
||||
datetime.datetime.fromisoformat(current_region_data[0]),
|
||||
current_region_data[1]
|
||||
)
|
||||
if datum != history.last_price_datum:
|
||||
return await history.add_price(datum)
|
||||
return []
|
||||
|
||||
|
||||
async def get_history(self, flavor, region) -> History:
|
||||
return self._history[flavor][region]
|
79
token_bot/history_manager/update_trigger.py
Normal file
79
token_bot/history_manager/update_trigger.py
Normal file
@ -0,0 +1,79 @@
|
||||
import datetime
|
||||
import operator
|
||||
from typing import Tuple, List
|
||||
|
||||
from token_bot.persistant_database import Alert, AlertType
|
||||
|
||||
|
||||
class UpdateTrigger:
|
||||
def __init__(self, alert: Alert):
|
||||
self._alert : Alert = alert
|
||||
self._last_trigger : Tuple[datetime.datetime, int] | None = None
|
||||
|
||||
@property
|
||||
def alert(self) -> Alert:
|
||||
return self._alert
|
||||
|
||||
def _find_next_trigger(self, comparison_operator: operator, starting_point: datetime.datetime, history: List[Tuple[datetime.datetime, int]]):
|
||||
candidate_datum : Tuple[datetime.datetime, int] | None = None
|
||||
for datum in history:
|
||||
if candidate_datum is None:
|
||||
candidate_datum = datum
|
||||
if (datum[0] > starting_point and datum != history[-1]) and comparison_operator(datum[1], candidate_datum[1]):
|
||||
candidate_datum = datum
|
||||
self._last_trigger = candidate_datum
|
||||
|
||||
def check_and_update(self, new_datum: Tuple[datetime.datetime, int], history: List[Tuple[datetime.datetime, int]]) -> bool:
|
||||
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
||||
match self._alert.alert_type:
|
||||
case AlertType.DAILY_LOW:
|
||||
time_range = datetime.timedelta(days=1)
|
||||
comparison_operator = operator.lt
|
||||
case AlertType.DAILY_HIGH:
|
||||
time_range = datetime.timedelta(days=1)
|
||||
comparison_operator = operator.gt
|
||||
case AlertType.WEEKLY_LOW:
|
||||
time_range = datetime.timedelta(weeks=1)
|
||||
comparison_operator = operator.lt
|
||||
case AlertType.WEEKLY_HIGH:
|
||||
time_range = datetime.timedelta(weeks=1)
|
||||
comparison_operator = operator.gt
|
||||
case AlertType.MONTHLY_LOW:
|
||||
time_range = datetime.timedelta(days=31)
|
||||
comparison_operator = operator.lt
|
||||
case AlertType.MONTHLY_HIGH:
|
||||
time_range = datetime.timedelta(days=31)
|
||||
comparison_operator = operator.gt
|
||||
case AlertType.YEARLY_LOW:
|
||||
time_range = datetime.timedelta(days=365)
|
||||
comparison_operator = operator.lt
|
||||
case AlertType.YEARLY_HIGH:
|
||||
time_range = datetime.timedelta(days=365)
|
||||
comparison_operator = operator.gt
|
||||
case AlertType.ALL_TIME_LOW:
|
||||
# TODO Make this calculate based on Flavor and current time the range back to look
|
||||
time_range = datetime.timedelta(days=int(365.25 * 6))
|
||||
comparison_operator = operator.lt
|
||||
case AlertType.ALL_TIME_HIGH:
|
||||
time_range = datetime.timedelta(days=int(365.25 * 6))
|
||||
comparison_operator = operator.gt
|
||||
case _:
|
||||
# TODO: The logic here is certainly wrong for Custom
|
||||
time_range = datetime.timedelta(days=int(365.25 * 6))
|
||||
comparison_operator = operator.eq
|
||||
|
||||
if new_datum[0] > now - time_range:
|
||||
if self._last_trigger is None:
|
||||
self._last_trigger = new_datum
|
||||
return True
|
||||
|
||||
# If the self._last_trigger falls out of scope of the alert, find the next thing that would have triggered
|
||||
# the alert so the next time a high or low comes up it's correctly comparing against the range of the alert
|
||||
# rather than since the alert triggered
|
||||
if self._last_trigger[0] < now - time_range:
|
||||
self._find_next_trigger(comparison_operator, now - time_range, history)
|
||||
|
||||
if comparison_operator(new_datum[1], self._last_trigger[1]):
|
||||
self._last_trigger = new_datum
|
||||
return True
|
||||
return False
|
4
token_bot/persistant_database/__init__.py
Normal file
4
token_bot/persistant_database/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from .alert_type import AlertType
|
||||
from .user_schema import User
|
||||
from .alert_schema import Alert
|
||||
|
19
token_bot/persistant_database/alert_category.py
Normal file
19
token_bot/persistant_database/alert_category.py
Normal file
@ -0,0 +1,19 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class AlertCategory(Enum):
|
||||
HIGH = 1
|
||||
LOW = 2
|
||||
CUSTOM = 3
|
||||
|
||||
@staticmethod
|
||||
def from_str(category: str): # It gets mad when I use the Type[AlertCategory] as a type hint
|
||||
match category:
|
||||
case "high_alert_button":
|
||||
return AlertCategory.HIGH
|
||||
case "low_alert_button":
|
||||
return AlertCategory.LOW
|
||||
case "sp_add_button":
|
||||
return AlertCategory.CUSTOM
|
||||
case _:
|
||||
return AlertCategory[category.upper()]
|
131
token_bot/persistant_database/alert_schema.py
Normal file
131
token_bot/persistant_database/alert_schema.py
Normal file
@ -0,0 +1,131 @@
|
||||
from typing import List
|
||||
|
||||
from aiodynamo.client import Table
|
||||
from aiodynamo.errors import ItemNotFound
|
||||
from aiodynamo.expressions import F
|
||||
|
||||
from token_bot.token_database.flavor import Flavor
|
||||
from token_bot.token_database.region import Region
|
||||
import token_bot.persistant_database as pdb
|
||||
|
||||
|
||||
class Alert:
|
||||
def __init__(self, alert: pdb.AlertType, flavor: Flavor, region: Region, price: int = 0) -> None:
|
||||
# AlertType is the Primary Key
|
||||
self.alert_type: pdb.AlertType = alert
|
||||
# Flavor (Retail, Classic) is the Sort Key
|
||||
self.flavor: Flavor = flavor
|
||||
self.region: Region = region
|
||||
self.price: int = price
|
||||
self._loaded: bool = False
|
||||
self._users: List[pdb.User] = []
|
||||
|
||||
@classmethod
|
||||
def from_item(cls, primary_key: int, sort_key: str, users: List[int]) -> 'Alert':
|
||||
alert_type = pdb.AlertType(primary_key)
|
||||
flavor_repr, region_repr, price_repr = sort_key.split('-')
|
||||
flavor = Flavor(int(flavor_repr))
|
||||
region = Region(region_repr)
|
||||
price = int(price_repr)
|
||||
return cls(alert_type, flavor, region, price)
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, string_trinity: str) -> 'Alert':
|
||||
alert_repr, flavor_repr, region_repr, price_repr = string_trinity.split('-')
|
||||
if len(string_trinity.split('-')) != 4:
|
||||
raise ValueError
|
||||
alert = pdb.AlertType(int(alert_repr))
|
||||
flavor = Flavor(int(flavor_repr))
|
||||
region = Region(region_repr)
|
||||
price = int(price_repr)
|
||||
return cls(alert, flavor, region, price)
|
||||
|
||||
@property
|
||||
def primary_key(self) -> int:
|
||||
return self.alert_type.value
|
||||
|
||||
@property
|
||||
def primary_key_name(self) -> str:
|
||||
return "alert"
|
||||
|
||||
@property
|
||||
def sort_key(self) -> str:
|
||||
return f"{self.flavor.value}-{self.region.value}-{self.price}"
|
||||
|
||||
@property
|
||||
def sort_key_name(self) -> str:
|
||||
return "flavor-region-price"
|
||||
|
||||
@property
|
||||
def key(self) -> dict[str, str | int]:
|
||||
return {
|
||||
self.primary_key_name: self.primary_key,
|
||||
self.sort_key_name: self.sort_key
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.alert_type.value}-{self.flavor.value}-{self.region.value}-{self.price}"
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.alert_type == other.alert_type and self.flavor == other.flavor and self.price == other.price
|
||||
|
||||
def to_human_string(self):
|
||||
if self.alert_type.SPECIFIC_PRICE:
|
||||
raise NotImplementedError
|
||||
else:
|
||||
return f"\n|Region: {self.region.value.upper()}\tFlavor: {self.flavor.value.upper()}\tAlert: {self.alert_type.name.lower()}|"
|
||||
|
||||
async def _lazy_load(self, table: Table, consistent: bool = False) -> None:
|
||||
if consistent or not self._loaded:
|
||||
await self.get(table, consistent=consistent)
|
||||
|
||||
async def _append_user(self, table: Table, user: pdb.User) -> None:
|
||||
self._users.append(user)
|
||||
await self.put(table)
|
||||
|
||||
async def _remove_user(self, table: Table, user: pdb.User) -> None:
|
||||
update_expression = F("users").delete({user.user_id})
|
||||
await table.update_item(
|
||||
key=self.key,
|
||||
update_expression=update_expression
|
||||
)
|
||||
|
||||
async def put(self, table: Table) -> None:
|
||||
user_ids = [user.user_id for user in self._users]
|
||||
await table.put_item(
|
||||
item={
|
||||
self.primary_key_name: self.primary_key,
|
||||
self.sort_key_name: self.sort_key,
|
||||
'users': user_ids
|
||||
}
|
||||
)
|
||||
|
||||
async def get(self, table: Table, consistent: bool = False) -> bool:
|
||||
try:
|
||||
response = await table.get_item(
|
||||
key=self.key,
|
||||
consistent_read=consistent
|
||||
)
|
||||
except ItemNotFound:
|
||||
return False
|
||||
if 'Item' in response:
|
||||
self._users = [pdb.User(int(user_id)) for user_id in response['Item']['users']['NS']]
|
||||
self._loaded = True
|
||||
return True
|
||||
|
||||
async def get_users(self, table: Table, consistent: bool = False) -> List[pdb.User]:
|
||||
await self._lazy_load(table, consistent=consistent)
|
||||
|
||||
return self._users
|
||||
|
||||
async def add_user(self, table: Table, user: pdb.User, consistent: bool = False) -> None:
|
||||
await self._lazy_load(table, consistent=consistent)
|
||||
|
||||
if user not in self._users:
|
||||
await self._append_user(table=table, user=user)
|
||||
|
||||
async def remove_user(self, table: Table, user: pdb.User, consistent: bool = True) -> None:
|
||||
await self._lazy_load(table, consistent=consistent)
|
||||
|
||||
if user in self._users:
|
||||
await self._remove_user(table=table, user=user)
|
36
token_bot/persistant_database/alert_type.py
Normal file
36
token_bot/persistant_database/alert_type.py
Normal file
@ -0,0 +1,36 @@
|
||||
from enum import Enum
|
||||
|
||||
class AlertType(Enum):
|
||||
ALL_TIME_HIGH = 1
|
||||
ALL_TIME_LOW = 2
|
||||
DAILY_HIGH = 3
|
||||
DAILY_LOW = 4
|
||||
WEEKLY_HIGH = 5
|
||||
WEEKLY_LOW = 6
|
||||
MONTHLY_HIGH = 7
|
||||
MONTHLY_LOW = 8
|
||||
YEARLY_HIGH = 9
|
||||
YEARLY_LOW = 10
|
||||
SPECIFIC_PRICE = 11
|
||||
|
||||
@staticmethod
|
||||
def from_str(category: str):
|
||||
match category:
|
||||
case "Daily High":
|
||||
return AlertType.DAILY_HIGH
|
||||
case "Daily Low":
|
||||
return AlertType.DAILY_LOW
|
||||
case "Weekly High":
|
||||
return AlertType.WEEKLY_HIGH
|
||||
case "Weekly Low":
|
||||
return AlertType.WEEKLY_LOW
|
||||
case "Monthly High":
|
||||
return AlertType.MONTHLY_HIGH
|
||||
case "Monthly Low":
|
||||
return AlertType.MONTHLY_LOW
|
||||
case "Yearly High":
|
||||
return AlertType.YEARLY_HIGH
|
||||
case "Yearly Low":
|
||||
return AlertType.YEARLY_LOW
|
||||
case _:
|
||||
return AlertType.SPECIFIC_PRICE
|
16
token_bot/persistant_database/database.py
Normal file
16
token_bot/persistant_database/database.py
Normal file
@ -0,0 +1,16 @@
|
||||
import os
|
||||
|
||||
import aiohttp
|
||||
|
||||
from aiodynamo.client import Client
|
||||
from aiodynamo.client import Table
|
||||
from aiodynamo.credentials import Credentials
|
||||
from aiodynamo.http.httpx import HTTPX
|
||||
from aiodynamo.http.aiohttp import AIOHTTP
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self, session: aiohttp.ClientSession):
|
||||
self.client = Client(AIOHTTP(session), Credentials.auto(), os.getenv('AWS_REGION'))
|
||||
|
80
token_bot/persistant_database/user_schema.py
Normal file
80
token_bot/persistant_database/user_schema.py
Normal file
@ -0,0 +1,80 @@
|
||||
from typing import List, Tuple, Dict
|
||||
|
||||
from aiodynamo.client import Table
|
||||
from aiodynamo.errors import ItemNotFound
|
||||
|
||||
|
||||
from token_bot.token_database.flavor import Flavor
|
||||
from token_bot.token_database.region import Region
|
||||
import token_bot.persistant_database as pdb
|
||||
|
||||
class User:
|
||||
def __init__(self, user_id: int, region: Region = None, subscribed_alerts: List['pdb.Alert'] = None) -> None:
|
||||
self.user_id: int = user_id
|
||||
self._loaded: bool = False
|
||||
self.region: Region = region
|
||||
self.subscribed_alerts: List[pdb.Alert] = subscribed_alerts
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.user_id == other.user_id
|
||||
|
||||
@classmethod
|
||||
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]
|
||||
return cls(primary_key, region, alerts)
|
||||
|
||||
@property
|
||||
def primary_key(self) -> int:
|
||||
return self.user_id
|
||||
|
||||
@property
|
||||
def primary_key_name(self) -> str:
|
||||
return 'user_id'
|
||||
|
||||
@property
|
||||
def key(self) -> Dict[str, int]:
|
||||
return {
|
||||
self.primary_key_name: self.primary_key
|
||||
}
|
||||
|
||||
|
||||
def _subscribed_alerts_as_trinity_list(self) -> List[str]:
|
||||
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:
|
||||
if consistent or not self._loaded:
|
||||
await self.get(table, consistent=consistent)
|
||||
|
||||
async def put(self, table: Table) -> None:
|
||||
await table.put_item(
|
||||
item={
|
||||
self.primary_key_name: self.primary_key,
|
||||
'region': self.region,
|
||||
'subscribed_alerts': self._subscribed_alerts_as_trinity_list()
|
||||
}
|
||||
)
|
||||
|
||||
async def delete(self, table: Table) -> None:
|
||||
if not self._loaded:
|
||||
await self._lazy_load(table, consistent=True)
|
||||
if self.subscribed_alerts:
|
||||
for alert in self.subscribed_alerts:
|
||||
await alert.remove_user(table, self)
|
||||
await table.delete_item(
|
||||
key={self.primary_key_name: self.primary_key},
|
||||
)
|
||||
|
||||
async def get(self, table: Table, consistent: bool = False) -> bool:
|
||||
try:
|
||||
response = await table.get_item(
|
||||
key=self.key,
|
||||
consistent_read=consistent
|
||||
)
|
||||
except ItemNotFound:
|
||||
return False
|
||||
|
||||
self.subscribed_alerts = []
|
||||
for string_trinity in response['subscribed_alerts']:
|
||||
self.subscribed_alerts.append(pdb.Alert.from_str(string_trinity))
|
||||
self.region = Region(response['region'])
|
||||
return True
|
25
token_bot/token_bot.py
Normal file
25
token_bot/token_bot.py
Normal file
@ -0,0 +1,25 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from dotenv import load_dotenv
|
||||
from interactions import Client, Intents
|
||||
|
||||
|
||||
class TokenBot:
|
||||
def __init__(self):
|
||||
load_dotenv()
|
||||
print("#### WoW Token Bot Startup ####")
|
||||
logging.basicConfig()
|
||||
log = logging.getLogger("TokenBotLogger")
|
||||
log.setLevel(logging.INFO)
|
||||
self.bot = Client(
|
||||
intents=Intents.DEFAULT,
|
||||
asyncio_debug=True,
|
||||
logger=log
|
||||
)
|
||||
|
||||
def run(self):
|
||||
self.bot.load_extension("token_bot.core")
|
||||
self.bot.load_extension("token_bot.tracker")
|
||||
self.bot.start(os.getenv("DISCORD_TOKEN"))
|
0
token_bot/token_database/__init__.py
Normal file
0
token_bot/token_database/__init__.py
Normal file
42
token_bot/token_database/database.py
Normal file
42
token_bot/token_database/database.py
Normal file
@ -0,0 +1,42 @@
|
||||
from typing import Dict, List, LiteralString
|
||||
|
||||
from token_bot.token_database.endpoints import Endpoints
|
||||
from token_bot.token_database.flavor import Flavor
|
||||
from token_bot.token_database.exceptions import *
|
||||
import aiohttp
|
||||
import json
|
||||
|
||||
from token_bot.token_database.region import Region
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self, session: aiohttp.ClientSession):
|
||||
self.data_url = "https://data.wowtoken.app/v2/"
|
||||
self.session = session
|
||||
|
||||
async def _get_data(self, endpoint: str) -> Dict | List:
|
||||
url = f"{self.data_url}{endpoint}"
|
||||
|
||||
success = False
|
||||
tries = 0
|
||||
|
||||
while not success and tries < 3:
|
||||
try:
|
||||
return await self._external_call(url)
|
||||
except TokenHttpException:
|
||||
tries += 1
|
||||
return {}
|
||||
|
||||
async def _external_call(self, url) -> Dict | List:
|
||||
async with self.session.get(url) as resp:
|
||||
await resp.text()
|
||||
if resp.ok:
|
||||
return await resp.json()
|
||||
else:
|
||||
raise TokenHttpException(resp.status)
|
||||
|
||||
async def current(self, flavor: Flavor) -> dict:
|
||||
return await self._get_data(f'current/{flavor.name.lower()}.json')
|
||||
|
||||
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')
|
7
token_bot/token_database/endpoints.py
Normal file
7
token_bot/token_database/endpoints.py
Normal file
@ -0,0 +1,7 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Endpoints(str, Enum):
|
||||
RETAIL_CURRENT = '/v2/current/retail.json'
|
||||
CLASSIC_CURRENT = '/v2/current/classic.json'
|
||||
RETAIL_HISTORY_ALL = '/v2/relative/retail/us/all.json'
|
2
token_bot/token_database/exceptions.py
Normal file
2
token_bot/token_database/exceptions.py
Normal file
@ -0,0 +1,2 @@
|
||||
class TokenHttpException(Exception):
|
||||
pass
|
7
token_bot/token_database/flavor.py
Normal file
7
token_bot/token_database/flavor.py
Normal file
@ -0,0 +1,7 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Flavor(Enum):
|
||||
RETAIL = 1
|
||||
CLASSIC = 2
|
||||
|
7
token_bot/token_database/region.py
Normal file
7
token_bot/token_database/region.py
Normal file
@ -0,0 +1,7 @@
|
||||
from enum import Enum
|
||||
|
||||
class Region(str, Enum):
|
||||
US = 'us'
|
||||
EU = 'eu'
|
||||
KR = 'kr'
|
||||
TW = 'tw'
|
265
token_bot/tracker.py
Normal file
265
token_bot/tracker.py
Normal file
@ -0,0 +1,265 @@
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from enum import Enum
|
||||
from typing import Any, Type, Dict, List
|
||||
|
||||
import aiohttp
|
||||
from interactions import Extension, Permissions, SlashContext, OptionType, slash_option, component_callback, \
|
||||
ComponentContext, StringSelectMenu, Message
|
||||
from interactions import check, is_owner, slash_command, slash_default_member_permission, listen
|
||||
from interactions.api.events import Startup, MessageCreate
|
||||
from interactions.api.events import Component
|
||||
from interactions import Task, IntervalTrigger
|
||||
|
||||
from token_bot.history_manager.history_manager import HistoryManager
|
||||
from token_bot.persistant_database import database as pdb
|
||||
from token_bot.token_database import database as tdb
|
||||
|
||||
from token_bot.token_database.flavor import Flavor
|
||||
from token_bot.token_database.region import Region
|
||||
|
||||
from token_bot.persistant_database.user_schema import User
|
||||
from token_bot.persistant_database.alert_schema import Alert
|
||||
from token_bot.persistant_database.alert_type import AlertType
|
||||
from token_bot.persistant_database.alert_category import AlertCategory
|
||||
|
||||
from token_bot.controller.users import UsersController
|
||||
from token_bot.controller.alerts import AlertsController
|
||||
from token_bot.ui.action_row.tracker import ALERT_TYPE_ROW
|
||||
|
||||
from token_bot.ui.select_menus.region_menu import REGION_MENU
|
||||
from token_bot.ui.select_menus.flavor_menu import FLAVOR_MENU
|
||||
from token_bot.ui.select_menus.alert_menu import HIGH_ALERT_MENU, LOW_ALERT_MENU
|
||||
|
||||
|
||||
class Tracker(Extension):
|
||||
def __init__(self, bot):
|
||||
self._users: UsersController | None = None
|
||||
self._alerts: AlertsController | None = None
|
||||
self._tdb: tdb.Database | None = None
|
||||
self._history_manager: HistoryManager | None = None
|
||||
|
||||
|
||||
###################################
|
||||
# Task Functions #
|
||||
###################################
|
||||
|
||||
@Task.create(IntervalTrigger(minutes=1))
|
||||
async def update_data(self):
|
||||
self.bot.logger.log(logging.INFO, "TokenBot Tracker: Updating Price")
|
||||
users_alerts: Dict[User, List[Alert]] = {}
|
||||
for flavor in Flavor:
|
||||
for region in Region:
|
||||
alerts = await self._history_manager.update_data(flavor, Region(region))
|
||||
for alert in alerts:
|
||||
users = await self._alerts.get_users(alert, consistent=True)
|
||||
for user in users:
|
||||
if user not in users_alerts:
|
||||
users_alerts[user] = [alert]
|
||||
else:
|
||||
users_alerts[user].append(alert)
|
||||
for user in users_alerts:
|
||||
discord_user = self.bot.get_user(user.user_id)
|
||||
alert_message = str()
|
||||
for alert in users_alerts[user]:
|
||||
alert_message += f"{alert.to_human_string()}"
|
||||
await discord_user.send(f"Hello, you requested to be sent an alert when the price of the World of Warcraft"
|
||||
f"token reaches a certain value. The following alerts have been triggered: {alert_message}")
|
||||
|
||||
|
||||
###################################
|
||||
# Slash Commands #
|
||||
###################################
|
||||
|
||||
@listen(Startup)
|
||||
async def on_start(self):
|
||||
self.bot.logger.log(logging.INFO, "TokenBot Tracker: Initializing")
|
||||
self._users = UsersController(aiohttp.ClientSession())
|
||||
self._alerts = AlertsController(aiohttp.ClientSession())
|
||||
self._tdb = tdb.Database(aiohttp.ClientSession())
|
||||
self._history_manager = HistoryManager(self._tdb)
|
||||
self.bot.logger.log(logging.INFO, "TokenBot Tracker: Initialized")
|
||||
self.bot.logger.log(logging.INFO, "TokenBot Tracker: Loading Historical 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: Started")
|
||||
self.update_data.start()
|
||||
|
||||
|
||||
@slash_command()
|
||||
async def register(self, ctx: SlashContext):
|
||||
text = ("## Select a region to register with \n\n"
|
||||
"Please note: \n"
|
||||
"* You can only be registered with one region at a time \n"
|
||||
"* Changing your region will remove all previous alerts you have signed up for")
|
||||
await self._users.add(ctx.member.id)
|
||||
await ctx.send(text, components=REGION_MENU, ephemeral=True)
|
||||
|
||||
@slash_command()
|
||||
async def remove_registration(self, ctx: SlashContext):
|
||||
await self._users.delete(ctx.member.id)
|
||||
await ctx.send("All alert subscriptions and user registration deleted", ephemeral=True)
|
||||
|
||||
@slash_command()
|
||||
async def delete(self, ctx: SlashContext):
|
||||
await self._users.delete(ctx.member.id)
|
||||
await ctx.send("Deletion Successful", ephemeral=True)
|
||||
|
||||
@slash_command()
|
||||
async def exists(self, ctx: SlashContext):
|
||||
await ctx.send(str(await self._users.exists(ctx.member.id)), ephemeral=True)
|
||||
|
||||
@slash_command()
|
||||
async def add_alert(self, ctx: SlashContext):
|
||||
if not await self._users.exists(ctx.member.id):
|
||||
await ctx.send("You are not registered with any region\n"
|
||||
"Please register with /register before adding alerts",
|
||||
ephemeral=True)
|
||||
return
|
||||
user = await self._users.get(ctx.member.id)
|
||||
|
||||
try:
|
||||
flavor = await self.flavor_select_menu(ctx)
|
||||
alert_category = 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
|
||||
|
||||
except TimeoutError:
|
||||
return
|
||||
|
||||
else:
|
||||
alert = Alert(alert_type, flavor, user.region)
|
||||
if not await self._users.is_subscribed(user, alert):
|
||||
await self._users.add_alert(user, alert)
|
||||
await self._alerts.add_user(user, alert)
|
||||
await ctx.send("Successfully added alert", ephemeral=True)
|
||||
|
||||
else:
|
||||
await ctx.send("You are already subscribed to this alert", ephemeral=True)
|
||||
|
||||
|
||||
@slash_command()
|
||||
async def remove_alert(self, ctx: SlashContext):
|
||||
pass
|
||||
|
||||
@slash_command()
|
||||
async def list_alerts(self, ctx: SlashContext):
|
||||
await ctx.send(str(await self._users.list_alerts(ctx.member.id)), ephemeral=True)
|
||||
|
||||
###################################
|
||||
# Callbacks Commands #
|
||||
###################################
|
||||
|
||||
# TODO: export to a separate file if possible
|
||||
|
||||
@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 alert_menu(self, ctx: ComponentContext):
|
||||
await ctx.send(f"Selected Alert: {ctx.values[0]}", ephemeral=True)
|
||||
|
||||
@component_callback('low_alert_menu')
|
||||
async def alert_menu(self, ctx: ComponentContext):
|
||||
await ctx.send(f"Selected Alert: {ctx.values[0]}", ephemeral=True)
|
||||
|
||||
@component_callback('region_menu')
|
||||
async def region_menu(self, ctx: ComponentContext):
|
||||
user = User(ctx.member.id, Region(ctx.values[0].lower()), subscribed_alerts=[])
|
||||
await self._users.add(user)
|
||||
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 #
|
||||
###################################
|
||||
|
||||
async def flavor_select_menu(self, ctx: SlashContext) -> Type[Flavor]:
|
||||
flavor_menu = copy.deepcopy(FLAVOR_MENU)
|
||||
|
||||
flavor_message = await ctx.send(
|
||||
"Select a flavor to add alerts for",
|
||||
components=flavor_menu,
|
||||
ephemeral=True)
|
||||
try:
|
||||
flavor_component: Component = await self.bot.wait_for_component(messages=flavor_message,
|
||||
components=flavor_menu, timeout=30)
|
||||
except TimeoutError:
|
||||
flavor_menu.disabled = True
|
||||
await flavor_message.edit(context=ctx, components=flavor_menu, content="Timed out")
|
||||
raise TimeoutError
|
||||
else:
|
||||
flavor = Flavor[flavor_component.ctx.values[0].upper()]
|
||||
flavor_menu.disabled = True
|
||||
await flavor_message.edit(context=ctx, components=flavor_menu)
|
||||
return flavor
|
||||
|
||||
async def alert_category_select_menu(self, ctx: SlashContext) -> AlertCategory:
|
||||
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)
|
||||
try:
|
||||
alert_type_component: Component = await self.bot.wait_for_component(messages=alert_type_message,
|
||||
components=alert_type_button,
|
||||
timeout=30)
|
||||
except TimeoutError:
|
||||
for button in alert_type_button[0].components:
|
||||
button.disabled = True
|
||||
await alert_type_message.edit(context=ctx, components=alert_type_button, content="Timed out")
|
||||
raise TimeoutError
|
||||
else:
|
||||
alert_type = AlertCategory.from_str(alert_type_component.ctx.custom_id)
|
||||
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
|
||||
|
||||
|
||||
async def _alert_select_menu_handler(self, ctx: SlashContext, menu: StringSelectMenu, message: Message) -> AlertType:
|
||||
try:
|
||||
component: Component = await self.bot.wait_for_component(messages=message, components=menu, timeout=30)
|
||||
except TimeoutError:
|
||||
menu.disabled = True
|
||||
await message.edit(context=ctx, components=menu, content="Timed out")
|
||||
raise TimeoutError
|
||||
else:
|
||||
menu.disabled = True
|
||||
await message.edit(context=ctx, components=menu)
|
||||
return AlertType.from_str(component.ctx.values[0])
|
||||
|
||||
async def high_alert_select_menu(self, ctx: SlashContext) -> AlertType:
|
||||
high_menu = copy.deepcopy(HIGH_ALERT_MENU)
|
||||
high_message = await ctx.send(
|
||||
"Select a time range to add a High Alert for",
|
||||
components=high_menu,
|
||||
ephemeral=True)
|
||||
return await self._alert_select_menu_handler(ctx, high_menu, high_message)
|
||||
|
||||
async def low_alert_select_menu(self, ctx: SlashContext) -> AlertType:
|
||||
low_menu = copy.deepcopy(LOW_ALERT_MENU)
|
||||
low_message = await ctx.send(
|
||||
"Select a time range to add a Low Alert for",
|
||||
components=low_menu,
|
||||
ephemeral=True)
|
||||
return await self._alert_select_menu_handler(ctx, low_menu, low_message)
|
11
token_bot/ui/action_row/tracker.py
Normal file
11
token_bot/ui/action_row/tracker.py
Normal file
@ -0,0 +1,11 @@
|
||||
from interactions import ActionRow
|
||||
|
||||
from token_bot.ui.buttons.tracker.alert_category import HIGH_ALERT_BUTTON, LOW_ALERT_BUTTON, CUSTOM_ALERT_BUTTON
|
||||
|
||||
|
||||
ALERT_TYPE_ROW: list[ActionRow] = [
|
||||
ActionRow(
|
||||
HIGH_ALERT_BUTTON,
|
||||
LOW_ALERT_BUTTON,
|
||||
)
|
||||
]
|
67
token_bot/ui/buttons/tracker/add.py
Normal file
67
token_bot/ui/buttons/tracker/add.py
Normal file
@ -0,0 +1,67 @@
|
||||
from interactions import Button, ButtonStyle
|
||||
|
||||
ATH_ADD_BUTTON = Button(
|
||||
custom_id='ath_add_button',
|
||||
style=ButtonStyle.GREEN,
|
||||
label="All Time High"
|
||||
)
|
||||
|
||||
ATL_ADD_BUTTON = Button(
|
||||
custom_id='atl_add_button',
|
||||
style=ButtonStyle.RED,
|
||||
label="All Time Low"
|
||||
)
|
||||
|
||||
DH_ADD_BUTTON = Button(
|
||||
custom_id='dh_add_button',
|
||||
style=ButtonStyle.GREEN,
|
||||
label="Daily High"
|
||||
)
|
||||
|
||||
DL_ADD_BUTTON = Button(
|
||||
custom_id='dl_add_button',
|
||||
style=ButtonStyle.RED,
|
||||
label="Daily Low"
|
||||
)
|
||||
|
||||
WH_ADD_BUTTON = Button(
|
||||
custom_id='wh_add_button',
|
||||
style=ButtonStyle.GREEN,
|
||||
label="Weekly High"
|
||||
)
|
||||
|
||||
WL_ADD_BUTTON = Button(
|
||||
custom_id='wl_add_button',
|
||||
style=ButtonStyle.RED,
|
||||
label="Weekly Low"
|
||||
)
|
||||
|
||||
MH_ADD_BUTTON = Button(
|
||||
custom_id='mh_add_button',
|
||||
style=ButtonStyle.GREEN,
|
||||
label="Monthly High"
|
||||
)
|
||||
|
||||
ML_ADD_BUTTON = Button(
|
||||
custom_id='ml_add_button',
|
||||
style=ButtonStyle.RED,
|
||||
label="Monthly Low"
|
||||
)
|
||||
|
||||
YH_ADD_BUTTON = Button(
|
||||
custom_id='yh_add_button',
|
||||
style=ButtonStyle.GREEN,
|
||||
label="Yearly High"
|
||||
)
|
||||
|
||||
YL_ADD_BUTTON = Button(
|
||||
custom_id='yl_add_button',
|
||||
style=ButtonStyle.RED,
|
||||
label="Yearly Low"
|
||||
)
|
||||
|
||||
SP_ADD_BUTTON = Button(
|
||||
custom_id='sp_add_button',
|
||||
style=ButtonStyle.GRAY,
|
||||
label="Custom Limit Price"
|
||||
)
|
19
token_bot/ui/buttons/tracker/alert_category.py
Normal file
19
token_bot/ui/buttons/tracker/alert_category.py
Normal file
@ -0,0 +1,19 @@
|
||||
from interactions import Button, ButtonStyle
|
||||
|
||||
HIGH_ALERT_BUTTON = Button(
|
||||
custom_id='high_alert_button',
|
||||
style=ButtonStyle.GREEN,
|
||||
label="High Price Alert"
|
||||
)
|
||||
|
||||
LOW_ALERT_BUTTON = Button(
|
||||
custom_id='low_alert_button',
|
||||
style=ButtonStyle.RED,
|
||||
label="Low Price Alert"
|
||||
)
|
||||
|
||||
CUSTOM_ALERT_BUTTON = Button(
|
||||
custom_id='sp_add_button',
|
||||
style=ButtonStyle.GRAY,
|
||||
label="Custom Price Alert"
|
||||
)
|
19
token_bot/ui/buttons/tracker/registration.py
Normal file
19
token_bot/ui/buttons/tracker/registration.py
Normal file
@ -0,0 +1,19 @@
|
||||
from interactions import Button, ButtonStyle
|
||||
|
||||
HIGH_ALERT = Button(
|
||||
custom_id='high_alert_button',
|
||||
style=ButtonStyle.GREEN,
|
||||
label="Add High Alert"
|
||||
)
|
||||
|
||||
LOW_ALERT = Button(
|
||||
custom_id='low_alert_button',
|
||||
style=ButtonStyle.RED,
|
||||
label="Add Low Alert"
|
||||
)
|
||||
|
||||
CUSTOM_ALERT = Button(
|
||||
custom_id='custom_alert_button',
|
||||
style=ButtonStyle.GRAY,
|
||||
label="Add Custom Alert"
|
||||
)
|
15
token_bot/ui/select_menus/alert_menu.py
Normal file
15
token_bot/ui/select_menus/alert_menu.py
Normal file
@ -0,0 +1,15 @@
|
||||
from interactions import StringSelectMenu
|
||||
|
||||
HIGH_ALERT_MENU = StringSelectMenu(
|
||||
"Daily High", "Weekly High", "Monthly High", "Yearly High", "All Time High",
|
||||
placeholder="Select a time period",
|
||||
min_values=1, max_values=1,
|
||||
custom_id='high_alert_menu'
|
||||
)
|
||||
|
||||
LOW_ALERT_MENU = StringSelectMenu(
|
||||
"Daily Low", "Weekly Low", "Monthly Low", "Yearly Low", "All Time Low",
|
||||
placeholder="Select a time period",
|
||||
min_values=1, max_values=1,
|
||||
custom_id='low_alert_menu'
|
||||
)
|
8
token_bot/ui/select_menus/flavor_menu.py
Normal file
8
token_bot/ui/select_menus/flavor_menu.py
Normal file
@ -0,0 +1,8 @@
|
||||
from interactions import StringSelectMenu
|
||||
|
||||
FLAVOR_MENU = StringSelectMenu(
|
||||
"Retail", "Classic",
|
||||
placeholder="Select version of WoW",
|
||||
min_values=1, max_values=1,
|
||||
custom_id='flavor_menu'
|
||||
)
|
8
token_bot/ui/select_menus/region_menu.py
Normal file
8
token_bot/ui/select_menus/region_menu.py
Normal file
@ -0,0 +1,8 @@
|
||||
from interactions import StringSelectMenu
|
||||
|
||||
REGION_MENU = StringSelectMenu(
|
||||
"US", "EU", "KR", "TW",
|
||||
placeholder="Select a region",
|
||||
min_values=1, max_values=1,
|
||||
custom_id='region_menu'
|
||||
)
|
Loading…
Reference in New Issue
Block a user