""" timings.py Holds classes related to maintaining consistent cooldowns for various commands and API operations. """ import asyncio import logging from datetime import datetime from typing import Union, Optional from bot import exceptions, constants logger = logging.getLogger(__file__) logger.setLevel(constants.LOGGING_LEVEL) class Cooldown(object): """ A cooldown object helps users manage a minimum time passed between activations of something. """ def __init__(self, cooldown: float, now: bool = False, last_hit: Union[float, int] = None): self.cooldown: float = max(0.0, cooldown) self.hot_until: Optional[float] = float(last_hit) if last_hit is not None else None if now: self.hot_until = datetime.utcnow().timestamp() def hit(self, safe: bool = False): """Activate the cooldown. Raises an exception if Safe is set to True and the cooldown has not passed.""" if safe and not self.ready: raise exceptions.CooldownRequired('The cooldown duration has not passed. {}') self.hot_until = datetime.utcnow().timestamp() + self.cooldown def change_expiration(self, timestamp: Union[float, int]) -> None: """ Change the epoch timestamp at which the cooldown's hot period expires. :param timestamp: A epoch timestamp. """ if self.hot_until is not None: change = round(abs(timestamp - self.hot_until), 2) change_word = 'longer' if timestamp > self.hot_until else 'sooner' logger.debug(f'Changing cooldown timestamp to {round(timestamp, 2)} ({change}s {change_word})') else: change = round(abs(timestamp - datetime.utcnow().timestamp()), 2) change_word = 'in the future' if timestamp >= datetime.utcnow().timestamp() else 'ago' logger.debug(f'Setting cooldown timestamp to {round(timestamp, 2)} ({change}s {change_word})') self.hot_until = timestamp async def sleep(self) -> None: if self.ready: return logger.debug(f'Sleeping for {round(self.time_left, 2)}s before sending a command.') await asyncio.sleep(self.time_left) @property def time_left(self) -> float: """Returns the non-negative time left until the cooldown is ready.""" return max(0.0, self.hot_until - datetime.utcnow().timestamp()) @property def ready(self, now: Union[float, int] = None) -> bool: """Returns True if the cooldown has passed.""" if self.hot_until: return now or datetime.utcnow().timestamp() >= self.hot_until return True