From 3e8690b6ac580ff49fac7022b74ba1b8f505ae83 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 28 Jul 2019 18:54:48 +0200 Subject: Revert 4d35f8f7137edb97e1124fa9087bd86399398047. --- bot/__main__.py | 1 + bot/cogs/events.py | 285 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 286 insertions(+) create mode 100644 bot/cogs/events.py diff --git a/bot/__main__.py b/bot/__main__.py index b3f80ef55..9bfd99098 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -31,6 +31,7 @@ bot.http_session = ClientSession( bot.api_client = APIClient(loop=asyncio.get_event_loop()) # Internal/debug +bot.load_extension("bot.cogs.events") bot.load_extension("bot.cogs.filtering") bot.load_extension("bot.cogs.logging") bot.load_extension("bot.cogs.modlog") diff --git a/bot/cogs/events.py b/bot/cogs/events.py new file mode 100644 index 000000000..160791fb0 --- /dev/null +++ b/bot/cogs/events.py @@ -0,0 +1,285 @@ +import logging + +from aiohttp import ClientResponseError +from discord import Colour, Embed, Member, Object +from discord.ext.commands import ( + BadArgument, Bot, BotMissingPermissions, + CommandError, CommandInvokeError, CommandNotFound, + Context, NoPrivateMessage, UserInputError +) + +from bot.cogs.modlog import ModLog +from bot.constants import ( + Channels, Colours, DEBUG_MODE, + Guild, Icons, Keys, + Roles, URLs +) +from bot.utils import chunks + +log = logging.getLogger(__name__) + +RESTORE_ROLES = (str(Roles.muted), str(Roles.announcements)) + + +class Events: + """No commands, just event handlers.""" + + def __init__(self, bot: Bot): + self.bot = bot + + @property + def mod_log(self) -> ModLog: + return self.bot.get_cog("ModLog") + + async def send_updated_users(self, *users, replace_all=False): + users = list(filter(lambda user: str(Roles.verified) in user["roles"], users)) + + for chunk in chunks(users, 1000): + response = None + + try: + if replace_all: + response = await self.bot.http_session.post( + url=URLs.site_user_api, + json=chunk, + headers={"X-API-Key": Keys.site_api} + ) + else: + response = await self.bot.http_session.put( + url=URLs.site_user_api, + json=chunk, + headers={"X-API-Key": Keys.site_api} + ) + + await response.json() # We do this to ensure we got a proper response from the site + except Exception: + if not response: + log.exception(f"Failed to send {len(chunk)} users") + else: + text = await response.text() + log.exception(f"Failed to send {len(chunk)} users", extra={"body": text}) + break # Stop right now, thank you very much + + result = {} + + if replace_all: + response = None + + try: + response = await self.bot.http_session.post( + url=URLs.site_user_complete_api, + headers={"X-API-Key": Keys.site_api} + ) + + result = await response.json() + except Exception: + if not response: + log.exception(f"Failed to send {len(chunk)} users") + else: + text = await response.text() + log.exception(f"Failed to send {len(chunk)} users", extra={"body": text}) + + return result + + async def send_delete_users(self, *users): + try: + response = await self.bot.http_session.delete( + url=URLs.site_user_api, + json=list(users), + headers={"X-API-Key": Keys.site_api} + ) + + return await response.json() + except Exception: + log.exception(f"Failed to delete {len(users)} users") + return {} + + async def get_user(self, user_id): + response = await self.bot.http_session.get( + url=URLs.site_user_api, + params={"user_id": user_id}, + headers={"X-API-Key": Keys.site_api} + ) + + resp = await response.json() + return resp["data"] + + async def on_command_error(self, ctx: Context, e: CommandError): + command = ctx.command + parent = None + + if command is not None: + parent = command.parent + + if parent and command: + help_command = (self.bot.get_command("help"), parent.name, command.name) + elif command: + help_command = (self.bot.get_command("help"), command.name) + else: + help_command = (self.bot.get_command("help"),) + + if hasattr(command, "on_error"): + log.debug(f"Command {command} has a local error handler, ignoring.") + return + + if isinstance(e, CommandNotFound) and not hasattr(ctx, "invoked_from_error_handler"): + tags_get_command = self.bot.get_command("tags get") + ctx.invoked_from_error_handler = True + + # Return to not raise the exception + return await ctx.invoke(tags_get_command, tag_name=ctx.invoked_with) + elif isinstance(e, BadArgument): + await ctx.send(f"Bad argument: {e}\n") + await ctx.invoke(*help_command) + elif isinstance(e, UserInputError): + await ctx.invoke(*help_command) + elif isinstance(e, NoPrivateMessage): + await ctx.send("Sorry, this command can't be used in a private message!") + elif isinstance(e, BotMissingPermissions): + await ctx.send( + f"Sorry, it looks like I don't have the permissions I need to do that.\n\n" + f"Here's what I'm missing: **{e.missing_perms}**" + ) + elif isinstance(e, CommandInvokeError): + if isinstance(e.original, ClientResponseError): + if e.original.code == 404: + await ctx.send("There does not seem to be anything matching your query.") + else: + await ctx.send("BEEP BEEP UNKNOWN API ERROR!=?!??!?!?!?") + + else: + await ctx.send( + f"Sorry, an unexpected error occurred. Please let us know!\n\n```{e}```" + ) + raise e.original + raise e + + async def on_ready(self): + users = [] + + for member in self.bot.get_guild(Guild.id).members: # type: Member + roles = [str(r.id) for r in member.roles] # type: List[int] + + users.append({ + "avatar": member.avatar_url_as(format="png"), + "user_id": str(member.id), + "roles": roles, + "username": member.name, + "discriminator": member.discriminator + }) + + if users: + log.info(f"{len(users)} user roles to be updated") + + done = await self.send_updated_users(*users, replace_all=True) + + if any(done.values()): + embed = Embed( + title="Users updated" + ) + + for key, value in done.items(): + if value: + if key == "deleted_oauth": + key = "Deleted (OAuth)" + elif key == "deleted_jam_profiles": + key = "Deleted (Jammer Profiles)" + elif key == "deleted_responses": + key = "Deleted (Jam Form Responses)" + elif key == "jam_bans": + key = "Ex-Jammer Bans" + else: + key = key.title() + + embed.add_field( + name=key, value=str(value) + ) + + if not DEBUG_MODE: + await self.bot.get_channel(Channels.devlog).send( + embed=embed + ) + + async def on_member_update(self, before: Member, after: Member): + if ( + before.roles == after.roles + and before.name == after.name + and before.discriminator == after.discriminator + and before.avatar == after.avatar): + return + + before_role_names = [role.name for role in before.roles] # type: List[str] + after_role_names = [role.name for role in after.roles] # type: List[str] + role_ids = [str(r.id) for r in after.roles] # type: List[str] + + log.debug(f"{before.display_name} roles changing from {before_role_names} to {after_role_names}") + + changes = await self.send_updated_users({ + "avatar": after.avatar_url_as(format="png"), + "user_id": str(after.id), + "roles": role_ids, + "username": after.name, + "discriminator": after.discriminator + }) + + log.debug(f"User {after.id} updated; changes: {changes}") + + async def on_member_join(self, member: Member): + role_ids = [str(r.id) for r in member.roles] # type: List[str] + new_roles = [] + + try: + user_objs = await self.get_user(str(member.id)) + except Exception as e: + log.exception("Failed to persist roles") + + await self.mod_log.send_log_message( + Icons.crown_red, Colour(Colours.soft_red), "Failed to persist roles", + f"```py\n{e}\n```", + member.avatar_url_as(static_format="png") + ) + else: + if user_objs: + old_roles = user_objs[0].get("roles", []) + + for role in RESTORE_ROLES: + if role in old_roles: + new_roles.append(Object(int(role))) + + for role in new_roles: + if str(role) not in role_ids: + role_ids.append(str(role.id)) + + changes = await self.send_updated_users({ + "avatar": member.avatar_url_as(format="png"), + "user_id": str(member.id), + "roles": role_ids, + "username": member.name, + "discriminator": member.discriminator + }) + + log.debug(f"User {member.id} joined; changes: {changes}") + + if new_roles: + await member.add_roles( + *new_roles, + reason="Roles restored" + ) + + await self.mod_log.send_log_message( + Icons.crown_blurple, Colour.blurple(), "Roles restored", + f"Restored {len(new_roles)} roles", + member.avatar_url_as(static_format="png") + ) + + async def on_member_remove(self, member: Member): + changes = await self.send_delete_users({ + "user_id": str(member.id) + }) + + log.debug(f"User {member.id} left; changes: {changes}") + + +def setup(bot): + bot.add_cog(Events(bot)) + log.info("Cog loaded: Events") -- cgit v1.2.3 From d1bd7c4e4cc9699d3c34ab56046e17f774991e28 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 28 Jul 2019 18:54:59 +0200 Subject: Drop user updating from `events` cog. --- bot/cogs/events.py | 203 ----------------------------------------------------- 1 file changed, 203 deletions(-) diff --git a/bot/cogs/events.py b/bot/cogs/events.py index 160791fb0..d69af365b 100644 --- a/bot/cogs/events.py +++ b/bot/cogs/events.py @@ -27,83 +27,6 @@ class Events: def __init__(self, bot: Bot): self.bot = bot - @property - def mod_log(self) -> ModLog: - return self.bot.get_cog("ModLog") - - async def send_updated_users(self, *users, replace_all=False): - users = list(filter(lambda user: str(Roles.verified) in user["roles"], users)) - - for chunk in chunks(users, 1000): - response = None - - try: - if replace_all: - response = await self.bot.http_session.post( - url=URLs.site_user_api, - json=chunk, - headers={"X-API-Key": Keys.site_api} - ) - else: - response = await self.bot.http_session.put( - url=URLs.site_user_api, - json=chunk, - headers={"X-API-Key": Keys.site_api} - ) - - await response.json() # We do this to ensure we got a proper response from the site - except Exception: - if not response: - log.exception(f"Failed to send {len(chunk)} users") - else: - text = await response.text() - log.exception(f"Failed to send {len(chunk)} users", extra={"body": text}) - break # Stop right now, thank you very much - - result = {} - - if replace_all: - response = None - - try: - response = await self.bot.http_session.post( - url=URLs.site_user_complete_api, - headers={"X-API-Key": Keys.site_api} - ) - - result = await response.json() - except Exception: - if not response: - log.exception(f"Failed to send {len(chunk)} users") - else: - text = await response.text() - log.exception(f"Failed to send {len(chunk)} users", extra={"body": text}) - - return result - - async def send_delete_users(self, *users): - try: - response = await self.bot.http_session.delete( - url=URLs.site_user_api, - json=list(users), - headers={"X-API-Key": Keys.site_api} - ) - - return await response.json() - except Exception: - log.exception(f"Failed to delete {len(users)} users") - return {} - - async def get_user(self, user_id): - response = await self.bot.http_session.get( - url=URLs.site_user_api, - params={"user_id": user_id}, - headers={"X-API-Key": Keys.site_api} - ) - - resp = await response.json() - return resp["data"] - async def on_command_error(self, ctx: Context, e: CommandError): command = ctx.command parent = None @@ -154,132 +77,6 @@ class Events: raise e.original raise e - async def on_ready(self): - users = [] - - for member in self.bot.get_guild(Guild.id).members: # type: Member - roles = [str(r.id) for r in member.roles] # type: List[int] - - users.append({ - "avatar": member.avatar_url_as(format="png"), - "user_id": str(member.id), - "roles": roles, - "username": member.name, - "discriminator": member.discriminator - }) - - if users: - log.info(f"{len(users)} user roles to be updated") - - done = await self.send_updated_users(*users, replace_all=True) - - if any(done.values()): - embed = Embed( - title="Users updated" - ) - - for key, value in done.items(): - if value: - if key == "deleted_oauth": - key = "Deleted (OAuth)" - elif key == "deleted_jam_profiles": - key = "Deleted (Jammer Profiles)" - elif key == "deleted_responses": - key = "Deleted (Jam Form Responses)" - elif key == "jam_bans": - key = "Ex-Jammer Bans" - else: - key = key.title() - - embed.add_field( - name=key, value=str(value) - ) - - if not DEBUG_MODE: - await self.bot.get_channel(Channels.devlog).send( - embed=embed - ) - - async def on_member_update(self, before: Member, after: Member): - if ( - before.roles == after.roles - and before.name == after.name - and before.discriminator == after.discriminator - and before.avatar == after.avatar): - return - - before_role_names = [role.name for role in before.roles] # type: List[str] - after_role_names = [role.name for role in after.roles] # type: List[str] - role_ids = [str(r.id) for r in after.roles] # type: List[str] - - log.debug(f"{before.display_name} roles changing from {before_role_names} to {after_role_names}") - - changes = await self.send_updated_users({ - "avatar": after.avatar_url_as(format="png"), - "user_id": str(after.id), - "roles": role_ids, - "username": after.name, - "discriminator": after.discriminator - }) - - log.debug(f"User {after.id} updated; changes: {changes}") - - async def on_member_join(self, member: Member): - role_ids = [str(r.id) for r in member.roles] # type: List[str] - new_roles = [] - - try: - user_objs = await self.get_user(str(member.id)) - except Exception as e: - log.exception("Failed to persist roles") - - await self.mod_log.send_log_message( - Icons.crown_red, Colour(Colours.soft_red), "Failed to persist roles", - f"```py\n{e}\n```", - member.avatar_url_as(static_format="png") - ) - else: - if user_objs: - old_roles = user_objs[0].get("roles", []) - - for role in RESTORE_ROLES: - if role in old_roles: - new_roles.append(Object(int(role))) - - for role in new_roles: - if str(role) not in role_ids: - role_ids.append(str(role.id)) - - changes = await self.send_updated_users({ - "avatar": member.avatar_url_as(format="png"), - "user_id": str(member.id), - "roles": role_ids, - "username": member.name, - "discriminator": member.discriminator - }) - - log.debug(f"User {member.id} joined; changes: {changes}") - - if new_roles: - await member.add_roles( - *new_roles, - reason="Roles restored" - ) - - await self.mod_log.send_log_message( - Icons.crown_blurple, Colour.blurple(), "Roles restored", - f"Restored {len(new_roles)} roles", - member.avatar_url_as(static_format="png") - ) - - async def on_member_remove(self, member: Member): - changes = await self.send_delete_users({ - "user_id": str(member.id) - }) - - log.debug(f"User {member.id} left; changes: {changes}") - - def setup(bot): bot.add_cog(Events(bot)) log.info("Cog loaded: Events") -- cgit v1.2.3 From a1ba380f90e370548608035ed0c32794f95a5a5b Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 28 Jul 2019 18:58:24 +0200 Subject: Move error handling to more descriptive `ErrorHandler` cog. --- bot/__main__.py | 2 +- bot/cogs/error_handler.py | 78 ++++++++++++++++++++++++++++++++++++++++++++ bot/cogs/events.py | 82 ----------------------------------------------- 3 files changed, 79 insertions(+), 83 deletions(-) create mode 100644 bot/cogs/error_handler.py delete mode 100644 bot/cogs/events.py diff --git a/bot/__main__.py b/bot/__main__.py index 9bfd99098..4bc7d1202 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -31,7 +31,7 @@ bot.http_session = ClientSession( bot.api_client = APIClient(loop=asyncio.get_event_loop()) # Internal/debug -bot.load_extension("bot.cogs.events") +bot.load_extension("bot.cogs.error_handler") bot.load_extension("bot.cogs.filtering") bot.load_extension("bot.cogs.logging") bot.load_extension("bot.cogs.modlog") diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py new file mode 100644 index 000000000..2db133372 --- /dev/null +++ b/bot/cogs/error_handler.py @@ -0,0 +1,78 @@ +import logging + +from aiohttp import ClientResponseError +from discord.ext.commands import Bot, Context +from discord.ext.commands import ( + BadArgument, + BotMissingPermissions, + CommandError, + CommandInvokeError, + CommandNotFound, + NoPrivateMessage, + UserInputError, +) + + +log = logging.getLogger(__name__) + + +class ErrorHandler: + """Handles errors emttted from commands.""" + + def __init__(self, bot: Bot): + self.bot = bot + + async def on_command_error(self, ctx: Context, e: CommandError): + command = ctx.command + parent = None + + if command is not None: + parent = command.parent + + if parent and command: + help_command = (self.bot.get_command("help"), parent.name, command.name) + elif command: + help_command = (self.bot.get_command("help"), command.name) + else: + help_command = (self.bot.get_command("help"),) + + if hasattr(command, "on_error"): + log.debug(f"Command {command} has a local error handler, ignoring.") + return + + if isinstance(e, CommandNotFound) and not hasattr(ctx, "invoked_from_error_handler"): + tags_get_command = self.bot.get_command("tags get") + ctx.invoked_from_error_handler = True + + # Return to not raise the exception + return await ctx.invoke(tags_get_command, tag_name=ctx.invoked_with) + elif isinstance(e, BadArgument): + await ctx.send(f"Bad argument: {e}\n") + await ctx.invoke(*help_command) + elif isinstance(e, UserInputError): + await ctx.invoke(*help_command) + elif isinstance(e, NoPrivateMessage): + await ctx.send("Sorry, this command can't be used in a private message!") + elif isinstance(e, BotMissingPermissions): + await ctx.send( + f"Sorry, it looks like I don't have the permissions I need to do that.\n\n" + f"Here's what I'm missing: **{e.missing_perms}**" + ) + elif isinstance(e, CommandInvokeError): + if isinstance(e.original, ClientResponseError): + if e.original.code == 404: + await ctx.send("There does not seem to be anything matching your query.") + else: + await ctx.send("BEEP BEEP UNKNOWN API ERROR!=?!??!?!?!?") + + else: + await ctx.send( + f"Sorry, an unexpected error occurred. Please let us know!\n\n```{e}```" + ) + raise e.original + raise e + + +def setup(bot: Bot): + bot.add_cog(ErrorHandler(bot)) + log.info("Cog loaded: Events") diff --git a/bot/cogs/events.py b/bot/cogs/events.py deleted file mode 100644 index d69af365b..000000000 --- a/bot/cogs/events.py +++ /dev/null @@ -1,82 +0,0 @@ -import logging - -from aiohttp import ClientResponseError -from discord import Colour, Embed, Member, Object -from discord.ext.commands import ( - BadArgument, Bot, BotMissingPermissions, - CommandError, CommandInvokeError, CommandNotFound, - Context, NoPrivateMessage, UserInputError -) - -from bot.cogs.modlog import ModLog -from bot.constants import ( - Channels, Colours, DEBUG_MODE, - Guild, Icons, Keys, - Roles, URLs -) -from bot.utils import chunks - -log = logging.getLogger(__name__) - -RESTORE_ROLES = (str(Roles.muted), str(Roles.announcements)) - - -class Events: - """No commands, just event handlers.""" - - def __init__(self, bot: Bot): - self.bot = bot - - async def on_command_error(self, ctx: Context, e: CommandError): - command = ctx.command - parent = None - - if command is not None: - parent = command.parent - - if parent and command: - help_command = (self.bot.get_command("help"), parent.name, command.name) - elif command: - help_command = (self.bot.get_command("help"), command.name) - else: - help_command = (self.bot.get_command("help"),) - - if hasattr(command, "on_error"): - log.debug(f"Command {command} has a local error handler, ignoring.") - return - - if isinstance(e, CommandNotFound) and not hasattr(ctx, "invoked_from_error_handler"): - tags_get_command = self.bot.get_command("tags get") - ctx.invoked_from_error_handler = True - - # Return to not raise the exception - return await ctx.invoke(tags_get_command, tag_name=ctx.invoked_with) - elif isinstance(e, BadArgument): - await ctx.send(f"Bad argument: {e}\n") - await ctx.invoke(*help_command) - elif isinstance(e, UserInputError): - await ctx.invoke(*help_command) - elif isinstance(e, NoPrivateMessage): - await ctx.send("Sorry, this command can't be used in a private message!") - elif isinstance(e, BotMissingPermissions): - await ctx.send( - f"Sorry, it looks like I don't have the permissions I need to do that.\n\n" - f"Here's what I'm missing: **{e.missing_perms}**" - ) - elif isinstance(e, CommandInvokeError): - if isinstance(e.original, ClientResponseError): - if e.original.code == 404: - await ctx.send("There does not seem to be anything matching your query.") - else: - await ctx.send("BEEP BEEP UNKNOWN API ERROR!=?!??!?!?!?") - - else: - await ctx.send( - f"Sorry, an unexpected error occurred. Please let us know!\n\n```{e}```" - ) - raise e.original - raise e - -def setup(bot): - bot.add_cog(Events(bot)) - log.info("Cog loaded: Events") -- cgit v1.2.3 From 58dc7fb07e2f4a12f65d91641fd78ab8c10a7933 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 28 Jul 2019 19:01:07 +0200 Subject: Handle more API status codes. --- bot/cogs/error_handler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py index 2db133372..0bb2faf43 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -62,8 +62,12 @@ class ErrorHandler: if isinstance(e.original, ClientResponseError): if e.original.code == 404: await ctx.send("There does not seem to be anything matching your query.") + elif e.original.code == 400: + await ctx.send("According to the API, your request is malformed.") + elif 500 <= e.original.code < 600: + await ctx.send("Sorry, there seems to be an internal issue with the API.") else: - await ctx.send("BEEP BEEP UNKNOWN API ERROR!=?!??!?!?!?") + await ctx.send(f"Got an unexpected status code from the API (`{e.original.code}`).") else: await ctx.send( -- cgit v1.2.3 From 528c5749ff410cbcdd3982d30ba4d966860282ef Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 4 Aug 2019 20:22:27 +0200 Subject: Raise specific exception for non-200s. --- bot/api.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/bot/api.py b/bot/api.py index 2e1a239ba..4f4ffeff3 100644 --- a/bot/api.py +++ b/bot/api.py @@ -1,3 +1,4 @@ +import typing from urllib.parse import quote as quote_url import aiohttp @@ -5,6 +6,10 @@ import aiohttp from .constants import Keys, URLs +class ResponseCodeError(typing.NamedTuple, ValueError): + response: aiohttp.ClientResponse + + class APIClient: def __init__(self, **kwargs): auth_headers = { @@ -16,33 +21,40 @@ class APIClient: else: kwargs['headers'] = auth_headers - self.session = aiohttp.ClientSession( - **kwargs, - raise_for_status=True - ) + self.session = aiohttp.ClientSession(**kwargs) @staticmethod def _url_for(endpoint: str): return f"{URLs.site_schema}{URLs.site_api}/{quote_url(endpoint)}" - async def get(self, endpoint: str, *args, **kwargs): + def maybe_raise_for_status(self, response: aiohttp.ClientResponse, should_raise: bool): + if should_raise and response.status_code >= 400: + raise ResponseCodeError(response=response) + + async def get(self, endpoint: str, *args, raise_for_status: bool = True, **kwargs): async with self.session.get(self._url_for(endpoint), *args, **kwargs) as resp: + self.maybe_raise_for_status(resp, raise_for_status) return await resp.json() - async def patch(self, endpoint: str, *args, **kwargs): + async def patch(self, endpoint: str, *args, raise_for_status: bool = True, **kwargs): async with self.session.patch(self._url_for(endpoint), *args, **kwargs) as resp: + self.maybe_raise_for_status(resp, raise_for_status) return await resp.json() - async def post(self, endpoint: str, *args, **kwargs): + async def post(self, endpoint: str, *args, raise_for_status: bool = True,**kwargs): async with self.session.post(self._url_for(endpoint), *args, **kwargs) as resp: + self.maybe_raise_for_status(resp, raise_for_status) return await resp.json() - async def put(self, endpoint: str, *args, **kwargs): + async def put(self, endpoint: str, *args, raise_for_status: bool = True, **kwargs): async with self.session.put(self._url_for(endpoint), *args, **kwargs) as resp: + self.maybe_raise_for_status(resp, raise_for_status) return await resp.json() - async def delete(self, endpoint: str, *args, **kwargs): + async def delete(self, endpoint: str, *args, raise_for_status: bool = True, **kwargs): async with self.session.delete(self._url_for(endpoint), *args, **kwargs) as resp: if resp.status == 204: return None + + self.maybe_raise_for_status(resp, raise_for_status) return await resp.json() -- cgit v1.2.3 From 7cb8a8181cf721c4b7539faca054994ce76a4685 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Mon, 5 Aug 2019 20:36:20 +0200 Subject: Update code to make use of the new `ResponseCodeError`. --- bot/api.py | 2 +- bot/cogs/error_handler.py | 16 ++++++++++------ bot/cogs/sync/cog.py | 10 +++++----- bot/cogs/watchchannels/talentpool.py | 6 +++--- bot/cogs/watchchannels/watchchannel.py | 6 +++--- 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/bot/api.py b/bot/api.py index 4f4ffeff3..935ff699f 100644 --- a/bot/api.py +++ b/bot/api.py @@ -41,7 +41,7 @@ class APIClient: self.maybe_raise_for_status(resp, raise_for_status) return await resp.json() - async def post(self, endpoint: str, *args, raise_for_status: bool = True,**kwargs): + async def post(self, endpoint: str, *args, raise_for_status: bool = True, **kwargs): async with self.session.post(self._url_for(endpoint), *args, **kwargs) as resp: self.maybe_raise_for_status(resp, raise_for_status) return await resp.json() diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py index 0bb2faf43..b6ca7fccf 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -1,6 +1,5 @@ import logging -from aiohttp import ClientResponseError from discord.ext.commands import Bot, Context from discord.ext.commands import ( BadArgument, @@ -12,6 +11,8 @@ from discord.ext.commands import ( UserInputError, ) +from bot.api import ResponseCodeError + log = logging.getLogger(__name__) @@ -59,15 +60,18 @@ class ErrorHandler: f"Here's what I'm missing: **{e.missing_perms}**" ) elif isinstance(e, CommandInvokeError): - if isinstance(e.original, ClientResponseError): - if e.original.code == 404: + if isinstance(e.original, ResponseCodeError): + if e.original.response.status_code == 404: await ctx.send("There does not seem to be anything matching your query.") - elif e.original.code == 400: + elif e.original.response.status_code == 400: await ctx.send("According to the API, your request is malformed.") - elif 500 <= e.original.code < 600: + elif 500 <= e.original.response.status_code < 600: await ctx.send("Sorry, there seems to be an internal issue with the API.") else: - await ctx.send(f"Got an unexpected status code from the API (`{e.original.code}`).") + await ctx.send( + "Got an unexpected status code from the " + f"API (`{e.original.response.code}`)." + ) else: await ctx.send( diff --git a/bot/cogs/sync/cog.py b/bot/cogs/sync/cog.py index ab591ebf8..9e71f749d 100644 --- a/bot/cogs/sync/cog.py +++ b/bot/cogs/sync/cog.py @@ -1,12 +1,12 @@ import logging from typing import Callable, Iterable -import aiohttp from discord import Guild, Member, Role from discord.ext import commands from discord.ext.commands import Bot from bot import constants +from bot.api import ResponseCodeError from bot.cogs.sync import syncers log = logging.getLogger(__name__) @@ -94,9 +94,9 @@ class Sync: # fields that may have changed since the last time we've seen them. await self.bot.api_client.put('bot/users/' + str(member.id), json=packed) - except aiohttp.client_exceptions.ClientResponseError as e: + except ResponseCodeError as e: # If we didn't get 404, something else broke - propagate it up. - if e.status != 404: + if e.response.status_code != 404: raise got_error = True # yikes @@ -137,8 +137,8 @@ class Sync: 'roles': sorted(role.id for role in after.roles) } ) - except aiohttp.client_exceptions.ClientResponseError as e: - if e.status != 404: + except ResponseCodeError as e: + if e.response.status_code != 404: raise log.warning( diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py index 6fbe2bc03..44bf6371b 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -3,10 +3,10 @@ import textwrap from collections import ChainMap from typing import Union -from aiohttp.client_exceptions import ClientResponseError from discord import Color, Embed, Member, User from discord.ext.commands import Context, group +from bot.api import ResponseCodeError from bot.constants import Channels, Guild, Roles, Webhooks from bot.decorators import with_role from bot.pagination import LinePaginator @@ -170,8 +170,8 @@ class TalentPool(WatchChannel): """ try: nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}") - except ClientResponseError as e: - if e.status == 404: + except ResponseCodeError as e: + if e.response.status_code == 404: self.log.trace(f"Nomination API 404: Can't nomination with id {nomination_id}") await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`") return diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py index fe6d6bb6e..3a24e3f21 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/cogs/watchchannels/watchchannel.py @@ -8,11 +8,11 @@ from collections import defaultdict, deque from dataclasses import dataclass from typing import Optional -import aiohttp import discord from discord import Color, Embed, Message, Object, errors from discord.ext.commands import BadArgument, Bot, Context +from bot.api import ResponseCodeError from bot.cogs.modlog import ModLog from bot.constants import BigBrother as BigBrotherConfig, Guild as GuildConfig, Icons from bot.pagination import LinePaginator @@ -157,8 +157,8 @@ class WatchChannel(ABC): """ try: data = await self.bot.api_client.get(self.api_endpoint, params=self.api_default_params) - except aiohttp.ClientResponseError as e: - self.log.exception(f"Failed to fetch the watched users from the API", exc_info=e) + except ResponseCodeError as err: + self.log.exception(f"Failed to fetch the watched users from the API", exc_info=err) return False self.watched_users = defaultdict(dict) -- cgit v1.2.3 From 0e73db0aab6845956bf40493a93c07c4d81e81fe Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Mon, 5 Aug 2019 21:11:14 +0200 Subject: Finalize error handling. --- bot/api.py | 8 ++++---- bot/cogs/error_handler.py | 13 ++++++++----- bot/cogs/sync/cog.py | 4 ++-- bot/cogs/watchchannels/talentpool.py | 2 +- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/bot/api.py b/bot/api.py index 935ff699f..e926a262e 100644 --- a/bot/api.py +++ b/bot/api.py @@ -1,4 +1,3 @@ -import typing from urllib.parse import quote as quote_url import aiohttp @@ -6,8 +5,9 @@ import aiohttp from .constants import Keys, URLs -class ResponseCodeError(typing.NamedTuple, ValueError): - response: aiohttp.ClientResponse +class ResponseCodeError(ValueError): + def __init__(self, response: aiohttp.ClientResponse): + self.response = response class APIClient: @@ -28,7 +28,7 @@ class APIClient: return f"{URLs.site_schema}{URLs.site_api}/{quote_url(endpoint)}" def maybe_raise_for_status(self, response: aiohttp.ClientResponse, should_raise: bool): - if should_raise and response.status_code >= 400: + if should_raise and response.status >= 400: raise ResponseCodeError(response=response) async def get(self, endpoint: str, *args, raise_for_status: bool = True, **kwargs): diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py index b6ca7fccf..25aa177e1 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -1,6 +1,5 @@ import logging -from discord.ext.commands import Bot, Context from discord.ext.commands import ( BadArgument, BotMissingPermissions, @@ -10,6 +9,7 @@ from discord.ext.commands import ( NoPrivateMessage, UserInputError, ) +from discord.ext.commands import Bot, Context from bot.api import ResponseCodeError @@ -61,11 +61,13 @@ class ErrorHandler: ) elif isinstance(e, CommandInvokeError): if isinstance(e.original, ResponseCodeError): - if e.original.response.status_code == 404: + if e.original.response.status == 404: await ctx.send("There does not seem to be anything matching your query.") - elif e.original.response.status_code == 400: + elif e.original.response.status == 400: + content = await e.original.resopnse.json() + log.debug("API gave bad request on command. Response: %r.", content) await ctx.send("According to the API, your request is malformed.") - elif 500 <= e.original.response.status_code < 600: + elif 500 <= e.original.response.status < 600: await ctx.send("Sorry, there seems to be an internal issue with the API.") else: await ctx.send( @@ -78,7 +80,8 @@ class ErrorHandler: f"Sorry, an unexpected error occurred. Please let us know!\n\n```{e}```" ) raise e.original - raise e + else: + raise e def setup(bot: Bot): diff --git a/bot/cogs/sync/cog.py b/bot/cogs/sync/cog.py index 9e71f749d..222c1668b 100644 --- a/bot/cogs/sync/cog.py +++ b/bot/cogs/sync/cog.py @@ -96,7 +96,7 @@ class Sync: except ResponseCodeError as e: # If we didn't get 404, something else broke - propagate it up. - if e.response.status_code != 404: + if e.response.status != 404: raise got_error = True # yikes @@ -138,7 +138,7 @@ class Sync: } ) except ResponseCodeError as e: - if e.response.status_code != 404: + if e.response.status != 404: raise log.warning( diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py index 44bf6371b..47d207d05 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -171,7 +171,7 @@ class TalentPool(WatchChannel): try: nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}") except ResponseCodeError as e: - if e.response.status_code == 404: + if e.response.status == 404: self.log.trace(f"Nomination API 404: Can't nomination with id {nomination_id}") await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`") return -- cgit v1.2.3 From bea4e8d24122a3ef80ba000d11a6bd68dad267a5 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Mon, 5 Aug 2019 21:37:12 +0200 Subject: Suppress response code errors on tag reinvoke. --- bot/cogs/error_handler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py index 25aa177e1..62e2e2d7b 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -1,3 +1,4 @@ +import contextlib import logging from discord.ext.commands import ( @@ -46,7 +47,8 @@ class ErrorHandler: ctx.invoked_from_error_handler = True # Return to not raise the exception - return await ctx.invoke(tags_get_command, tag_name=ctx.invoked_with) + with contextlib.suppress(ResponseCodeError): + return await ctx.invoke(tags_get_command, tag_name=ctx.invoked_with) elif isinstance(e, BadArgument): await ctx.send(f"Bad argument: {e}\n") await ctx.invoke(*help_command) -- cgit v1.2.3 From f6c73864c5851235fa26689f614bd6af0d347c9e Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Mon, 5 Aug 2019 21:39:01 +0200 Subject: Fix typo in bot/cogs/error_handler.py. Originally authored by @MarkKoz. Co-Authored-By: Mark --- bot/cogs/error_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py index 62e2e2d7b..5033c95cf 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -19,7 +19,7 @@ log = logging.getLogger(__name__) class ErrorHandler: - """Handles errors emttted from commands.""" + """Handles errors emitted from commands.""" def __init__(self, bot: Bot): self.bot = bot -- cgit v1.2.3 From 74f4162d2859333909c74b038ee3a1dcaee65c9c Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Mon, 5 Aug 2019 22:01:30 +0200 Subject: Be more helpful. --- bot/cogs/error_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py index 5033c95cf..de880fcf5 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -53,6 +53,7 @@ class ErrorHandler: await ctx.send(f"Bad argument: {e}\n") await ctx.invoke(*help_command) elif isinstance(e, UserInputError): + await ctx.send("Something about your input seems off. Check the arguments:") await ctx.invoke(*help_command) elif isinstance(e, NoPrivateMessage): await ctx.send("Sorry, this command can't be used in a private message!") -- cgit v1.2.3 From 7083e892efe1ba0cb33537dc182b593abdad272f Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Mon, 5 Aug 2019 22:19:30 +0200 Subject: Blame Mark. --- bot/cogs/error_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py index de880fcf5..2063df09d 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -67,7 +67,7 @@ class ErrorHandler: if e.original.response.status == 404: await ctx.send("There does not seem to be anything matching your query.") elif e.original.response.status == 400: - content = await e.original.resopnse.json() + content = await e.original.response.json() log.debug("API gave bad request on command. Response: %r.", content) await ctx.send("According to the API, your request is malformed.") elif 500 <= e.original.response.status < 600: -- cgit v1.2.3