diff --git a/pyproject.toml b/pyproject.toml index c21bb6f4..b9ee9d51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ [tool.poetry] name = "royalpack" - version = "5.11.1" + version = "5.12.0" description = "A Royalnet command pack for the Royal Games community" authors = ["Stefano Pigozzi "] license = "AGPL-3.0+" diff --git a/royalpack/commands/abstract/linker.py b/royalpack/commands/abstract/linker.py index 3f10327b..48963dd1 100644 --- a/royalpack/commands/abstract/linker.py +++ b/royalpack/commands/abstract/linker.py @@ -1,10 +1,198 @@ from typing import * -import royalnet import royalnet.commands as rc +import royalnet.utils as ru +import royalnet.serf.telegram as rst +import royalnet.backpack.tables as rbt import abc +import logging +import asyncio as aio +from ...types import Updatable + + +log = logging.getLogger(__name__) class LinkerCommand(rc.Command, metaclass=abc.ABCMeta): + def __init__(self, interface: rc.CommandInterface): + super().__init__(interface) + + self.updater_task = None + if self.enabled(): + # Run updaters only on Telegram + if self.interface.name == "telegram": + self.updater_task = self.loop.create_task(self.run_updater()) + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + author = await data.get_author(error_if_none=True) + if len(args) == 0: + message = [] + for obj in await self.get_updatables_of_user(session=data.session, user=author): + async def change(attribute: str, value: Any): + """A shortcut for self.__change.""" + await self._change(session=data.session, + obj=obj, + attribute=attribute, + new=value) + + await self.update(session=data.session, obj=obj, change=change) + message.append(self.describe(obj)) + if len(message) == 0: + raise rc.UserError("Nessun account connesso.") + await data.session_commit() + await data.reply("\n".join(message)) + else: + message = ["🔗 Account collegato!\n"] + created = await self.create(session=data.session, user=author, args=args) + message.append(self.describe(created)) + await data.session_commit() + await data.reply("\n".join(message)) + + def describe(self, obj: Updatable) -> str: + """The text that should be appended to the report message for a given Updatable.""" + return str(obj) + + @abc.abstractmethod + async def get_updatables_of_user(self, session, user: rbt.User) -> List[Updatable]: + """Get the updatables of a specific user.""" ... + + @abc.abstractmethod + async def get_updatables(self, session) -> List[Updatable]: + """Return a list of all objects that should be updated at this updater cycle.""" + ... + + @abc.abstractmethod + async def create(self, session, user: rbt.User, args) -> Updatable: + """Create a new updatable object for a user.""" + ... + + @abc.abstractmethod + async def update(self, session, obj, change: Callable[[str, Any], Awaitable[None]]): + """Update a single updatable object. Use the change method to change values on the object!""" + ... + + @abc.abstractmethod + async def on_increase(self, session, obj: Updatable, attribute: str, old: Any, new: Any) -> None: + """Called when the attribute has increased from the old value.""" + ... + + @abc.abstractmethod + async def on_unchanged(self, session, obj: Updatable, attribute: str, old: Any, new: Any) -> None: + """Called when the attribute stayed the same as the old value.""" + ... + + @abc.abstractmethod + async def on_decrease(self, session, obj: Updatable, attribute: str, old: Any, new: Any) -> None: + """Called when the attribute has decreased from the old value.""" + ... + + @abc.abstractmethod + async def on_first(self, session, obj: Updatable, attribute: str, old: None, new: Any) -> None: + """Called when the attribute changed from None.""" + ... + + @abc.abstractmethod + async def on_reset(self, session, obj: Updatable, attribute: str, old: Any, new: None) -> None: + """Called when the attribute changed to None.""" + ... + + async def _change(self, + session, + obj, + attribute: str, + new) -> None: + """Set the value of an attribute of an object to a value, and call the corresponding method.""" + old = obj.__getattribute__(attribute) + if new == old: + await self.on_unchanged(session=session, + obj=obj, + attribute=attribute, + old=old, + new=new) + else: + if old is None: + await self.on_first(session=session, + obj=obj, + attribute=attribute, + old=old, + new=new) + elif new is None: + await self.on_reset(session=session, + obj=obj, + attribute=attribute, + old=old, + new=new) + elif new > old: + await self.on_increase(session=session, + obj=obj, + attribute=attribute, + old=old, + new=new) + else: + await self.on_decrease(session=session, + obj=obj, + attribute=attribute, + old=old, + new=new) + obj.__setattr__(attribute, new) + + def enabled(self) -> bool: + """Whether the updater is enabled or not.""" + return self.config[self.name]["updater"]["enabled"] + + def period(self) -> int: + """The time between two updater cycles.""" + return self.config[self.name]["updater"]["period"] + + def delay(self) -> int: + """The time between two object updates.""" + return self.config[self.name]["updater"]["rate"] + + def target(self) -> int: + """The id of the Telegram chat where notifications should be sent.""" + return self.config[self.name]["updater"]["target"] + + async def run_updater(self): + log.info(f"Starting updater: {self.name}") + + while True: + log.debug(f"Updater cycle: {self.name}") + session = self.alchemy.Session() + objects = await self.get_updatables(session) + + for obj in objects: + log.debug(f"Updating: {obj} ({self.name})") + + async def change(attribute: str, value: Any): + """A shortcut for self.__change.""" + await self._change(session=session, + obj=obj, + attribute=attribute, + new=value) + + try: + await self.update(session=session, + obj=obj, + change=change) + except Exception as e: + ru.sentry_exc(e) + + delay = self.delay() + log.debug(f"Waiting for: {delay} seconds (delay)") + await aio.sleep(delay) + + log.debug(f"Committing updates: {self.name}") + await ru.asyncify(session.commit) + session.close() + + period = self.period() + log.debug(f"Waiting for: {period} seconds (period)") + await aio.sleep(period) + + async def notify(self, message): + await self.serf.api_call(self.serf.client.send_message, + chat_id=self.target(), + text=rst.escape(message), + parse_mode="HTML", + disable_webpage_preview=True) diff --git a/royalpack/commands/brawlhalla.py b/royalpack/commands/brawlhalla.py index f83d8b08..f775aeb7 100644 --- a/royalpack/commands/brawlhalla.py +++ b/royalpack/commands/brawlhalla.py @@ -3,41 +3,145 @@ from typing import * import asyncio import logging import aiohttp + +from royalnet.backpack import tables as rbt from royalnet.commands import * from royalnet.utils import * -from royalnet.serf.telegram.escape import escape as tg_escape from sqlalchemy import or_, and_ +from .abstract.linker import LinkerCommand from ..tables import Steam, Brawlhalla, BrawlhallaDuo -from ..types import BrawlhallaRank, BrawlhallaMetal, BrawlhallaTier +from ..types import BrawlhallaRank, BrawlhallaMetal, BrawlhallaTier, Updatable log = logging.getLogger(__name__) -class BrawlhallaCommand(Command): +class BrawlhallaCommand(LinkerCommand): name: str = "brawlhalla" aliases = ["bh", "bruhalla", "bruhlalla"] - description: str = "Visualizza le tue statistiche di Dota!" + description: str = "Visualizza le tue statistiche di Brawlhalla." syntax: str = "" - def __init__(self, interface: CommandInterface): - super().__init__(interface) - if self.interface.name == "telegram" and self.config["Brawlhalla"]["updater"]["enabled"]: - self.loop.create_task(self._updater(int(self.config["Brawlhalla"]["updater"]["delay"]))) + async def get_updatables_of_user(self, session, user: rbt.User) -> List[Brawlhalla]: + return user.steam - async def _send(self, message): - client = self.serf.client - await self.serf.api_call(client.send_message, - chat_id=self.config["Telegram"]["main_group_id"], - text=tg_escape(message), - parse_mode="HTML", - disable_webpage_preview=True) + async def get_updatables(self, session) -> List[Brawlhalla]: + return await asyncify(session.query(self.alchemy.get(Steam)).all) + + async def create(self, session, user: rbt.User, args) -> Updatable: + raise InvalidInputError("Brawlhalla accounts are automatically linked from Steam.") + + async def update(self, session, obj, change: Callable[[str, Any], Awaitable[None]]): + BrawlhallaT = self.alchemy.get(Brawlhalla) + DuoT = self.alchemy.get(BrawlhallaDuo) + log.info(f"Updating: {obj}") + async with aiohttp.ClientSession() as hcs: + bh: Brawlhalla = obj.brawlhalla + if bh is None: + log.debug(f"Checking if player has an account...") + async with hcs.get(f"https://api.brawlhalla.com/search?steamid={obj.steamid.as_64}&api_key={self.config['Brawlhalla']['api_key']}") as response: + if response.status != 200: + raise ExternalError(f"Brawlhalla API /search returned {response.status}!") + j = await response.json() + if j == {} or j == []: + log.debug("No account found.") + return + bh = BrawlhallaT( + steam=obj, + brawlhalla_id=j["brawlhalla_id"], + name=j["name"] + ) + session.add(bh) + session.flush() + + async with hcs.get(f"https://api.brawlhalla.com/player/{bh.brawlhalla_id}/ranked?api_key={self.config['Brawlhalla']['api_key']}") as response: + if response.status != 200: + raise ExternalError(f"Brawlhalla API /ranked returned {response.status}!") + j = await response.json() + if j == {} or j == []: + log.debug("No ranked info found.") + else: + await self._change(session=session, obj=bh, attribute="rating_1v1", new=j["rating"]) + metal_name, tier_name = j["tier"].split(" ", 1) + metal = BrawlhallaMetal[metal_name.upper()] + tier = BrawlhallaTier(int(tier_name)) + rank = BrawlhallaRank(metal=metal, tier=tier) + await self._change(session=session, obj=bh, attribute="rank_1v1", new=rank) + + for jduo in j.get("2v2", []): + bhduo: Optional[BrawlhallaDuo] = await asyncify( + session.query(DuoT) + .filter( + or_( + and_( + DuoT.id_one == jduo["brawlhalla_id_one"], + DuoT.id_two == jduo["brawlhalla_id_two"] + ), + and_( + DuoT.id_one == jduo["brawlhalla_id_two"], + DuoT.id_two == jduo["brawlhalla_id_one"] + ) + ) + ) + .one_or_none + ) + if bhduo is None: + if bh.brawlhalla_id == jduo["brawlhalla_id_one"]: + otherbh: Optional[Brawlhalla] = await asyncify( + session.query(BrawlhallaT).get, jduo["brawlhalla_id_two"] + ) + else: + otherbh: Optional[Brawlhalla] = await asyncify( + session.query(BrawlhallaT).get, jduo["brawlhalla_id_one"] + ) + if otherbh is None: + continue + bhduo = DuoT( + one=bh, + two=otherbh, + ) + + session.add(bhduo) + await self._change(session=session, obj=bhduo, attribute="rating_2v2", new=jduo["rating"]) + metal_name, tier_name = jduo["tier"].split(" ", 1) + metal = BrawlhallaMetal[metal_name.upper()] + tier = BrawlhallaTier(int(tier_name)) + rank = BrawlhallaRank(metal=metal, tier=tier) + await self._change(session=session, obj=bhduo, attribute="rank_2v2", new=rank) + + async def on_increase(self, session, obj: Union[Brawlhalla, BrawlhallaDuo], attribute: str, old: Any, new: Any) -> None: + if attribute == "rank_1v1": + await self.notify(f"📈 [b]{obj.steam.user}[/b] è salito a [b]{new}[/b] ({obj.rating_1v1} MMR) in 1v1 su Brawlhalla! Congratulazioni!") + elif attribute == "rank_2v2": + await self.notify(f"📈 [b]{obj.one.steam.user}[/b] e [b]{obj.two.steam.user}[/b] sono saliti a [b]{new}[/b] ({obj.rating_2v2} MMR) in 2v2 su Brawlhalla! Congratulazioni!") + + async def on_unchanged(self, session, obj: Union[Brawlhalla, BrawlhallaDuo], attribute: str, old: Any, new: Any) -> None: + pass + + async def on_decrease(self, session, obj: Union[Brawlhalla, BrawlhallaDuo], attribute: str, old: Any, new: Any) -> None: + if attribute == "rank_1v1": + await self.notify(f"📉 [b]{obj.steam.user}[/b] è sceso a [b]{new}[/b] ({obj.rating_1v1} MMR) in 1v1 su Brawlhalla.") + elif attribute == "rank_2v2": + await self.notify(f"📉 [b]{obj.one.steam.user}[/b] e [b]{obj.two.steam.user}[/b] sono scesi a [b]{new}[/b] ({obj.rating_2v2} MMR) in 2v2 su Brawlhalla.") + + async def on_first(self, session, obj: Union[Brawlhalla, BrawlhallaDuo], attribute: str, old: None, new: Any) -> None: + if attribute == "rank_1v1": + await self.notify(f"🌟 [b]{obj.steam.user}[/b] si è classificato a [b]{new}[/b] ({obj.rating_1v1} MMR) in 1v1 su Brawlhalla!") + elif attribute == "rank_2v2": + await self.notify(f"🌟 [b]{obj.one.steam.user}[/b] e [b]{obj.two.steam.user}[/b] si sono classificati a [b]{new}[/b] ({obj.rating_2v2} MMR) in 2v2 su Brawlhalla!") + + async def on_reset(self, session, obj: Union[Brawlhalla, BrawlhallaDuo], attribute: str, old: Any, new: None) -> None: + if attribute == "rank_1v1": + await self.notify(f"⬜️ [b]{obj.steam.user}[/b] non ha più un rank su Brawlhalla.") + elif attribute == "rank_2v2": + await self.notify(f"⬜️ [b]{obj.one.steam.user}[/b] e [b]{obj.two.steam.user}[/b] non hanno più un rank su Brawlhalla.") + + def describe(self, obj: Steam) -> str: + bh = obj.brawlhalla - @staticmethod - def _display(bh: Brawlhalla) -> str: string = [f"ℹ️ [b]{bh.name}[/b]", ""] if bh.rank_1v1: @@ -56,150 +160,3 @@ class BrawlhallaCommand(Command): string.append("") return "\n".join(string) - - async def _notify(self, - obj: Union[Brawlhalla, BrawlhallaDuo], - attribute_name: str, - old_value: Any, - new_value: Any): - if attribute_name == "rank_1v1": - old_rank: Optional[BrawlhallaRank] = old_value - new_rank: Optional[BrawlhallaRank] = new_value - if new_rank > old_rank: - message = f"📈 [b]{obj.steam.user}[/b] è salito a [b]{new_value}[/b] ({obj.rating_1v1} MMR) in 1v1 su Brawlhalla! Congratulazioni!" - else: - message = f"📉 [b]{obj.steam.user}[/b] è sceso a [b]{new_value}[/b] ({obj.rating_1v1} MMR) in 1v1 su Brawlhalla." - await self._send(message) - elif attribute_name == "rank_2v2": - old_rank: Optional[BrawlhallaRank] = old_value - new_rank: Optional[BrawlhallaRank] = new_value - if new_rank > old_rank: - message = f"📈 [b]{obj.one.steam.user}[/b] e [b]{obj.two.steam.user}[/b] sono saliti a [b]{new_value}[/b] ({obj.rating_2v2} MMR) in 2v2 su Brawlhalla! Congratulazioni!" - else: - message = f"📉 [b]{obj.one.steam.user}[/b] e [b]{obj.two.steam.user}[/b] sono scesi a [b]{new_value}[/b] ({obj.rating_2v2} MMR) in 2v2 su Brawlhalla." - await self._send(message) - - @staticmethod - async def _change(obj: Union[Brawlhalla, BrawlhallaDuo], - attribute_name: str, - new_value: Any, - callback: Callable[[Union[Brawlhalla, BrawlhallaDuo], str, Any, Any], Awaitable[None]]): - old_value = obj.__getattribute__(attribute_name) - if old_value != new_value: - await callback(obj, attribute_name, old_value, new_value) - obj.__setattr__(attribute_name, new_value) - - async def _update(self, steam: Steam, db_session): - BrawlhallaT = self.alchemy.get(Brawlhalla) - DuoT = self.alchemy.get(BrawlhallaDuo) - log.info(f"Updating: {steam}") - async with aiohttp.ClientSession() as session: - bh: Brawlhalla = steam.brawlhalla - if bh is None: - log.debug(f"Checking if player has an account...") - async with session.get(f"https://api.brawlhalla.com/search?steamid={steam.steamid.as_64}&api_key={self.config['Brawlhalla']['api_key']}") as response: - if response.status != 200: - raise ExternalError(f"Brawlhalla API /search returned {response.status}!") - j = await response.json() - if j == {} or j == []: - log.debug("No account found.") - return - bh = BrawlhallaT( - steam=steam, - brawlhalla_id=j["brawlhalla_id"], - name=j["name"] - ) - db_session.add(bh) - message = f"↔️ Account {bh} connesso a {bh.steam.user}!" - await self._send(message) - async with session.get(f"https://api.brawlhalla.com/player/{bh.brawlhalla_id}/ranked?api_key={self.config['Brawlhalla']['api_key']}") as response: - if response.status != 200: - raise ExternalError(f"Brawlhalla API /ranked returned {response.status}!") - j = await response.json() - if j == {} or j == []: - log.debug("No ranked info found.") - else: - await self._change(bh, "rating_1v1", j["rating"], self._notify) - metal_name, tier_name = j["tier"].split(" ", 1) - metal = BrawlhallaMetal[metal_name.upper()] - tier = BrawlhallaTier(int(tier_name)) - rank = BrawlhallaRank(metal=metal, tier=tier) - await self._change(bh, "rank_1v1", rank, self._notify) - - for jduo in j.get("2v2", []): - bhduo: Optional[BrawlhallaDuo] = await asyncify( - db_session.query(DuoT) - .filter( - or_( - and_( - DuoT.id_one == jduo["brawlhalla_id_one"], - DuoT.id_two == jduo["brawlhalla_id_two"] - ), - and_( - DuoT.id_one == jduo["brawlhalla_id_two"], - DuoT.id_two == jduo["brawlhalla_id_one"] - ) - ) - ) - .one_or_none - ) - if bhduo is None: - if bh.brawlhalla_id == jduo["brawlhalla_id_one"]: - otherbh: Optional[Brawlhalla] = await asyncify( - db_session.query(BrawlhallaT).get, jduo["brawlhalla_id_two"] - ) - else: - otherbh: Optional[Brawlhalla] = await asyncify( - db_session.query(BrawlhallaT).get, jduo["brawlhalla_id_one"] - ) - if otherbh is None: - continue - bhduo = DuoT( - one=bh, - two=otherbh, - ) - - db_session.add(bhduo) - await self._change(bhduo, "rating_2v2", jduo["rating"], self._notify) - metal_name, tier_name = jduo["tier"].split(" ", 1) - metal = BrawlhallaMetal[metal_name.upper()] - tier = BrawlhallaTier(int(tier_name)) - rank = BrawlhallaRank(metal=metal, tier=tier) - await self._change(bhduo, "rank_2v2", rank, self._notify) - - await asyncify(db_session.commit) - - async def _updater(self, period: int): - log.info(f"Started updater with {period}s period") - while True: - log.info(f"Updating...") - session = self.alchemy.Session() - log.info("") - steams = session.query(self.alchemy.get(Steam)).all() - for steam in steams: - try: - await self._update(steam, session) - except Exception as e: - sentry_exc(e) - await asyncio.sleep(1) - await asyncify(session.commit) - session.close() - log.info(f"Sleeping for {period}s") - await asyncio.sleep(period) - - async def run(self, args: CommandArgs, data: CommandData) -> None: - author = await data.get_author(error_if_none=True) - - found_something = False - - message = "" - for steam in author.steam: - await self._update(steam, data.session) - if steam.brawlhalla is None: - continue - found_something = True - message += self._display(steam.brawlhalla) - message += "\n" - if not found_something: - raise UserError("Nessun account di Brawlhalla trovato.") - await data.reply(message) diff --git a/royalpack/commands/dota.py b/royalpack/commands/dota.py index 4943e5e0..14b7850f 100644 --- a/royalpack/commands/dota.py +++ b/royalpack/commands/dota.py @@ -1,11 +1,10 @@ from typing import * -import asyncio import logging -import sentry_sdk import aiohttp import royalnet.commands as rc import royalnet.utils as ru -import royalnet.serf.telegram as rst +from royalnet.backpack import tables as rbt +from .abstract.linker import LinkerCommand from ..tables import Steam, Dota from ..types import DotaRank @@ -13,139 +12,91 @@ from ..types import DotaRank log = logging.getLogger(__name__) -class DotaCommand(rc.Command): +class DotaCommand(LinkerCommand): name: str = "dota" aliases = ["dota2", "doto", "doto2", "dotka", "dotka2"] - description: str = "Visualizza le tue statistiche di Dota!" + description: str = "Visualizza le tue statistiche di Dota." syntax: str = "" - def __init__(self, interface: rc.CommandInterface): - super().__init__(interface) - if self.interface.name == "telegram" and self.config["Dota"]["updater"]["enabled"]: - self.loop.create_task(self._updater(int(self.config["Dota"]["updater"]["delay"]))) - - async def _send(self, message): - client = self.serf.client - await self.serf.api_call(client.send_message, - chat_id=self.config["Telegram"]["main_group_id"], - text=rst.escape(message), - parse_mode="HTML", - disable_webpage_preview=True) - - @staticmethod - def _display(dota: Dota) -> str: - string = f"ℹ️ [b]{dota.steam.persona_name}[/b]\n" - - if dota.rank: - string += f"{dota.rank}\n" - + def describe(self, obj: Steam) -> str: + string = f"ℹ️ [b]{obj.persona_name}[/b]\n" + if obj.dota.rank: + string += f"{obj.dota.rank}\n" string += f"\n" \ - f"Wins: [b]{dota.wins}[/b]\n" \ - f"Losses: [b]{dota.losses}[/b]\n" \ + f"Wins: [b]{obj.dota.wins}[/b]\n" \ + f"Losses: [b]{obj.dota.losses}[/b]\n" \ f"\n" - return string - async def _notify(self, - obj: Dota, - attribute_name: str, - old_value: Any, - new_value: Any): - if attribute_name == "wins": - if old_value is None: - message = f"↔️ Account {obj} connesso a {obj.steam.user}!" - await self._send(message) - elif attribute_name == "rank": - old_rank: Optional[DotaRank] = old_value - new_rank: Optional[DotaRank] = new_value - if new_rank > old_rank: - message = f"📈 [b]{obj.steam.user}[/b] è salito a [b]{new_value}[/b] su Dota 2! Congratulazioni!" - elif new_rank < old_rank: - message = f"📉 [b]{obj.steam.user}[/b] è sceso a [b]{new_value}[/b] su Dota 2." - else: - return - await self._send(message) + async def get_updatables_of_user(self, session, user: rbt.User) -> List[Dota]: + return user.steam - @staticmethod - async def _change(obj: Dota, - attribute_name: str, - new_value: Any, - callback: Callable[[Dota, str, Any, Any], Awaitable[None]]): - old_value = obj.__getattribute__(attribute_name) - if old_value != new_value: - await callback(obj, attribute_name, old_value, new_value) - obj.__setattr__(attribute_name, new_value) + async def get_updatables(self, session) -> List[Dota]: + return await ru.asyncify(session.query(self.alchemy.get(Steam)).all) - async def _update(self, steam: Steam, db_session): - log.info(f"Updating: {steam}") + async def create(self, session, user: rbt.User, args): + raise rc.InvalidInputError("Dota accounts are automatically linked from Steam.") + + async def update(self, session, obj: Steam, change: Callable[[str, Any], Awaitable[None]]): log.debug(f"Getting player data from OpenDota...") - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession() as hcs: # Get profile data - async with session.get(f"https://api.opendota.com/api/players/{steam.steamid.as_32}/") as response: + async with hcs.get(f"https://api.opendota.com/api/players/{obj.steamid.as_32}/") as response: if response.status != 200: raise rc.ExternalError(f"OpenDota / returned {response.status}!") p = await response.json() # No such user if "profile" not in p: - log.debug(f"Not found: {steam}") + log.debug(f"Not found: {obj}") return # Get win/loss data - async with session.get(f"https://api.opendota.com/api/players/{steam.steamid.as_32}/wl") as response: + async with hcs.get(f"https://api.opendota.com/api/players/{obj.steamid.as_32}/wl") as response: if response.status != 200: raise rc.ExternalError(f"OpenDota /wl returned {response.status}!") wl = await response.json() # No such user if wl["win"] == 0 and wl["lose"] == 0: - log.debug(f"Not found: {steam}") + log.debug(f"Not found: {obj}") return # Find the Dota record, if it exists - dota: Dota = steam.dota + dota: Dota = obj.dota if dota is None: - dota = self.alchemy.get(Dota)(steam=steam) - db_session.add(dota) - db_session.flush() - await self._change(dota, "wins", wl["win"], self._notify) - await self._change(dota, "losses", wl["lose"], self._notify) + # Autocreate the Dota record + dota = self.alchemy.get(Dota)(steam=obj) + session.add(dota) + session.flush() + + # Make a custom change function + async def change(attribute: str, new: Any): + await self._change(session=session, obj=dota, attribute=attribute, new=new) + + await change("wins", wl["win"]) + await change("losses", wl["lose"]) if p["rank_tier"]: - await self._change(dota, "rank", DotaRank(rank_tier=p["rank_tier"]), self._notify) + await change("rank", DotaRank(rank_tier=p["rank_tier"])) else: - await self._change(dota, "rank", None, self._notify) + await change("rank", None) - async def _updater(self, period: int): - log.info(f"Started updater with {period}s period") - while True: - log.info(f"Updating...") - session = self.alchemy.Session() - log.info("") - steams = session.query(self.alchemy.get(Steam)).all() - for steam in steams: - try: - await self._update(steam, session) - except Exception as e: - sentry_sdk.capture_exception(e) - log.error(f"Error while updating {steam.user.username}: {e}") - await asyncio.sleep(1) - await ru.asyncify(session.commit) - session.close() - log.info(f"Sleeping for {period}s") - await asyncio.sleep(period) + async def on_increase(self, session, obj: Dota, attribute: str, old: Any, new: Any) -> None: + if attribute == "rank": + await self.notify(f"📈 [b]{obj.steam.user}[/b] è salito a [b]{new}[/b] su Dota 2! Congratulazioni!") - async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: - author = await data.get_author(error_if_none=True) + async def on_unchanged(self, session, obj: Dota, attribute: str, old: Any, new: Any) -> None: + pass - found_something = False + async def on_decrease(self, session, obj: Dota, attribute: str, old: Any, new: Any) -> None: + if attribute == "rank": + await self.notify(f"📉 [b]{obj.steam.user}[/b] è sceso a [b]{new}[/b] su Dota 2.") - message = "" - for steam in author.steam: - await self._update(steam, data.session) - if steam.dota is None: - continue - found_something = True - message += self._display(steam.dota) - message += "\n" - if not found_something: - raise rc.UserError("Nessun account di Dota 2 trovato.") - await data.reply(message) + async def on_first(self, session, obj: Dota, attribute: str, old: None, new: Any) -> None: + if attribute == "wins": + await self.notify(f"↔️ Account {obj} connesso a {obj.steam.user}!") + elif attribute == "rank": + await self.notify(f"🌟 [b]{obj.steam.user}[/b] si è classificato [b]{new}[/b] su Dota 2!") + + async def on_reset(self, session, obj: Dota, attribute: str, old: Any, new: None) -> None: + if attribute == "rank": + await self.notify(f"⬜️ [b]{obj.steam.user}[/b] non ha più un rank su Dota 2.") diff --git a/royalpack/commands/leagueoflegends.py b/royalpack/commands/leagueoflegends.py index f383a1ee..55b4b574 100644 --- a/royalpack/commands/leagueoflegends.py +++ b/royalpack/commands/leagueoflegends.py @@ -6,93 +6,79 @@ import sentry_sdk import royalnet.commands as rc import royalnet.utils as ru import royalnet.serf.telegram as rst +from royalnet.backpack import tables as rbt +from .abstract.linker import LinkerCommand from ..tables import LeagueOfLegends, FiorygiTransaction -from ..types import LeagueLeague +from ..types import LeagueLeague, Updatable log = logging.getLogger(__name__) -class LeagueoflegendsCommand(rc.Command): +class LeagueoflegendsCommand(LinkerCommand): name: str = "leagueoflegends" aliases = ["lol", "league"] - description: str = "Connetti un account di League of Legends a un account Royalnet, e visualizzane le statistiche." + description: str = "Connetti un account di League of Legends a un account Royalnet, o visualizzane le statistiche." syntax = "[nomeevocatore]" + queue_names = { + "rank_soloq": "Solo/Duo", + "rank_flexq": "Flex", + "rank_twtrq": "3v3", + "rank_tftq": "TFT" + } + def __init__(self, interface: rc.CommandInterface): super().__init__(interface) self._riotwatcher: Optional[riotwatcher.RiotWatcher] = None - if self.interface.name == "telegram" and self.config["Lol"]["updater"]["enabled"]: - self._riotwatcher = riotwatcher.RiotWatcher(api_key=self.config["Lol"]["token"]) - self.loop.create_task(self._updater(int(self.config["Lol"]["updater"]["delay"]))) + if self.interface.name == "telegram" and self.enabled(): + self._riotwatcher = riotwatcher.RiotWatcher(api_key=self.token()) - async def _send(self, message): - client = self.serf.client - await self.serf.api_call(client.send_message, - chat_id=self.config["Telegram"]["main_group_id"], - text=rst.escape(message), - parse_mode="HTML", - disable_webpage_preview=True) + def token(self): + return self.config["leagueoflegends"]["token"] - async def _notify(self, - obj: LeagueOfLegends, - attribute_name: str, - old_value: Any, - new_value: Any): - if isinstance(old_value, LeagueLeague): - # This is a rank change! - # Don't send messages for every rank change, send messages just if the TIER or RANK changes! - if old_value.tier == new_value.tier and old_value.rank == new_value.rank: - return - # Find the queue - queue_names = { - "rank_soloq": "Solo/Duo", - "rank_flexq": "Flex", - "rank_twtrq": "3v3", - "rank_tftq": "TFT" - } - # Prepare the message - if new_value > old_value: - message = f"📈 [b]{obj.user}[/b] è salito a {new_value} su League of Legends " \ - f"({queue_names[attribute_name]})! Congratulazioni!" - else: - message = f"📉 [b]{obj.user}[/b] è sceso a {new_value} su League of Legends " \ - f"({queue_names[attribute_name]})." - # Send the message - await self._send(message) - # Level up! - elif attribute_name == "summoner_level": - if new_value == 30 or (new_value >= 50 and (new_value % 25 == 0)): - await self._send(f"🆙 [b]{obj.user}[/b] è salito al livello [b]{new_value}[/b] su League of Legends!") + def region(self): + return self.config["leagueoflegends"]["region"] - @staticmethod - async def _change(obj: LeagueOfLegends, - attribute_name: str, - new_value: Any, - callback: Callable[ - [LeagueOfLegends, str, Any, Any], Awaitable[None]]): - old_value = obj.__getattribute__(attribute_name) - if old_value != new_value: - await callback(obj, attribute_name, old_value, new_value) - obj.__setattr__(attribute_name, new_value) + def describe(self, obj: LeagueOfLegends) -> str: + string = f"ℹ️ [b]{obj.summoner_name}[/b]\n" \ + f"Lv. {obj.summoner_level}\n" \ + f"Mastery score: {obj.mastery_score}\n" \ + f"\n" + if obj.rank_soloq: + string += f"Solo: {obj.rank_soloq}\n" + if obj.rank_flexq: + string += f"Flex: {obj.rank_flexq}\n" + if obj.rank_twtrq: + string += f"3v3: {obj.rank_twtrq}\n" + if obj.rank_tftq: + string += f"TFT: {obj.rank_tftq}\n" + return string - async def _update(self, lol: LeagueOfLegends): - log.info(f"Updating: {lol}") - log.debug(f"Getting summoner data: {lol}") - summoner = await ru.asyncify(self._riotwatcher.summoner.by_id, region=self.config["Lol"]["region"], - encrypted_summoner_id=lol.summoner_id) - await self._change(lol, "profile_icon_id", summoner["profileIconId"], self._notify) - await self._change(lol, "summoner_name", summoner["name"], self._notify) - await self._change(lol, "puuid", summoner["puuid"], self._notify) - await self._change(lol, "summoner_level", summoner["summonerLevel"], self._notify) - await self._change(lol, "summoner_id", summoner["id"], self._notify) - await self._change(lol, "account_id", summoner["accountId"], self._notify) - log.debug(f"Getting leagues data: {lol}") - leagues = await ru.asyncify(self._riotwatcher.league.by_summoner, region=self.config["Lol"]["region"], - encrypted_summoner_id=lol.summoner_id) + async def get_updatables_of_user(self, session, user: rbt.User) -> List[LeagueOfLegends]: + return await ru.asyncify(session.query(self.alchemy.get(LeagueOfLegends)).filter_by(user=user).all) + + async def get_updatables(self, session) -> List[LeagueOfLegends]: + return await ru.asyncify(session.query(self.alchemy.get(LeagueOfLegends)).all) + + async def create(self, session, user: rbt.User, args) -> LeagueOfLegends: + name = args.joined() + + # Connect a new League of Legends account to Royalnet + log.debug(f"Searching for: {name}") + summoner = self._riotwatcher.summoner.by_name(region=self.region(), summoner_name=name) + # Ensure the account isn't already connected to something else + leagueoflegends = await ru.asyncify( + session.query(self.alchemy.get(LeagueOfLegends)).filter_by(summoner_id=summoner["id"]).one_or_none) + if leagueoflegends: + raise rc.CommandError(f"L'account {leagueoflegends} è già registrato su Royalnet.") + # Get rank information + log.debug(f"Getting leagues data: {name}") + leagues = self._riotwatcher.league.by_summoner(region=self.region(), + encrypted_summoner_id=summoner["id"]) soloq = LeagueLeague() flexq = LeagueLeague() twtrq = LeagueLeague() @@ -106,120 +92,80 @@ class LeagueoflegendsCommand(rc.Command): twtrq = LeagueLeague.from_dict(league) if league["queueType"] == "RANKED_TFT": tftq = LeagueLeague.from_dict(league) - await self._change(lol, "rank_soloq", soloq, self._notify) - await self._change(lol, "rank_flexq", flexq, self._notify) - await self._change(lol, "rank_twtrq", twtrq, self._notify) - await self._change(lol, "rank_tftq", tftq, self._notify) - log.debug(f"Getting mastery data: {lol}") + # Get mastery score + log.debug(f"Getting mastery data: {name}") + mastery = self._riotwatcher.champion_mastery.scores_by_summoner(region=self.region(), + encrypted_summoner_id=summoner["id"]) + # Create database row + leagueoflegends = self.alchemy.get(LeagueOfLegends)( + region=self.region(), + user=user, + profile_icon_id=summoner["profileIconId"], + summoner_name=summoner["name"], + puuid=summoner["puuid"], + summoner_level=summoner["summonerLevel"], + summoner_id=summoner["id"], + account_id=summoner["accountId"], + rank_soloq=soloq, + rank_flexq=flexq, + rank_twtrq=twtrq, + rank_tftq=tftq, + mastery_score=mastery + ) + session.add(leagueoflegends) + return leagueoflegends + + async def update(self, session, obj: LeagueOfLegends, change: Callable[[str, Any], Awaitable[None]]): + log.debug(f"Getting summoner data: {obj}") + summoner = await ru.asyncify(self._riotwatcher.summoner.by_id, region=self.region(), + encrypted_summoner_id=obj.summoner_id) + await change("profile_icon_id", summoner["profileIconId"]) + await change("summoner_name", summoner["name"]) + await change("puuid", summoner["puuid"]) + await change("summoner_level", summoner["summonerLevel"]) + await change("summoner_id", summoner["id"]) + await change("account_id", summoner["accountId"]) + log.debug(f"Getting leagues data: {obj}") + leagues = await ru.asyncify(self._riotwatcher.league.by_summoner, region=self.region(), + encrypted_summoner_id=obj.summoner_id) + soloq = LeagueLeague() + flexq = LeagueLeague() + twtrq = LeagueLeague() + tftq = LeagueLeague() + for league in leagues: + if league["queueType"] == "RANKED_SOLO_5x5": + soloq = LeagueLeague.from_dict(league) + if league["queueType"] == "RANKED_FLEX_SR": + flexq = LeagueLeague.from_dict(league) + if league["queueType"] == "RANKED_FLEX_TT": + twtrq = LeagueLeague.from_dict(league) + if league["queueType"] == "RANKED_TFT": + tftq = LeagueLeague.from_dict(league) + await change("rank_soloq", soloq) + await change("rank_flexq", flexq) + await change("rank_twtrq", twtrq) + await change("rank_tftq", tftq) + log.debug(f"Getting mastery data: {obj}") mastery = await ru.asyncify(self._riotwatcher.champion_mastery.scores_by_summoner, - region=self.config["Lol"]["region"], - encrypted_summoner_id=lol.summoner_id) - await self._change(lol, "mastery_score", mastery, self._notify) + region=self.region(), + encrypted_summoner_id=obj.summoner_id) + await change("mastery_score", mastery) - async def _updater(self, period: int): - log.info(f"Started updater with {period}s period") - while True: - log.info(f"Updating...") - session = self.alchemy.Session() - log.info("") - lols = session.query(self.alchemy.get(LeagueOfLegends)).all() - for lol in lols: - try: - await self._update(lol) - except Exception as e: - sentry_sdk.capture_exception(e) - log.error(f"Error while updating {lol.user.username}: {e}") - await asyncio.sleep(1) - await ru.asyncify(session.commit) - session.close() - log.info(f"Sleeping for {period}s") - await asyncio.sleep(period) + async def on_increase(self, session, obj: LeagueOfLegends, attribute: str, old: Any, new: Any) -> None: + if attribute in self.queue_names.keys(): + await self.notify(f"📈 [b]{obj.user}[/b] è salito a {new} su League of Legends ({self.queue_names[attribute]})! Congratulazioni!") - @staticmethod - def _display(lol: LeagueOfLegends) -> str: - string = f"ℹ️ [b]{lol.summoner_name}[/b]\n" \ - f"Lv. {lol.summoner_level}\n" \ - f"Mastery score: {lol.mastery_score}\n" \ - f"\n" - if lol.rank_soloq: - string += f"Solo: {lol.rank_soloq}\n" - if lol.rank_flexq: - string += f"Flex: {lol.rank_flexq}\n" - if lol.rank_twtrq: - string += f"3v3: {lol.rank_twtrq}\n" - if lol.rank_tftq: - string += f"TFT: {lol.rank_tftq}\n" - return string + async def on_unchanged(self, session, obj: LeagueOfLegends, attribute: str, old: Any, new: Any) -> None: + pass - async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: - author = await data.get_author(error_if_none=True) + async def on_decrease(self, session, obj: LeagueOfLegends, attribute: str, old: Any, new: Any) -> None: + if attribute in self.queue_names.keys(): + await self.notify(f"📉 [b]{obj.user}[/b] è sceso a {new} su League of Legends ({self.queue_names[attribute]}).") - name = args.joined() + async def on_first(self, session, obj: LeagueOfLegends, attribute: str, old: None, new: Any) -> None: + if attribute in self.queue_names.keys(): + await self.notify(f"🌟 [b]{obj.user}[/b] si è classificato {new} su League of Legends ({self.queue_names[attribute]}!") - if name: - # Connect a new League of Legends account to Royalnet - log.debug(f"Searching for: {name}") - summoner = self._riotwatcher.summoner.by_name(region=self.config["Lol"]["region"], summoner_name=name) - # Ensure the account isn't already connected to something else - leagueoflegends = await ru.asyncify( - data.session.query(self.alchemy.get(LeagueOfLegends)).filter_by(summoner_id=summoner["id"]).one_or_none) - if leagueoflegends: - raise rc.CommandError(f"L'account {leagueoflegends} è già registrato su Royalnet.") - # Get rank information - log.debug(f"Getting leagues data: {name}") - leagues = self._riotwatcher.league.by_summoner(region=self.config["Lol"]["region"], - encrypted_summoner_id=summoner["id"]) - soloq = LeagueLeague() - flexq = LeagueLeague() - twtrq = LeagueLeague() - tftq = LeagueLeague() - for league in leagues: - if league["queueType"] == "RANKED_SOLO_5x5": - soloq = LeagueLeague.from_dict(league) - if league["queueType"] == "RANKED_FLEX_SR": - flexq = LeagueLeague.from_dict(league) - if league["queueType"] == "RANKED_FLEX_TT": - twtrq = LeagueLeague.from_dict(league) - if league["queueType"] == "RANKED_TFT": - tftq = LeagueLeague.from_dict(league) - # Get mastery score - log.debug(f"Getting mastery data: {name}") - mastery = self._riotwatcher.champion_mastery.scores_by_summoner(region=self.config["Lol"]["region"], - encrypted_summoner_id=summoner["id"]) - # Create database row - leagueoflegends = self.alchemy.get(LeagueOfLegends)( - region=self.config["Lol"]["region"], - user=author, - profile_icon_id=summoner["profileIconId"], - summoner_name=summoner["name"], - puuid=summoner["puuid"], - summoner_level=summoner["summonerLevel"], - summoner_id=summoner["id"], - account_id=summoner["accountId"], - rank_soloq=soloq, - rank_flexq=flexq, - rank_twtrq=twtrq, - rank_tftq=tftq, - mastery_score=mastery - ) - log.debug(f"Saving to the DB: {name}") - data.session.add(leagueoflegends) - await data.session_commit() - await data.reply(f"↔️ Account {leagueoflegends} connesso a {author}!") - await FiorygiTransaction.spawn_fiorygi(data, author, 1, - "aver connesso il proprio account di League of Legends a Royalnet") - else: - # Update and display the League of Legends stats for the current account - if len(author.leagueoflegends) == 0: - raise rc.UserError("Nessun account di League of Legends trovato.") - message = "" - for account in author.leagueoflegends: - try: - await self._update(account) - message += self._display(account) - except riotwatcher.ApiError as e: - message += f"⚠️ [b]{account.summoner_name}[/b]\n" \ - f"{e}" - message += "\n" - await data.session_commit() - await data.reply(message) + async def on_reset(self, session, obj: LeagueOfLegends, attribute: str, old: Any, new: None) -> None: + if attribute in self.queue_names.keys(): + await self.notify(f"⬜️ [b]{obj.user}[/b] non ha più un rank su League of Legends ({self.queue_names[attribute]}).") diff --git a/royalpack/tables/brawlhalla.py b/royalpack/tables/brawlhalla.py index aa8555eb..cbc01466 100644 --- a/royalpack/tables/brawlhalla.py +++ b/royalpack/tables/brawlhalla.py @@ -2,11 +2,11 @@ from sqlalchemy import * from sqlalchemy.orm import * from sqlalchemy.ext.declarative import declared_attr import steam.steamid -from ..types import BrawlhallaRank, BrawlhallaTier, BrawlhallaMetal +from ..types import BrawlhallaRank, BrawlhallaTier, BrawlhallaMetal, Updatable # noinspection PyAttributeOutsideInit -class Brawlhalla: +class Brawlhalla(Updatable): __tablename__ = "brawlhalla" @declared_attr diff --git a/royalpack/tables/dota.py b/royalpack/tables/dota.py index 8ff9ccdc..0064be95 100644 --- a/royalpack/tables/dota.py +++ b/royalpack/tables/dota.py @@ -2,11 +2,11 @@ from typing import * from sqlalchemy import * from sqlalchemy.orm import relationship, backref from sqlalchemy.ext.declarative import declared_attr -from ..types import DotaMedal, DotaStars, DotaRank +from ..types import DotaMedal, DotaStars, DotaRank, Updatable import steam.steamid -class Dota: +class Dota(Updatable): __tablename__ = "dota" @declared_attr diff --git a/royalpack/tables/leagueoflegends.py b/royalpack/tables/leagueoflegends.py index 02618748..c778c4a8 100644 --- a/royalpack/tables/leagueoflegends.py +++ b/royalpack/tables/leagueoflegends.py @@ -1,10 +1,10 @@ from sqlalchemy import * from sqlalchemy.orm import relationship, composite from sqlalchemy.ext.declarative import declared_attr -from ..types import LeagueRank, LeagueTier, LeagueLeague +from ..types import LeagueRank, LeagueTier, LeagueLeague, Updatable -class LeagueOfLegends: +class LeagueOfLegends(Updatable): __tablename__ = "leagueoflegends" @declared_attr diff --git a/royalpack/types/__init__.py b/royalpack/types/__init__.py index 88f28a85..ed67a89c 100644 --- a/royalpack/types/__init__.py +++ b/royalpack/types/__init__.py @@ -10,6 +10,7 @@ from .brawlhallatier import BrawlhallaTier from .brawlhallametal import BrawlhallaMetal from .brawlhallarank import BrawlhallaRank from .pollmood import PollMood +from .updatable import Updatable __all__ = [ @@ -26,4 +27,5 @@ __all__ = [ "BrawlhallaRank", "BrawlhallaTier", "PollMood", + "Updatable", ] diff --git a/royalpack/types/updatable.py b/royalpack/types/updatable.py new file mode 100644 index 00000000..db18377e --- /dev/null +++ b/royalpack/types/updatable.py @@ -0,0 +1,2 @@ +class Updatable: + pass diff --git a/royalpack/version.py b/royalpack/version.py index 64511ad8..25edae59 100644 --- a/royalpack/version.py +++ b/royalpack/version.py @@ -1 +1 @@ -semantic = "5.11.1" +semantic = "5.12.0"