diff options
| author | 2021-01-06 09:35:58 +0200 | |
|---|---|---|
| committer | 2021-01-06 09:35:58 +0200 | |
| commit | 1496557b757b6cea5448110ccd5143e5a5e82e61 (patch) | |
| tree | 1f2518fab45d6d7a958de2f6d9269c13d60ed25a | |
| parent | Remove sir lancebot names from seasons (diff) | |
Refactor branding manager to keep everything in one directory
To keep everything at one place, moved all branding manager special
things to one module.
| -rw-r--r-- | bot/constants.py | 29 | ||||
| -rw-r--r-- | bot/decorators.py | 23 | ||||
| -rw-r--r-- | bot/errors.py | 4 | ||||
| -rw-r--r-- | bot/exts/backend/branding/__init__.py | 7 | ||||
| -rw-r--r-- | bot/exts/backend/branding/_cog.py (renamed from bot/exts/backend/branding.py) | 101 | ||||
| -rw-r--r-- | bot/exts/backend/branding/_constants.py | 49 | ||||
| -rw-r--r-- | bot/exts/backend/branding/_decorators.py | 27 | ||||
| -rw-r--r-- | bot/exts/backend/branding/_errors.py | 2 | ||||
| -rw-r--r-- | bot/exts/backend/branding/_seasons.py (renamed from bot/seasons.py) | 5 |
9 files changed, 135 insertions, 112 deletions
diff --git a/bot/constants.py b/bot/constants.py index ded6e386d..41a538802 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -651,35 +651,6 @@ class Event(Enum): voice_state_update = "voice_state_update" -class Month(IntEnum): - JANUARY = 1 - FEBRUARY = 2 - MARCH = 3 - APRIL = 4 - MAY = 5 - JUNE = 6 - JULY = 7 - AUGUST = 8 - SEPTEMBER = 9 - OCTOBER = 10 - NOVEMBER = 11 - DECEMBER = 12 - - def __str__(self) -> str: - return self.name.title() - - -class AssetType(Enum): - """ - Discord media assets. - - The values match exactly the kwarg keys that can be passed to `Guild.edit`. - """ - - BANNER = "banner" - SERVER_ICON = "icon" - - # Debug mode DEBUG_MODE = 'local' in os.environ.get("SITE_URL", "local") diff --git a/bot/decorators.py b/bot/decorators.py index 0b50cc365..063c8f878 100644 --- a/bot/decorators.py +++ b/bot/decorators.py @@ -1,5 +1,4 @@ import asyncio -import functools import logging import typing as t from contextlib import suppress @@ -9,7 +8,7 @@ from discord import Member, NotFound from discord.ext import commands from discord.ext.commands import Cog, Context -from bot.constants import Channels, DEBUG_MODE, RedirectOutput +from bot.constants import Channels, RedirectOutput from bot.utils import function from bot.utils.checks import in_whitelist_check @@ -154,23 +153,3 @@ def respect_role_hierarchy(member_arg: function.Argument) -> t.Callable: await func(*args, **kwargs) return wrapper return decorator - - -def mock_in_debug(return_value: t.Any) -> t.Callable: - """ - Short-circuit function execution if in debug mode and return `return_value`. - - The original function name, and the incoming args and kwargs are DEBUG level logged - upon each call. This is useful for expensive operations, i.e. media asset uploads - that are prone to rate-limits but need to be tested extensively. - """ - def decorator(func: t.Callable) -> t.Callable: - @functools.wraps(func) - async def wrapped(*args, **kwargs) -> t.Any: - """Short-circuit and log if in debug mode.""" - if DEBUG_MODE: - log.debug(f"Function {func.__name__} called with args: {args}, kwargs: {kwargs}") - return return_value - return await func(*args, **kwargs) - return wrapped - return decorator diff --git a/bot/errors.py b/bot/errors.py index ea6fb36ec..65d715203 100644 --- a/bot/errors.py +++ b/bot/errors.py @@ -18,7 +18,3 @@ class LockedResourceError(RuntimeError): f"Cannot operate on {self.type.lower()} `{self.id}`; " "it is currently locked and in use by another operation." ) - - -class BrandingError(Exception): - """Exception raised by the BrandingManager cog.""" diff --git a/bot/exts/backend/branding/__init__.py b/bot/exts/backend/branding/__init__.py new file mode 100644 index 000000000..81ea3bf49 --- /dev/null +++ b/bot/exts/backend/branding/__init__.py @@ -0,0 +1,7 @@ +from bot.bot import Bot +from bot.exts.backend.branding._cog import BrandingManager + + +def setup(bot: Bot) -> None: + """Loads BrandingManager cog.""" + bot.add_cog(BrandingManager(bot)) diff --git a/bot/exts/backend/branding.py b/bot/exts/backend/branding/_cog.py index 7ce85aab2..d7fa78bb5 100644 --- a/bot/exts/backend/branding.py +++ b/bot/exts/backend/branding/_cog.py @@ -12,29 +12,12 @@ from async_rediscache import RedisCache from discord.ext import commands from bot.bot import Bot -from bot.constants import AssetType, Branding, Colours, Emojis, Guild, Keys, MODERATION_ROLES -from bot.decorators import in_whitelist, mock_in_debug -from bot.errors import BrandingError -from bot.seasons import SeasonBase, get_all_seasons, get_current_season, get_season +from bot.constants import Branding, Colours, Emojis, Guild, MODERATION_ROLES +from bot.decorators import in_whitelist +from bot.exts.backend.branding import _constants, _decorators, _errors, _seasons log = logging.getLogger(__name__) -STATUS_OK = 200 # HTTP status code - -FILE_BANNER = "banner.png" -FILE_AVATAR = "avatar.png" -SERVER_ICONS = "server_icons" - -BRANDING_URL = "https://api.github.com/repos/python-discord/branding/contents" - -PARAMS = {"ref": "master"} # Target branch -HEADERS = {"Accept": "application/vnd.github.v3+json"} # Ensure we use API v3 - -# A GitHub token is not necessary for the cog to operate, -# unauthorized requests are however limited to 60 per hour -if Keys.github: - HEADERS["Authorization"] = f"token {Keys.github}" - class GitHubFile(t.NamedTuple): """ @@ -120,7 +103,7 @@ class BrandingManager(commands.Cog): to test this cog's behaviour. """ - current_season: t.Type[SeasonBase] + current_season: t.Type[_seasons.SeasonBase] banner: t.Optional[GitHubFile] @@ -143,7 +126,7 @@ class BrandingManager(commands.Cog): the `refresh` command is used. """ self.bot = bot - self.current_season = get_current_season() + self.current_season = _seasons.get_current_season() self.banner = None @@ -183,7 +166,7 @@ class BrandingManager(commands.Cog): await self.bot.wait_until_guild_available() while True: - self.current_season = get_current_season() + self.current_season = _seasons.get_current_season() branding_changed = await self.refresh() if branding_changed: @@ -200,7 +183,7 @@ class BrandingManager(commands.Cog): info_embed = discord.Embed(description=self.current_season.description, colour=self.current_season.colour) # If we're in a non-evergreen season, also show active months - if self.current_season is not SeasonBase: + if self.current_season is not _seasons.SeasonBase: title = f"{self.current_season.season_name} ({', '.join(str(m) for m in self.current_season.months)})" else: title = self.current_season.season_name @@ -252,10 +235,12 @@ class BrandingManager(commands.Cog): This may return an empty dict if the response status is non-200, or if the target directory is empty. """ - url = f"{BRANDING_URL}/{path}" - async with self.bot.http_session.get(url, headers=HEADERS, params=PARAMS) as resp: + url = f"{_constants.BRANDING_URL}/{path}" + async with self.bot.http_session.get( + url, headers=_constants.HEADERS, params=_constants.PARAMS + ) as resp: # Short-circuit if we get non-200 response - if resp.status != STATUS_OK: + if resp.status != _constants.STATUS_OK: log.error(f"GitHub API returned non-200 response: {resp}") return {} directory = await resp.json() # Directory at `path` @@ -287,23 +272,32 @@ class BrandingManager(commands.Cog): # Only make a call to the fallback directory if there is something to be gained branding_incomplete = any( asset not in seasonal_dir - for asset in (FILE_BANNER, FILE_AVATAR, SERVER_ICONS) + for asset in (_constants.FILE_BANNER, _constants.FILE_AVATAR, _constants.SERVER_ICONS) ) - if branding_incomplete and self.current_season is not SeasonBase: - fallback_dir = await self._get_files(SeasonBase.branding_path, include_dirs=True) + if branding_incomplete and self.current_season is not _seasons.SeasonBase: + fallback_dir = await self._get_files( + _seasons.SeasonBase.branding_path, include_dirs=True + ) else: fallback_dir = {} # Resolve assets in this directory, None is a safe value - self.banner = seasonal_dir.get(FILE_BANNER) or fallback_dir.get(FILE_BANNER) + self.banner = ( + seasonal_dir.get(_constants.FILE_BANNER) + or fallback_dir.get(_constants.FILE_BANNER) + ) # Now resolve server icons by making a call to the proper sub-directory - if SERVER_ICONS in seasonal_dir: - icons_dir = await self._get_files(f"{self.current_season.branding_path}/{SERVER_ICONS}") + if _constants.SERVER_ICONS in seasonal_dir: + icons_dir = await self._get_files( + f"{self.current_season.branding_path}/{_constants.SERVER_ICONS}" + ) self.available_icons = list(icons_dir.values()) - elif SERVER_ICONS in fallback_dir: - icons_dir = await self._get_files(f"{SeasonBase.branding_path}/{SERVER_ICONS}") + elif _constants.SERVER_ICONS in fallback_dir: + icons_dir = await self._get_files( + f"{_seasons.SeasonBase.branding_path}/{_constants.SERVER_ICONS}" + ) self.available_icons = list(icons_dir.values()) else: @@ -373,8 +367,8 @@ class BrandingManager(commands.Cog): """List all available seasons and branding sources.""" embed = discord.Embed(title="Available seasons", colour=Colours.soft_green) - for season in get_all_seasons(): - if season is SeasonBase: + for season in _seasons.get_all_seasons(): + if season is _seasons.SeasonBase: active_when = "always" else: active_when = f"in {', '.join(str(m) for m in season.months)}" @@ -407,14 +401,14 @@ class BrandingManager(commands.Cog): what it should be - the daemon will make sure that it's set back properly. """ if season_name is None: - new_season = get_current_season() + new_season = _seasons.get_current_season() else: - new_season = get_season(season_name) + new_season = _seasons.get_season(season_name) if new_season is None: - raise BrandingError("No such season exists") + raise _errors.BrandingError("No such season exists") if self.current_season is new_season: - raise BrandingError(f"Season {self.current_season.season_name} already active") + raise _errors.BrandingError(f"Season {self.current_season.season_name} already active") self.current_season = new_season await self.branding_refresh(ctx) @@ -447,7 +441,9 @@ class BrandingManager(commands.Cog): async with ctx.typing(): failed_assets = await self.apply() if failed_assets: - raise BrandingError(f"Failed to apply following assets: {', '.join(failed_assets)}") + raise _errors.BrandingError( + f"Failed to apply following assets: {', '.join(failed_assets)}" + ) response = discord.Embed(description=f"All assets applied {Emojis.ok_hand}", colour=Colours.soft_green) await ctx.send(embed=response) @@ -462,7 +458,7 @@ class BrandingManager(commands.Cog): async with ctx.typing(): success = await self.cycle() if not success: - raise BrandingError("Failed to cycle icon") + raise _errors.BrandingError("Failed to cycle icon") response = discord.Embed(description=f"Success {Emojis.ok_hand}", colour=Colours.soft_green) await ctx.send(embed=response) @@ -489,7 +485,7 @@ class BrandingManager(commands.Cog): async def daemon_start(self, ctx: commands.Context) -> None: """If the daemon isn't running, start it.""" if self._daemon_running: - raise BrandingError("Daemon already running!") + raise _errors.BrandingError("Daemon already running!") self.daemon = self.bot.loop.create_task(self._daemon_func()) await self.branding_configuration.set("daemon_active", True) @@ -501,7 +497,7 @@ class BrandingManager(commands.Cog): async def daemon_stop(self, ctx: commands.Context) -> None: """If the daemon is running, stop it.""" if not self._daemon_running: - raise BrandingError("Daemon not running!") + raise _errors.BrandingError("Daemon not running!") self.daemon.cancel() await self.branding_configuration.set("daemon_active", False) @@ -515,7 +511,7 @@ class BrandingManager(commands.Cog): async with self.bot.http_session.get(url) as resp: return await resp.read() - async def _apply_asset(self, target: discord.Guild, asset: AssetType, url: str) -> bool: + async def _apply_asset(self, target: discord.Guild, asset: _constants.AssetType, url: str) -> bool: """ Internal method for applying media assets to the guild. @@ -543,7 +539,7 @@ class BrandingManager(commands.Cog): log.info("Asset successfully applied") return True - @mock_in_debug(return_value=True) + @_decorators.mock_in_debug(return_value=True) async def set_banner(self, url: str) -> bool: """Set the guild's banner to image at `url`.""" guild = self.bot.get_guild(Guild.id) @@ -551,9 +547,9 @@ class BrandingManager(commands.Cog): log.info("Failed to get guild instance, aborting asset upload") return False - return await self._apply_asset(guild, AssetType.BANNER, url) + return await self._apply_asset(guild, _constants.AssetType.BANNER, url) - @mock_in_debug(return_value=True) + @_decorators.mock_in_debug(return_value=True) async def set_icon(self, url: str) -> bool: """Sets the guild's icon to image at `url`.""" guild = self.bot.get_guild(Guild.id) @@ -561,9 +557,4 @@ class BrandingManager(commands.Cog): log.info("Failed to get guild instance, aborting asset upload") return False - return await self._apply_asset(guild, AssetType.SERVER_ICON, url) - - -def setup(bot: Bot) -> None: - """Load BrandingManager cog.""" - bot.add_cog(BrandingManager(bot)) + return await self._apply_asset(guild, _constants.AssetType.SERVER_ICON, url) diff --git a/bot/exts/backend/branding/_constants.py b/bot/exts/backend/branding/_constants.py new file mode 100644 index 000000000..f4c815fbd --- /dev/null +++ b/bot/exts/backend/branding/_constants.py @@ -0,0 +1,49 @@ +from enum import Enum, IntEnum + +from bot.constants import Keys + + +class Month(IntEnum): + JANUARY = 1 + FEBRUARY = 2 + MARCH = 3 + APRIL = 4 + MAY = 5 + JUNE = 6 + JULY = 7 + AUGUST = 8 + SEPTEMBER = 9 + OCTOBER = 10 + NOVEMBER = 11 + DECEMBER = 12 + + def __str__(self) -> str: + return self.name.title() + + +class AssetType(Enum): + """ + Discord media assets. + + The values match exactly the kwarg keys that can be passed to `Guild.edit`. + """ + + BANNER = "banner" + SERVER_ICON = "icon" + + +STATUS_OK = 200 # HTTP status code + +FILE_BANNER = "banner.png" +FILE_AVATAR = "avatar.png" +SERVER_ICONS = "server_icons" + +BRANDING_URL = "https://api.github.com/repos/python-discord/branding/contents" + +PARAMS = {"ref": "master"} # Target branch +HEADERS = {"Accept": "application/vnd.github.v3+json"} # Ensure we use API v3 + +# A GitHub token is not necessary for the cog to operate, +# unauthorized requests are however limited to 60 per hour +if Keys.github: + HEADERS["Authorization"] = f"token {Keys.github}" diff --git a/bot/exts/backend/branding/_decorators.py b/bot/exts/backend/branding/_decorators.py new file mode 100644 index 000000000..6a1e7e869 --- /dev/null +++ b/bot/exts/backend/branding/_decorators.py @@ -0,0 +1,27 @@ +import functools +import logging +import typing as t + +from bot.constants import DEBUG_MODE + +log = logging.getLogger(__name__) + + +def mock_in_debug(return_value: t.Any) -> t.Callable: + """ + Short-circuit function execution if in debug mode and return `return_value`. + + The original function name, and the incoming args and kwargs are DEBUG level logged + upon each call. This is useful for expensive operations, i.e. media asset uploads + that are prone to rate-limits but need to be tested extensively. + """ + def decorator(func: t.Callable) -> t.Callable: + @functools.wraps(func) + async def wrapped(*args, **kwargs) -> t.Any: + """Short-circuit and log if in debug mode.""" + if DEBUG_MODE: + log.debug(f"Function {func.__name__} called with args: {args}, kwargs: {kwargs}") + return return_value + return await func(*args, **kwargs) + return wrapped + return decorator diff --git a/bot/exts/backend/branding/_errors.py b/bot/exts/backend/branding/_errors.py new file mode 100644 index 000000000..7cd271af3 --- /dev/null +++ b/bot/exts/backend/branding/_errors.py @@ -0,0 +1,2 @@ +class BrandingError(Exception): + """Exception raised by the BrandingManager cog.""" diff --git a/bot/seasons.py b/bot/exts/backend/branding/_seasons.py index d4a9dfcc5..2f785bec0 100644 --- a/bot/seasons.py +++ b/bot/exts/backend/branding/_seasons.py @@ -2,8 +2,9 @@ import logging import typing as t from datetime import datetime -from bot.constants import Colours, Month -from bot.errors import BrandingError +from ._constants import Month +from ._errors import BrandingError +from bot.constants import Colours log = logging.getLogger(__name__) |