aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Johannes Christ <[email protected]>2018-12-04 20:05:07 +0100
committerGravatar GitHub <[email protected]>2018-12-04 20:05:07 +0100
commita3b92a8d1224aaafe2fd141843f5763688ca8027 (patch)
tree4d4edcad152214752d2e34fca1f512e4c9f7e741
parentMerge pull request #87 from python-discord/bytecommander-aoc2 (diff)
parentUse contextlib.suppress, remove unnecessary `u` prefix. (diff)
Merge pull request #80 from scragly/season_improvements
Season Improvements
-rw-r--r--bot/constants.py52
-rw-r--r--bot/resources/avatars/christmas.pngbin44843 -> 0 bytes
-rw-r--r--bot/resources/avatars/spooky.pngbin37202 -> 0 bytes
-rw-r--r--bot/resources/avatars/standard.pngbin52156 -> 0 bytes
-rw-r--r--bot/seasons/christmas/__init__.py21
-rw-r--r--bot/seasons/christmas/adventofcode.py5
-rw-r--r--bot/seasons/evergreen/__init__.py10
-rw-r--r--bot/seasons/halloween/__init__.py14
-rw-r--r--bot/seasons/season.py451
9 files changed, 461 insertions, 92 deletions
diff --git a/bot/constants.py b/bot/constants.py
index 1294912a..71bdbf5f 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -4,14 +4,27 @@ from typing import NamedTuple
from bot.bot import SeasonalBot
-__all__ = ('Client', 'Roles', 'bot')
+__all__ = (
+ "AdventOfCode", "Channels", "Client", "Colours", "Emojis", "Hacktoberfest", "Roles",
+ "Tokens", "bot"
+)
log = logging.getLogger(__name__)
+class AdventOfCode:
+ leaderboard_cache_age_threshold_seconds = 3600
+ leaderboard_id = 363275
+ leaderboard_join_code = "363275-442b6939"
+ leaderboard_max_displayed_members = 10
+ year = 2018
+ channel_id = int(environ.get("AOC_CHANNEL_ID", 517745814039166986))
+ role_id = int(environ.get("AOC_ROLE_ID", 518565788744024082))
+
+
class Channels(NamedTuple):
admins = 365960823622991872
- announcements = 354619224620138496
+ announcements = int(environ.get('CHANNEL_ANNOUNCEMENTS', 354619224620138496))
big_brother_logs = 468507907357409333
bot = 267659945086812160
checkpoint_test = 422077681434099723
@@ -45,13 +58,26 @@ class Client(NamedTuple):
season_override = environ.get('SEASON_OVERRIDE')
+class Colours:
+ soft_red = 0xcd6d6d
+ soft_green = 0x68c290
+ dark_green = 0x1f8b4c
+ orange = 0xe67e22
+
+
+class Emojis:
+ star = "\u2B50"
+ christmas_tree = "\U0001F384"
+ check = "\u2611"
+
+
class Hacktoberfest(NamedTuple):
channel_id = 498804484324196362
voice_id = 514420006474219521
class Roles(NamedTuple):
- admin = 267628507062992896
+ admin = int(environ.get('SEASONALBOT_ADMIN_ROLE_ID', 267628507062992896))
announcements = 463658397560995840
champion = 430492892331769857
contributor = 295488872404484098
@@ -66,29 +92,9 @@ class Roles(NamedTuple):
rockstars = 458226413825294336
-class Colours:
- soft_red = 0xcd6d6d
- soft_green = 0x68c290
-
-
-class Emojis:
- star = "\u2B50"
- christmas_tree = u"\U0001F384"
-
-
class Tokens(NamedTuple):
giphy = environ.get("GIPHY_TOKEN")
aoc_session_cookie = environ.get("AOC_SESSION_COOKIE")
-class AdventOfCode:
- leaderboard_cache_age_threshold_seconds = 3600
- leaderboard_id = 363275
- leaderboard_join_code = "363275-442b6939"
- leaderboard_max_displayed_members = 10
- year = 2018
- channel_id = int(environ.get("AOC_CHANNEL_ID", 517745814039166986))
- role_id = int(environ.get("AOC_ROLE_ID", 518565788744024082))
-
-
bot = SeasonalBot(command_prefix=Client.prefix)
diff --git a/bot/resources/avatars/christmas.png b/bot/resources/avatars/christmas.png
deleted file mode 100644
index 55b72fac..00000000
--- a/bot/resources/avatars/christmas.png
+++ /dev/null
Binary files differ
diff --git a/bot/resources/avatars/spooky.png b/bot/resources/avatars/spooky.png
deleted file mode 100644
index 4ab33188..00000000
--- a/bot/resources/avatars/spooky.png
+++ /dev/null
Binary files differ
diff --git a/bot/resources/avatars/standard.png b/bot/resources/avatars/standard.png
deleted file mode 100644
index c14ff42a..00000000
--- a/bot/resources/avatars/standard.png
+++ /dev/null
Binary files differ
diff --git a/bot/seasons/christmas/__init__.py b/bot/seasons/christmas/__init__.py
index cd5ce307..99d81b0c 100644
--- a/bot/seasons/christmas/__init__.py
+++ b/bot/seasons/christmas/__init__.py
@@ -1,16 +1,21 @@
+from bot.constants import Colours
from bot.seasons import SeasonBase
class Christmas(SeasonBase):
+ """
+ We are getting into the festive spirit with a new server icon, new
+ bot name and avatar, and some new commands for you to check out!
+
+ No matter who you are, where you are or what beliefs you may follow,
+ we hope every one of you enjoy this festive season!
+ """
name = "christmas"
+ bot_name = "Merrybot"
+ greeting = "Happy Holidays!"
+
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())
+ colour = Colours.dark_green
+ icon = "/logos/logo_seasonal/christmas/festive.png"
diff --git a/bot/seasons/christmas/adventofcode.py b/bot/seasons/christmas/adventofcode.py
index 58d083ab..fcd3e171 100644
--- a/bot/seasons/christmas/adventofcode.py
+++ b/bot/seasons/christmas/adventofcode.py
@@ -108,6 +108,9 @@ async def day_countdown(bot: commands.Bot):
class AdventOfCode:
+ """
+ Advent of Code festivities! Ho Ho Ho!
+ """
def __init__(self, bot: commands.Bot):
self.bot = bot
@@ -133,7 +136,7 @@ class AdventOfCode:
@commands.group(name="adventofcode", aliases=("aoc",), invoke_without_command=True)
async def adventofcode_group(self, ctx: commands.Context):
"""
- Advent of Code festivities! Ho Ho Ho!
+ All of the Advent of Code commands
"""
await ctx.invoke(self.bot.get_command("help"), "adventofcode")
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..4b371f14 100644
--- a/bot/seasons/halloween/__init__.py
+++ b/bot/seasons/halloween/__init__.py
@@ -1,16 +1,14 @@
+from bot.constants import Colours
from bot.seasons import SeasonBase
class Halloween(SeasonBase):
name = "halloween"
- start_date = "01/10"
- end_date = "31/10"
bot_name = "Spookybot"
+ greeting = "Happy Halloween!"
- def __init__(self, bot):
- self.bot = bot
+ start_date = "01/10"
+ end_date = "31/10"
- @property
- def bot_avatar(self):
- with open(self.avatar_path("spooky.png"), "rb") as avatar:
- return bytearray(avatar.read())
+ colour = Colours.orange
+ icon = "/logos/logo_seasonal/halloween/spooky.png"
diff --git a/bot/seasons/season.py b/bot/seasons/season.py
index 591bbc2d..f1d570e0 100644
--- a/bot/seasons/season.py
+++ b/bot/seasons/season.py
@@ -1,38 +1,46 @@
import asyncio
+import contextlib
import datetime
import importlib
+import inspect
import logging
import pkgutil
from pathlib import Path
+from typing import List, Optional, Type, Union
+import async_timeout
+import discord
from discord.ext import commands
-from bot.constants import Client, Roles
+from bot.constants import Channels, Client, Roles, bot
from bot.decorators import with_role
log = logging.getLogger(__name__)
-def get_seasons():
+def get_seasons() -> List[str]:
"""
- 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}')
+def get_season_class(season_name: str) -> Type["SeasonBase"]:
+ """
+ Get's the season class of the season module.
+ """
+
+ 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) -> "SeasonBase":
"""
Returns a Season object based on either a string or a date.
"""
@@ -52,82 +60,283 @@ 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"
+ """
+ Base class for Seasonal classes.
+ """
+
+ name: Optional[str] = "evergreen"
+ bot_name: str = "SeasonalBot"
+
+ start_date: Optional[str] = None
+ end_date: Optional[str] = None
+
+ colour: Optional[int] = None
+ icon: str = "/logos/logo_full/logo_full.png"
+
+ date_format: str = "%d/%m/%Y"
@staticmethod
- def current_year():
+ def current_year() -> int:
+ """
+ Returns the current year.
+ """
+
return datetime.date.today().year
@classmethod
- def start(cls):
- return datetime.datetime.strptime(f"{cls.start_date}-{cls.current_year()}", cls.date_format).date()
+ def start(cls) -> datetime.datetime:
+ """
+ Returns the start date using current year and start_date attribute.
+
+ 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)
@classmethod
- def end(cls):
- return datetime.datetime.strptime(f"{cls.end_date}-{cls.current_year()}", cls.date_format).date()
+ def end(cls) -> datetime.datetime:
+ """
+ Returns the start date using current year and end_date attribute.
- @staticmethod
- def avatar_path(*path_segments):
- return Path('bot', 'resources', 'avatars', *path_segments)
+ If no end_date was defined, returns the minimum datetime to ensure
+ it's always above checked dates.
+ """
- async def load(self):
+ if not cls.end_date:
+ return datetime.datetime.max
+ return datetime.datetime.strptime(f"{cls.end_date}/{cls.current_year()}", cls.date_format)
+
+ @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 greeting(self) -> str:
+ """
+ Provides a default greeting based on the season name if one wasn't
+ defined in the season class.
+
+ It's recommended to define one in most cases by overwriting this as a
+ normal attribute in the inhertiting class.
+ """
+
+ season_name = " ".join(self.name.split("_")).title()
+ return f"New Season, {season_name}!"
+
+ async def get_icon(self) -> bytes:
"""
- Loads in the bot name, the bot avatar,
- and the extensions that are relevant to that season.
+ Retrieves the icon image from the branding repository, using the
+ defined icon attribute for the season.
+
+ The icon attribute must provide the url path, starting from the master
+ branch base url, including the starting slash:
+ `https://raw.githubusercontent.com/python-discord/branding/master`
+ """
+
+ base_url = "https://raw.githubusercontent.com/python-discord/branding/master"
+ full_url = base_url + self.icon
+ log.debug(f"Getting icon from: {full_url}")
+ async with bot.http_session.get(full_url) as resp:
+ return await resp.read()
+
+ async def apply_username(self, *, debug: bool = False) -> Union[bool, None]:
+ """
+ Applies the username for the current season. Only changes nickname if
+ `bool` is False, otherwise only changes the nickname.
+
+ Returns True if it successfully changed the username.
+ 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 = self.bot.get_guild(Client.guild)
+ guild = bot.get_guild(Client.guild)
+ result = None
# Change only nickname if in debug mode due to ratelimits for user edits
- if Client.debug:
+ if debug:
if guild.me.display_name != self.bot_name:
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)
+ with contextlib.suppress(discord.HTTPException):
+ await bot.user.edit(username=self.bot_name)
# fallback on nickname if failed due to ratelimit
- if self.bot.user.name != self.bot_name:
- log.info(f"User details failed to change: Changing nickname to {self.bot_name}")
+ 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)
+ result = False
+ else:
+ result = True
# 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)
+ return result
+
+ async def apply_avatar(self) -> bool:
+ """
+ Applies the avatar for the current season. Returns if it was successful.
+ """
+
+ # track old avatar hash for later comparison
+ old_avatar = bot.user.avatar
+
+ # attempt the change
+ log.debug(f"Changing avatar to {self.icon}")
+ icon = await self.get_icon()
+ 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.icon}")
+ return True
+
+ log.warning(f"Changing avatar failed: {self.icon}")
+ return False
+
+ async def apply_server_icon(self) -> bool:
+ """
+ Applies the server icon for the current season. Returns if it was successful.
+ """
+
+ guild = bot.get_guild(Client.guild)
+
+ # 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()
+ 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}")
+ return True
+
+ log.warning(f"Changing server icon failed: {self.icon}")
+ return False
+
+ 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
+ if self.name == "evergreen":
+ log.debug(f"Season Changed: {self.name}")
+ return
+
+ guild = bot.get_guild(Client.guild)
+ channel = guild.get_channel(Channels.announcements)
+ mention = f"<@&{Roles.announcements}>"
+
+ # collect seasonal cogs
+ cogs = []
+ for cog in bot.cogs.values():
+ if "evergreen" in cog.__module__:
+ continue
+ cog_name = type(cog).__name__
+ if cog_name != "SeasonManager":
+ cogs.append(cog_name)
+
+ # no cogs, so no seasonal commands
+ if not cogs:
+ return
+
+ # build cog info output
+ doc = inspect.getdoc(self)
+ announce_text = doc + "\n\n" if doc else ""
+
+ def cog_name(cog):
+ return type(cog).__name__
+
+ cog_info = []
+ for cog in sorted(cogs, key=cog_name):
+ doc = inspect.getdoc(bot.get_cog(cog))
+ if doc:
+ cog_info.append(f"**{cog}**\n*{doc}*")
+ else:
+ cog_info.append(f"**{cog}**")
+
+ embed = discord.Embed(description=announce_text, colour=self.colour or guild.me.colour)
+ embed.set_author(name=self.greeting)
+ cogs_text = "\n".join(cog_info)
+ embed.add_field(name="New Command Categories", value=cogs_text)
+ embed.set_footer(text="To see the new commands, use .help Category")
+
+ await channel.send(mention, embed=embed)
+
+ async def load(self):
+ """
+ Loads extensions, bot name and avatar, server icon and announces new season.
+
+ If in debug mode, the avatar, server icon, and announcement will be skipped.
+ """
+
# 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)
+ 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)
+
+ # Apply seasonal elements after extensions successfully load
+ username_changed = await self.apply_username(debug=Client.debug)
+
+ # Avoid heavy API ratelimited elements when debugging
+ if not Client.debug:
+ log.info("Applying avatar.")
+ await self.apply_avatar()
+ log.info("Applying server icon.")
+ await self.apply_server_icon()
+ if username_changed:
+ log.info(f"Announcing season {self.name}.")
+ await self.announce_season()
+ else:
+ log.info(f"Skipping season announcement due to username not being changed.")
class SeasonManager:
@@ -137,7 +346,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,20 +370,176 @@ 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()
@with_role(Roles.moderator, Roles.admin, Roles.owner)
- @commands.command(name='season')
+ @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(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
+
+ forced_space = "\u200b "
+
+ 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 ""
+
+ entries.append(
+ f"**{sdec}{season.__name__}:{sdec}**\n"
+ f"{forced_space*3}{pdec}{period}{pdec}\n"
+ )
+
+ 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.
+ """
+
+ # attempt the change
+ is_changed = await self.season.apply_avatar()
+
+ if is_changed:
+ colour = ctx.guild.me.colour
+ title = "Avatar Refreshed"
+ else:
+ 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.icon}",
+ 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="icon")
+ async def refresh_server_icon(self, ctx):
+ """
+ Re-applies the server icon for the currently loaded season.
+ """
+
+ # attempt the change
+ is_changed = await self.season.apply_server_icon()
+
+ if is_changed:
+ colour = ctx.guild.me.colour
+ title = "Server Icon Refreshed"
+ else:
+ colour = discord.Colour.red()
+ title = "Server Icon Failed to Refresh"
+
+ # report back details
+ season_name = type(self.season).__name__
+ embed = discord.Embed(
+ description=f"**Season:** {season_name}\n**Icon:** {self.season.icon}",
+ colour=colour
+ )
+ embed.set_author(name=title)
+ embed.set_thumbnail(url=bot.get_guild(Client.guild).icon_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.
+ """
+
+ old_username = str(bot.user)
+ old_display_name = ctx.guild.me.display_name
+
+ # attempt the change
+ is_changed = await self.season.apply_username()
+
+ if is_changed:
+ colour = ctx.guild.me.colour
+ title = "Username Refreshed"
+ changed_element = "Username"
+ old_name = old_username
+ new_name = str(bot.user)
+ else:
+ colour = discord.Colour.red()
+
+ # if None, it's because it wasn't meant to change username
+ if is_changed is None:
+ title = "Nickname Refreshed"
+ else:
+ title = "Username Failed to Refresh"
+ changed_element = "Nickname"
+ old_name = old_display_name
+ new_name = self.season.bot_name
+
+ # report back details
+ season_name = type(self.season).__name__
+ embed = discord.Embed(
+ description=f"**Season:** {season_name}\n"
+ f"**Old {changed_element}:** {old_name}\n"
+ f"**New {changed_element}:** {new_name}",
+ colour=colour
+ )
+ embed.set_author(name=title)
+ await ctx.send(embed=embed)
+
+ @with_role(Roles.moderator, Roles.admin, Roles.owner)
+ @commands.command()
+ async def announce(self, ctx):
+ """
+ Announces the currently loaded season.
+ """
+ await self.season.announce_season()
+
def __unload(self):
self.season_task.cancel()