aboutsummaryrefslogtreecommitdiffstats
path: root/bot/seasons/season.py
diff options
context:
space:
mode:
Diffstat (limited to 'bot/seasons/season.py')
-rw-r--r--bot/seasons/season.py133
1 files changed, 68 insertions, 65 deletions
diff --git a/bot/seasons/season.py b/bot/seasons/season.py
index 6d992276..71324127 100644
--- a/bot/seasons/season.py
+++ b/bot/seasons/season.py
@@ -6,7 +6,7 @@ import inspect
import logging
import pkgutil
from pathlib import Path
-from typing import List, Optional, Type, Union
+from typing import List, Optional, Tuple, Type, Union
import async_timeout
import discord
@@ -22,10 +22,9 @@ ICON_BASE_URL = "https://raw.githubusercontent.com/python-discord/branding/maste
def get_seasons() -> List[str]:
"""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.name)
return seasons
@@ -33,7 +32,6 @@ def get_seasons() -> List[str]:
def get_season_class(season_name: str) -> Type["SeasonBase"]:
"""Gets the season class of the season module."""
-
season_lib = importlib.import_module(f"bot.seasons.{season_name}")
class_name = season_name.replace("_", " ").title().replace(" ", "")
return getattr(season_lib, class_name)
@@ -41,7 +39,6 @@ def get_season_class(season_name: str) -> Type["SeasonBase"]:
def get_season(season_name: str = None, date: datetime.datetime = None) -> "SeasonBase":
"""Returns a Season object based on either a string or a date."""
-
# If either both or neither are set, raise an error.
if not bool(season_name) ^ bool(date):
raise UserWarning("This function requires either a season or a date in order to run.")
@@ -83,15 +80,16 @@ class SeasonBase:
end_date: Optional[str] = None
colour: Optional[int] = None
- icon: str = "/logos/logo_full/logo_full.png"
+ icon: Tuple[str, ...] = ("/logos/logo_full/logo_full.png",)
bot_icon: Optional[str] = None
date_format: str = "%d/%m/%Y"
+ index: int = 0
+
@staticmethod
def current_year() -> int:
"""Returns the current year."""
-
return datetime.date.today().year
@classmethod
@@ -101,7 +99,6 @@ class SeasonBase:
If no start_date was defined, returns the minimum datetime to ensure it's always below checked dates.
"""
-
if not cls.start_date:
return datetime.datetime.min
return datetime.datetime.strptime(f"{cls.start_date}/{cls.current_year()}", cls.date_format)
@@ -113,7 +110,6 @@ class SeasonBase:
If no end_date was defined, returns the minimum datetime to ensure it's always above checked dates.
"""
-
if not cls.end_date:
return datetime.datetime.max
return datetime.datetime.strptime(f"{cls.end_date}/{cls.current_year()}", cls.date_format)
@@ -121,13 +117,11 @@ class SeasonBase:
@classmethod
def is_between_dates(cls, date: datetime.datetime) -> bool:
"""Determines if the given date falls between the season's date range."""
-
return cls.start() <= date <= cls.end()
@property
def name_clean(self) -> str:
"""Return the Season's name with underscores replaced by whitespace."""
-
return self.name.replace("_", " ").title()
@property
@@ -138,28 +132,28 @@ class SeasonBase:
It's recommended to define one in most cases by overwriting this as a normal attribute in the
inheriting class.
"""
-
return f"New Season, {self.name_clean}!"
- async def get_icon(self, avatar: bool = False) -> bytes:
+ async def get_icon(self, avatar: bool = False, index: int = 0) -> Tuple[bytes, str]:
"""
Retrieve the season's icon from the branding repository using the Season's icon attribute.
+ This also returns the relative URL path for logging purposes
If `avatar` is True, uses optional bot-only avatar icon if present.
+ Returns the data for the given `index`, defaulting to the first item.
The icon attribute must provide the url path, starting from the master branch base url,
including the starting slash.
e.g. `/logos/logo_seasonal/valentines/loved_up.png`
"""
+ icon = self.icon[index]
+ if avatar and self.bot_icon:
+ icon = self.bot_icon
- if avatar:
- icon = self.bot_icon or self.icon
- else:
- icon = self.icon
full_url = ICON_BASE_URL + icon
log.debug(f"Getting icon from: {full_url}")
async with bot.http_session.get(full_url) as resp:
- return await resp.read()
+ return (await resp.read(), icon)
async def apply_username(self, *, debug: bool = False) -> Union[bool, None]:
"""
@@ -171,7 +165,6 @@ class SeasonBase:
Returns False if it failed to change the username, falling back to nick.
Returns None if `debug` was True and username change wasn't attempted.
"""
-
guild = bot.get_guild(Client.guild)
result = None
@@ -183,12 +176,12 @@ class SeasonBase:
else:
if bot.user.name != self.bot_name:
- # attempt to change user details
+ # Attempt to change user details
log.debug(f"Changing username to {self.bot_name}")
with contextlib.suppress(discord.HTTPException):
await bot.user.edit(username=self.bot_name)
- # fallback on nickname if failed due to ratelimit
+ # Fallback on nickname if failed due to ratelimit
if bot.user.name != self.bot_name:
log.warning(f"Username failed to change: Changing nickname to {self.bot_name}")
await guild.me.edit(nick=self.bot_name)
@@ -196,7 +189,7 @@ class SeasonBase:
else:
result = True
- # remove nickname if an old one exists
+ # Remove nickname if an old one exists
if guild.me.nick and guild.me.nick != self.bot_name:
log.debug(f"Clearing old nickname of {guild.me.nick}")
await guild.me.edit(nick=None)
@@ -209,22 +202,21 @@ class SeasonBase:
Returns True if successful.
"""
-
- # track old avatar hash for later comparison
+ # Track old avatar hash for later comparison
old_avatar = bot.user.avatar
- # attempt the change
- log.debug(f"Changing avatar to {self.bot_icon or self.icon}")
- icon = await self.get_icon(avatar=True)
+ # Attempt the change
+ icon, name = await self.get_icon(avatar=True)
+ log.debug(f"Changing avatar to {name}")
with contextlib.suppress(discord.HTTPException, asyncio.TimeoutError):
async with async_timeout.timeout(5):
await bot.user.edit(avatar=icon)
if bot.user.avatar != old_avatar:
- log.debug(f"Avatar changed to {self.bot_icon or self.icon}")
+ log.debug(f"Avatar changed to {name}")
return True
- log.warning(f"Changing avatar failed: {self.bot_icon or self.icon}")
+ log.warning(f"Changing avatar failed: {name}")
return False
async def apply_server_icon(self) -> bool:
@@ -233,35 +225,51 @@ class SeasonBase:
Returns True if was successful.
"""
-
guild = bot.get_guild(Client.guild)
- # track old icon hash for later comparison
+ # Track old icon hash for later comparison
old_icon = guild.icon
- # attempt the change
- log.debug(f"Changing server icon to {self.icon}")
- icon = await self.get_icon()
+ # Attempt the change
+
+ icon, name = await self.get_icon(index=self.index)
+
+ log.debug(f"Changing server icon to {name}")
+
with contextlib.suppress(discord.HTTPException, asyncio.TimeoutError):
async with async_timeout.timeout(5):
await guild.edit(icon=icon, reason=f"Seasonbot Season Change: {self.name}")
new_icon = bot.get_guild(Client.guild).icon
if new_icon != old_icon:
- log.debug(f"Server icon changed to {self.icon}")
+ log.debug(f"Server icon changed to {name}")
return True
- log.warning(f"Changing server icon failed: {self.icon}")
+ log.warning(f"Changing server icon failed: {name}")
return False
+ async def change_server_icon(self) -> bool:
+ """
+ Changes the server icon.
+
+ This only has an effect when the Season's icon attribute is a list, in which it cycles through.
+ Returns True if was successful.
+ """
+ if len(self.icon) == 1:
+ return
+
+ self.index += 1
+ self.index %= len(self.icon)
+
+ return await self.apply_server_icon()
+
async def announce_season(self):
"""
Announces a change in season in the announcement channel.
It will skip the announcement if the current active season is the "evergreen" default season.
"""
-
- # don't actually announce if reverting to normal season
+ # Don't actually announce if reverting to normal season
if self.name == "evergreen":
log.debug(f"Season Changed: {self.name}")
return
@@ -270,11 +278,11 @@ class SeasonBase:
channel = guild.get_channel(Channels.announcements)
mention = f"<@&{Roles.announcements}>"
- # build cog info output
+ # Build cog info output
doc = inspect.getdoc(self)
announce = "\n\n".join(l.replace("\n", " ") for l in doc.split("\n\n"))
- # no announcement message found
+ # No announcement message found
if not doc:
return
@@ -282,9 +290,9 @@ class SeasonBase:
embed.set_author(name=self.greeting)
if self.icon:
- embed.set_image(url=ICON_BASE_URL+self.icon)
+ embed.set_image(url=ICON_BASE_URL+self.icon[0])
- # find any seasonal commands
+ # Find any seasonal commands
cogs = []
for cog in bot.cogs.values():
if "evergreen" in cog.__module__:
@@ -317,13 +325,13 @@ class SeasonBase:
If in debug mode, the avatar, server icon, and announcement will be skipped.
"""
-
+ self.index = 0
# Prepare all the seasonal cogs, and then the evergreen ones.
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)
+ 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}")
@@ -371,24 +379,25 @@ class SeasonManager(commands.Cog):
async def load_seasons(self):
"""Asynchronous timer loop to check for a new season every midnight."""
-
await self.bot.wait_until_ready()
await self.season.load()
while True:
- await asyncio.sleep(self.sleep_time) # sleep until midnight
- self.sleep_time = 86400 # next time, sleep for 24 hours.
+ await asyncio.sleep(self.sleep_time) # Sleep until midnight
+ self.sleep_time = 86400 # Next time, sleep for 24 hours.
# If the season has changed, load it.
new_season = get_season(date=datetime.datetime.utcnow())
if new_season.name != self.season.name:
+ self.season = new_season
await self.season.load()
+ else:
+ await self.season.change_server_icon()
@with_role(Roles.moderator, Roles.admin, Roles.owner)
@commands.command(name="season")
async def change_season(self, ctx, new_season: str):
"""Changes the currently active season on the bot."""
-
self.season = get_season(season_name=new_season)
await self.season.load()
await ctx.send(f"Season changed to {new_season}.")
@@ -397,8 +406,7 @@ class SeasonManager(commands.Cog):
@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
+ # Sort by start order, followed by lower duration
def season_key(season_class: Type[SeasonBase]):
return season_class.start(), season_class.end() - datetime.datetime.max
@@ -420,11 +428,11 @@ class SeasonManager(commands.Cog):
else:
period = f"{start} to {end}"
- # bold period if current date matches season date range
+ # 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
+ # Underline currently active season
is_active = current_season == season.name
sdec = "__" if is_active else ""
@@ -442,13 +450,12 @@ class SeasonManager(commands.Cog):
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")
+ await ctx.send_help(ctx.command)
@refresh.command(name="avatar")
async def refresh_avatar(self, ctx):
"""Re-applies the bot avatar for the currently loaded season."""
-
- # attempt the change
+ # Attempt the change
is_changed = await self.season.apply_avatar()
if is_changed:
@@ -458,7 +465,7 @@ class SeasonManager(commands.Cog):
colour = discord.Colour.red()
title = "Avatar Failed to Refresh"
- # report back details
+ # Report back details
season_name = type(self.season).__name__
embed = discord.Embed(
description=f"**Season:** {season_name}\n**Avatar:** {self.season.bot_icon or self.season.icon}",
@@ -471,8 +478,7 @@ class SeasonManager(commands.Cog):
@refresh.command(name="icon")
async def refresh_server_icon(self, ctx):
"""Re-applies the server icon for the currently loaded season."""
-
- # attempt the change
+ # Attempt the change
is_changed = await self.season.apply_server_icon()
if is_changed:
@@ -482,7 +488,7 @@ class SeasonManager(commands.Cog):
colour = discord.Colour.red()
title = "Server Icon Failed to Refresh"
- # report back details
+ # Report back details
season_name = type(self.season).__name__
embed = discord.Embed(
description=f"**Season:** {season_name}\n**Icon:** {self.season.icon}",
@@ -495,11 +501,10 @@ class SeasonManager(commands.Cog):
@refresh.command(name="username", aliases=("name",))
async def refresh_username(self, ctx):
"""Re-applies the bot username for the currently loaded season."""
-
old_username = str(bot.user)
old_display_name = ctx.guild.me.display_name
- # attempt the change
+ # Attempt the change
is_changed = await self.season.apply_username()
if is_changed:
@@ -511,7 +516,7 @@ class SeasonManager(commands.Cog):
else:
colour = discord.Colour.red()
- # if None, it's because it wasn't meant to change username
+ # If None, it's because it wasn't meant to change username
if is_changed is None:
title = "Nickname Refreshed"
else:
@@ -520,7 +525,7 @@ class SeasonManager(commands.Cog):
old_name = old_display_name
new_name = self.season.bot_name
- # report back details
+ # Report back details
season_name = type(self.season).__name__
embed = discord.Embed(
description=f"**Season:** {season_name}\n"
@@ -535,10 +540,8 @@ class SeasonManager(commands.Cog):
@commands.command()
async def announce(self, ctx):
"""Announces the currently loaded season."""
-
await self.season.announce_season()
def cog_unload(self):
"""Cancel season-related tasks on cog unload."""
-
self.season_task.cancel()