From ab78216f4bc6cda63ae22a2538e408394c21fe30 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 6 Dec 2019 04:05:12 +1000 Subject: Add meta methods to Bot class. --- bot/bot.py | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) (limited to 'bot/bot.py') diff --git a/bot/bot.py b/bot/bot.py index 2a723021..7da00bf1 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -1,8 +1,12 @@ +import asyncio +import contextlib import logging import socket from traceback import format_exc -from typing import List +from typing import List, Optional +import async_timeout +import discord from aiohttp import AsyncResolver, ClientSession, TCPConnector from discord import DiscordException, Embed from discord.ext import commands @@ -63,5 +67,97 @@ class SeasonalBot(commands.Bot): else: await super().on_command_error(context, exception) + @property + def member(self) -> Optional[discord.Member]: + """Retrieves the guild member object for the bot.""" + guild = bot.get_guild(Client.guild) + if not guild: + return None + return guild.me + + async def set_avatar(self, url: str) -> bool: + """Sets the bot's avatar based on a URL.""" + # Track old avatar hash for later comparison + old_avatar = bot.user.avatar + + image = await self._fetch_image(url) + with contextlib.suppress(discord.HTTPException, asyncio.TimeoutError): + async with async_timeout.timeout(5): + await bot.user.edit(avatar=image) + + if bot.user.avatar != old_avatar: + log.debug(f"Avatar changed to {url}") + return True + + log.warning(f"Changing avatar failed: {url}") + return False + + async def set_icon(self, url: str) -> bool: + """Sets the guild's icon based on a URL.""" + guild = bot.get_guild(Client.guild) + # Track old icon hash for later comparison + old_icon = guild.icon + + image = await self._fetch_image(url) + with contextlib.suppress(discord.HTTPException, asyncio.TimeoutError): + async with async_timeout.timeout(5): + await guild.edit(icon=image) + + new_icon = bot.get_guild(Client.guild).icon + if new_icon != old_icon: + log.debug(f"Icon changed to {url}") + return True + + log.warning(f"Changing icon failed: {url}") + return False + + async def _fetch_image(self, url: str) -> bytes: + """Retrieve an image based on a URL.""" + log.debug(f"Getting image from: {url}") + async with self.http_session.get(url) as resp: + return await resp.read() + + async def set_username(self, new_name: str, nick_only: bool = False) -> Optional[bool]: + """ + Set the bot username and/or nickname to given new name. + + Returns True/False based on success, or None if nickname fallback also failed. + """ + old_username = self.user.name + + if nick_only: + return await self.set_nickname(new_name) + + if old_username == new_name: + # since the username is correct, make sure nickname is removed + return await self.set_nickname() + + log.debug(f"Changing username to {new_name}") + with contextlib.suppress(discord.HTTPException): + await bot.user.edit(username=new_name, nick=None) + + if not new_name == self.member.display_name: + # name didn't change, try to changing nickname as fallback + if await self.set_nickname(new_name): + log.warning(f"Changing username failed, changed nickname instead.") + return False + log.warning(f"Changing username and nickname failed.") + return None + + return True + + async def set_nickname(self, new_name: str = None) -> bool: + """Set the bot nickname in the main guild.""" + old_display_name = self.member.display_name + + if old_display_name == new_name: + return False + + log.debug(f"Changing nickname to {new_name}") + with contextlib.suppress(discord.HTTPException): + await self.member.edit(nick=new_name) + + return not old_display_name == self.member.display_name + bot = SeasonalBot(command_prefix=Client.prefix) -- cgit v1.2.3 From 0cee9682212252a9b27ae2b9a1f7afbf4548e38b Mon Sep 17 00:00:00 2001 From: kwzrd Date: Sat, 14 Mar 2020 19:44:32 +0100 Subject: Deseasonify: add `set_banner` method to SeasonalBot --- bot/bot.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'bot/bot.py') diff --git a/bot/bot.py b/bot/bot.py index 7da00bf1..2443852c 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -92,6 +92,24 @@ class SeasonalBot(commands.Bot): log.warning(f"Changing avatar failed: {url}") return False + async def set_banner(self, url: str) -> bool: + """Sets the guild's banner based on the provided `url`.""" + guild = bot.get_guild(Client.guild) + old_banner = guild.banner + + image = await self._fetch_image(url) + with contextlib.suppress(discord.HTTPException, asyncio.TimeoutError): + async with async_timeout.timeout(5): + await guild.edit(banner=image) + + new_banner = bot.get_guild(Client.guild).banner + if new_banner != old_banner: + log.debug(f"Banner changed to {url}") + return True + + log.warning(f"Changing banner failed: {url}") + return False + async def set_icon(self, url: str) -> bool: """Sets the guild's icon based on a URL.""" guild = bot.get_guild(Client.guild) -- cgit v1.2.3 From 6491897e9330911ff1d39816c5a147fb8ee3b93b Mon Sep 17 00:00:00 2001 From: kwzrd Date: Mon, 23 Mar 2020 16:46:17 +0100 Subject: Deseasonify: remove `load_extensions` method This is unused and no longer necessary, as all extensions load only once: on start-up, in `__main__.py`. --- bot/bot.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) (limited to 'bot/bot.py') diff --git a/bot/bot.py b/bot/bot.py index c6ec3357..ab196bc1 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -2,8 +2,7 @@ import asyncio import contextlib import logging import socket -from traceback import format_exc -from typing import List, Optional +from typing import Optional import async_timeout import discord @@ -27,23 +26,6 @@ class SeasonalBot(commands.Bot): connector=TCPConnector(resolver=AsyncResolver(), family=socket.AF_INET) ) - def load_extensions(self, exts: List[str]) -> None: - """Unload all current extensions, then load the given extensions.""" - # Unload all cogs - extensions = list(self.extensions.keys()) - for extension in extensions: - if extension not in ["bot.branding", "bot.help"]: # We shouldn't unload the manager and help. - self.unload_extension(extension) - - # Load in the list of cogs that was passed in here - for extension in exts: - cog = extension.split(".")[-1] - try: - self.load_extension(extension) - log.info(f'Successfully loaded extension: {cog}') - except Exception as e: - log.error(f'Failed to load extension {cog}: {repr(e)} {format_exc()}') - async def send_log(self, title: str, details: str = None, *, icon: str = None) -> None: """Send an embed message to the devlog channel.""" devlog = self.get_channel(Channels.devlog) -- cgit v1.2.3 From 51e3f49a7195640a7d9be919f6578864f2d74eca Mon Sep 17 00:00:00 2001 From: kwzrd Date: Mon, 23 Mar 2020 20:18:26 +0100 Subject: Deseasonify: mock expensive API calls in debug mode The methods will pretend that the selected asset was uploaded successfully. This allows extensive testing of the branding manager without API abuse. --- bot/bot.py | 14 +++++++++++++- bot/branding.py | 4 ++++ 2 files changed, 17 insertions(+), 1 deletion(-) (limited to 'bot/bot.py') diff --git a/bot/bot.py b/bot/bot.py index ab196bc1..b47e1289 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -11,6 +11,7 @@ from discord import DiscordException, Embed from discord.ext import commands from bot.constants import Channels, Client +from bot.decorators import mock_in_debug log = logging.getLogger(__name__) @@ -18,7 +19,13 @@ __all__ = ('SeasonalBot', 'bot') class SeasonalBot(commands.Bot): - """Base bot instance.""" + """ + Base bot instance. + + While in debug mode, the asset upload methods (avatar, banner, ...) will not + perform the upload, and will instead only log the passed download urls and pretend + that the upload was successful. See the `mock_in_debug` decorator for further details. + """ def __init__(self, **kwargs): super().__init__(**kwargs) @@ -57,6 +64,7 @@ class SeasonalBot(commands.Bot): return None return guild.me + @mock_in_debug(return_value=True) async def set_avatar(self, url: str) -> bool: """Sets the bot's avatar based on a URL.""" # Track old avatar hash for later comparison @@ -74,6 +82,7 @@ class SeasonalBot(commands.Bot): log.warning(f"Changing avatar failed: {url}") return False + @mock_in_debug(return_value=True) async def set_banner(self, url: str) -> bool: """Sets the guild's banner based on the provided `url`.""" guild = bot.get_guild(Client.guild) @@ -92,6 +101,7 @@ class SeasonalBot(commands.Bot): log.warning(f"Changing banner failed: {url}") return False + @mock_in_debug(return_value=True) async def set_icon(self, url: str) -> bool: """Sets the guild's icon based on a URL.""" guild = bot.get_guild(Client.guild) @@ -117,6 +127,7 @@ class SeasonalBot(commands.Bot): async with self.http_session.get(url) as resp: return await resp.read() + @mock_in_debug(return_value=True) async def set_username(self, new_name: str, nick_only: bool = False) -> Optional[bool]: """ Set the bot username and/or nickname to given new name. @@ -146,6 +157,7 @@ class SeasonalBot(commands.Bot): return True + @mock_in_debug(return_value=True) async def set_nickname(self, new_name: str = None) -> bool: """Set the bot nickname in the main guild.""" old_display_name = self.member.display_name diff --git a/bot/branding.py b/bot/branding.py index 06f91a38..7ea76e43 100644 --- a/bot/branding.py +++ b/bot/branding.py @@ -110,6 +110,10 @@ class BrandingManager(commands.Cog): rate-limits - the `apply` command should be used with caution. The `set` command can, however, be used freely to 'preview' seasonal branding and check whether paths have been resolved as appropriate. + + While the bot is in debug mode, it will 'mock' asset uploads by logging the passed + download urls and pretending that the upload was successful. Make use of this + to test this cog's behaviour. """ current_season: t.Type[SeasonBase] -- cgit v1.2.3 From 99b23ff1500aa15b076ee2e1c5e1b5560ba2c366 Mon Sep 17 00:00:00 2001 From: kwzrd Date: Sat, 28 Mar 2020 14:51:14 +0100 Subject: Deseasonify: move decorators module under utils --- bot/__main__.py | 2 +- bot/bot.py | 2 +- bot/branding.py | 2 +- bot/decorators.py | 321 ------------------------ bot/seasons/christmas/adventofcode.py | 2 +- bot/seasons/christmas/hanukkah_embed.py | 2 +- bot/seasons/easter/egg_facts.py | 2 +- bot/seasons/evergreen/error_handler.py | 2 +- bot/seasons/evergreen/game.py | 2 +- bot/seasons/evergreen/issues.py | 2 +- bot/seasons/evergreen/snakes/snakes_cog.py | 2 +- bot/seasons/halloween/candy_collection.py | 2 +- bot/seasons/halloween/hacktober-issue-finder.py | 2 +- bot/seasons/halloween/hacktoberstats.py | 2 +- bot/seasons/halloween/spookyreact.py | 2 +- bot/seasons/pride/pride_facts.py | 2 +- bot/seasons/valentines/be_my_valentine.py | 2 +- bot/utils/decorators.py | 321 ++++++++++++++++++++++++ 18 files changed, 337 insertions(+), 337 deletions(-) delete mode 100644 bot/decorators.py create mode 100644 bot/utils/decorators.py (limited to 'bot/bot.py') diff --git a/bot/__main__.py b/bot/__main__.py index 780c8c4d..3662a63b 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -2,8 +2,8 @@ import logging from bot.bot import bot from bot.constants import Client, STAFF_ROLES, WHITELISTED_CHANNELS -from bot.decorators import in_channel_check from bot.seasons import get_extensions +from bot.utils.decorators import in_channel_check log = logging.getLogger(__name__) diff --git a/bot/bot.py b/bot/bot.py index b47e1289..47d63de9 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -11,7 +11,7 @@ from discord import DiscordException, Embed from discord.ext import commands from bot.constants import Channels, Client -from bot.decorators import mock_in_debug +from bot.utils.decorators import mock_in_debug log = logging.getLogger(__name__) diff --git a/bot/branding.py b/bot/branding.py index 6f5e7a5c..2eb563ea 100644 --- a/bot/branding.py +++ b/bot/branding.py @@ -12,8 +12,8 @@ from discord.ext import commands from bot.bot import SeasonalBot from bot.constants import Branding, Colours, Emojis, MODERATION_ROLES, Tokens -from bot.decorators import with_role from bot.seasons import SeasonBase, get_current_season, get_season +from bot.utils.decorators import with_role from bot.utils.exceptions import BrandingError log = logging.getLogger(__name__) diff --git a/bot/decorators.py b/bot/decorators.py deleted file mode 100644 index f85996b5..00000000 --- a/bot/decorators.py +++ /dev/null @@ -1,321 +0,0 @@ -import asyncio -import functools -import logging -import random -import typing as t -from asyncio import Lock -from datetime import datetime -from functools import wraps -from weakref import WeakValueDictionary - -from discord import Colour, Embed -from discord.ext import commands -from discord.ext.commands import CheckFailure, Command, Context - -from bot.constants import Client, ERROR_REPLIES, Month - -ONE_DAY = 24 * 60 * 60 - -log = logging.getLogger(__name__) - - -class InChannelCheckFailure(CheckFailure): - """Check failure when the user runs a command in a non-whitelisted channel.""" - - pass - - -class InMonthCheckFailure(CheckFailure): - """Check failure for when a command is invoked outside of its allowed month.""" - - pass - - -def seasonal_task(*allowed_months: Month, sleep_time: t.Union[float, int] = ONE_DAY) -> t.Callable: - """ - Perform the decorated method periodically in `allowed_months`. - - This provides a convenience wrapper to avoid code repetition where some task shall - perform an operation repeatedly in a constant interval, but only in specific months. - - The decorated function will be called once every `sleep_time` seconds while - the current UTC month is in `allowed_months`. Sleep time defaults to 24 hours. - - The wrapped task is responsible for waiting for the bot to be ready, if necessary. - """ - def decorator(task_body: t.Callable) -> t.Callable: - @functools.wraps(task_body) - async def decorated_task(*args, **kwargs) -> None: - """Call `task_body` once every `sleep_time` seconds in `allowed_months`.""" - log.info(f"Starting seasonal task {task_body.__qualname__} ({allowed_months})") - - while True: - current_month = Month(datetime.utcnow().month) - - if current_month in allowed_months: - await task_body(*args, **kwargs) - else: - log.debug(f"Seasonal task {task_body.__qualname__} sleeps in {current_month.name}") - - await asyncio.sleep(sleep_time) - return decorated_task - return decorator - - -def in_month_listener(*allowed_months: Month) -> t.Callable: - """ - Shield a listener from being invoked outside of `allowed_months`. - - The check is performed against current UTC month. - """ - def decorator(listener: t.Callable) -> t.Callable: - @functools.wraps(listener) - async def guarded_listener(*args, **kwargs) -> None: - """Wrapped listener will abort if not in allowed month.""" - current_month = Month(datetime.utcnow().month) - - if current_month in allowed_months: - # Propagate return value although it should always be None - return await listener(*args, **kwargs) - else: - log.debug(f"Guarded {listener.__qualname__} from invoking in {current_month.name}") - return guarded_listener - return decorator - - -def in_month_command(*allowed_months: Month) -> t.Callable: - """ - Check whether the command was invoked in one of `enabled_months`. - - Uses the current UTC month at the time of running the predicate. - """ - async def predicate(ctx: Context) -> bool: - current_month = datetime.utcnow().month - can_run = current_month in allowed_months - - human_months = ", ".join(m.name for m in allowed_months) - log.debug( - f"Command '{ctx.command}' is locked to months {human_months}. " - f"Invoking it in month {current_month} is {'allowed' if can_run else 'disallowed'}." - ) - if can_run: - return True - else: - raise InMonthCheckFailure(f"Command can only be used in {human_months}") - - return commands.check(predicate) - - -def in_month(*allowed_months: Month) -> t.Callable: - """ - Universal decorator for season-locking commands and listeners alike. - - This only serves to determine whether the decorated callable is a command, - a listener, or neither. It then delegates to either `in_month_command`, - or `in_month_listener`, or raises TypeError, respectively. - - Please note that in order for this decorator to correctly determine whether - the decorated callable is a cmd or listener, it **has** to first be turned - into one. This means that this decorator should always be placed **above** - the d.py one that registers it as either. - - This will decorate groups as well, as those subclass Command. In order to lock - all subcommands of a group, its `invoke_without_command` param must **not** be - manually set to True - this causes a circumvention of the group's callback - and the seasonal check applied to it. - """ - def decorator(callable_: t.Callable) -> t.Callable: - # Functions decorated as commands are turned into instances of `Command` - if isinstance(callable_, Command): - logging.debug(f"Command {callable_.qualified_name} will be locked to {allowed_months}") - actual_deco = in_month_command(*allowed_months) - - # D.py will assign this attribute when `callable_` is registered as a listener - elif hasattr(callable_, "__cog_listener__"): - logging.debug(f"Listener {callable_.__qualname__} will be locked to {allowed_months}") - actual_deco = in_month_listener(*allowed_months) - - # Otherwise we're unsure exactly what has been decorated - # This happens before the bot starts, so let's just raise - else: - raise TypeError(f"Decorated object {callable_} is neither a command nor a listener") - - return actual_deco(callable_) - return decorator - - -def with_role(*role_ids: int) -> t.Callable: - """Check to see whether the invoking user has any of the roles specified in role_ids.""" - async def predicate(ctx: Context) -> bool: - if not ctx.guild: # Return False in a DM - log.debug( - f"{ctx.author} tried to use the '{ctx.command.name}'command from a DM. " - "This command is restricted by the with_role decorator. Rejecting request." - ) - return False - - for role in ctx.author.roles: - if role.id in role_ids: - log.debug(f"{ctx.author} has the '{role.name}' role, and passes the check.") - return True - - log.debug( - f"{ctx.author} does not have the required role to use " - f"the '{ctx.command.name}' command, so the request is rejected." - ) - return False - return commands.check(predicate) - - -def without_role(*role_ids: int) -> t.Callable: - """Check whether the invoking user does not have all of the roles specified in role_ids.""" - async def predicate(ctx: Context) -> bool: - if not ctx.guild: # Return False in a DM - log.debug( - f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM. " - "This command is restricted by the without_role decorator. Rejecting request." - ) - return False - - author_roles = [role.id for role in ctx.author.roles] - check = all(role not in author_roles for role in role_ids) - log.debug( - f"{ctx.author} tried to call the '{ctx.command.name}' command. " - f"The result of the without_role check was {check}." - ) - return check - return commands.check(predicate) - - -def in_channel_check(*channels: int, bypass_roles: t.Container[int] = None) -> t.Callable[[Context], bool]: - """ - Checks that the message is in a whitelisted channel or optionally has a bypass role. - - If `in_channel_override` is present, check if it contains channels - and use them in place of the global whitelist. - """ - def predicate(ctx: Context) -> bool: - if not ctx.guild: - log.debug(f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM.") - return True - if ctx.channel.id in channels: - log.debug( - f"{ctx.author} tried to call the '{ctx.command.name}' command " - f"and the command was used in a whitelisted channel." - ) - return True - - if bypass_roles and any(r.id in bypass_roles for r in ctx.author.roles): - log.debug( - f"{ctx.author} called the '{ctx.command.name}' command and " - f"had a role to bypass the in_channel check." - ) - return True - - if hasattr(ctx.command.callback, "in_channel_override"): - override = ctx.command.callback.in_channel_override - if override is None: - log.debug( - f"{ctx.author} called the '{ctx.command.name}' command " - f"and the command was whitelisted to bypass the in_channel check." - ) - return True - else: - if ctx.channel.id in override: - log.debug( - f"{ctx.author} tried to call the '{ctx.command.name}' command " - f"and the command was used in an overridden whitelisted channel." - ) - return True - - log.debug( - f"{ctx.author} tried to call the '{ctx.command.name}' command. " - f"The overridden in_channel check failed." - ) - channels_str = ', '.join(f"<#{c_id}>" for c_id in override) - raise InChannelCheckFailure( - f"Sorry, but you may only use this command within {channels_str}." - ) - - log.debug( - f"{ctx.author} tried to call the '{ctx.command.name}' command. " - f"The in_channel check failed." - ) - - channels_str = ', '.join(f"<#{c_id}>" for c_id in channels) - raise InChannelCheckFailure( - f"Sorry, but you may only use this command within {channels_str}." - ) - - return predicate - - -in_channel = commands.check(in_channel_check) - - -def override_in_channel(channels: t.Tuple[int] = None) -> t.Callable: - """ - Set command callback attribute for detection in `in_channel_check`. - - Override global whitelist if channels are specified. - - This decorator has to go before (below) below the `command` decorator. - """ - def inner(func: t.Callable) -> t.Callable: - func.in_channel_override = channels - return func - - return inner - - -def locked() -> t.Union[t.Callable, None]: - """ - Allows the user to only run one instance of the decorated command at a time. - - Subsequent calls to the command from the same author are ignored until the command has completed invocation. - - This decorator has to go before (below) the `command` decorator. - """ - def wrap(func: t.Callable) -> t.Union[t.Callable, None]: - func.__locks = WeakValueDictionary() - - @wraps(func) - async def inner(self: t.Callable, ctx: Context, *args, **kwargs) -> t.Union[t.Callable, None]: - lock = func.__locks.setdefault(ctx.author.id, Lock()) - if lock.locked(): - embed = Embed() - embed.colour = Colour.red() - - log.debug(f"User tried to invoke a locked command.") - embed.description = ( - "You're already using this command. Please wait until " - "it is done before you use it again." - ) - embed.title = random.choice(ERROR_REPLIES) - await ctx.send(embed=embed) - return - - async with func.__locks.setdefault(ctx.author.id, Lock()): - return await func(self, ctx, *args, **kwargs) - return inner - return wrap - - -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 Client.debug: - 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/seasons/christmas/adventofcode.py b/bot/seasons/christmas/adventofcode.py index e6100056..f7590e04 100644 --- a/bot/seasons/christmas/adventofcode.py +++ b/bot/seasons/christmas/adventofcode.py @@ -14,8 +14,8 @@ from discord.ext import commands from pytz import timezone from bot.constants import AdventOfCode as AocConfig, Channels, Colours, Emojis, Month, Tokens, WHITELISTED_CHANNELS -from bot.decorators import in_month, override_in_channel from bot.utils import unlocked_role +from bot.utils.decorators import in_month, override_in_channel log = logging.getLogger(__name__) diff --git a/bot/seasons/christmas/hanukkah_embed.py b/bot/seasons/christmas/hanukkah_embed.py index e73a33ad..62efd04e 100644 --- a/bot/seasons/christmas/hanukkah_embed.py +++ b/bot/seasons/christmas/hanukkah_embed.py @@ -6,7 +6,7 @@ from discord import Embed from discord.ext import commands from bot.constants import Colours, Month -from bot.decorators import in_month +from bot.utils.decorators import in_month log = logging.getLogger(__name__) diff --git a/bot/seasons/easter/egg_facts.py b/bot/seasons/easter/egg_facts.py index d20df3de..99a80b28 100644 --- a/bot/seasons/easter/egg_facts.py +++ b/bot/seasons/easter/egg_facts.py @@ -7,7 +7,7 @@ import discord from discord.ext import commands from bot.constants import Channels, Colours, Month -from bot.decorators import seasonal_task +from bot.utils.decorators import seasonal_task log = logging.getLogger(__name__) diff --git a/bot/seasons/evergreen/error_handler.py b/bot/seasons/evergreen/error_handler.py index d2accbd1..d268dab1 100644 --- a/bot/seasons/evergreen/error_handler.py +++ b/bot/seasons/evergreen/error_handler.py @@ -7,7 +7,7 @@ from discord import Embed, Message from discord.ext import commands from bot.constants import Colours, ERROR_REPLIES, NEGATIVE_REPLIES -from bot.decorators import InChannelCheckFailure, InMonthCheckFailure +from bot.utils.decorators import InChannelCheckFailure, InMonthCheckFailure from bot.utils.exceptions import BrandingError log = logging.getLogger(__name__) diff --git a/bot/seasons/evergreen/game.py b/bot/seasons/evergreen/game.py index ace77e9d..d43b1ad6 100644 --- a/bot/seasons/evergreen/game.py +++ b/bot/seasons/evergreen/game.py @@ -12,7 +12,7 @@ from discord.ext.commands import Cog, Context, group from bot.bot import SeasonalBot from bot.constants import STAFF_ROLES, Tokens -from bot.decorators import with_role +from bot.utils.decorators import with_role from bot.utils.pagination import ImagePaginator, LinePaginator # Base URL of IGDB API diff --git a/bot/seasons/evergreen/issues.py b/bot/seasons/evergreen/issues.py index fba5b174..fb18b62a 100644 --- a/bot/seasons/evergreen/issues.py +++ b/bot/seasons/evergreen/issues.py @@ -4,7 +4,7 @@ import discord from discord.ext import commands from bot.constants import Colours, Emojis, WHITELISTED_CHANNELS -from bot.decorators import override_in_channel +from bot.utils.decorators import override_in_channel log = logging.getLogger(__name__) diff --git a/bot/seasons/evergreen/snakes/snakes_cog.py b/bot/seasons/evergreen/snakes/snakes_cog.py index 09f5e250..e5a03a20 100644 --- a/bot/seasons/evergreen/snakes/snakes_cog.py +++ b/bot/seasons/evergreen/snakes/snakes_cog.py @@ -18,9 +18,9 @@ from discord import Colour, Embed, File, Member, Message, Reaction from discord.ext.commands import BadArgument, Bot, Cog, CommandError, Context, bot_has_permissions, group from bot.constants import ERROR_REPLIES, Tokens -from bot.decorators import locked from bot.seasons.evergreen.snakes import utils from bot.seasons.evergreen.snakes.converter import Snake +from bot.utils.decorators import locked log = logging.getLogger(__name__) diff --git a/bot/seasons/halloween/candy_collection.py b/bot/seasons/halloween/candy_collection.py index 967a62aa..3f2b895e 100644 --- a/bot/seasons/halloween/candy_collection.py +++ b/bot/seasons/halloween/candy_collection.py @@ -9,7 +9,7 @@ import discord from discord.ext import commands from bot.constants import Channels, Month -from bot.decorators import in_month +from bot.utils.decorators import in_month log = logging.getLogger(__name__) diff --git a/bot/seasons/halloween/hacktober-issue-finder.py b/bot/seasons/halloween/hacktober-issue-finder.py index e90796f1..f15a665a 100644 --- a/bot/seasons/halloween/hacktober-issue-finder.py +++ b/bot/seasons/halloween/hacktober-issue-finder.py @@ -8,7 +8,7 @@ import discord from discord.ext import commands from bot.constants import Month -from bot.decorators import in_month +from bot.utils.decorators import in_month log = logging.getLogger(__name__) diff --git a/bot/seasons/halloween/hacktoberstats.py b/bot/seasons/halloween/hacktoberstats.py index 3b1444ab..5dfa2f51 100644 --- a/bot/seasons/halloween/hacktoberstats.py +++ b/bot/seasons/halloween/hacktoberstats.py @@ -11,7 +11,7 @@ import discord from discord.ext import commands from bot.constants import Channels, Month, WHITELISTED_CHANNELS -from bot.decorators import in_month, override_in_channel +from bot.utils.decorators import in_month, override_in_channel from bot.utils.persist import make_persistent log = logging.getLogger(__name__) diff --git a/bot/seasons/halloween/spookyreact.py b/bot/seasons/halloween/spookyreact.py index 37f42a86..16f18019 100644 --- a/bot/seasons/halloween/spookyreact.py +++ b/bot/seasons/halloween/spookyreact.py @@ -5,7 +5,7 @@ import discord from discord.ext.commands import Bot, Cog from bot.constants import Month -from bot.decorators import in_month +from bot.utils.decorators import in_month log = logging.getLogger(__name__) diff --git a/bot/seasons/pride/pride_facts.py b/bot/seasons/pride/pride_facts.py index 1a02eaaa..2db8f5c2 100644 --- a/bot/seasons/pride/pride_facts.py +++ b/bot/seasons/pride/pride_facts.py @@ -10,7 +10,7 @@ import discord from discord.ext import commands from bot.constants import Channels, Colours, Month -from bot.decorators import seasonal_task +from bot.utils.decorators import seasonal_task log = logging.getLogger(__name__) diff --git a/bot/seasons/valentines/be_my_valentine.py b/bot/seasons/valentines/be_my_valentine.py index 67d8796a..1e883d21 100644 --- a/bot/seasons/valentines/be_my_valentine.py +++ b/bot/seasons/valentines/be_my_valentine.py @@ -9,7 +9,7 @@ from discord.ext import commands from discord.ext.commands.cooldowns import BucketType from bot.constants import Channels, Client, Colours, Lovefest, Month -from bot.decorators import in_month +from bot.utils.decorators import in_month log = logging.getLogger(__name__) diff --git a/bot/utils/decorators.py b/bot/utils/decorators.py new file mode 100644 index 00000000..f85996b5 --- /dev/null +++ b/bot/utils/decorators.py @@ -0,0 +1,321 @@ +import asyncio +import functools +import logging +import random +import typing as t +from asyncio import Lock +from datetime import datetime +from functools import wraps +from weakref import WeakValueDictionary + +from discord import Colour, Embed +from discord.ext import commands +from discord.ext.commands import CheckFailure, Command, Context + +from bot.constants import Client, ERROR_REPLIES, Month + +ONE_DAY = 24 * 60 * 60 + +log = logging.getLogger(__name__) + + +class InChannelCheckFailure(CheckFailure): + """Check failure when the user runs a command in a non-whitelisted channel.""" + + pass + + +class InMonthCheckFailure(CheckFailure): + """Check failure for when a command is invoked outside of its allowed month.""" + + pass + + +def seasonal_task(*allowed_months: Month, sleep_time: t.Union[float, int] = ONE_DAY) -> t.Callable: + """ + Perform the decorated method periodically in `allowed_months`. + + This provides a convenience wrapper to avoid code repetition where some task shall + perform an operation repeatedly in a constant interval, but only in specific months. + + The decorated function will be called once every `sleep_time` seconds while + the current UTC month is in `allowed_months`. Sleep time defaults to 24 hours. + + The wrapped task is responsible for waiting for the bot to be ready, if necessary. + """ + def decorator(task_body: t.Callable) -> t.Callable: + @functools.wraps(task_body) + async def decorated_task(*args, **kwargs) -> None: + """Call `task_body` once every `sleep_time` seconds in `allowed_months`.""" + log.info(f"Starting seasonal task {task_body.__qualname__} ({allowed_months})") + + while True: + current_month = Month(datetime.utcnow().month) + + if current_month in allowed_months: + await task_body(*args, **kwargs) + else: + log.debug(f"Seasonal task {task_body.__qualname__} sleeps in {current_month.name}") + + await asyncio.sleep(sleep_time) + return decorated_task + return decorator + + +def in_month_listener(*allowed_months: Month) -> t.Callable: + """ + Shield a listener from being invoked outside of `allowed_months`. + + The check is performed against current UTC month. + """ + def decorator(listener: t.Callable) -> t.Callable: + @functools.wraps(listener) + async def guarded_listener(*args, **kwargs) -> None: + """Wrapped listener will abort if not in allowed month.""" + current_month = Month(datetime.utcnow().month) + + if current_month in allowed_months: + # Propagate return value although it should always be None + return await listener(*args, **kwargs) + else: + log.debug(f"Guarded {listener.__qualname__} from invoking in {current_month.name}") + return guarded_listener + return decorator + + +def in_month_command(*allowed_months: Month) -> t.Callable: + """ + Check whether the command was invoked in one of `enabled_months`. + + Uses the current UTC month at the time of running the predicate. + """ + async def predicate(ctx: Context) -> bool: + current_month = datetime.utcnow().month + can_run = current_month in allowed_months + + human_months = ", ".join(m.name for m in allowed_months) + log.debug( + f"Command '{ctx.command}' is locked to months {human_months}. " + f"Invoking it in month {current_month} is {'allowed' if can_run else 'disallowed'}." + ) + if can_run: + return True + else: + raise InMonthCheckFailure(f"Command can only be used in {human_months}") + + return commands.check(predicate) + + +def in_month(*allowed_months: Month) -> t.Callable: + """ + Universal decorator for season-locking commands and listeners alike. + + This only serves to determine whether the decorated callable is a command, + a listener, or neither. It then delegates to either `in_month_command`, + or `in_month_listener`, or raises TypeError, respectively. + + Please note that in order for this decorator to correctly determine whether + the decorated callable is a cmd or listener, it **has** to first be turned + into one. This means that this decorator should always be placed **above** + the d.py one that registers it as either. + + This will decorate groups as well, as those subclass Command. In order to lock + all subcommands of a group, its `invoke_without_command` param must **not** be + manually set to True - this causes a circumvention of the group's callback + and the seasonal check applied to it. + """ + def decorator(callable_: t.Callable) -> t.Callable: + # Functions decorated as commands are turned into instances of `Command` + if isinstance(callable_, Command): + logging.debug(f"Command {callable_.qualified_name} will be locked to {allowed_months}") + actual_deco = in_month_command(*allowed_months) + + # D.py will assign this attribute when `callable_` is registered as a listener + elif hasattr(callable_, "__cog_listener__"): + logging.debug(f"Listener {callable_.__qualname__} will be locked to {allowed_months}") + actual_deco = in_month_listener(*allowed_months) + + # Otherwise we're unsure exactly what has been decorated + # This happens before the bot starts, so let's just raise + else: + raise TypeError(f"Decorated object {callable_} is neither a command nor a listener") + + return actual_deco(callable_) + return decorator + + +def with_role(*role_ids: int) -> t.Callable: + """Check to see whether the invoking user has any of the roles specified in role_ids.""" + async def predicate(ctx: Context) -> bool: + if not ctx.guild: # Return False in a DM + log.debug( + f"{ctx.author} tried to use the '{ctx.command.name}'command from a DM. " + "This command is restricted by the with_role decorator. Rejecting request." + ) + return False + + for role in ctx.author.roles: + if role.id in role_ids: + log.debug(f"{ctx.author} has the '{role.name}' role, and passes the check.") + return True + + log.debug( + f"{ctx.author} does not have the required role to use " + f"the '{ctx.command.name}' command, so the request is rejected." + ) + return False + return commands.check(predicate) + + +def without_role(*role_ids: int) -> t.Callable: + """Check whether the invoking user does not have all of the roles specified in role_ids.""" + async def predicate(ctx: Context) -> bool: + if not ctx.guild: # Return False in a DM + log.debug( + f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM. " + "This command is restricted by the without_role decorator. Rejecting request." + ) + return False + + author_roles = [role.id for role in ctx.author.roles] + check = all(role not in author_roles for role in role_ids) + log.debug( + f"{ctx.author} tried to call the '{ctx.command.name}' command. " + f"The result of the without_role check was {check}." + ) + return check + return commands.check(predicate) + + +def in_channel_check(*channels: int, bypass_roles: t.Container[int] = None) -> t.Callable[[Context], bool]: + """ + Checks that the message is in a whitelisted channel or optionally has a bypass role. + + If `in_channel_override` is present, check if it contains channels + and use them in place of the global whitelist. + """ + def predicate(ctx: Context) -> bool: + if not ctx.guild: + log.debug(f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM.") + return True + if ctx.channel.id in channels: + log.debug( + f"{ctx.author} tried to call the '{ctx.command.name}' command " + f"and the command was used in a whitelisted channel." + ) + return True + + if bypass_roles and any(r.id in bypass_roles for r in ctx.author.roles): + log.debug( + f"{ctx.author} called the '{ctx.command.name}' command and " + f"had a role to bypass the in_channel check." + ) + return True + + if hasattr(ctx.command.callback, "in_channel_override"): + override = ctx.command.callback.in_channel_override + if override is None: + log.debug( + f"{ctx.author} called the '{ctx.command.name}' command " + f"and the command was whitelisted to bypass the in_channel check." + ) + return True + else: + if ctx.channel.id in override: + log.debug( + f"{ctx.author} tried to call the '{ctx.command.name}' command " + f"and the command was used in an overridden whitelisted channel." + ) + return True + + log.debug( + f"{ctx.author} tried to call the '{ctx.command.name}' command. " + f"The overridden in_channel check failed." + ) + channels_str = ', '.join(f"<#{c_id}>" for c_id in override) + raise InChannelCheckFailure( + f"Sorry, but you may only use this command within {channels_str}." + ) + + log.debug( + f"{ctx.author} tried to call the '{ctx.command.name}' command. " + f"The in_channel check failed." + ) + + channels_str = ', '.join(f"<#{c_id}>" for c_id in channels) + raise InChannelCheckFailure( + f"Sorry, but you may only use this command within {channels_str}." + ) + + return predicate + + +in_channel = commands.check(in_channel_check) + + +def override_in_channel(channels: t.Tuple[int] = None) -> t.Callable: + """ + Set command callback attribute for detection in `in_channel_check`. + + Override global whitelist if channels are specified. + + This decorator has to go before (below) below the `command` decorator. + """ + def inner(func: t.Callable) -> t.Callable: + func.in_channel_override = channels + return func + + return inner + + +def locked() -> t.Union[t.Callable, None]: + """ + Allows the user to only run one instance of the decorated command at a time. + + Subsequent calls to the command from the same author are ignored until the command has completed invocation. + + This decorator has to go before (below) the `command` decorator. + """ + def wrap(func: t.Callable) -> t.Union[t.Callable, None]: + func.__locks = WeakValueDictionary() + + @wraps(func) + async def inner(self: t.Callable, ctx: Context, *args, **kwargs) -> t.Union[t.Callable, None]: + lock = func.__locks.setdefault(ctx.author.id, Lock()) + if lock.locked(): + embed = Embed() + embed.colour = Colour.red() + + log.debug(f"User tried to invoke a locked command.") + embed.description = ( + "You're already using this command. Please wait until " + "it is done before you use it again." + ) + embed.title = random.choice(ERROR_REPLIES) + await ctx.send(embed=embed) + return + + async with func.__locks.setdefault(ctx.author.id, Lock()): + return await func(self, ctx, *args, **kwargs) + return inner + return wrap + + +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 Client.debug: + log.debug(f"Function {func.__name__} called with args: {args}, kwargs: {kwargs}") + return return_value + return await func(*args, **kwargs) + return wrapped + return decorator -- cgit v1.2.3 From 5feabe642ed5b685c7aa9515bc70ff4ff8b278bc Mon Sep 17 00:00:00 2001 From: kwzrd Date: Sat, 28 Mar 2020 16:33:20 +0100 Subject: Deseasonify: log in `add_cog` rather than in each `setup` The previous system required each extension's `setup` func to log that the cog was loaded. This leads to inconsistent messages all trying to convey the same thing, variable logger names in the output file are difficult to read, and several extensions were not logging at all. By logging directly in the `add_cog` method, we reduce code repetition, ensure consistent format, and remove the responsibility to remember that a log should be made. --- bot/bot.py | 9 +++++++++ bot/exts/christmas/adventofcode.py | 1 - bot/exts/christmas/hanukkah_embed.py | 1 - bot/exts/easter/april_fools_vids.py | 1 - bot/exts/easter/avatar_easterifier.py | 1 - bot/exts/easter/bunny_name_generator.py | 1 - bot/exts/easter/conversationstarters.py | 1 - bot/exts/easter/easter_riddle.py | 1 - bot/exts/easter/egg_decorating.py | 1 - bot/exts/easter/egg_facts.py | 1 - bot/exts/easter/egghead_quiz.py | 1 - bot/exts/easter/traditions.py | 1 - bot/exts/evergreen/battleship.py | 1 - bot/exts/evergreen/bookmark.py | 1 - bot/exts/evergreen/branding.py | 1 - bot/exts/evergreen/error_handler.py | 1 - bot/exts/evergreen/fun.py | 1 - bot/exts/evergreen/help.py | 2 -- bot/exts/evergreen/issues.py | 1 - bot/exts/evergreen/magic_8ball.py | 1 - bot/exts/evergreen/minesweeper.py | 1 - bot/exts/evergreen/recommend_game.py | 1 - bot/exts/evergreen/reddit.py | 1 - bot/exts/evergreen/showprojects.py | 1 - bot/exts/evergreen/snakes/__init__.py | 1 - bot/exts/evergreen/speedrun.py | 1 - bot/exts/evergreen/trivia_quiz.py | 1 - bot/exts/evergreen/uptime.py | 1 - bot/exts/halloween/8ball.py | 1 - bot/exts/halloween/candy_collection.py | 1 - bot/exts/halloween/hacktober-issue-finder.py | 1 - bot/exts/halloween/hacktoberstats.py | 1 - bot/exts/halloween/halloween_facts.py | 1 - bot/exts/halloween/halloweenify.py | 1 - bot/exts/halloween/monsterbio.py | 1 - bot/exts/halloween/monstersurvey.py | 1 - bot/exts/halloween/scarymovie.py | 1 - bot/exts/halloween/spookyavatar.py | 1 - bot/exts/halloween/spookygif.py | 1 - bot/exts/halloween/spookyrating.py | 1 - bot/exts/halloween/spookyreact.py | 1 - bot/exts/halloween/spookysound.py | 1 - bot/exts/halloween/timeleft.py | 1 - bot/exts/pride/drag_queen_name.py | 1 - bot/exts/pride/pride_anthem.py | 1 - bot/exts/pride/pride_avatar.py | 1 - bot/exts/pride/pride_facts.py | 1 - bot/exts/valentines/be_my_valentine.py | 1 - bot/exts/valentines/lovecalculator.py | 1 - bot/exts/valentines/movie_generator.py | 1 - bot/exts/valentines/myvalenstate.py | 1 - bot/exts/valentines/pickuplines.py | 1 - bot/exts/valentines/savethedate.py | 1 - bot/exts/valentines/valentine_zodiac.py | 1 - bot/exts/valentines/whoisvalentine.py | 1 - 55 files changed, 9 insertions(+), 55 deletions(-) (limited to 'bot/bot.py') diff --git a/bot/bot.py b/bot/bot.py index 47d63de9..3e20e3ab 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -33,6 +33,15 @@ class SeasonalBot(commands.Bot): connector=TCPConnector(resolver=AsyncResolver(), family=socket.AF_INET) ) + def add_cog(self, cog: commands.Cog) -> None: + """ + Delegate to super to register `cog`. + + This only serves to make the info log, so that extensions don't have to. + """ + super().add_cog(cog) + log.info(f"Cog loaded: {cog.qualified_name}") + async def send_log(self, title: str, details: str = None, *, icon: str = None) -> None: """Send an embed message to the devlog channel.""" devlog = self.get_channel(Channels.devlog) diff --git a/bot/exts/christmas/adventofcode.py b/bot/exts/christmas/adventofcode.py index f7590e04..8f7aa13b 100644 --- a/bot/exts/christmas/adventofcode.py +++ b/bot/exts/christmas/adventofcode.py @@ -741,4 +741,3 @@ def _error_embed_helper(title: str, description: str) -> discord.Embed: def setup(bot: commands.Bot) -> None: """Advent of Code Cog load.""" bot.add_cog(AdventOfCode(bot)) - log.info("AdventOfCode cog loaded") diff --git a/bot/exts/christmas/hanukkah_embed.py b/bot/exts/christmas/hanukkah_embed.py index 62efd04e..718ca32a 100644 --- a/bot/exts/christmas/hanukkah_embed.py +++ b/bot/exts/christmas/hanukkah_embed.py @@ -111,4 +111,3 @@ class HanukkahEmbed(commands.Cog): def setup(bot: commands.Bot) -> None: """Cog load.""" bot.add_cog(HanukkahEmbed(bot)) - log.info("Hanukkah embed cog loaded") diff --git a/bot/exts/easter/april_fools_vids.py b/bot/exts/easter/april_fools_vids.py index 4869f510..06108f02 100644 --- a/bot/exts/easter/april_fools_vids.py +++ b/bot/exts/easter/april_fools_vids.py @@ -36,4 +36,3 @@ class AprilFoolVideos(commands.Cog): def setup(bot: commands.Bot) -> None: """April Fools' Cog load.""" bot.add_cog(AprilFoolVideos(bot)) - log.info('April Fools videos cog loaded!') diff --git a/bot/exts/easter/avatar_easterifier.py b/bot/exts/easter/avatar_easterifier.py index e21e35fc..8e8a3500 100644 --- a/bot/exts/easter/avatar_easterifier.py +++ b/bot/exts/easter/avatar_easterifier.py @@ -126,4 +126,3 @@ class AvatarEasterifier(commands.Cog): def setup(bot: commands.Bot) -> None: """Avatar Easterifier Cog load.""" bot.add_cog(AvatarEasterifier(bot)) - log.info("AvatarEasterifier cog loaded") diff --git a/bot/exts/easter/bunny_name_generator.py b/bot/exts/easter/bunny_name_generator.py index 97c467e1..3ecf9be9 100644 --- a/bot/exts/easter/bunny_name_generator.py +++ b/bot/exts/easter/bunny_name_generator.py @@ -90,4 +90,3 @@ class BunnyNameGenerator(commands.Cog): def setup(bot: commands.Bot) -> None: """Bunny Name Generator Cog load.""" bot.add_cog(BunnyNameGenerator(bot)) - log.info("BunnyNameGenerator cog loaded.") diff --git a/bot/exts/easter/conversationstarters.py b/bot/exts/easter/conversationstarters.py index 3f38ae82..a5f40445 100644 --- a/bot/exts/easter/conversationstarters.py +++ b/bot/exts/easter/conversationstarters.py @@ -26,4 +26,3 @@ class ConvoStarters(commands.Cog): def setup(bot: commands.Bot) -> None: """Conversation starters Cog load.""" bot.add_cog(ConvoStarters(bot)) - log.info("ConvoStarters cog loaded") diff --git a/bot/exts/easter/easter_riddle.py b/bot/exts/easter/easter_riddle.py index f5b1aac7..8977534f 100644 --- a/bot/exts/easter/easter_riddle.py +++ b/bot/exts/easter/easter_riddle.py @@ -98,4 +98,3 @@ class EasterRiddle(commands.Cog): def setup(bot: commands.Bot) -> None: """Easter Riddle Cog load.""" bot.add_cog(EasterRiddle(bot)) - log.info("Easter Riddle bot loaded") diff --git a/bot/exts/easter/egg_decorating.py b/bot/exts/easter/egg_decorating.py index 23df95f1..be228b2c 100644 --- a/bot/exts/easter/egg_decorating.py +++ b/bot/exts/easter/egg_decorating.py @@ -116,4 +116,3 @@ class EggDecorating(commands.Cog): def setup(bot: commands.bot) -> None: """Egg decorating Cog load.""" bot.add_cog(EggDecorating(bot)) - log.info("EggDecorating cog loaded.") diff --git a/bot/exts/easter/egg_facts.py b/bot/exts/easter/egg_facts.py index 99a80b28..bc64d26e 100644 --- a/bot/exts/easter/egg_facts.py +++ b/bot/exts/easter/egg_facts.py @@ -58,4 +58,3 @@ class EasterFacts(commands.Cog): def setup(bot: commands.Bot) -> None: """Easter Egg facts cog load.""" bot.add_cog(EasterFacts(bot)) - log.info("EasterFacts cog loaded") diff --git a/bot/exts/easter/egghead_quiz.py b/bot/exts/easter/egghead_quiz.py index bd179fe2..0498d9db 100644 --- a/bot/exts/easter/egghead_quiz.py +++ b/bot/exts/easter/egghead_quiz.py @@ -117,4 +117,3 @@ class EggheadQuiz(commands.Cog): def setup(bot: commands.Bot) -> None: """Egghead Quiz Cog load.""" bot.add_cog(EggheadQuiz(bot)) - log.info("EggheadQuiz bot loaded") diff --git a/bot/exts/easter/traditions.py b/bot/exts/easter/traditions.py index 9529823f..85b4adfb 100644 --- a/bot/exts/easter/traditions.py +++ b/bot/exts/easter/traditions.py @@ -28,4 +28,3 @@ class Traditions(commands.Cog): def setup(bot: commands.Bot) -> None: """Traditions Cog load.""" bot.add_cog(Traditions(bot)) - log.info("Traditions cog loaded") diff --git a/bot/exts/evergreen/battleship.py b/bot/exts/evergreen/battleship.py index 9b8aaa48..9bc374e6 100644 --- a/bot/exts/evergreen/battleship.py +++ b/bot/exts/evergreen/battleship.py @@ -441,4 +441,3 @@ class Battleship(commands.Cog): def setup(bot: commands.Bot) -> None: """Cog load.""" bot.add_cog(Battleship(bot)) - log.info("Battleship cog loaded") diff --git a/bot/exts/evergreen/bookmark.py b/bot/exts/evergreen/bookmark.py index e703e07b..73908702 100644 --- a/bot/exts/evergreen/bookmark.py +++ b/bot/exts/evergreen/bookmark.py @@ -62,4 +62,3 @@ class Bookmark(commands.Cog): def setup(bot: commands.Bot) -> None: """Load the Bookmark cog.""" bot.add_cog(Bookmark(bot)) - log.info("Bookmark cog loaded") diff --git a/bot/exts/evergreen/branding.py b/bot/exts/evergreen/branding.py index 2eb563ea..0a14cd84 100644 --- a/bot/exts/evergreen/branding.py +++ b/bot/exts/evergreen/branding.py @@ -501,4 +501,3 @@ class BrandingManager(commands.Cog): def setup(bot: SeasonalBot) -> None: """Load BrandingManager cog.""" bot.add_cog(BrandingManager(bot)) - log.info("BrandingManager cog loaded") diff --git a/bot/exts/evergreen/error_handler.py b/bot/exts/evergreen/error_handler.py index f756c321..33b1a3f2 100644 --- a/bot/exts/evergreen/error_handler.py +++ b/bot/exts/evergreen/error_handler.py @@ -127,4 +127,3 @@ class CommandErrorHandler(commands.Cog): def setup(bot: commands.Bot) -> None: """Error handler Cog load.""" bot.add_cog(CommandErrorHandler(bot)) - log.info("CommandErrorHandler cog loaded") diff --git a/bot/exts/evergreen/fun.py b/bot/exts/evergreen/fun.py index 889ae079..67a4bae5 100644 --- a/bot/exts/evergreen/fun.py +++ b/bot/exts/evergreen/fun.py @@ -145,4 +145,3 @@ class Fun(Cog): def setup(bot: commands.Bot) -> None: """Fun Cog load.""" bot.add_cog(Fun(bot)) - log.info("Fun cog loaded") diff --git a/bot/exts/evergreen/help.py b/bot/exts/evergreen/help.py index f4d76402..ccd76d76 100644 --- a/bot/exts/evergreen/help.py +++ b/bot/exts/evergreen/help.py @@ -540,8 +540,6 @@ def setup(bot: SeasonalBot) -> None: except Exception: unload(bot) raise - else: - log.info("Help cog loaded") def teardown(bot: SeasonalBot) -> None: diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index fb18b62a..4129156a 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -74,4 +74,3 @@ class Issues(commands.Cog): def setup(bot: commands.Bot) -> None: """Cog Retrieves Issues From Github.""" bot.add_cog(Issues(bot)) - log.info("Issues cog loaded") diff --git a/bot/exts/evergreen/magic_8ball.py b/bot/exts/evergreen/magic_8ball.py index e47ef454..c10f1f51 100644 --- a/bot/exts/evergreen/magic_8ball.py +++ b/bot/exts/evergreen/magic_8ball.py @@ -29,4 +29,3 @@ class Magic8ball(commands.Cog): def setup(bot: commands.Bot) -> None: """Magic 8ball Cog load.""" bot.add_cog(Magic8ball(bot)) - log.info("Magic8ball cog loaded") diff --git a/bot/exts/evergreen/minesweeper.py b/bot/exts/evergreen/minesweeper.py index b0ba8145..b59cdb14 100644 --- a/bot/exts/evergreen/minesweeper.py +++ b/bot/exts/evergreen/minesweeper.py @@ -282,4 +282,3 @@ class Minesweeper(commands.Cog): def setup(bot: commands.Bot) -> None: """Load the Minesweeper cog.""" bot.add_cog(Minesweeper(bot)) - log.info("Minesweeper cog loaded") diff --git a/bot/exts/evergreen/recommend_game.py b/bot/exts/evergreen/recommend_game.py index 835a4e53..7cd52c2c 100644 --- a/bot/exts/evergreen/recommend_game.py +++ b/bot/exts/evergreen/recommend_game.py @@ -48,4 +48,3 @@ class RecommendGame(commands.Cog): def setup(bot: commands.Bot) -> None: """Loads the RecommendGame cog.""" bot.add_cog(RecommendGame(bot)) - log.info("RecommendGame cog loaded") diff --git a/bot/exts/evergreen/reddit.py b/bot/exts/evergreen/reddit.py index a07e591f..fe204419 100644 --- a/bot/exts/evergreen/reddit.py +++ b/bot/exts/evergreen/reddit.py @@ -126,4 +126,3 @@ class Reddit(commands.Cog): def setup(bot: commands.Bot) -> None: """Load the Cog.""" bot.add_cog(Reddit(bot)) - log.debug('Loaded') diff --git a/bot/exts/evergreen/showprojects.py b/bot/exts/evergreen/showprojects.py index a943e548..328a7aa5 100644 --- a/bot/exts/evergreen/showprojects.py +++ b/bot/exts/evergreen/showprojects.py @@ -31,4 +31,3 @@ class ShowProjects(commands.Cog): def setup(bot: commands.Bot) -> None: """Show Projects Reaction Cog.""" bot.add_cog(ShowProjects(bot)) - log.info("ShowProjects cog loaded") diff --git a/bot/exts/evergreen/snakes/__init__.py b/bot/exts/evergreen/snakes/__init__.py index 06ebfb47..2eae2751 100644 --- a/bot/exts/evergreen/snakes/__init__.py +++ b/bot/exts/evergreen/snakes/__init__.py @@ -10,4 +10,3 @@ log = logging.getLogger(__name__) def setup(bot: commands.Bot) -> None: """Snakes Cog load.""" bot.add_cog(Snakes(bot)) - log.info("Snakes cog loaded") diff --git a/bot/exts/evergreen/speedrun.py b/bot/exts/evergreen/speedrun.py index 76c5e8d3..4e8d7aee 100644 --- a/bot/exts/evergreen/speedrun.py +++ b/bot/exts/evergreen/speedrun.py @@ -25,4 +25,3 @@ class Speedrun(commands.Cog): def setup(bot: commands.Bot) -> None: """Load the Speedrun cog.""" bot.add_cog(Speedrun(bot)) - log.info("Speedrun cog loaded") diff --git a/bot/exts/evergreen/trivia_quiz.py b/bot/exts/evergreen/trivia_quiz.py index 99b64497..c1a271e8 100644 --- a/bot/exts/evergreen/trivia_quiz.py +++ b/bot/exts/evergreen/trivia_quiz.py @@ -300,4 +300,3 @@ class TriviaQuiz(commands.Cog): def setup(bot: commands.Bot) -> None: """Load the cog.""" bot.add_cog(TriviaQuiz(bot)) - logger.debug("TriviaQuiz cog loaded") diff --git a/bot/exts/evergreen/uptime.py b/bot/exts/evergreen/uptime.py index 6f24f545..a9ad9dfb 100644 --- a/bot/exts/evergreen/uptime.py +++ b/bot/exts/evergreen/uptime.py @@ -31,4 +31,3 @@ class Uptime(commands.Cog): def setup(bot: commands.Bot) -> None: """Uptime Cog load.""" bot.add_cog(Uptime(bot)) - log.info("Uptime cog loaded") diff --git a/bot/exts/halloween/8ball.py b/bot/exts/halloween/8ball.py index 2e1c2804..1df48fbf 100644 --- a/bot/exts/halloween/8ball.py +++ b/bot/exts/halloween/8ball.py @@ -31,4 +31,3 @@ class SpookyEightBall(commands.Cog): def setup(bot: commands.Bot) -> None: """Spooky Eight Ball Cog Load.""" bot.add_cog(SpookyEightBall(bot)) - log.info("SpookyEightBall cog loaded") diff --git a/bot/exts/halloween/candy_collection.py b/bot/exts/halloween/candy_collection.py index 3f2b895e..00061fc2 100644 --- a/bot/exts/halloween/candy_collection.py +++ b/bot/exts/halloween/candy_collection.py @@ -222,4 +222,3 @@ class CandyCollection(commands.Cog): def setup(bot: commands.Bot) -> None: """Candy Collection game Cog load.""" bot.add_cog(CandyCollection(bot)) - log.info("CandyCollection cog loaded") diff --git a/bot/exts/halloween/hacktober-issue-finder.py b/bot/exts/halloween/hacktober-issue-finder.py index f15a665a..3a9d38d2 100644 --- a/bot/exts/halloween/hacktober-issue-finder.py +++ b/bot/exts/halloween/hacktober-issue-finder.py @@ -108,4 +108,3 @@ class HacktoberIssues(commands.Cog): def setup(bot: commands.Bot) -> None: """Hacktober issue finder Cog Load.""" bot.add_cog(HacktoberIssues(bot)) - log.info("hacktober-issue-finder cog loaded") diff --git a/bot/exts/halloween/hacktoberstats.py b/bot/exts/halloween/hacktoberstats.py index 5dfa2f51..0c1c837c 100644 --- a/bot/exts/halloween/hacktoberstats.py +++ b/bot/exts/halloween/hacktoberstats.py @@ -339,4 +339,3 @@ class HacktoberStats(commands.Cog): def setup(bot: commands.Bot) -> None: """Hacktoberstats Cog load.""" bot.add_cog(HacktoberStats(bot)) - log.info("HacktoberStats cog loaded") diff --git a/bot/exts/halloween/halloween_facts.py b/bot/exts/halloween/halloween_facts.py index 222768f4..44a66ab2 100644 --- a/bot/exts/halloween/halloween_facts.py +++ b/bot/exts/halloween/halloween_facts.py @@ -56,4 +56,3 @@ class HalloweenFacts(commands.Cog): def setup(bot: commands.Bot) -> None: """Halloween facts Cog load.""" bot.add_cog(HalloweenFacts(bot)) - log.info("HalloweenFacts cog loaded") diff --git a/bot/exts/halloween/halloweenify.py b/bot/exts/halloween/halloweenify.py index dfcc2b1e..5c433a81 100644 --- a/bot/exts/halloween/halloweenify.py +++ b/bot/exts/halloween/halloweenify.py @@ -49,4 +49,3 @@ class Halloweenify(commands.Cog): def setup(bot: commands.Bot) -> None: """Halloweenify Cog load.""" bot.add_cog(Halloweenify(bot)) - log.info("Halloweenify cog loaded") diff --git a/bot/exts/halloween/monsterbio.py b/bot/exts/halloween/monsterbio.py index bfa8a026..016a66d1 100644 --- a/bot/exts/halloween/monsterbio.py +++ b/bot/exts/halloween/monsterbio.py @@ -53,4 +53,3 @@ class MonsterBio(commands.Cog): def setup(bot: commands.Bot) -> None: """Monster bio Cog load.""" bot.add_cog(MonsterBio(bot)) - log.info("MonsterBio cog loaded.") diff --git a/bot/exts/halloween/monstersurvey.py b/bot/exts/halloween/monstersurvey.py index 12e1d022..27da79b6 100644 --- a/bot/exts/halloween/monstersurvey.py +++ b/bot/exts/halloween/monstersurvey.py @@ -203,4 +203,3 @@ class MonsterSurvey(Cog): def setup(bot: Bot) -> None: """Monster survey Cog load.""" bot.add_cog(MonsterSurvey(bot)) - log.info("MonsterSurvey cog loaded") diff --git a/bot/exts/halloween/scarymovie.py b/bot/exts/halloween/scarymovie.py index 3823a3e4..c80e0298 100644 --- a/bot/exts/halloween/scarymovie.py +++ b/bot/exts/halloween/scarymovie.py @@ -129,4 +129,3 @@ class ScaryMovie(commands.Cog): def setup(bot: commands.Bot) -> None: """Scary movie Cog load.""" bot.add_cog(ScaryMovie(bot)) - log.info("ScaryMovie cog loaded") diff --git a/bot/exts/halloween/spookyavatar.py b/bot/exts/halloween/spookyavatar.py index 268de3fb..2d7df678 100644 --- a/bot/exts/halloween/spookyavatar.py +++ b/bot/exts/halloween/spookyavatar.py @@ -50,4 +50,3 @@ class SpookyAvatar(commands.Cog): def setup(bot: commands.Bot) -> None: """Spooky avatar Cog load.""" bot.add_cog(SpookyAvatar(bot)) - log.info("SpookyAvatar cog loaded") diff --git a/bot/exts/halloween/spookygif.py b/bot/exts/halloween/spookygif.py index 818de8cd..f402437f 100644 --- a/bot/exts/halloween/spookygif.py +++ b/bot/exts/halloween/spookygif.py @@ -36,4 +36,3 @@ class SpookyGif(commands.Cog): def setup(bot: commands.Bot) -> None: """Spooky GIF Cog load.""" bot.add_cog(SpookyGif(bot)) - log.info("SpookyGif cog loaded") diff --git a/bot/exts/halloween/spookyrating.py b/bot/exts/halloween/spookyrating.py index 7f78f536..1a48194e 100644 --- a/bot/exts/halloween/spookyrating.py +++ b/bot/exts/halloween/spookyrating.py @@ -64,4 +64,3 @@ class SpookyRating(commands.Cog): def setup(bot: commands.Bot) -> None: """Spooky Rating Cog load.""" bot.add_cog(SpookyRating(bot)) - log.info("SpookyRating cog loaded") diff --git a/bot/exts/halloween/spookyreact.py b/bot/exts/halloween/spookyreact.py index 16f18019..a3896cfb 100644 --- a/bot/exts/halloween/spookyreact.py +++ b/bot/exts/halloween/spookyreact.py @@ -73,4 +73,3 @@ class SpookyReact(Cog): def setup(bot: Bot) -> None: """Spooky reaction Cog load.""" bot.add_cog(SpookyReact(bot)) - log.info("SpookyReact cog loaded") diff --git a/bot/exts/halloween/spookysound.py b/bot/exts/halloween/spookysound.py index e0676d0a..325447e5 100644 --- a/bot/exts/halloween/spookysound.py +++ b/bot/exts/halloween/spookysound.py @@ -45,4 +45,3 @@ class SpookySound(commands.Cog): def setup(bot: commands.Bot) -> None: """Spooky sound Cog load.""" bot.add_cog(SpookySound(bot)) - log.info("SpookySound cog loaded") diff --git a/bot/exts/halloween/timeleft.py b/bot/exts/halloween/timeleft.py index 8cb3f4f6..295acc89 100644 --- a/bot/exts/halloween/timeleft.py +++ b/bot/exts/halloween/timeleft.py @@ -57,4 +57,3 @@ class TimeLeft(commands.Cog): def setup(bot: commands.Bot) -> None: """Cog load.""" bot.add_cog(TimeLeft(bot)) - log.info("TimeLeft cog loaded") diff --git a/bot/exts/pride/drag_queen_name.py b/bot/exts/pride/drag_queen_name.py index 43813fbd..95297745 100644 --- a/bot/exts/pride/drag_queen_name.py +++ b/bot/exts/pride/drag_queen_name.py @@ -30,4 +30,3 @@ class DragNames(commands.Cog): def setup(bot: commands.Bot) -> None: """Cog loader for drag queen name generator.""" bot.add_cog(DragNames(bot)) - log.info("Drag queen name generator cog loaded!") diff --git a/bot/exts/pride/pride_anthem.py b/bot/exts/pride/pride_anthem.py index b0c6d34e..186c5fff 100644 --- a/bot/exts/pride/pride_anthem.py +++ b/bot/exts/pride/pride_anthem.py @@ -55,4 +55,3 @@ class PrideAnthem(commands.Cog): def setup(bot: commands.Bot) -> None: """Cog loader for pride anthem.""" bot.add_cog(PrideAnthem(bot)) - log.info("Pride anthems cog loaded!") diff --git a/bot/exts/pride/pride_avatar.py b/bot/exts/pride/pride_avatar.py index 85e49d5c..3f9878e3 100644 --- a/bot/exts/pride/pride_avatar.py +++ b/bot/exts/pride/pride_avatar.py @@ -142,4 +142,3 @@ class PrideAvatar(commands.Cog): def setup(bot: commands.Bot) -> None: """Cog load.""" bot.add_cog(PrideAvatar(bot)) - log.info("PrideAvatar cog loaded") diff --git a/bot/exts/pride/pride_facts.py b/bot/exts/pride/pride_facts.py index 2db8f5c2..bd9c087b 100644 --- a/bot/exts/pride/pride_facts.py +++ b/bot/exts/pride/pride_facts.py @@ -104,4 +104,3 @@ class PrideFacts(commands.Cog): def setup(bot: commands.Bot) -> None: """Cog loader for pride facts.""" bot.add_cog(PrideFacts(bot)) - log.info("Pride facts cog loaded!") diff --git a/bot/exts/valentines/be_my_valentine.py b/bot/exts/valentines/be_my_valentine.py index 1e883d21..4b0818da 100644 --- a/bot/exts/valentines/be_my_valentine.py +++ b/bot/exts/valentines/be_my_valentine.py @@ -234,4 +234,3 @@ class BeMyValentine(commands.Cog): def setup(bot: commands.Bot) -> None: """Be my Valentine Cog load.""" bot.add_cog(BeMyValentine(bot)) - log.info("BeMyValentine cog loaded") diff --git a/bot/exts/valentines/lovecalculator.py b/bot/exts/valentines/lovecalculator.py index 03d3d7d5..e11e062b 100644 --- a/bot/exts/valentines/lovecalculator.py +++ b/bot/exts/valentines/lovecalculator.py @@ -101,4 +101,3 @@ class LoveCalculator(Cog): def setup(bot: commands.Bot) -> None: """Love calculator Cog load.""" bot.add_cog(LoveCalculator(bot)) - log.info("LoveCalculator cog loaded") diff --git a/bot/exts/valentines/movie_generator.py b/bot/exts/valentines/movie_generator.py index ce1d7d5b..0843175a 100644 --- a/bot/exts/valentines/movie_generator.py +++ b/bot/exts/valentines/movie_generator.py @@ -60,4 +60,3 @@ class RomanceMovieFinder(commands.Cog): def setup(bot: commands.Bot) -> None: """Romance movie Cog load.""" bot.add_cog(RomanceMovieFinder(bot)) - log.info("RomanceMovieFinder cog loaded") diff --git a/bot/exts/valentines/myvalenstate.py b/bot/exts/valentines/myvalenstate.py index 0256c39a..7d8737c4 100644 --- a/bot/exts/valentines/myvalenstate.py +++ b/bot/exts/valentines/myvalenstate.py @@ -84,4 +84,3 @@ class MyValenstate(commands.Cog): def setup(bot: commands.Bot) -> None: """Valenstate Cog load.""" bot.add_cog(MyValenstate(bot)) - log.info("MyValenstate cog loaded") diff --git a/bot/exts/valentines/pickuplines.py b/bot/exts/valentines/pickuplines.py index 8b2c9822..74c7e68b 100644 --- a/bot/exts/valentines/pickuplines.py +++ b/bot/exts/valentines/pickuplines.py @@ -42,4 +42,3 @@ class PickupLine(commands.Cog): def setup(bot: commands.Bot) -> None: """Pickup lines Cog load.""" bot.add_cog(PickupLine(bot)) - log.info('PickupLine cog loaded') diff --git a/bot/exts/valentines/savethedate.py b/bot/exts/valentines/savethedate.py index e0bc3904..ac38d279 100644 --- a/bot/exts/valentines/savethedate.py +++ b/bot/exts/valentines/savethedate.py @@ -39,4 +39,3 @@ class SaveTheDate(commands.Cog): def setup(bot: commands.Bot) -> None: """Save the date Cog Load.""" bot.add_cog(SaveTheDate(bot)) - log.info("SaveTheDate cog loaded") diff --git a/bot/exts/valentines/valentine_zodiac.py b/bot/exts/valentines/valentine_zodiac.py index c8d77e75..1a1273aa 100644 --- a/bot/exts/valentines/valentine_zodiac.py +++ b/bot/exts/valentines/valentine_zodiac.py @@ -55,4 +55,3 @@ class ValentineZodiac(commands.Cog): def setup(bot: commands.Bot) -> None: """Valentine zodiac Cog load.""" bot.add_cog(ValentineZodiac(bot)) - log.info("ValentineZodiac cog loaded") diff --git a/bot/exts/valentines/whoisvalentine.py b/bot/exts/valentines/whoisvalentine.py index b8586dca..4ca0289c 100644 --- a/bot/exts/valentines/whoisvalentine.py +++ b/bot/exts/valentines/whoisvalentine.py @@ -50,4 +50,3 @@ class ValentineFacts(commands.Cog): def setup(bot: commands.Bot) -> None: """Who is Valentine Cog load.""" bot.add_cog(ValentineFacts(bot)) - log.info("ValentineFacts cog loaded") -- cgit v1.2.3 From c97a75a12999f638cd413d8e0dbd623102cdd1b8 Mon Sep 17 00:00:00 2001 From: kwzrd Date: Sun, 29 Mar 2020 13:30:30 +0200 Subject: Deseasonify: remove unused `set_username` method The method is a left-over from the old seasonal system. We no longer use it, the bot's username never changes, only the nickname. The amount of internal branching logic makes it difficult to maintain. --- bot/bot.py | 30 ------------------------------ 1 file changed, 30 deletions(-) (limited to 'bot/bot.py') diff --git a/bot/bot.py b/bot/bot.py index 3e20e3ab..b9ebb7f2 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -136,36 +136,6 @@ class SeasonalBot(commands.Bot): async with self.http_session.get(url) as resp: return await resp.read() - @mock_in_debug(return_value=True) - async def set_username(self, new_name: str, nick_only: bool = False) -> Optional[bool]: - """ - Set the bot username and/or nickname to given new name. - - Returns True/False based on success, or None if nickname fallback also failed. - """ - old_username = self.user.name - - if nick_only: - return await self.set_nickname(new_name) - - if old_username == new_name: - # since the username is correct, make sure nickname is removed - return await self.set_nickname() - - log.debug(f"Changing username to {new_name}") - with contextlib.suppress(discord.HTTPException): - await bot.user.edit(username=new_name, nick=None) - - if not new_name == self.member.display_name: - # name didn't change, try to changing nickname as fallback - if await self.set_nickname(new_name): - log.warning(f"Changing username failed, changed nickname instead.") - return False - log.warning(f"Changing username and nickname failed.") - return None - - return True - @mock_in_debug(return_value=True) async def set_nickname(self, new_name: str = None) -> bool: """Set the bot nickname in the main guild.""" -- cgit v1.2.3 From cb5870235be303f2a4aaffb9f26a42f06da32a97 Mon Sep 17 00:00:00 2001 From: kwzrd Date: Sun, 29 Mar 2020 14:26:28 +0200 Subject: Deseasonify: add AssetType enum --- bot/bot.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) (limited to 'bot/bot.py') diff --git a/bot/bot.py b/bot/bot.py index b9ebb7f2..76ce5607 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -1,5 +1,5 @@ import asyncio -import contextlib +import enum import logging import socket from typing import Optional @@ -15,7 +15,19 @@ from bot.utils.decorators import mock_in_debug log = logging.getLogger(__name__) -__all__ = ('SeasonalBot', 'bot') +__all__ = ("AssetType", "SeasonalBot", "bot") + + +class AssetType(enum.Enum): + """ + Discord media assets. + + The values match exactly the kwarg keys that can be passed to `Guild.edit` or `User.edit`. + """ + + banner = "banner" + avatar = "avatar" + server_icon = "icon" class SeasonalBot(commands.Bot): -- cgit v1.2.3 From abc62d3e09a39decfdd400ed6f2e9c631ef2eca1 Mon Sep 17 00:00:00 2001 From: kwzrd Date: Sun, 29 Mar 2020 14:29:36 +0200 Subject: Deseasonify: implement generic media asset setter Current `set_avatar`, `set_icon` and `set_banner` methods are almost identical - they only differ in the type of asset they upload. This leads to a lot of code repetition, especially w.r.t. error handling. We instead add a generic method that is parametrized by an AssetType param, and by the target entity (i.e. bot, or guild) that the asset should be applied to. All error handling can then be done in one place. Error handling methodology is adjusted - instead of suppressing errors, we catch and log them. Since we no longer determine whether the upload succeeded based on 'before' != 'after', this solves a bug where re-applying the same asset resulted in a warning-level log, triggering Sentry. --- bot/bot.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) (limited to 'bot/bot.py') diff --git a/bot/bot.py b/bot/bot.py index 76ce5607..0809f7a5 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -2,12 +2,12 @@ import asyncio import enum import logging import socket -from typing import Optional +from typing import Optional, Union import async_timeout import discord from aiohttp import AsyncResolver, ClientSession, TCPConnector -from discord import DiscordException, Embed +from discord import DiscordException, Embed, Guild, User from discord.ext import commands from bot.constants import Channels, Client @@ -85,6 +85,34 @@ class SeasonalBot(commands.Bot): return None return guild.me + async def _apply_asset(self, target: Union[Guild, User], asset: AssetType, url: str) -> bool: + """ + Internal method for applying media assets to the guild or the bot. + + This shouldn't be called directly. The purpose of this method is mainly generic + error handling to reduce needless code repetition. + + Return True if upload was successful, False otherwise. + """ + log.info(f"Attempting to set {asset.name}: {url}") + + kwargs = {asset.value: await self._fetch_image(url)} + try: + async with async_timeout.timeout(5): + await target.edit(**kwargs) + + except asyncio.TimeoutError: + log.info("Asset upload timed out") + return False + + except discord.HTTPException as discord_error: + log.exception("Asset upload failed", exc_info=discord_error) + return False + + else: + log.info(f"Asset successfully applied") + return True + @mock_in_debug(return_value=True) async def set_avatar(self, url: str) -> bool: """Sets the bot's avatar based on a URL.""" -- cgit v1.2.3 From 7d7a78ba90e3b054867498c48bbd8197454c22bc Mon Sep 17 00:00:00 2001 From: kwzrd Date: Sun, 29 Mar 2020 14:32:59 +0200 Subject: Deseasonify: use new `_apply_asset` setter The diff should demonstrate how much code repetition we prevent. We do not make use of `_apply_asset` for nickname changes - due to the comparative simplicity and conceptual difference, this method provides its own error handling. --- bot/bot.py | 80 ++++++++++++++++++++------------------------------------------ 1 file changed, 26 insertions(+), 54 deletions(-) (limited to 'bot/bot.py') diff --git a/bot/bot.py b/bot/bot.py index 0809f7a5..167b0ee7 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -115,60 +115,28 @@ class SeasonalBot(commands.Bot): @mock_in_debug(return_value=True) async def set_avatar(self, url: str) -> bool: - """Sets the bot's avatar based on a URL.""" - # Track old avatar hash for later comparison - old_avatar = bot.user.avatar - - image = await self._fetch_image(url) - with contextlib.suppress(discord.HTTPException, asyncio.TimeoutError): - async with async_timeout.timeout(5): - await bot.user.edit(avatar=image) - - if bot.user.avatar != old_avatar: - log.debug(f"Avatar changed to {url}") - return True - - log.warning(f"Changing avatar failed: {url}") - return False + """Set the bot's avatar to image at `url`.""" + return await self._apply_asset(self.user, AssetType.avatar, url) @mock_in_debug(return_value=True) async def set_banner(self, url: str) -> bool: - """Sets the guild's banner based on the provided `url`.""" + """Set the guild's banner to image at `url`.""" guild = bot.get_guild(Client.guild) - old_banner = guild.banner - - image = await self._fetch_image(url) - with contextlib.suppress(discord.HTTPException, asyncio.TimeoutError): - async with async_timeout.timeout(5): - await guild.edit(banner=image) - - new_banner = bot.get_guild(Client.guild).banner - if new_banner != old_banner: - log.debug(f"Banner changed to {url}") - return True + if guild is None: + log.info("Failed to get guild instance, aborting asset upload") + return False - log.warning(f"Changing banner failed: {url}") - return False + return await self._apply_asset(guild, AssetType.banner, url) @mock_in_debug(return_value=True) async def set_icon(self, url: str) -> bool: - """Sets the guild's icon based on a URL.""" + """Sets the guild's icon to image at `url`.""" guild = bot.get_guild(Client.guild) - # Track old icon hash for later comparison - old_icon = guild.icon - - image = await self._fetch_image(url) - with contextlib.suppress(discord.HTTPException, asyncio.TimeoutError): - async with async_timeout.timeout(5): - await guild.edit(icon=image) - - new_icon = bot.get_guild(Client.guild).icon - if new_icon != old_icon: - log.debug(f"Icon changed to {url}") - return True + if guild is None: + log.info("Failed to get guild instance, aborting asset upload") + return False - log.warning(f"Changing icon failed: {url}") - return False + return await self._apply_asset(guild, AssetType.server_icon, url) async def _fetch_image(self, url: str) -> bytes: """Retrieve an image based on a URL.""" @@ -177,18 +145,22 @@ class SeasonalBot(commands.Bot): return await resp.read() @mock_in_debug(return_value=True) - async def set_nickname(self, new_name: str = None) -> bool: - """Set the bot nickname in the main guild.""" - old_display_name = self.member.display_name - - if old_display_name == new_name: + async def set_nickname(self, new_name: str) -> bool: + """Set the bot nickname in the main guild to `new_name`.""" + member = self.member + if member is None: + log.info("Failed to get bot member instance, aborting asset upload") return False - log.debug(f"Changing nickname to {new_name}") - with contextlib.suppress(discord.HTTPException): - await self.member.edit(nick=new_name) - - return not old_display_name == self.member.display_name + log.info(f"Attempting to set nickname to {new_name}") + try: + await member.edit(nick=new_name) + except discord.HTTPException as discord_error: + log.exception("Setting nickname failed", exc_info=discord_error) + return False + else: + log.info("Nickname set successfully") + return True bot = SeasonalBot(command_prefix=Client.prefix) -- cgit v1.2.3 From 9781ec7bc06ae509766d2ec4a3d275b7e4acedea Mon Sep 17 00:00:00 2001 From: kwzrd Date: Sun, 29 Mar 2020 14:40:30 +0200 Subject: Deseasonify: adjust bot method defs to a more logical order --- bot/bot.py | 66 +++++++++++++++++++++++++++++++------------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) (limited to 'bot/bot.py') diff --git a/bot/bot.py b/bot/bot.py index 167b0ee7..00cc6e31 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -45,6 +45,14 @@ class SeasonalBot(commands.Bot): connector=TCPConnector(resolver=AsyncResolver(), family=socket.AF_INET) ) + @property + def member(self) -> Optional[discord.Member]: + """Retrieves the guild member object for the bot.""" + guild = bot.get_guild(Client.guild) + if not guild: + return None + return guild.me + def add_cog(self, cog: commands.Cog) -> None: """ Delegate to super to register `cog`. @@ -54,22 +62,6 @@ class SeasonalBot(commands.Bot): super().add_cog(cog) log.info(f"Cog loaded: {cog.qualified_name}") - async def send_log(self, title: str, details: str = None, *, icon: str = None) -> None: - """Send an embed message to the devlog channel.""" - devlog = self.get_channel(Channels.devlog) - - if not devlog: - log.warning("Log failed to send. Devlog channel not found.") - return - - if not icon: - icon = self.user.avatar_url_as(format="png") - - embed = Embed(description=details) - embed.set_author(name=title, icon_url=icon) - - await devlog.send(embed=embed) - async def on_command_error(self, context: commands.Context, exception: DiscordException) -> None: """Check command errors for UserInputError and reset the cooldown if thrown.""" if isinstance(exception, commands.UserInputError): @@ -77,13 +69,11 @@ class SeasonalBot(commands.Bot): else: await super().on_command_error(context, exception) - @property - def member(self) -> Optional[discord.Member]: - """Retrieves the guild member object for the bot.""" - guild = bot.get_guild(Client.guild) - if not guild: - return None - return guild.me + async def _fetch_image(self, url: str) -> bytes: + """Retrieve and read image from `url`.""" + log.debug(f"Getting image from: {url}") + async with self.http_session.get(url) as resp: + return await resp.read() async def _apply_asset(self, target: Union[Guild, User], asset: AssetType, url: str) -> bool: """ @@ -113,11 +103,6 @@ class SeasonalBot(commands.Bot): log.info(f"Asset successfully applied") return True - @mock_in_debug(return_value=True) - async def set_avatar(self, url: str) -> bool: - """Set the bot's avatar to image at `url`.""" - return await self._apply_asset(self.user, AssetType.avatar, url) - @mock_in_debug(return_value=True) async def set_banner(self, url: str) -> bool: """Set the guild's banner to image at `url`.""" @@ -138,11 +123,10 @@ class SeasonalBot(commands.Bot): return await self._apply_asset(guild, AssetType.server_icon, url) - async def _fetch_image(self, url: str) -> bytes: - """Retrieve an image based on a URL.""" - log.debug(f"Getting image from: {url}") - async with self.http_session.get(url) as resp: - return await resp.read() + @mock_in_debug(return_value=True) + async def set_avatar(self, url: str) -> bool: + """Set the bot's avatar to image at `url`.""" + return await self._apply_asset(self.user, AssetType.avatar, url) @mock_in_debug(return_value=True) async def set_nickname(self, new_name: str) -> bool: @@ -162,5 +146,21 @@ class SeasonalBot(commands.Bot): log.info("Nickname set successfully") return True + async def send_log(self, title: str, details: str = None, *, icon: str = None) -> None: + """Send an embed message to the devlog channel.""" + devlog = self.get_channel(Channels.devlog) + + if not devlog: + log.warning("Log failed to send. Devlog channel not found.") + return + + if not icon: + icon = self.user.avatar_url_as(format="png") + + embed = Embed(description=details) + embed.set_author(name=title, icon_url=icon) + + await devlog.send(embed=embed) + bot = SeasonalBot(command_prefix=Client.prefix) -- cgit v1.2.3 From 27327825fe7f840e2326721e4cc12261c89f1805 Mon Sep 17 00:00:00 2001 From: kwzrd Date: Sun, 29 Mar 2020 14:45:52 +0200 Subject: Deseasonify: use `self` rather than local `bot` instance var --- bot/bot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'bot/bot.py') diff --git a/bot/bot.py b/bot/bot.py index 00cc6e31..81b49386 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -48,7 +48,7 @@ class SeasonalBot(commands.Bot): @property def member(self) -> Optional[discord.Member]: """Retrieves the guild member object for the bot.""" - guild = bot.get_guild(Client.guild) + guild = self.get_guild(Client.guild) if not guild: return None return guild.me @@ -106,7 +106,7 @@ class SeasonalBot(commands.Bot): @mock_in_debug(return_value=True) async def set_banner(self, url: str) -> bool: """Set the guild's banner to image at `url`.""" - guild = bot.get_guild(Client.guild) + guild = self.get_guild(Client.guild) if guild is None: log.info("Failed to get guild instance, aborting asset upload") return False @@ -116,7 +116,7 @@ class SeasonalBot(commands.Bot): @mock_in_debug(return_value=True) async def set_icon(self, url: str) -> bool: """Sets the guild's icon to image at `url`.""" - guild = bot.get_guild(Client.guild) + guild = self.get_guild(Client.guild) if guild is None: log.info("Failed to get guild instance, aborting asset upload") return False -- cgit v1.2.3 From 2b9c64466026c40f7f2430c80cb47d67dda0756d Mon Sep 17 00:00:00 2001 From: kwzrd Date: Tue, 31 Mar 2020 16:22:10 +0200 Subject: Refactor: capitalize AssetType enum members Co-authored-by: MarkKoz --- bot/bot.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'bot/bot.py') diff --git a/bot/bot.py b/bot/bot.py index 81b49386..87575fde 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -25,9 +25,9 @@ class AssetType(enum.Enum): The values match exactly the kwarg keys that can be passed to `Guild.edit` or `User.edit`. """ - banner = "banner" - avatar = "avatar" - server_icon = "icon" + BANNER = "banner" + AVATAR = "avatar" + SERVER_ICON = "icon" class SeasonalBot(commands.Bot): @@ -111,7 +111,7 @@ class SeasonalBot(commands.Bot): 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, AssetType.BANNER, url) @mock_in_debug(return_value=True) async def set_icon(self, url: str) -> bool: @@ -121,12 +121,12 @@ class SeasonalBot(commands.Bot): log.info("Failed to get guild instance, aborting asset upload") return False - return await self._apply_asset(guild, AssetType.server_icon, url) + return await self._apply_asset(guild, AssetType.SERVER_ICON, url) @mock_in_debug(return_value=True) async def set_avatar(self, url: str) -> bool: """Set the bot's avatar to image at `url`.""" - return await self._apply_asset(self.user, AssetType.avatar, url) + return await self._apply_asset(self.user, AssetType.AVATAR, url) @mock_in_debug(return_value=True) async def set_nickname(self, new_name: str) -> bool: -- cgit v1.2.3