diff options
Diffstat (limited to 'bot')
-rw-r--r-- | bot/bot.py | 98 | ||||
-rw-r--r-- | bot/decorators.py | 4 | ||||
-rw-r--r-- | bot/seasons/season.py | 242 |
3 files changed, 146 insertions, 198 deletions
@@ -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) diff --git a/bot/decorators.py b/bot/decorators.py index 58f67a15..d0371df4 100644 --- a/bot/decorators.py +++ b/bot/decorators.py @@ -20,7 +20,7 @@ class InChannelCheckFailure(CheckFailure): pass -def with_role(*role_ids: int) -> bool: +def with_role(*role_ids: int) -> typing.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 @@ -43,7 +43,7 @@ def with_role(*role_ids: int) -> bool: return commands.check(predicate) -def without_role(*role_ids: int) -> bool: +def without_role(*role_ids: int) -> typing.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 diff --git a/bot/seasons/season.py b/bot/seasons/season.py index e7b7a69c..2ebe3e63 100644 --- a/bot/seasons/season.py +++ b/bot/seasons/season.py @@ -1,4 +1,3 @@ -import asyncio import contextlib import datetime import importlib @@ -8,7 +7,6 @@ import pkgutil from pathlib import Path from typing import List, Optional, Tuple, Type, Union -import async_timeout import discord from discord.ext import commands @@ -136,27 +134,6 @@ class SeasonBase: """ return f"New Season, {self.name_clean}!" - async def get_icon(self, avatar: bool = False, index: int = 0) -> Tuple[bytes, str]: - """ - Retrieve the season's icon from the branding repository using the Season's icon attribute. - - This also returns the relative URL path for logging purposes - If `avatar` is True, uses optional bot-only avatar icon if present. - Returns the data for the given `index`, defaulting to the first item. - - The icon attribute must provide the url path, starting from the master branch base url, - including the starting slash. - e.g. `/logos/logo_seasonal/valentines/loved_up.png` - """ - icon = self.icon[index] - if avatar and self.bot_icon: - icon = self.bot_icon - - full_url = ICON_BASE_URL + icon - log.debug(f"Getting icon from: {full_url}") - async with bot.http_session.get(full_url) as resp: - return (await resp.read(), icon) - async def apply_username(self, *, debug: bool = False) -> Union[bool, None]: """ Applies the username for the current season. @@ -198,73 +175,6 @@ class SeasonBase: return result - async def apply_avatar(self) -> bool: - """ - Applies the avatar for the current season. - - Returns True if successful. - """ - # Track old avatar hash for later comparison - old_avatar = bot.user.avatar - - # Attempt the change - icon, name = await self.get_icon(avatar=True) - log.debug(f"Changing avatar to {name}") - with contextlib.suppress(discord.HTTPException, asyncio.TimeoutError): - async with async_timeout.timeout(5): - await bot.user.edit(avatar=icon) - - if bot.user.avatar != old_avatar: - log.debug(f"Avatar changed to {name}") - return True - - log.warning(f"Changing avatar failed: {name}") - return False - - async def apply_server_icon(self) -> bool: - """ - Applies the server icon for the current season. - - Returns True if was successful. - """ - guild = bot.get_guild(Client.guild) - - # Track old icon hash for later comparison - old_icon = guild.icon - - # Attempt the change - - icon, name = await self.get_icon(index=self.index) - - log.debug(f"Changing server icon to {name}") - - with contextlib.suppress(discord.HTTPException, asyncio.TimeoutError): - async with async_timeout.timeout(5): - await guild.edit(icon=icon, reason=f"Seasonbot Season Change: {self.name}") - - new_icon = bot.get_guild(Client.guild).icon - if new_icon != old_icon: - log.debug(f"Server icon changed to {name}") - return True - - log.warning(f"Changing server icon failed: {name}") - return False - - async def change_server_icon(self) -> bool: - """ - Changes the server icon. - - This only has an effect when the Season's icon attribute is a list, in which it cycles through. - Returns True if was successful. - """ - if len(self.icon) == 1: - return - - self.index += 1 - self.index %= len(self.icon) - - return await self.apply_server_icon() - async def announce_season(self) -> None: """ Announces a change in season in the announcement channel. @@ -364,37 +274,9 @@ class SeasonManager(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - self.season = get_season(date=datetime.datetime.utcnow()) - self.season_task = bot.loop.create_task(self.load_seasons()) - - # Figure out number of seconds until a minute past midnight - tomorrow = datetime.datetime.now() + datetime.timedelta(1) - midnight = datetime.datetime( - year=tomorrow.year, - month=tomorrow.month, - day=tomorrow.day, - hour=0, - minute=0, - second=0 - ) - self.sleep_time = (midnight - datetime.datetime.now()).seconds + 60 - - async def load_seasons(self) -> None: - """Asynchronous timer loop to check for a new season every midnight.""" - await self.bot.wait_until_ready() - await self.season.load() - - while True: - await asyncio.sleep(self.sleep_time) # Sleep until midnight - self.sleep_time = 86400 # Next time, sleep for 24 hours. - - # If the season has changed, load it. - new_season = get_season(date=datetime.datetime.utcnow()) - if new_season.name != self.season.name: - self.season = new_season - await self.season.load() - else: - await self.season.change_server_icon() + season_class = get_season_class("evergreen") + self.season = season_class() + self.season_task = bot.loop.create_task(self.season.load()) @with_role(Roles.moderator, Roles.admin, Roles.owner) @commands.command(name="season") @@ -449,101 +331,71 @@ class SeasonManager(commands.Cog): @with_role(Roles.moderator, Roles.admin, Roles.owner) @commands.group() - async def refresh(self, ctx: commands.Context) -> None: - """Refreshes certain seasonal elements without reloading seasons.""" + async def set(self, ctx: commands.Context) -> None: + """Change aspects of the bot or server.""" if not ctx.invoked_subcommand: await ctx.send_help(ctx.command) - @refresh.command(name="avatar") - async def refresh_avatar(self, ctx: commands.Context) -> None: - """Re-applies the bot avatar for the currently loaded season.""" - # Attempt the change - is_changed = await self.season.apply_avatar() + @set.command(name="avatar", aliases=["avy"]) + async def set_avatar(self, ctx: commands.Context, *, image_url: str = None) -> None: + """Sets the bot avatar.""" + if not image_url: + image_url = f"{ICON_BASE_URL}/logos/logo_seasonal/evergreen/logo_evergreen.png" + elif image_url.startswith("<"): + image_url = image_url.strip("<>") - if is_changed: - colour = ctx.guild.me.colour - title = "Avatar Refreshed" - else: - colour = discord.Colour.red() - title = "Avatar Failed to Refresh" + is_changed = await ctx.bot.set_avatar(image_url) - # Report back details - season_name = type(self.season).__name__ embed = discord.Embed( - description=f"**Season:** {season_name}\n**Avatar:** {self.season.bot_icon or self.season.icon}", - colour=colour + title="Avatar changed." if is_changed else "Avatar change failed.", + colour=discord.Colour.green() if is_changed else discord.Colour.red() ) - embed.set_author(name=title) - embed.set_thumbnail(url=bot.user.avatar_url_as(format="png")) await ctx.send(embed=embed) - @refresh.command(name="icon") - async def refresh_server_icon(self, ctx: commands.Context) -> None: - """Re-applies the server icon for the currently loaded season.""" - # Attempt the change - is_changed = await self.season.apply_server_icon() + @set.command(name="icon") + async def set_server_icon(self, ctx: commands.Context, *, image_url: str = None) -> None: + """Sets the server icon.""" + if not image_url: + image_url = f"{ICON_BASE_URL}/logos/logo_full/logo_full.png" + elif image_url.startswith("<"): + image_url = image_url.strip("<>") - if is_changed: - colour = ctx.guild.me.colour - title = "Server Icon Refreshed" - else: - colour = discord.Colour.red() - title = "Server Icon Failed to Refresh" + is_changed = await ctx.bot.set_icon(image_url) - # Report back details - season_name = type(self.season).__name__ embed = discord.Embed( - description=f"**Season:** {season_name}\n**Icon:** {self.season.icon}", - colour=colour + title="Server icon changed." if is_changed else "Server icon change failed.", + colour=discord.Colour.green() if is_changed else discord.Colour.red() ) - embed.set_author(name=title) - embed.set_thumbnail(url=bot.get_guild(Client.guild).icon_url_as(format="png")) await ctx.send(embed=embed) - @refresh.command(name="username", aliases=("name",)) - async def refresh_username(self, ctx: commands.Context) -> None: - """Re-applies the bot username for the currently loaded season.""" - old_username = str(bot.user) - old_display_name = ctx.guild.me.display_name - - # Attempt the change - is_changed = await self.season.apply_username() + @set.command(name="username", aliases=["name"]) + async def set_username(self, ctx: commands.Context, *, new_name: str = None) -> None: + """Sets the bot username.""" + new_name = new_name or "SeasonalBot" + is_changed = await ctx.bot.set_username(new_name) if is_changed: - colour = ctx.guild.me.colour - title = "Username Refreshed" - changed_element = "Username" - old_name = old_username - new_name = str(bot.user) + title = f"Username changed to {new_name}" else: - colour = discord.Colour.red() - - # If None, it's because it wasn't meant to change username - if is_changed is None: - title = "Nickname Refreshed" + if is_changed is False: + title = f"Username change failed, Nickname changed to {new_name} instead." else: - title = "Username Failed to Refresh" - changed_element = "Nickname" - old_name = old_display_name - new_name = self.season.bot_name + title = "Username change failed." - # Report back details - season_name = type(self.season).__name__ embed = discord.Embed( - description=f"**Season:** {season_name}\n" - f"**Old {changed_element}:** {old_name}\n" - f"**New {changed_element}:** {new_name}", - colour=colour + colour=discord.Colour.green() if is_changed else discord.Colour.red(), + title=title ) - embed.set_author(name=title) await ctx.send(embed=embed) - @with_role(Roles.moderator, Roles.admin, Roles.owner) - @commands.command() - async def announce(self, ctx: commands.Context) -> None: - """Announces the currently loaded season.""" - await self.season.announce_season() - - def cog_unload(self) -> None: - """Cancel season-related tasks on cog unload.""" - self.season_task.cancel() + @set.command(name="nickname", aliases=["nick"]) + async def set_nickname(self, ctx: commands.Context, *, new_name: str = None) -> None: + """Sets the bot nickname.""" + new_name = new_name or "SeasonalBot" + is_changed = await ctx.bot.set_nickname(new_name) + + embed = discord.Embed( + colour=discord.Colour.green() if is_changed else discord.Colour.red(), + title=f"Nickname changed to {new_name}" if is_changed else "Nickname change failed." + ) + await ctx.send(embed=embed) |