diff options
| -rw-r--r-- | bot/constants.py | 1 | ||||
| -rw-r--r-- | bot/seasons/christmas/__init__.py | 9 | ||||
| -rw-r--r-- | bot/seasons/evergreen/__init__.py | 10 | ||||
| -rw-r--r-- | bot/seasons/halloween/__init__.py | 9 | ||||
| -rw-r--r-- | bot/seasons/season.py | 198 | 
5 files changed, 170 insertions, 57 deletions
| diff --git a/bot/constants.py b/bot/constants.py index 1294912a..5382d5f3 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -74,6 +74,7 @@ class Colours:  class Emojis:      star = "\u2B50"      christmas_tree = u"\U0001F384" +    check = "\u2611"  class Tokens(NamedTuple): diff --git a/bot/seasons/christmas/__init__.py b/bot/seasons/christmas/__init__.py index cd5ce307..8eb78251 100644 --- a/bot/seasons/christmas/__init__.py +++ b/bot/seasons/christmas/__init__.py @@ -6,11 +6,4 @@ class Christmas(SeasonBase):      start_date = "01/12"      end_date = "31/12"      bot_name = "Santabot" - -    def __init__(self, bot): -        self.bot = bot - -    @property -    def bot_avatar(self): -        with open(self.avatar_path("christmas.png"), "rb") as avatar: -            return bytearray(avatar.read()) +    bot_avatar = "christmas.png" diff --git a/bot/seasons/evergreen/__init__.py b/bot/seasons/evergreen/__init__.py index e4367aaa..db5b5684 100644 --- a/bot/seasons/evergreen/__init__.py +++ b/bot/seasons/evergreen/__init__.py @@ -2,12 +2,4 @@ from bot.seasons import SeasonBase  class Evergreen(SeasonBase): -    bot_name = "SeasonalBot" - -    def __init__(self, bot): -        self.bot = bot - -    @property -    def bot_avatar(self): -        with open(self.avatar_path("standard.png"), "rb") as avatar: -            return bytearray(avatar.read()) +    pass diff --git a/bot/seasons/halloween/__init__.py b/bot/seasons/halloween/__init__.py index 40b9ce90..4dd195fb 100644 --- a/bot/seasons/halloween/__init__.py +++ b/bot/seasons/halloween/__init__.py @@ -6,11 +6,4 @@ class Halloween(SeasonBase):      start_date = "01/10"      end_date = "31/10"      bot_name = "Spookybot" - -    def __init__(self, bot): -        self.bot = bot - -    @property -    def bot_avatar(self): -        with open(self.avatar_path("spooky.png"), "rb") as avatar: -            return bytearray(avatar.read()) +    bot_avatar = "spooky.png" diff --git a/bot/seasons/season.py b/bot/seasons/season.py index 591bbc2d..a86330e9 100644 --- a/bot/seasons/season.py +++ b/bot/seasons/season.py @@ -5,9 +5,10 @@ import logging  import pkgutil  from pathlib import Path +import discord  from discord.ext import commands -from bot.constants import Client, Roles +from bot.constants import Client, Roles, bot  from bot.decorators import with_role  log = logging.getLogger(__name__) @@ -15,24 +16,23 @@ log = logging.getLogger(__name__)  def get_seasons():      """ -    Returns all the Season objects -    located in bot/seasons/ +    Returns all the Season objects located in bot/seasons/      """      seasons = [] -    for module in pkgutil.iter_modules([Path('bot', 'seasons')]): +    for module in pkgutil.iter_modules([Path("bot", "seasons")]):          if module.ispkg: -            seasons.append(module[1]) +            seasons.append(module.name)      return seasons  def get_season_class(season_name): -    season_lib = importlib.import_module(f'bot.seasons.{season_name}') +    season_lib = importlib.import_module(f"bot.seasons.{season_name}")      return getattr(season_lib, season_name.capitalize()) -def get_season(bot, season_name: str = None, date: datetime.date = None): +def get_season(season_name: str = None, date: datetime.datetime = None):      """      Returns a Season object based on either a string or a date.      """ @@ -52,25 +52,29 @@ def get_season(bot, season_name: str = None, date: datetime.date = None):      if season_name:          season_name = season_name.lower()          if season_name not in seasons: -            season_name = 'evergreen' +            season_name = "evergreen"          season_class = get_season_class(season_name) -        return season_class(bot) +        return season_class()      # If not, we have to figure out if the date matches any of the seasons. -    seasons.remove('evergreen') +    seasons.remove("evergreen")      for season_name in seasons:          season_class = get_season_class(season_name)          # check if date matches before returning an instance -        if season_class.start() <= date <= season_class.end(): -            return season_class(bot) +        if season_class.is_between_dates(date): +            return season_class()      else: -        evergreen_class = get_season_class('evergreen') -        return evergreen_class(bot) +        evergreen_class = get_season_class("evergreen") +        return evergreen_class()  class SeasonBase:      name = None -    date_format = "%d/%m-%Y" +    date_format = "%d/%m/%Y" +    start_date = None +    end_date = None +    bot_name: str = "SeasonalBot" +    bot_avatar: str = "standard.png"      @staticmethod      def current_year(): @@ -78,23 +82,31 @@ class SeasonBase:      @classmethod      def start(cls): -        return datetime.datetime.strptime(f"{cls.start_date}-{cls.current_year()}", cls.date_format).date() +        if not cls.start_date: +            return datetime.datetime.min +        return datetime.datetime.strptime(f"{cls.start_date}/{cls.current_year()}", cls.date_format)      @classmethod      def end(cls): -        return datetime.datetime.strptime(f"{cls.end_date}-{cls.current_year()}", cls.date_format).date() +        if not cls.end_date: +            return datetime.datetime.max +        return datetime.datetime.strptime(f"{cls.end_date}/{cls.current_year()}", cls.date_format) -    @staticmethod -    def avatar_path(*path_segments): -        return Path('bot', 'resources', 'avatars', *path_segments) +    @classmethod +    def is_between_dates(cls, date): +        return cls.start() <= date <= cls.end() + +    def get_avatar(self): +        avatar_path = Path("bot", "resources", "avatars", self.bot_avatar) +        with open(avatar_path, "rb") as avatar_file: +            return bytearray(avatar_file.read())      async def load(self):          """ -        Loads in the bot name, the bot avatar, -        and the extensions that are relevant to that season. +        Loads in the bot name, the bot avatar, and the extensions that are relevant to that season.          """ -        guild = self.bot.get_guild(Client.guild) +        guild = bot.get_guild(Client.guild)          # Change only nickname if in debug mode due to ratelimits for user edits          if Client.debug: @@ -102,13 +114,13 @@ class SeasonBase:                  log.debug(f"Changing nickname to {self.bot_name}")                  await guild.me.edit(nick=self.bot_name)          else: -            if self.bot.user.name != self.bot_name: +            if bot.user.name != self.bot_name:                  # attempt to change user details                  log.debug(f"Changing username to {self.bot_name}") -                await self.bot.user.edit(name=self.bot_name, avatar=self.bot_avatar) +                await bot.user.edit(username=self.bot_name, avatar=self.get_avatar())                  # fallback on nickname if failed due to ratelimit -                if self.bot.user.name != self.bot_name: +                if bot.user.name != self.bot_name:                      log.info(f"User details failed to change: Changing nickname to {self.bot_name}")                      await guild.me.edit(nick=self.bot_name) @@ -121,13 +133,13 @@ class SeasonBase:          extensions = []          for ext_folder in {self.name, "evergreen"}:              if ext_folder: -                log.info(f'Start loading extensions from seasons/{ext_folder}/') -                path = Path('bot', 'seasons', ext_folder) +                log.info(f"Start loading extensions from seasons/{ext_folder}/") +                path = Path("bot", "seasons", ext_folder)                  for ext_name in [i[1] for i in pkgutil.iter_modules([path])]:                      extensions.append(f"bot.seasons.{ext_folder}.{ext_name}")          # Finally we can load all the cogs we've prepared. -        self.bot.load_extensions(extensions) +        bot.load_extensions(extensions)  class SeasonManager: @@ -137,7 +149,7 @@ class SeasonManager:      def __init__(self, bot):          self.bot = bot -        self.season = get_season(bot, date=datetime.date.today()) +        self.season = get_season(date=datetime.datetime.utcnow())          self.season_task = bot.loop.create_task(self.load_seasons())          # Figure out number of seconds until a minute past midnight @@ -161,7 +173,7 @@ class SeasonManager:              self.sleep_time = 86400  # next time, sleep for 24 hours.              # If the season has changed, load it. -            new_season = get_season(self.bot, date=datetime.date.today()) +            new_season = get_season(date=datetime.datetime.utcnow())              if new_season != self.season:                  await self.season.load() @@ -172,9 +184,131 @@ class SeasonManager:          Changes the currently active season on the bot.          """ -        self.season = get_season(self.bot, season_name=new_season) +        self.season = get_season(season_name=new_season)          await self.season.load()          await ctx.send(f"Season changed to {new_season}.") +    @with_role(Roles.moderator, Roles.admin, Roles.owner) +    @commands.command(name="seasons") +    async def show_seasons(self, ctx): +        """ +        Shows the available seasons and their dates. +        """ + +        # sort by start order, followed by lower duration +        def season_key(season: SeasonBase): +            return season.start(), season.end() - datetime.datetime.max + +        current_season = self.season.name + +        entries = [] +        seasons = [get_season_class(s) for s in get_seasons()] +        for season in sorted(seasons, key=season_key): +            start = season.start_date +            end = season.end_date +            if start and not end: +                period = f"From {start}" +            elif end and not start: +                period = f"Until {end}" +            elif not end and not start: +                period = f"Always" +            else: +                period = f"{start} to {end}" + +            # bold period if current date matches season date range +            is_current = season.is_between_dates(datetime.datetime.utcnow()) +            pdec = "**" if is_current else "" + +            # underline currently active season +            is_active = current_season == season.name +            sdec = "__" if is_active else "" + +            forced_space = "\u200b " +            entries.append( +                f"**{sdec}{season.__name__}:{sdec}**\n" +                f"{forced_space*3}{pdec}{period}{pdec}" +            ) + +        embed = discord.Embed(description="\n".join(entries), colour=ctx.guild.me.colour) +        embed.set_author(name="Seasons") +        await ctx.send(embed=embed) + +    @with_role(Roles.moderator, Roles.admin, Roles.owner) +    @commands.group() +    async def refresh(self, ctx): +        """ +        Refreshes certain seasonal elements without reloading seasons. +        """ +        if not ctx.invoked_subcommand: +            await ctx.invoke(bot.get_command("help"), "refresh") + +    @refresh.command(name="avatar") +    async def refresh_avatar(self, ctx): +        """ +        Re-applies the bot avatar for the currently loaded season. +        """ + +        # track old avatar hash for later comparison +        old_avatar = bot.user.avatar + +        # attempt the change +        await bot.user.edit(avatar=self.season.get_avatar()) + +        if bot.user.avatar != old_avatar: +            log.debug(f"Avatar changed to {self.season.bot_avatar}") +            colour = ctx.guild.me.colour +            title = "Avatar Refreshed" +        else: +            log.debug(f"Changing avatar failed: {self.season.bot_avatar}") +            colour = discord.Colour.red() +            title = "Avatar Failed to Refresh" + +        # report back details +        season_name = type(self.season).__name__ +        embed = discord.Embed( +            description=f"**Season:** {season_name}\n**Avatar:** {self.season.bot_avatar}", +            colour=colour +        ) +        embed.set_author(name=title) +        embed.set_thumbnail(url=bot.user.avatar_url_as(format="png")) +        await ctx.send(embed=embed) + +    @refresh.command(name="username", aliases=("name",)) +    async def refresh_username(self, ctx): +        """ +        Re-applies the bot username for the currently loaded season. +        """ + +        # track old username for later comparison +        old_username = str(bot.user) + +        # attempt the change +        await bot.user.edit(username=self.season.bot_name) + +        if str(bot.user) != old_username: +            log.debug(f"Username changed to {self.season.bot_name}") +            colour = ctx.guild.me.colour +            title = "Username Refreshed" +            changed_element = "Username" +            new_name = str(bot.user) +        else: +            log.debug(f"Changing username failed: Changing nickname to {self.season.bot_name}") +            new_name = self.season.bot_name +            await ctx.guild.me.edit(nick=new_name) +            colour = discord.Colour.red() +            title = "Username Failed to Refresh" +            changed_element = "Nickname" + +        # report back details +        season_name = type(self.season).__name__ +        embed = discord.Embed( +            description=f"**Season:** {season_name}\n" +                        f"**Old Username:** {old_username}\n" +                        f"**New {changed_element}:** {new_name}", +            colour=colour +        ) +        embed.set_author(name=title) +        await ctx.send(embed=embed) +      def __unload(self):          self.season_task.cancel() | 
