From 6bf86152b9eefc09e65d79537074f64904e04e04 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 12 Sep 2019 13:32:05 +0200 Subject: Fixed AntiSpam cog reload bug https://github.com/python-discord/bot/issues/411 The AntiSpam code suffered from a bug where the attribute self.muted_role was not defined after reloading the cog. The bug was caused by the cog setting the attribute in on_ready instead of directly in __init__. Fixed by setting the attribute in the __init__. Closes #411 --- bot/cogs/antispam.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index 0c6a02bf9..6104ec08b 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -12,7 +12,7 @@ from bot.constants import ( AntiSpam as AntiSpamConfig, Channels, Colours, DEBUG_MODE, Event, Filter, Guild as GuildConfig, Icons, - Roles, STAFF_ROLES, + STAFF_ROLES, ) @@ -35,16 +35,13 @@ RULE_FUNCTION_MAPPING = { class AntiSpam: def __init__(self, bot: Bot): self.bot = bot - self._muted_role = Object(Roles.muted) + role_id = AntiSpamConfig.punishment['role_id'] + self.muted_role = Object(role_id) @property def mod_log(self) -> ModLog: return self.bot.get_cog("ModLog") - async def on_ready(self): - role_id = AntiSpamConfig.punishment['role_id'] - self.muted_role = Object(role_id) - async def on_message(self, message: Message): if ( not message.guild -- cgit v1.2.3 From e160254e9deaadfbe54a653ac126af928a86bb9b Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 12 Sep 2019 13:40:01 +0200 Subject: Fixed incorrect API request field in superstarify https://github.com/python-discord/bot/issues/409 The superstarify cog specified an incorrect infraction type in the API request in the on_member_join event listener. I've fixed it by giving it the correct infraction type, 'superstar'. closes #409 --- bot/cogs/superstarify/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/superstarify/__init__.py b/bot/cogs/superstarify/__init__.py index 4b26f3f40..cccd91304 100644 --- a/bot/cogs/superstarify/__init__.py +++ b/bot/cogs/superstarify/__init__.py @@ -103,7 +103,7 @@ class Superstarify: 'bot/infractions', params={ 'active': 'true', - 'type': 'superstarify', + 'type': 'superstar', 'user__id': member.id } ) -- cgit v1.2.3 From 874cc1ba111e23973198773a2e4f14392e9c0fff Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 12 Sep 2019 15:18:14 +0200 Subject: Fix AntiSpam incorrectly invoking tempmute. https://github.com/python-discord/bot/issues/400 The AntiSpam punish method incorrectly invoked the tempmute command, as it provided an unconverted duration argument. Since direct invocation of commands bypasses converters, the conversion of the duration string to a datetime object is now done manually. Closes #400 --- bot/cogs/antispam.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index 6104ec08b..02d5d64ce 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -14,6 +14,7 @@ from bot.constants import ( Guild as GuildConfig, Icons, STAFF_ROLES, ) +from bot.converters import ExpirationDate log = logging.getLogger(__name__) @@ -37,6 +38,7 @@ class AntiSpam: self.bot = bot role_id = AntiSpamConfig.punishment['role_id'] self.muted_role = Object(role_id) + self.expiration_date_converter = ExpirationDate() @property def mod_log(self) -> ModLog: @@ -130,8 +132,9 @@ class AntiSpam: ping_everyone=AntiSpamConfig.ping_everyone ) - # Run a tempmute - await mod_log_ctx.invoke(Moderation.tempmute, member, f"{remove_role_after}S", reason=reason) + # Since we're going to invoke the tempmute command directly, we need to manually call the converter. + dt_remove_role_after = await self.expiration_date_converter.convert(mod_log_ctx, f"{remove_role_after}S") + await mod_log_ctx.invoke(Moderation.tempmute, member, dt_remove_role_after, reason=reason) async def maybe_delete_messages(self, channel: TextChannel, messages: List[Message]): # Is deletion of offending messages actually enabled? -- cgit v1.2.3 From 00f985875f04c752630e13add232944e46870779 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 12 Sep 2019 16:17:28 +0200 Subject: Add help-6 and help-7 to constants We never added channel IDs for the new help channels to the constants after adding them, so I'm adding them in. --- bot/constants.py | 2 ++ config-default.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/bot/constants.py b/bot/constants.py index ead26c91d..c2b778b6e 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -341,6 +341,8 @@ class Channels(metaclass=YAMLGetter): help_3: int help_4: int help_5: int + help_6: int + help_7: int helpers: int message_log: int mod_alerts: int diff --git a/config-default.yml b/config-default.yml index e8ad1d572..4c9cb72dc 100644 --- a/config-default.yml +++ b/config-default.yml @@ -103,6 +103,8 @@ guild: help_3: 439702951246692352 help_4: 451312046647148554 help_5: 454941769734422538 + help_6: 587375753306570782 + help_7: 587375768556797982 helpers: 385474242440986624 message_log: &MESSAGE_LOG 467752170159079424 mod_alerts: 473092532147060736 -- cgit v1.2.3 From 5303f4c7f708f010450ba187a868bd8ef05fa780 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 12 Sep 2019 16:30:46 +0200 Subject: Update bot cog with recent changes. The bot cog was not updated with recent changes to our community, so I've: - Updated the links in the about embed to GitHub; - Added help-6 and help-7 to the codeblock detection. --- bot/cogs/bot.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/cogs/bot.py b/bot/cogs/bot.py index 828e2514c..4a0f208f4 100644 --- a/bot/cogs/bot.py +++ b/bot/cogs/bot.py @@ -32,6 +32,8 @@ class Bot: Channels.help_3: 0, Channels.help_4: 0, Channels.help_5: 0, + Channels.help_6: 0, + Channels.help_7: 0, Channels.python: 0, } @@ -62,13 +64,13 @@ class Bot: embed = Embed( description="A utility bot designed just for the Python server! Try `!help` for more info.", - url="https://gitlab.com/discord-python/projects/bot" + url="https://github.com/python-discord/bot" ) embed.add_field(name="Total Users", value=str(len(self.bot.get_guild(Guild.id).members))) embed.set_author( name="Python Bot", - url="https://gitlab.com/discord-python/projects/bot", + url="https://github.com/python-discord/bot", icon_url=URLs.bot_avatar ) -- cgit v1.2.3 From ccb37f310bdf936223a83707c2541f98e0e61354 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Fri, 13 Sep 2019 14:06:30 +0200 Subject: Fix bugs and inconsistencies in moderation cog Recent changes and updates to the moderation cog introduced some inconsistencies that were causing bugs or differences in behavior between very similar commands. I've remedied the problems by: - Consistently making sure we stop if a post_infraction API call fails; - Factoring out the check for active infractions to a utility function; - Updating commands that expected a pre-migration API response format. In addition, I've also added function annotations. --- bot/cogs/moderation.py | 283 ++++++++++++++++++++++-------------------------- bot/utils/moderation.py | 20 ++++ 2 files changed, 149 insertions(+), 154 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 1dc2c70d6..bec2f98c1 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -2,7 +2,7 @@ import asyncio import logging import textwrap from datetime import datetime -from typing import Union +from typing import Dict, Union from discord import ( Colour, Embed, Forbidden, Guild, HTTPException, Member, Object, User @@ -17,7 +17,7 @@ from bot.constants import Colours, Event, Icons, MODERATION_ROLES from bot.converters import ExpirationDate, InfractionSearchQuery from bot.decorators import with_role from bot.pagination import LinePaginator -from bot.utils.moderation import post_infraction +from bot.utils.moderation import already_has_active_infraction, post_infraction from bot.utils.scheduling import Scheduler, create_task from bot.utils.time import wait_until @@ -81,15 +81,11 @@ class Moderation(Scheduler): **`reason`:** The reason for the warning. """ - response_object = await post_infraction(ctx, user, type="warning", reason=reason) - if response_object is None: + infraction = await post_infraction(ctx, user, type="warning", reason=reason) + if infraction is None: return - notified = await self.notify_infraction( - user=user, - infr_type="Warning", - reason=reason - ) + notified = await self.notify_infraction(user=user, infr_type="Warning", reason=reason) dm_result = ":incoming_envelope: " if notified else "" action = f"{dm_result}:ok_hand: warned {user.mention}" @@ -118,7 +114,7 @@ class Moderation(Scheduler): Reason: {reason} """), content=log_content, - footer=f"ID {response_object['infraction']['id']}" + footer=f"ID {infraction['id']}" ) @with_role(*MODERATION_ROLES) @@ -136,15 +132,11 @@ class Moderation(Scheduler): # Warning is sent to ctx by the helper method return - response_object = await post_infraction(ctx, user, type="kick", reason=reason) - if response_object is None: + infraction = await post_infraction(ctx, user, type="kick", reason=reason) + if infraction is None: return - notified = await self.notify_infraction( - user=user, - infr_type="Kick", - reason=reason - ) + notified = await self.notify_infraction(user=user, infr_type="Kick", reason=reason) self.mod_log.ignore(Event.member_remove, user.id) @@ -178,7 +170,7 @@ class Moderation(Scheduler): Reason: {reason} """), content=log_content, - footer=f"ID {response_object['infraction']['id']}" + footer=f"ID {infraction['id']}" ) @with_role(*MODERATION_ROLES) @@ -196,22 +188,11 @@ class Moderation(Scheduler): # Warning is sent to ctx by the helper method return - active_bans = await self.bot.api_client.get( - 'bot/infractions', - params={ - 'active': 'true', - 'type': 'ban', - 'user__id': str(user.id) - } - ) - if active_bans: - return await ctx.send( - ":x: According to my records, this user is already banned. " - f"See infraction **#{active_bans[0]['id']}**." - ) + if await already_has_active_infraction(ctx=ctx, user=user, type="ban"): + return - response_object = await post_infraction(ctx, user, type="ban", reason=reason) - if response_object is None: + infraction = await post_infraction(ctx, user, type="ban", reason=reason) + if infraction is None: return notified = await self.notify_infraction( @@ -255,7 +236,7 @@ class Moderation(Scheduler): Reason: {reason} """), content=log_content, - footer=f"ID {response_object['infraction']['id']}" + footer=f"ID {infraction['id']}" ) @with_role(*MODERATION_ROLES) @@ -268,22 +249,11 @@ class Moderation(Scheduler): **`reason`:** The reason for the mute. """ - active_mutes = await self.bot.api_client.get( - 'bot/infractions', - params={ - 'active': 'true', - 'type': 'mute', - 'user__id': str(user.id) - } - ) - if active_mutes: - return await ctx.send( - ":x: According to my records, this user is already muted. " - f"See infraction **#{active_mutes[0]['id']}**." - ) + if await already_has_active_infraction(ctx=ctx, user=user, type="mute"): + return - response_object = await post_infraction(ctx, user, type="mute", reason=reason) - if response_object is None: + infraction = await post_infraction(ctx, user, type="mute", reason=reason) + if infraction is None: return self.mod_log.ignore(Event.member_update, user.id) @@ -323,7 +293,7 @@ class Moderation(Scheduler): Reason: {reason} """), content=log_content, - footer=f"ID {response_object['infraction']['id']}" + footer=f"ID {infraction['id']}" ) # endregion @@ -331,10 +301,7 @@ class Moderation(Scheduler): @with_role(*MODERATION_ROLES) @command() - async def tempmute( - self, ctx: Context, user: Member, expiration: ExpirationDate, - *, reason: str = None - ): + async def tempmute(self, ctx: Context, user: Member, duration: ExpirationDate, *, reason: str = None) -> None: """ Create a temporary mute infraction in the database for a user. @@ -342,26 +309,14 @@ class Moderation(Scheduler): **`duration`:** The duration for the temporary mute infraction **`reason`:** The reason for the temporary mute. """ + expiration = duration - active_mutes = await self.bot.api_client.get( - 'bot/infractions', - params={ - 'active': 'true', - 'type': 'mute', - 'user__id': str(user.id) - } - ) - if active_mutes: - return await ctx.send( - ":x: According to my records, this user is already muted. " - f"See infraction **#{active_mutes[0]['id']}**." - ) + if await already_has_active_infraction(ctx=ctx, user=user, type="mute"): + return - infraction = await post_infraction( - ctx, user, - type="mute", reason=reason, - expires_at=expiration - ) + infraction = await post_infraction(ctx, user, type="mute", reason=reason, expires_at=expiration) + if infraction is None: + return self.mod_log.ignore(Event.member_update, user.id) await user.add_roles(self._muted_role, reason=reason) @@ -414,47 +369,32 @@ class Moderation(Scheduler): @with_role(*MODERATION_ROLES) @command() - async def tempban( - self, ctx: Context, user: UserTypes, expiry: ExpirationDate, *, reason: str = None - ): + async def tempban(self, ctx: Context, user: UserTypes, duration: ExpirationDate, *, reason: str = None) -> None: """ Create a temporary ban infraction in the database for a user. **`user`:** Accepts user mention, ID, etc. - **`expiry`:** The duration for the temporary ban infraction + **`duration`:** The duration for the temporary ban infraction **`reason`:** The reason for the temporary ban. """ + expiration = duration if not await self.respect_role_hierarchy(ctx, user, 'tempban'): # Ensure ctx author has a higher top role than the target user # Warning is sent to ctx by the helper method return - active_bans = await self.bot.api_client.get( - 'bot/infractions', - params={ - 'active': 'true', - 'type': 'ban', - 'user__id': str(user.id) - } - ) - if active_bans: - return await ctx.send( - ":x: According to my records, this user is already banned. " - f"See infraction **#{active_bans[0]['id']}**." - ) + if await already_has_active_infraction(ctx=ctx, user=user, type="ban"): + return - infraction = await post_infraction( - ctx, user, type="ban", - reason=reason, expires_at=expiry - ) + infraction = await post_infraction(ctx, user, type="ban", reason=reason, expires_at=expiration) if infraction is None: return notified = await self.notify_infraction( user=user, infr_type="Ban", - expires_at=expiry, + expires_at=expiration, reason=reason ) @@ -510,7 +450,7 @@ class Moderation(Scheduler): @with_role(*MODERATION_ROLES) @command(hidden=True, aliases=['shadowwarn', 'swarn', 'shadow_warn']) - async def note(self, ctx: Context, user: UserTypes, *, reason: str = None): + async def note(self, ctx: Context, user: UserTypes, *, reason: str = None) -> None: """ Create a private infraction note in the database for a user. @@ -518,11 +458,8 @@ class Moderation(Scheduler): **`reason`:** The reason for the warning. """ - response_object = await post_infraction( - ctx, user, type="warning", reason=reason, hidden=True - ) - - if response_object is None: + infraction = await post_infraction(ctx, user, type="warning", reason=reason, hidden=True) + if infraction is None: return if reason is None: @@ -540,12 +477,12 @@ class Moderation(Scheduler): Actor: {ctx.message.author} Reason: {reason} """), - footer=f"ID {response_object['infraction']['id']}" + footer=f"ID {infraction['id']}" ) @with_role(*MODERATION_ROLES) @command(hidden=True, aliases=['shadowkick', 'skick']) - async def shadow_kick(self, ctx: Context, user: Member, *, reason: str = None): + async def shadow_kick(self, ctx: Context, user: Member, *, reason: str = None) -> None: """ Kicks a user. @@ -558,8 +495,8 @@ class Moderation(Scheduler): # Warning is sent to ctx by the helper method return - response_object = await post_infraction(ctx, user, type="kick", reason=reason, hidden=True) - if response_object is None: + infraction = await post_infraction(ctx, user, type="kick", reason=reason, hidden=True) + if infraction is None: return self.mod_log.ignore(Event.member_remove, user.id) @@ -593,12 +530,12 @@ class Moderation(Scheduler): Reason: {reason} """), content=log_content, - footer=f"ID {response_object['infraction']['id']}" + footer=f"ID {infraction['id']}" ) @with_role(*MODERATION_ROLES) @command(hidden=True, aliases=['shadowban', 'sban']) - async def shadow_ban(self, ctx: Context, user: UserTypes, *, reason: str = None): + async def shadow_ban(self, ctx: Context, user: UserTypes, *, reason: str = None) -> None: """ Create a permanent ban infraction in the database for a user. @@ -611,8 +548,11 @@ class Moderation(Scheduler): # Warning is sent to ctx by the helper method return - response_object = await post_infraction(ctx, user, type="ban", reason=reason, hidden=True) - if response_object is None: + if await already_has_active_infraction(ctx=ctx, user=user, type="ban"): + return + + infraction = await post_infraction(ctx, user, type="ban", reason=reason, hidden=True) + if infraction is None: return self.mod_log.ignore(Event.member_ban, user.id) @@ -647,12 +587,12 @@ class Moderation(Scheduler): Reason: {reason} """), content=log_content, - footer=f"ID {response_object['infraction']['id']}" + footer=f"ID {infraction['id']}" ) @with_role(*MODERATION_ROLES) @command(hidden=True, aliases=['shadowmute', 'smute']) - async def shadow_mute(self, ctx: Context, user: Member, *, reason: str = None): + async def shadow_mute(self, ctx: Context, user: Member, *, reason: str = None) -> None: """ Create a permanent mute infraction in the database for a user. @@ -660,8 +600,11 @@ class Moderation(Scheduler): **`reason`:** The reason for the mute. """ - response_object = await post_infraction(ctx, user, type="mute", reason=reason, hidden=True) - if response_object is None: + if await already_has_active_infraction(ctx=ctx, user=user, type="mute"): + return + + infraction = await post_infraction(ctx, user, type="mute", reason=reason, hidden=True) + if infraction is None: return self.mod_log.ignore(Event.member_update, user.id) @@ -682,7 +625,7 @@ class Moderation(Scheduler): Actor: {ctx.message.author} Reason: {reason} """), - footer=f"ID {response_object['infraction']['id']}" + footer=f"ID {infraction['id']}" ) # endregion @@ -691,8 +634,13 @@ class Moderation(Scheduler): @with_role(*MODERATION_ROLES) @command(hidden=True, aliases=["shadowtempmute, stempmute"]) async def shadow_tempmute( - self, ctx: Context, user: Member, duration: str, *, reason: str = None - ): + self, + ctx: Context, + user: Member, + duration: ExpirationDate, + *, + reason: str = None + ) -> None: """ Create a temporary mute infraction in the database for a user. @@ -700,20 +648,25 @@ class Moderation(Scheduler): **`duration`:** The duration for the temporary mute infraction **`reason`:** The reason for the temporary mute. """ + expiration = duration - response_object = await post_infraction( - ctx, user, type="mute", reason=reason, duration=duration, hidden=True - ) - if response_object is None: + if await already_has_active_infraction(ctx=ctx, user=user, type="mute"): + return + + infraction = await post_infraction(ctx, user, type="mute", reason=reason, expires_at=expiration, hidden=True) + if infraction is None: return self.mod_log.ignore(Event.member_update, user.id) await user.add_roles(self._muted_role, reason=reason) - infraction_object = response_object["infraction"] - infraction_expiration = infraction_object["expires_at"] + infraction_expiration = ( + datetime + .fromisoformat(infraction["expires_at"][:-1]) + .strftime('%c') + ) - self.schedule_expiration(ctx.bot.loop, infraction_object) + self.schedule_task(ctx.bot.loop, infraction["id"], infraction) if reason is None: await ctx.send(f":ok_hand: muted {user.mention} until {infraction_expiration}.") @@ -731,17 +684,21 @@ class Moderation(Scheduler): Member: {user.mention} (`{user.id}`) Actor: {ctx.message.author} Reason: {reason} - Duration: {duration} Expires: {infraction_expiration} """), - footer=f"ID {response_object['infraction']['id']}" + footer=f"ID {infraction['id']}" ) @with_role(*MODERATION_ROLES) @command(hidden=True, aliases=["shadowtempban, stempban"]) async def shadow_tempban( - self, ctx: Context, user: UserTypes, duration: str, *, reason: str = None - ): + self, + ctx: Context, + user: UserTypes, + duration: ExpirationDate, + *, + reason: str = None + ) -> None: """ Create a temporary ban infraction in the database for a user. @@ -749,16 +706,18 @@ class Moderation(Scheduler): **`duration`:** The duration for the temporary ban infraction **`reason`:** The reason for the temporary ban. """ + expiration = duration if not await self.respect_role_hierarchy(ctx, user, 'shadowtempban'): # Ensure ctx author has a higher top role than the target user # Warning is sent to ctx by the helper method return - response_object = await post_infraction( - ctx, user, type="ban", reason=reason, duration=duration, hidden=True - ) - if response_object is None: + if await already_has_active_infraction(ctx=ctx, user=user, type="ban"): + return + + infraction = await post_infraction(ctx, user, type="ban", reason=reason, expires_at=expiration, hidden=True) + if infraction is None: return self.mod_log.ignore(Event.member_ban, user.id) @@ -770,10 +729,13 @@ class Moderation(Scheduler): except Forbidden: action_result = False - infraction_object = response_object["infraction"] - infraction_expiration = infraction_object["expires_at"] + infraction_expiration = ( + datetime + .fromisoformat(infraction["expires_at"][:-1]) + .strftime('%c') + ) - self.schedule_expiration(ctx.bot.loop, infraction_object) + self.schedule_task(ctx.bot.loop, infraction["id"], infraction) if reason is None: await ctx.send(f":ok_hand: banned {user.mention} until {infraction_expiration}.") @@ -799,11 +761,10 @@ class Moderation(Scheduler): Member: {user.mention} (`{user.id}`) Actor: {ctx.message.author} Reason: {reason} - Duration: {duration} Expires: {infraction_expiration} """), content=log_content, - footer=f"ID {response_object['infraction']['id']}" + footer=f"ID {infraction['id']}" ) # endregion @@ -811,7 +772,7 @@ class Moderation(Scheduler): @with_role(*MODERATION_ROLES) @command() - async def unmute(self, ctx: Context, user: Member): + async def unmute(self, ctx: Context, user: Member) -> None: """ Deactivates the active mute infraction for a user. @@ -833,9 +794,10 @@ class Moderation(Scheduler): if not response: # no active infraction - return await ctx.send( + await ctx.send( f":x: There is no active mute infraction for user {user.mention}." ) + return infraction = response[0] await self._deactivate_infraction(infraction) @@ -881,7 +843,7 @@ class Moderation(Scheduler): @with_role(*MODERATION_ROLES) @command() - async def unban(self, ctx: Context, user: UserTypes): + async def unban(self, ctx: Context, user: UserTypes) -> None: """ Deactivates the active ban infraction for a user. @@ -906,9 +868,10 @@ class Moderation(Scheduler): if not response: # no active infraction - return await ctx.send( + await ctx.send( f":x: There is no active ban infraction for user {user.mention}." ) + return infraction = response[0] await self._deactivate_infraction(infraction) @@ -1043,7 +1006,7 @@ class Moderation(Scheduler): @with_role(*MODERATION_ROLES) @infraction_edit_group.command(name="reason") - async def edit_reason(self, ctx: Context, infraction_id: int, *, reason: str): + async def edit_reason(self, ctx: Context, infraction_id: int, *, reason: str) -> None: """ Sets the reason of the given infraction. **`infraction_id`:** the id of the infraction @@ -1063,7 +1026,8 @@ class Moderation(Scheduler): except Exception: log.exception("There was an error updating an infraction.") - return await ctx.send(":x: There was an error updating the infraction.") + await ctx.send(":x: There was an error updating the infraction.") + return # Get information about the infraction's user user_id = updated_infraction['user'] @@ -1169,7 +1133,11 @@ class Moderation(Scheduler): # endregion # region: Utility functions - def schedule_expiration(self, loop: asyncio.AbstractEventLoop, infraction_object: dict): + def schedule_expiration( + self, + loop: asyncio.AbstractEventLoop, + infraction_object: Dict[str, Union[str, int, bool]] + ) -> None: """ Schedules a task to expire a temporary infraction. @@ -1199,7 +1167,7 @@ class Moderation(Scheduler): log.debug(f"Unscheduled {infraction_id}.") del self.scheduled_tasks[infraction_id] - async def _scheduled_task(self, infraction_object: dict): + async def _scheduled_task(self, infraction_object: Dict[str, Union[str, int, bool]]) -> None: """ A co-routine which marks an infraction as expired after the delay from the time of scheduling to the time of expiration. At the time of expiration, the infraction is @@ -1229,7 +1197,7 @@ class Moderation(Scheduler): icon_url=Icons.user_unmute ) - async def _deactivate_infraction(self, infraction_object): + async def _deactivate_infraction(self, infraction_object: Dict[str, Union[str, int, bool]]) -> None: """ A co-routine which marks an infraction as inactive on the website. This co-routine does not cancel or un-schedule an expiration task. @@ -1258,7 +1226,7 @@ class Moderation(Scheduler): json={"active": False} ) - def _infraction_to_string(self, infraction_object): + def _infraction_to_string(self, infraction_object: Dict[str, Union[str, int, bool]]) -> str: actor_id = infraction_object["actor"] guild: Guild = self.bot.get_guild(constants.Guild.id) actor = guild.get_member(actor_id) @@ -1283,9 +1251,12 @@ class Moderation(Scheduler): return lines.strip() async def notify_infraction( - self, user: Union[User, Member], infr_type: str, - expires_at: Union[datetime, str] = 'N/A', reason: str = "No reason provided." - ): + self, + user: Union[User, Member], + infr_type: str, + expires_at: Union[datetime, str] = 'N/A', + reason: str = "No reason provided." + ) -> bool: """ Notify a user of their fresh infraction :) @@ -1318,9 +1289,12 @@ class Moderation(Scheduler): return await self.send_private_embed(user, embed) async def notify_pardon( - self, user: Union[User, Member], title: str, content: str, - icon_url: str = Icons.user_verified - ): + self, + user: Union[User, Member], + title: str, + content: str, + icon_url: str = Icons.user_verified + ) -> bool: """ Notify a user that an infraction has been lifted. @@ -1339,7 +1313,7 @@ class Moderation(Scheduler): return await self.send_private_embed(user, embed) - async def send_private_embed(self, user: Union[User, Member], embed: Embed): + async def send_private_embed(self, user: Union[User, Member], embed: Embed) -> bool: """ A helper method for sending an embed to a user's DMs. @@ -1374,7 +1348,7 @@ class Moderation(Scheduler): # endregion - async def __error(self, ctx, error): + async def __error(self, ctx: Context, error) -> None: if isinstance(error, BadUnionArgument): if User in error.converters: await ctx.send(str(error.errors[0])) @@ -1409,6 +1383,7 @@ class Moderation(Scheduler): return target_is_lower -def setup(bot): +def setup(bot: Bot) -> None: + """Sets up the Moderation cog.""" bot.add_cog(Moderation(bot)) log.info("Cog loaded: Moderation") diff --git a/bot/utils/moderation.py b/bot/utils/moderation.py index c1eb98dd6..152f9d538 100644 --- a/bot/utils/moderation.py +++ b/bot/utils/moderation.py @@ -44,3 +44,23 @@ async def post_infraction( return return response + + +async def already_has_active_infraction(ctx: Context, user: Union[Member, Object, User], type: str) -> bool: + """Checks if a user already has an active infraction of the given type.""" + active_infractions = await ctx.bot.api_client.get( + 'bot/infractions', + params={ + 'active': 'true', + 'type': type, + 'user__id': str(user.id) + } + ) + if active_infractions: + await ctx.send( + f":x: According to my records, this user already has a {type} infraction. " + f"See infraction **#{active_infractions[0]['id']}**." + ) + return True + else: + return False -- cgit v1.2.3 From f287c5a690a78a886b29363634b0cdc2499dafb4 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Fri, 13 Sep 2019 21:37:16 +0200 Subject: Fix one-off error in the !clean command https://github.com/python-discord/bot/issues/413 The message indexing phase of the `!clean` did not account for the presence of the invocation message, resulting in a one-off error in the amount of messages being indexed. Fixed it by adding one to the amount of messages we index from the message history. Closes #413 --- bot/cogs/clean.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index e7b6bac85..1f3e1caa9 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -133,7 +133,8 @@ class Clean: self.cleaning = True invocation_deleted = False - async for message in ctx.channel.history(limit=amount): + # To account for the invocation message, we index `amount + 1` messages. + async for message in ctx.channel.history(limit=amount + 1): # If at any point the cancel command is invoked, we should stop. if not self.cleaning: -- cgit v1.2.3 From 51832a333ea544df5c94943b7dc000c2dfcd0979 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 13 Sep 2019 14:46:35 -0700 Subject: Add error handlers for more command exceptions MissingPermissions, CheckFailure, DisabledCommand, and CommandOnCooldown will now have a simple message logged. * Log BotMissingPermissions and remove sending missing permissions as a message --- bot/cogs/error_handler.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py index 2063df09d..d2a67fd76 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -4,9 +4,13 @@ import logging from discord.ext.commands import ( BadArgument, BotMissingPermissions, + CheckFailure, CommandError, CommandInvokeError, CommandNotFound, + CommandOnCooldown, + DisabledCommand, + MissingPermissions, NoPrivateMessage, UserInputError, ) @@ -58,10 +62,12 @@ class ErrorHandler: 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}**" - ) + await ctx.send(f"Sorry, it looks like I don't have the permissions I need to do that.") + log.warning(f"The bot is missing permissions to execute command {command}: {e.missing_perms}") + elif isinstance(e, MissingPermissions): + log.debug(f"{ctx.message.author} is missing permissions to invoke command {command}: {e.missing_perms}") + elif isinstance(e, (CheckFailure, CommandOnCooldown, DisabledCommand)): + log.debug(f"Command {command} invoked by {ctx.message.author} with error {e.__class__.__name__}: {e}") elif isinstance(e, CommandInvokeError): if isinstance(e.original, ResponseCodeError): if e.original.response.status == 404: @@ -77,7 +83,6 @@ class ErrorHandler: "Got an unexpected status code from the " f"API (`{e.original.response.code}`)." ) - else: await ctx.send( f"Sorry, an unexpected error occurred. Please let us know!\n\n```{e}```" -- cgit v1.2.3 From f714fa93d691a264a2f4e2b8cd798aa4276caab0 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Sat, 14 Sep 2019 13:02:23 +0200 Subject: Add API response dict to ResponseCodeError The ReponseCodeError held a reference to `aiohttp.ResonseObject` to make sure the response data was available. However, the response data is not actually included in the Response Object, but needs to be awaited. Unfortunately, the ResponseCodeError is usually inspected after the connection has been closed, triggering a ClientConnectionError when the data was retrieved. I solved this by adding the awaited reponse data directly to our custom exception by awaiting the response.json() before raising the exception. --- bot/api.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/bot/api.py b/bot/api.py index cd19896e1..36f9cfcd4 100644 --- a/bot/api.py +++ b/bot/api.py @@ -10,9 +10,14 @@ log = logging.getLogger(__name__) class ResponseCodeError(ValueError): - def __init__(self, response: aiohttp.ClientResponse): + def __init__(self, response: aiohttp.ClientResponse, response_data: dict): + self.status = response.status + self.response_data = response_data self.response = response + def __str__(self): + return f"Status: {self.status} Response: {self.response_data}" + class APIClient: def __init__(self, **kwargs): @@ -31,28 +36,29 @@ class APIClient: def _url_for(endpoint: str): return f"{URLs.site_schema}{URLs.site_api}/{quote_url(endpoint)}" - def maybe_raise_for_status(self, response: aiohttp.ClientResponse, should_raise: bool): + async def maybe_raise_for_status(self, response: aiohttp.ClientResponse, should_raise: bool): if should_raise and response.status >= 400: - raise ResponseCodeError(response=response) + response_data = await response.json() + raise ResponseCodeError(response=response, response_data=response_data) 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) + await self.maybe_raise_for_status(resp, raise_for_status) return await resp.json() 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) + await 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 with self.session.post(self._url_for(endpoint), *args, **kwargs) as resp: - self.maybe_raise_for_status(resp, raise_for_status) + await self.maybe_raise_for_status(resp, raise_for_status) return await resp.json() 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) + await self.maybe_raise_for_status(resp, raise_for_status) return await resp.json() async def delete(self, endpoint: str, *args, raise_for_status: bool = True, **kwargs): @@ -60,7 +66,7 @@ class APIClient: if resp.status == 204: return None - self.maybe_raise_for_status(resp, raise_for_status) + await self.maybe_raise_for_status(resp, raise_for_status) return await resp.json() -- cgit v1.2.3 From f983b8bd4766a8bfd4dfe1b7d0c249b039244dc1 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Sat, 14 Sep 2019 13:10:08 +0200 Subject: Make 'post_infraction' catch the right exception The internal 'api' of our API client has changed: It raises a custom RespondeCodeError instead of an `aiohttp.ClientError` when an API was not successful. I updated this utility function to catch the right exception and added handling for unknown users by notifying the user of that problem directly. --- bot/utils/moderation.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/bot/utils/moderation.py b/bot/utils/moderation.py index 152f9d538..b295e4649 100644 --- a/bot/utils/moderation.py +++ b/bot/utils/moderation.py @@ -1,11 +1,11 @@ import logging from datetime import datetime -from typing import Union +from typing import Optional, Union -from aiohttp import ClientError from discord import Member, Object, User from discord.ext.commands import Context +from bot.api import ResponseCodeError from bot.constants import Keys log = logging.getLogger(__name__) @@ -21,8 +21,8 @@ async def post_infraction( expires_at: datetime = None, hidden: bool = False, active: bool = True, -): - +) -> Optional[dict]: + """Posts an infraction to the API.""" payload = { "actor": ctx.message.author.id, "hidden": hidden, @@ -35,13 +35,19 @@ async def post_infraction( payload['expires_at'] = expires_at.isoformat() try: - response = await ctx.bot.api_client.post( - 'bot/infractions', json=payload - ) - except ClientError: - log.exception("There was an error adding an infraction.") - await ctx.send(":x: There was an error adding the infraction.") - return + response = await ctx.bot.api_client.post('bot/infractions', json=payload) + except ResponseCodeError as exp: + if exp.status == 400 and 'user' in exp.response_data: + log.info( + f"{ctx.author} tried to add a {type} infraction to `{user.id}`, " + "but that user id was not found in the database." + ) + await ctx.send(f":x: Cannot add infraction, the specified user is not known to the database.") + return + else: + log.exception("An unexpected ResponseCodeError occurred while adding an infraction:") + await ctx.send(":x: There was an error adding the infraction.") + return return response -- cgit v1.2.3 From 279fa85b93d5017a63d68516b2477a86920caa16 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 14 Sep 2019 13:23:44 +0200 Subject: Improvements to the Wolfram cog. This commit adds clearer error messages for all the errors produced by the Wolfram cog if the API key is missing in the config, or if the key isn't valid anymore. It also humanizes the timedelta returned in the error users get when they run out their cooldown. Instead of telling them they need to wait 84000 seconds, it will now tell them they need to wait 23 hours, 59 minutes ... --- bot/cogs/wolfram.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/bot/cogs/wolfram.py b/bot/cogs/wolfram.py index e8b16b243..7dd613083 100644 --- a/bot/cogs/wolfram.py +++ b/bot/cogs/wolfram.py @@ -4,12 +4,14 @@ from typing import List, Optional, Tuple from urllib import parse import discord +from dateutil.relativedelta import relativedelta from discord import Embed from discord.ext import commands from discord.ext.commands import BucketType, Context, check, group from bot.constants import Colours, STAFF_ROLES, Wolfram from bot.pagination import ImagePaginator +from bot.utils.time import humanize_delta log = logging.getLogger(__name__) @@ -79,9 +81,11 @@ def custom_cooldown(*ignore: List[int]) -> check: if user_rate: # Can't use api; cause: member limit + delta = relativedelta(seconds=int(user_rate)) + cooldown = humanize_delta(delta) message = ( "You've used up your limit for Wolfram|Alpha requests.\n" - f"Cooldown: {int(user_rate)}" + f"Cooldown: {cooldown}" ) await send_embed(ctx, message) return False @@ -121,17 +125,27 @@ async def get_pod_pages(ctx, bot, query: str) -> Optional[List[Tuple]]: result = json["queryresult"] - if not result["success"]: - message = f"I couldn't find anything for {query}." - await send_embed(ctx, message) - return - if result["error"]: + # API key not set up correctly + if result["error"]["msg"] == "Invalid appid": + message = "Wolfram API key is invalid or missing." + log.warning( + "API key seems to be missing, or invalid when " + f"processing a wolfram request: {url_str}, Response: {json}" + ) + await send_embed(ctx, message) + return + message = "Something went wrong internally with your request, please notify staff!" log.warning(f"Something went wrong getting a response from wolfram: {url_str}, Response: {json}") await send_embed(ctx, message) return + if not result["success"]: + message = f"I couldn't find anything for {query}." + await send_embed(ctx, message) + return + if not result["numpods"]: message = "Could not find any results." await send_embed(ctx, message) @@ -191,6 +205,10 @@ class Wolfram: message = "No input found" footer = "" color = Colours.soft_red + elif status == 403: + message = "Wolfram API key is invalid or missing." + footer = "" + color = Colours.soft_red else: message = "" footer = "View original for a bigger picture." @@ -272,10 +290,12 @@ class Wolfram: if status == 501: message = "Failed to get response" color = Colours.soft_red - elif status == 400: message = "No input found" color = Colours.soft_red + elif response_text == "Error 1: Invalid appid": + message = "Wolfram API key is invalid or missing." + color = Colours.soft_red else: message = response_text color = Colours.soft_orange -- cgit v1.2.3 From 042a5fcbd90a99e8f15ee3d891e87d6c867d1b06 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 14 Sep 2019 13:46:27 +0200 Subject: Adds a !nominees alias. This invokes the `!nomination list` command, showing all currently nominated users. --- bot/cogs/alias.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bot/cogs/alias.py b/bot/cogs/alias.py index 85d101448..dbdd2ee6a 100644 --- a/bot/cogs/alias.py +++ b/bot/cogs/alias.py @@ -197,6 +197,14 @@ class Alias: await self.invoke(ctx, "nomination end", user, reason=reason) + @command(name="nominees", hidden=True) + async def nominees_alias(self, ctx): + """ + Alias for invoking tp watched. + """ + + await self.invoke(ctx, "talentpool watched") + def setup(bot): bot.add_cog(Alias(bot)) -- cgit v1.2.3 From b103fc0d5fb16122271a77db99f01a6b86175994 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 14 Sep 2019 14:18:39 +0200 Subject: We now also detect bot tokens outside quotes. Previously, the regex to detect tokens would only trigger on messages where the token was inside of single or double quotes. This commit changes this so that we look for bot tokens regardless of context. --- bot/cogs/token_remover.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bot/cogs/token_remover.py b/bot/cogs/token_remover.py index 05298a2ff..b2c4cd522 100644 --- a/bot/cogs/token_remover.py +++ b/bot/cogs/token_remover.py @@ -26,13 +26,11 @@ DELETION_MESSAGE_TEMPLATE = ( DISCORD_EPOCH_TIMESTAMP = datetime(2017, 1, 1) TOKEN_EPOCH = 1_293_840_000 TOKEN_RE = re.compile( - r"(?<=(\"|'))" # Lookbehind: Only match if there's a double or single quote in front r"[^\s\.]+" # Matches token part 1: The user ID string, encoded as base64 r"\." # Matches a literal dot between the token parts r"[^\s\.]+" # Matches token part 2: The creation timestamp, as an integer r"\." # Matches a literal dot between the token parts r"[^\s\.]+" # Matches token part 3: The HMAC, unused by us, but check that it isn't empty - r"(?=(\"|'))" # Lookahead: Only match if there's a double or single quote after ) -- cgit v1.2.3 From bbbdfcec41c06eaf0d031d7f2a58f111e8e88a16 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Sat, 14 Sep 2019 14:39:58 +0200 Subject: Make ResponseErrorCode handle non-JSON response The previous commit assumed the API respone to always be JSON. This leads to issues when that is not the case, such as when the API is completely unreachable (text/html 404 response). Updated the ResponseErrorCode exception to account for that and updated the moderation util `post_infraction` to reflect that. --- bot/api.py | 22 +++++++++++++++++----- bot/utils/moderation.py | 2 +- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/bot/api.py b/bot/api.py index 36f9cfcd4..9a0ebaa26 100644 --- a/bot/api.py +++ b/bot/api.py @@ -1,5 +1,6 @@ import asyncio import logging +from typing import Optional from urllib.parse import quote as quote_url import aiohttp @@ -10,13 +11,20 @@ log = logging.getLogger(__name__) class ResponseCodeError(ValueError): - def __init__(self, response: aiohttp.ClientResponse, response_data: dict): + def __init__( + self, + response: aiohttp.ClientResponse, + response_json: Optional[dict] = None, + response_text: str = "" + ): self.status = response.status - self.response_data = response_data + self.response_json = response_json or {} + self.response_text = response_text self.response = response def __str__(self): - return f"Status: {self.status} Response: {self.response_data}" + response = self.response_json if self.response_json else self.response_text + return f"Status: {self.status} Response: {response}" class APIClient: @@ -38,8 +46,12 @@ class APIClient: async def maybe_raise_for_status(self, response: aiohttp.ClientResponse, should_raise: bool): if should_raise and response.status >= 400: - response_data = await response.json() - raise ResponseCodeError(response=response, response_data=response_data) + try: + response_json = await response.json() + raise ResponseCodeError(response=response, response_json=response_json) + except aiohttp.ContentTypeError: + response_text = await response.text() + raise ResponseCodeError(response=response, response_text=response_text) 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: diff --git a/bot/utils/moderation.py b/bot/utils/moderation.py index b295e4649..7860f14a1 100644 --- a/bot/utils/moderation.py +++ b/bot/utils/moderation.py @@ -37,7 +37,7 @@ async def post_infraction( try: response = await ctx.bot.api_client.post('bot/infractions', json=payload) except ResponseCodeError as exp: - if exp.status == 400 and 'user' in exp.response_data: + if exp.status == 400 and 'user' in exp.response_json: log.info( f"{ctx.author} tried to add a {type} infraction to `{user.id}`, " "but that user id was not found in the database." -- cgit v1.2.3 From 85d0c1b54e2e70ae17d824c360113ea55985520c Mon Sep 17 00:00:00 2001 From: kosayoda Date: Sat, 14 Sep 2019 20:59:50 +0800 Subject: Add more information to team creation messages --- bot/cogs/jams.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/bot/cogs/jams.py b/bot/cogs/jams.py index 96b98e559..4cf878791 100644 --- a/bot/cogs/jams.py +++ b/bot/cogs/jams.py @@ -35,9 +35,11 @@ class CodeJams: # two members or at some times even 1 member. This fixes that by checking that there # are always 3 members in the members list. if len(members) < 3: - await ctx.send(":no_entry_sign: One of your arguments was invalid - there must be a " - f"minimum of 3 valid members in your team. Found: {len(members)} " - "members") + await ctx.send( + ":no_entry_sign: One of your arguments was invalid\n" + f"There must be a minimum of 3 valid members in your team. Found: {len(members)}" + " members" + ) return code_jam_category = utils.get(ctx.guild.categories, name="Code Jam") @@ -102,7 +104,11 @@ class CodeJams: for member in members: await member.add_roles(jammer_role) - await ctx.send(f":ok_hand: Team created: {team_channel.mention}") + await ctx.send( + f":ok_hand: Team created: {team_channel.mention}\n" + f"**Team Leader:** {members[0].mention}\n" + f"**Team Members:** {' '.join(member.mention for member in members[1:])}" + ) def setup(bot): -- cgit v1.2.3 From 6521fcc3727e4b5daba0b68487ef1f3df4ceb27f Mon Sep 17 00:00:00 2001 From: kosayoda Date: Sat, 14 Sep 2019 21:00:38 +0800 Subject: Remove duplicate members passed into team creation command --- bot/cogs/jams.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/cogs/jams.py b/bot/cogs/jams.py index 4cf878791..28a84a0c9 100644 --- a/bot/cogs/jams.py +++ b/bot/cogs/jams.py @@ -2,6 +2,7 @@ import logging from discord import Member, PermissionOverwrite, utils from discord.ext import commands +from more_itertools import unique_everseen from bot.constants import Roles from bot.decorators import with_role @@ -29,6 +30,8 @@ class CodeJams: The first user passed will always be the team leader. """ + # Ignore duplicate members + members = list(unique_everseen(members)) # We had a little issue during Code Jam 4 here, the greedy converter did it's job # and ignored anything which wasn't a valid argument which left us with teams of -- cgit v1.2.3 From 9ae39f343a685a985778797e9f9195d205bc2e43 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 14 Sep 2019 15:05:42 +0200 Subject: Nuking the Roles.developer role. We're not really using this, we're using Roles.verified. This provides superiour readability, and there's no reason we should use two instead of just one. I also added a comment to clarify that this role represents the Developers role on pydis. --- bot/cogs/jams.py | 2 +- bot/constants.py | 3 +-- config-default.yml | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/bot/cogs/jams.py b/bot/cogs/jams.py index 96b98e559..43b31672b 100644 --- a/bot/cogs/jams.py +++ b/bot/cogs/jams.py @@ -65,7 +65,7 @@ class CodeJams: connect=True ), ctx.guild.default_role: PermissionOverwrite(read_messages=False, connect=False), - ctx.guild.get_role(Roles.developer): PermissionOverwrite( + ctx.guild.get_role(Roles.verified): PermissionOverwrite( read_messages=False, connect=False ) diff --git a/bot/constants.py b/bot/constants.py index c2b778b6e..45c332438 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -374,13 +374,12 @@ class Roles(metaclass=YAMLGetter): announcements: int champion: int contributor: int - developer: int devops: int jammer: int moderator: int muted: int owner: int - verified: int + verified: int # This is the Developers role on PyDis, here named verified for readability reasons. helpers: int team_leader: int diff --git a/config-default.yml b/config-default.yml index 4c9cb72dc..fb33caaf6 100644 --- a/config-default.yml +++ b/config-default.yml @@ -127,7 +127,6 @@ guild: announcements: 463658397560995840 champion: 430492892331769857 contributor: 295488872404484098 - developer: 352427296948486144 devops: &DEVOPS_ROLE 409416496733880320 jammer: 423054537079783434 moderator: &MOD_ROLE 267629731250176001 -- cgit v1.2.3 From a7f4d71ab858b810f9e0fe569af8936a1c2c81e3 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Sat, 14 Sep 2019 21:53:48 +0800 Subject: Add more_itertools as a dependency --- Pipfile | 1 + Pipfile.lock | 51 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/Pipfile b/Pipfile index 2e56a3d7b..739507ac3 100644 --- a/Pipfile +++ b/Pipfile @@ -18,6 +18,7 @@ python-dateutil = "*" deepdiff = "*" requests = "*" dateparser = "*" +more_itertools = "~=7.2" urllib3 = ">=1.24.2,<1.25" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 6b91ff8aa..f655943b4 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "61607e940ea00e1197900c04fe1298e048d1f415db2f1a2a3a157406c6ea2b0c" + "sha256": "987e3fc1840e8050f159daa9c23a2c67bd18d17914d4295eb469a42c778daa10" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,11 @@ "default": { "aio-pika": { "hashes": [ - "sha256:c1424962ef9e28fbda840d76425cdd139605e480a4d68164303cda8d356ba9de", - "sha256:dcfe9c11af2ab9ff6c1c5a366d094c2a7542bab534d98a4aea29518672c9d7ac" + "sha256:31e08189841a8350db5bec70608b4d2fbacb89c0a555a18ec47511716a9bfc41", + "sha256:adfe0acf34356ccd9654a9a1c46f7e8db1dc4497a774c0e54bf2d3af14571bd0" ], "index": "pypi", - "version": "==6.1.0" + "version": "==6.1.1" }, "aiodns": { "hashes": [ @@ -62,10 +62,10 @@ }, "aiormq": { "hashes": [ - "sha256:472734ab3cf18001fb8cedb38ee13008292230a461b6482dbdf65590441ce32c", - "sha256:4b6b2b43616b7a6b353ecf9896ae29ac2f74a38c4c53bfe73824ac2807faca5d" + "sha256:0b755b748d87d5ec63b4b7f162102333bf0901caf1f8a2bf29467bbdd54e637d", + "sha256:f8eef1f98bc331a266404d925745fac589dab30412688564d740754d6d643863" ], - "version": "==2.7.4" + "version": "==2.7.5" }, "alabaster": { "hashes": [ @@ -105,10 +105,10 @@ }, "certifi": { "hashes": [ - "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", - "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695" + "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", + "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" ], - "version": "==2019.6.16" + "version": "==2019.9.11" }, "cffi": { "hashes": [ @@ -293,6 +293,14 @@ ], "version": "==1.1.1" }, + "more-itertools": { + "hashes": [ + "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", + "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" + ], + "index": "pypi", + "version": "==7.2.0" + }, "multidict": { "hashes": [ "sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f", @@ -494,9 +502,9 @@ }, "snowballstemmer": { "hashes": [ - "sha256:9f3b9ffe0809d174f7047e121431acf99c89a7040f0ca84f94ba53a498e6d0c9" + "sha256:713e53b79cbcf97bc5245a06080a33d54a77e7cce2f789c835a143bcdb5c033e" ], - "version": "==1.9.0" + "version": "==1.9.1" }, "soupsieve": { "hashes": [ @@ -637,10 +645,10 @@ }, "certifi": { "hashes": [ - "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", - "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695" + "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", + "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" ], - "version": "==2019.6.16" + "version": "==2019.9.11" }, "cfgv": { "hashes": [ @@ -747,11 +755,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:9ff1b1c5a354142de080b8a4e9803e5d0d59283c93aed808617c787d16768375", - "sha256:b7143592e374e50584564794fcb8aaf00a23025f9db866627f89a21491847a8d" + "sha256:652234b6ab8f2506ae58e528b6fbcc668831d3cc758e1bc01ef438d328b68cdb", + "sha256:6f264986fb88042bc1f0535fa9a557e6a376cfe5679dc77caac7fe8b5d43d05f" ], "markers": "python_version < '3.8'", - "version": "==0.20" + "version": "==0.22" }, "mccabe": { "hashes": [ @@ -765,6 +773,7 @@ "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" ], + "index": "pypi", "version": "==7.2.0" }, "nodeenv": { @@ -782,10 +791,10 @@ }, "pluggy": { "hashes": [ - "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc", - "sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c" + "sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", + "sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34" ], - "version": "==0.12.0" + "version": "==0.13.0" }, "pre-commit": { "hashes": [ -- cgit v1.2.3 From 4bc4dc797258abe9f1b432260b47a9abf7e999c8 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Sat, 14 Sep 2019 22:09:35 +0800 Subject: Allow multiple words for !otn a --- bot/cogs/off_topic_names.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/bot/cogs/off_topic_names.py b/bot/cogs/off_topic_names.py index c0d2e5dc5..5a61425be 100644 --- a/bot/cogs/off_topic_names.py +++ b/bot/cogs/off_topic_names.py @@ -95,27 +95,31 @@ class OffTopicNames: @otname_group.command(name='add', aliases=('a',)) @with_role(*MODERATION_ROLES) - async def add_command(self, ctx, name: OffTopicName): + async def add_command(self, ctx, *names: OffTopicName): """Adds a new off-topic name to the rotation.""" + # Chain multiple words to a single one + name = "-".join(names) await self.bot.api_client.post(f'bot/off-topic-channel-names', params={'name': name}) log.info( f"{ctx.author.name}#{ctx.author.discriminator}" f" added the off-topic channel name '{name}" ) - await ctx.send(":ok_hand:") + await ctx.send(f":ok_hand: Added `{name}` to the names list.") @otname_group.command(name='delete', aliases=('remove', 'rm', 'del', 'd')) @with_role(*MODERATION_ROLES) - async def delete_command(self, ctx, name: OffTopicName): + async def delete_command(self, ctx, *names: OffTopicName): """Removes a off-topic name from the rotation.""" + # Chain multiple words to a single one + name = "-".join(names) await self.bot.api_client.delete(f'bot/off-topic-channel-names/{name}') log.info( f"{ctx.author.name}#{ctx.author.discriminator}" f" deleted the off-topic channel name '{name}" ) - await ctx.send(":ok_hand:") + await ctx.send(f":ok_hand: Removed `{name}` from the names list.") @otname_group.command(name='list', aliases=('l',)) @with_role(*MODERATION_ROLES) -- cgit v1.2.3 From d97e0355818fd994b24a6057a43fda3b5c66cb2f Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 14 Sep 2019 16:24:38 +0200 Subject: Don't allow tag invocations in #checkpoint. There was a bug which would permit tag invocations here, because these were triggered by an error handler on CommandNotFound. This commit prevents that from being possible. --- bot/cogs/error_handler.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py index d2a67fd76..a57cabf1e 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -17,7 +17,7 @@ from discord.ext.commands import ( from discord.ext.commands import Bot, Context from bot.api import ResponseCodeError - +from bot.constants import Channels log = logging.getLogger(__name__) @@ -47,12 +47,13 @@ class ErrorHandler: 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 + if not ctx.channel.id == Channels.verification: + tags_get_command = self.bot.get_command("tags get") + ctx.invoked_from_error_handler = True - # Return to not raise the exception - with contextlib.suppress(ResponseCodeError): - return await ctx.invoke(tags_get_command, tag_name=ctx.invoked_with) + # Return to not raise the exception + 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 75428c457c527ed7411685c006e5fe700c68b9a3 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 14 Sep 2019 17:33:44 +0200 Subject: Fixes all URLs in the Site cog. This changes URLs for stuff like FAQ, rules, and the Asking Good Questions page to fit the Django format. --- bot/cogs/site.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bot/cogs/site.py b/bot/cogs/site.py index 37bf4f4ea..b5e63fb41 100644 --- a/bot/cogs/site.py +++ b/bot/cogs/site.py @@ -9,7 +9,7 @@ from bot.pagination import LinePaginator log = logging.getLogger(__name__) -INFO_URL = f"{URLs.site_schema}{URLs.site}/info" +PAGES_URL = f"{URLs.site_schema}{URLs.site}/pages" class Site: @@ -46,7 +46,7 @@ class Site: async def site_resources(self, ctx: Context): """Info about the site's Resources page.""" - url = f"{INFO_URL}/resources" + url = f"{PAGES_URL}/resources" embed = Embed(title="Resources") embed.set_footer(text=url) @@ -63,9 +63,9 @@ class Site: async def site_help(self, ctx: Context): """Info about the site's Getting Help page.""" - url = f"{INFO_URL}/help" + url = f"{PAGES_URL}/asking-good-questions" - embed = Embed(title="Getting Help") + embed = Embed(title="Asking Good Questions") embed.set_footer(text=url) embed.colour = Colour.blurple() embed.description = ( @@ -80,7 +80,7 @@ class Site: async def site_faq(self, ctx: Context): """Info about the site's FAQ page.""" - url = f"{INFO_URL}/faq" + url = f"{PAGES_URL}/frequently-asked-questions" embed = Embed(title="FAQ") embed.set_footer(text=url) @@ -105,7 +105,7 @@ class Site: **`rules`:** The rules a user wants to get. """ rules_embed = Embed(title='Rules', color=Colour.blurple()) - rules_embed.url = f"{URLs.site_schema}{URLs.site}/about/rules" + rules_embed.url = f"{PAGES_URL}/rules" if not rules: # Rules were not submitted. Return the default description. -- cgit v1.2.3 From b8eee655cbfd7767912134c934f962e2aa76415b Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Sat, 14 Sep 2019 18:01:53 +0200 Subject: Deal with multiple active infractions in database While there are measures to prevent duplicate active infractions at the side of the bot, there's no formal restriction at the sid of the database. This means that it's possible for a user two get two active ban infractions or two active mute infractions at the same time, for instance after a manual table edit in Admin. This leads to an inconsistent state when unmuting/unbanning that user, as the ban or mute role will be removed on Discord, but only one of the entries in the database would be set to inactive, while the other(s) remain active. This means that an unmuted user will be remuted if they leave and rejoin the Guild. To handle this, I've inserted code that sets all the infractions to inactive and cancels all related infraction tasks (in the case of temporary infractions) when issuing an unban or unmute command. A note will be added to the mod_log embed to notify us as well. --- bot/cogs/moderation.py | 74 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index bec2f98c1..63c0e4417 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -5,7 +5,7 @@ from datetime import datetime from typing import Dict, Union from discord import ( - Colour, Embed, Forbidden, Guild, HTTPException, Member, Object, User + Colour, Embed, Forbidden, Guild, HTTPException, Member, NotFound, Object, User ) from discord.ext.commands import ( BadArgument, BadUnionArgument, Bot, Context, command, group @@ -772,7 +772,7 @@ class Moderation(Scheduler): @with_role(*MODERATION_ROLES) @command() - async def unmute(self, ctx: Context, user: Member) -> None: + async def unmute(self, ctx: Context, user: UserTypes) -> None: """ Deactivates the active mute infraction for a user. @@ -799,10 +799,10 @@ class Moderation(Scheduler): ) return - infraction = response[0] - await self._deactivate_infraction(infraction) - if infraction["expires_at"] is not None: - self.cancel_expiration(infraction["id"]) + for infraction in response: + await self._deactivate_infraction(infraction) + if infraction["expires_at"] is not None: + self.cancel_expiration(infraction["id"]) notified = await self.notify_pardon( user=user, @@ -822,19 +822,31 @@ class Moderation(Scheduler): await ctx.send(f"{dm_emoji}:ok_hand: Un-muted {user.mention}.") + embed_text = textwrap.dedent( + f""" + Member: {user.mention} (`{user.id}`) + Actor: {ctx.message.author} + DM: {dm_status} + """ + ) + + if len(response) > 1: + footer = f"Infraction IDs: {', '.join(str(infr['id']) for infr in response)}" + title = "Member unmuted" + embed_text += "Note: User had multiple **active** mute infractions in the database." + else: + infraction = response[0] + footer = f"Infraction ID: {infraction['id']}" + title = "Member unmuted" + # Send a log message to the mod log await self.mod_log.send_log_message( icon_url=Icons.user_unmute, colour=Colour(Colours.soft_green), - title="Member unmuted", + title=title, thumbnail=user.avatar_url_as(static_format="png"), - text=textwrap.dedent(f""" - Member: {user.mention} (`{user.id}`) - Actor: {ctx.message.author} - Intended expiry: {infraction['expires_at']} - DM: {dm_status} - """), - footer=infraction["id"], + text=embed_text, + footer=footer, content=log_content ) except Exception: @@ -873,10 +885,24 @@ class Moderation(Scheduler): ) return - infraction = response[0] - await self._deactivate_infraction(infraction) - if infraction["expires_at"] is not None: - self.cancel_expiration(infraction["id"]) + for infraction in response: + await self._deactivate_infraction(infraction) + if infraction["expires_at"] is not None: + self.cancel_expiration(infraction["id"]) + + embed_text = textwrap.dedent( + f""" + Member: {user.mention} (`{user.id}`) + Actor: {ctx.message.author} + """ + ) + + if len(response) > 1: + footer = f"Infraction IDs: {', '.join(str(infr['id']) for infr in response)}" + embed_text += "Note: User had multiple **active** ban infractions in the database." + else: + infraction = response[0] + footer = f"Infraction ID: {infraction['id']}" await ctx.send(f":ok_hand: Un-banned {user.mention}.") @@ -886,11 +912,8 @@ class Moderation(Scheduler): colour=Colour(Colours.soft_green), title="Member unbanned", thumbnail=user.avatar_url_as(static_format="png"), - text=textwrap.dedent(f""" - Member: {user.mention} (`{user.id}`) - Actor: {ctx.message.author} - Intended expiry: {infraction['expires_at']} - """) + text=embed_text, + footer=footer, ) except Exception: log.exception("There was an error removing an infraction.") @@ -1219,7 +1242,10 @@ class Moderation(Scheduler): log.warning(f"Failed to un-mute user: {user_id} (not found)") elif infraction_type == "ban": user: Object = Object(user_id) - await guild.unban(user) + try: + await guild.unban(user) + except NotFound: + log.info(f"Tried to unban user `{user_id}`, but Discord does not have an active ban registered.") await self.bot.api_client.patch( 'bot/infractions/' + str(infraction_object['id']), -- cgit v1.2.3 From d538d8fa4e63cb12c79bdc8f32b3f133a2f16c62 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 14 Sep 2019 18:39:03 +0200 Subject: Nuking Roles.devops and the Deployment cog. The Deployment cog does not work in our new server environment, nor are we particularly inclined to make it work (for security reasons). For this reason, I've removed it. I've also removed all usages of Roles.devops, replacing them with Roles.core_developer whenever this made sense to do, such as in the Cogs cog. --- bot/__main__.py | 1 - bot/cogs/cogs.py | 10 +++--- bot/cogs/deployment.py | 90 -------------------------------------------------- bot/constants.py | 2 +- config-default.yml | 2 +- 5 files changed, 7 insertions(+), 98 deletions(-) delete mode 100644 bot/cogs/deployment.py diff --git a/bot/__main__.py b/bot/__main__.py index e12508e6d..b1a6a5fcd 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -53,7 +53,6 @@ if not DEBUG_MODE: # Feature cogs bot.load_extension("bot.cogs.alias") bot.load_extension("bot.cogs.defcon") -bot.load_extension("bot.cogs.deployment") bot.load_extension("bot.cogs.eval") bot.load_extension("bot.cogs.free") bot.load_extension("bot.cogs.information") diff --git a/bot/cogs/cogs.py b/bot/cogs/cogs.py index 5bef52c0a..eec611824 100644 --- a/bot/cogs/cogs.py +++ b/bot/cogs/cogs.py @@ -37,14 +37,14 @@ class Cogs: self.cogs.update({v: k for k, v in self.cogs.items()}) @group(name='cogs', aliases=('c',), invoke_without_command=True) - @with_role(*MODERATION_ROLES, Roles.devops) + @with_role(*MODERATION_ROLES, Roles.core_developer) async def cogs_group(self, ctx: Context): """Load, unload, reload, and list active cogs.""" await ctx.invoke(self.bot.get_command("help"), "cogs") @cogs_group.command(name='load', aliases=('l',)) - @with_role(*MODERATION_ROLES, Roles.devops) + @with_role(*MODERATION_ROLES, Roles.core_developer) async def load_command(self, ctx: Context, cog: str): """ Load up an unloaded cog, given the module containing it @@ -97,7 +97,7 @@ class Cogs: await ctx.send(embed=embed) @cogs_group.command(name='unload', aliases=('ul',)) - @with_role(*MODERATION_ROLES, Roles.devops) + @with_role(*MODERATION_ROLES, Roles.core_developer) async def unload_command(self, ctx: Context, cog: str): """ Unload an already-loaded cog, given the module containing it @@ -149,7 +149,7 @@ class Cogs: await ctx.send(embed=embed) @cogs_group.command(name='reload', aliases=('r',)) - @with_role(*MODERATION_ROLES, Roles.devops) + @with_role(*MODERATION_ROLES, Roles.core_developer) async def reload_command(self, ctx: Context, cog: str): """ Reload an unloaded cog, given the module containing it @@ -254,7 +254,7 @@ class Cogs: await ctx.send(embed=embed) @cogs_group.command(name='list', aliases=('all',)) - @with_role(*MODERATION_ROLES, Roles.devops) + @with_role(*MODERATION_ROLES, Roles.core_developer) async def list_command(self, ctx: Context): """ Get a list of all cogs, including their loaded status. diff --git a/bot/cogs/deployment.py b/bot/cogs/deployment.py deleted file mode 100644 index e71e07c2f..000000000 --- a/bot/cogs/deployment.py +++ /dev/null @@ -1,90 +0,0 @@ -import logging - -from discord import Colour, Embed -from discord.ext.commands import Bot, Context, command, group - -from bot.constants import Keys, MODERATION_ROLES, Roles, URLs -from bot.decorators import with_role - -log = logging.getLogger(__name__) - - -class Deployment: - """ - Bot information commands - """ - - def __init__(self, bot: Bot): - self.bot = bot - - @group(name='redeploy', invoke_without_command=True) - @with_role(*MODERATION_ROLES) - async def redeploy_group(self, ctx: Context): - """Redeploy the bot or the site.""" - - await ctx.invoke(self.bot.get_command("help"), "redeploy") - - @redeploy_group.command(name='bot') - @with_role(Roles.admin, Roles.owner, Roles.devops) - async def bot_command(self, ctx: Context): - """ - Trigger bot deployment on the server - will only redeploy if there were changes to deploy - """ - - response = await self.bot.http_session.get(URLs.deploy, headers={"token": Keys.deploy_bot}) - result = await response.text() - - if result == "True": - log.debug(f"{ctx.author} triggered deployment for bot. Deployment was started.") - await ctx.send(f"{ctx.author.mention} Bot deployment started.") - else: - log.error(f"{ctx.author} triggered deployment for bot. Deployment failed to start.") - await ctx.send(f"{ctx.author.mention} Bot deployment failed - check the logs!") - - @redeploy_group.command(name='site') - @with_role(Roles.admin, Roles.owner, Roles.devops) - async def site_command(self, ctx: Context): - """ - Trigger website deployment on the server - will only redeploy if there were changes to deploy - """ - - response = await self.bot.http_session.get(URLs.deploy, headers={"token": Keys.deploy_bot}) - result = await response.text() - - if result == "True": - log.debug(f"{ctx.author} triggered deployment for site. Deployment was started.") - await ctx.send(f"{ctx.author.mention} Site deployment started.") - else: - log.error(f"{ctx.author} triggered deployment for site. Deployment failed to start.") - await ctx.send(f"{ctx.author.mention} Site deployment failed - check the logs!") - - @command(name='uptimes') - @with_role(Roles.admin, Roles.owner, Roles.devops) - async def uptimes_command(self, ctx: Context): - """ - Check the various deployment uptimes for each service - """ - - log.debug(f"{ctx.author} requested service uptimes.") - response = await self.bot.http_session.get(URLs.status) - data = await response.json() - - embed = Embed( - title="Service status", - color=Colour.blurple() - ) - - for obj in data: - key, value = list(obj.items())[0] - - embed.add_field( - name=key, value=value, inline=True - ) - - log.debug("Uptimes retrieved and parsed, returning data.") - await ctx.send(embed=embed) - - -def setup(bot): - bot.add_cog(Deployment(bot)) - log.info("Cog loaded: Deployment") diff --git a/bot/constants.py b/bot/constants.py index 45c332438..4e14a85a8 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -374,7 +374,7 @@ class Roles(metaclass=YAMLGetter): announcements: int champion: int contributor: int - devops: int + core_developer: int jammer: int moderator: int muted: int diff --git a/config-default.yml b/config-default.yml index fb33caaf6..20897f78b 100644 --- a/config-default.yml +++ b/config-default.yml @@ -127,7 +127,7 @@ guild: announcements: 463658397560995840 champion: 430492892331769857 contributor: 295488872404484098 - devops: &DEVOPS_ROLE 409416496733880320 + core_developer: 587606783669829632 jammer: 423054537079783434 moderator: &MOD_ROLE 267629731250176001 muted: &MUTED_ROLE 277914926603829249 -- cgit v1.2.3 From a00a55257ba57f26716aef4cc632737d890d75fa Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 14 Sep 2019 19:01:47 +0200 Subject: Oops, forgot to remove DEVOPS role alias, that would crash the bot. --- config-default.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/config-default.yml b/config-default.yml index 20897f78b..b31f79272 100644 --- a/config-default.yml +++ b/config-default.yml @@ -223,7 +223,6 @@ filter: - *ADMIN_ROLE - *MOD_ROLE - *OWNER_ROLE - - *DEVOPS_ROLE - *ROCKSTARS_ROLE -- cgit v1.2.3 From fd7fb3508ae7be9fcad146ea74be5b07736ba266 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 14 Sep 2019 19:47:30 +0200 Subject: Cogs.reload now has more readable error info. When you run '!cogs reload *', the load failures will now include the Exception type, and has overall more readable formatting. Similarly, trying to do '!cogs load ' on a broken cog will show the Exception type as well. --- bot/cogs/cogs.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bot/cogs/cogs.py b/bot/cogs/cogs.py index eec611824..ebdbf5ad8 100644 --- a/bot/cogs/cogs.py +++ b/bot/cogs/cogs.py @@ -84,8 +84,8 @@ class Cogs: except Exception as e: log.error(f"{ctx.author} requested we load the '{cog}' cog, " "but the loading failed with the following error: \n" - f"{e}") - embed.description = f"Failed to load cog: {cog}\n\n```{e}```" + f"**{e.__class__.__name__}: {e}**") + embed.description = f"Failed to load cog: {cog}\n\n{e.__class__.__name__}: {e}" else: log.debug(f"{ctx.author} requested we load the '{cog}' cog. Cog loaded!") embed.description = f"Cog loaded: {cog}" @@ -200,7 +200,7 @@ class Cogs: try: self.bot.unload_extension(loaded_cog) except Exception as e: - failed_unloads[loaded_cog] = str(e) + failed_unloads[loaded_cog] = f"{e.__class__.__name__}: {e}" else: unloaded += 1 @@ -208,7 +208,7 @@ class Cogs: try: self.bot.load_extension(unloaded_cog) except Exception as e: - failed_loads[unloaded_cog] = str(e) + failed_loads[unloaded_cog] = f"{e.__class__.__name__}: {e}" else: loaded += 1 @@ -221,13 +221,13 @@ class Cogs: lines.append("\n**Unload failures**") for cog, error in failed_unloads: - lines.append(f"`{cog}` {Emojis.status_dnd} `{error}`") + lines.append(f"{Emojis.status_dnd} **{cog}:** `{error}`") if failed_loads: lines.append("\n**Load failures**") - for cog, error in failed_loads: - lines.append(f"`{cog}` {Emojis.status_dnd} `{error}`") + for cog, error in failed_loads.items(): + lines.append(f"{Emojis.status_dnd} **{cog}:** `{error}`") log.debug(f"{ctx.author} requested we reload all cogs. Here are the results: \n" f"{lines}") -- cgit v1.2.3 From 5ec4a7044f9bbf41dc9460c452335cabcba602f3 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 14 Sep 2019 11:14:32 -0700 Subject: Fix tag command invocation in aliases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Leon Sandøy --- bot/cogs/alias.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/alias.py b/bot/cogs/alias.py index dbdd2ee6a..a44c47331 100644 --- a/bot/cogs/alias.py +++ b/bot/cogs/alias.py @@ -143,7 +143,7 @@ class Alias: Alias for invoking tags get traceback. """ - await self.invoke(ctx, "tags get traceback") + await self.invoke(ctx, "tags get", tag_name="traceback") @group(name="get", aliases=("show", "g"), @@ -167,7 +167,7 @@ class Alias: tag_name: str - tag to be viewed. """ - await self.invoke(ctx, "tags get", tag_name) + await self.invoke(ctx, "tags get", tag_name=tag_name) @get_group_alias.command(name="docs", aliases=("doc", "d"), hidden=True) async def docs_get_alias( -- cgit v1.2.3 From 6dec487d58c936be9f224b2b2b2e871cc67f34d2 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Sat, 14 Sep 2019 20:17:43 +0200 Subject: Improve handling of long deleted messsages The `modlog` cog failed on long messages with attachments, since the inclusion of attachment metadata would bring the length of the embed description to go over the character limit of 2048. To fix this, I moved the addition of the attachment metadata to earlier in the code so it is taken into account for the character limit. In addition to this, I changed the truncation behavior. Instead of just truncating the message if it's too long, we now truncate and upload the full message to the `deleted messages` endpoint so the full message is still available. A link to the log will be included in the message-log embed. --- bot/cogs/modlog.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py index 9f0c88424..808ba667b 100644 --- a/bot/cogs/modlog.py +++ b/bot/cogs/modlog.py @@ -528,19 +528,22 @@ class ModLog: "\n" ) + if message.attachments: + # Prepend the message metadata with the number of attachments + response = f"**Attachments:** {len(message.attachments)}\n" + response + # Shorten the message content if necessary content = message.clean_content remaining_chars = 2040 - len(response) if len(content) > remaining_chars: - content = content[:remaining_chars] + "..." + botlog_url = await self.upload_log(messages=[message], actor_id=message.author.id) + ending = f"\n\nMessage truncated, [full message here]({botlog_url})." + truncation_point = remaining_chars - len(ending) + content = f"{content[:truncation_point]}...{ending}" response += f"{content}" - if message.attachments: - # Prepend the message metadata with the number of attachments - response = f"**Attachments:** {len(message.attachments)}\n" + response - await self.send_log_message( Icons.message_delete, Colours.soft_red, "Message deleted", -- cgit v1.2.3 From 4178973a5bc53a584ea31540ecf6638f0e8307fc Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 14 Sep 2019 20:49:48 +0200 Subject: Fixes a sneaky genexp exhaustion bug in @without_role. This problem made the decorator only check the first role that was passed into it, instead of checking all the roles. In other words, the check would fail on *STAFF_ROLES unless you had the Helpers role. Solved by refactoring the genexp to a listcomp. --- bot/utils/checks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/utils/checks.py b/bot/utils/checks.py index 37dc657f7..195edab0f 100644 --- a/bot/utils/checks.py +++ b/bot/utils/checks.py @@ -37,7 +37,7 @@ def without_role_check(ctx: Context, *role_ids: int) -> bool: "This command is restricted by the without_role decorator. Rejecting request.") return False - author_roles = (role.id for role in ctx.author.roles) + author_roles = [role.id for role in ctx.author.roles] check = all(role not in author_roles for role in role_ids) log.trace(f"{ctx.author} tried to call the '{ctx.command.name}' command. " f"The result of the without_role check was {check}.") -- cgit v1.2.3 From ea2a30bc7b13b2d5da5a1a997d9279975e0e7e8f Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 14 Sep 2019 21:53:57 +0200 Subject: Fixes a bug syncing roles for members who leave. The event that was supposed to handle this was called on_member_leave instead of on_member_remove, so the even was never called when it should have been. This commit renames the method. --- bot/cogs/sync/cog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/sync/cog.py b/bot/cogs/sync/cog.py index 79177b69e..ec6c5f447 100644 --- a/bot/cogs/sync/cog.py +++ b/bot/cogs/sync/cog.py @@ -118,7 +118,7 @@ class Sync: # If we got `404`, the user is new. Create them. await self.bot.api_client.post('bot/users', json=packed) - async def on_member_leave(self, member: Member) -> None: + async def on_member_remove(self, member: Member) -> None: """Updates the user information when a member leaves the guild.""" await self.bot.api_client.put( f'bot/users/{member.id}', -- cgit v1.2.3 From e50e05398cbe9084a39fa9afc2e85ac544014913 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 14 Sep 2019 14:16:49 -0700 Subject: Ignore errors from cogs with their own error handlers --- bot/cogs/error_handler.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py index a57cabf1e..d65419ae8 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -42,8 +42,8 @@ class ErrorHandler: else: help_command = (self.bot.get_command("help"),) - if hasattr(command, "on_error"): - log.debug(f"Command {command} has a local error handler, ignoring.") + if hasattr(command, "on_error") or hasattr(command.instance, f"_{command.cog_name}__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"): @@ -64,11 +64,19 @@ class ErrorHandler: 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.") - log.warning(f"The bot is missing permissions to execute command {command}: {e.missing_perms}") + log.warning( + f"The bot is missing permissions to execute command {command}: {e.missing_perms}" + ) elif isinstance(e, MissingPermissions): - log.debug(f"{ctx.message.author} is missing permissions to invoke command {command}: {e.missing_perms}") + log.debug( + f"{ctx.message.author} is missing permissions to invoke command {command}: " + f"{e.missing_perms}" + ) elif isinstance(e, (CheckFailure, CommandOnCooldown, DisabledCommand)): - log.debug(f"Command {command} invoked by {ctx.message.author} with error {e.__class__.__name__}: {e}") + log.debug( + f"Command {command} invoked by {ctx.message.author} with error " + f"{e.__class__.__name__}: {e}" + ) elif isinstance(e, CommandInvokeError): if isinstance(e.original, ResponseCodeError): if e.original.response.status == 404: -- cgit v1.2.3 From 6a6590124217cd51ff17c17d2cec0dca9bea1090 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 14 Sep 2019 15:04:58 -0700 Subject: Display no-DM error message originating from security cog's global check The check will behave like Discord.py's guild_only check by raising the NoPrivateMessage exception. --- bot/cogs/security.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/cogs/security.py b/bot/cogs/security.py index f4a843fbf..9523766af 100644 --- a/bot/cogs/security.py +++ b/bot/cogs/security.py @@ -1,6 +1,6 @@ import logging -from discord.ext.commands import Bot, Context +from discord.ext.commands import Bot, Context, NoPrivateMessage log = logging.getLogger(__name__) @@ -19,7 +19,9 @@ class Security: return not ctx.author.bot def check_on_guild(self, ctx: Context): - return ctx.guild is not None + if ctx.guild is None: + raise NoPrivateMessage("This command cannot be used in private messages.") + return True def setup(bot): -- cgit v1.2.3 From 64e8e7423e0d1c765b30552d5d5a390df767c9c1 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 14 Sep 2019 15:55:06 -0700 Subject: Improve logging of command errors --- bot/cogs/error_handler.py | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py index d65419ae8..59b6c0573 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -35,6 +35,7 @@ class ErrorHandler: if command is not None: parent = command.parent + # Retrieve the help command for the invoked command. if parent and command: help_command = (self.bot.get_command("help"), parent.name, command.name) elif command: @@ -46,6 +47,7 @@ class ErrorHandler: log.debug(f"Command {command} has a local error handler; ignoring.") return + # Try to look for a tag with the command's name if the command isn't found. if isinstance(e, CommandNotFound) and not hasattr(ctx, "invoked_from_error_handler"): if not ctx.channel.id == Channels.verification: tags_get_command = self.bot.get_command("tags get") @@ -60,6 +62,10 @@ class ErrorHandler: elif isinstance(e, UserInputError): await ctx.send("Something about your input seems off. Check the arguments:") await ctx.invoke(*help_command) + log.debug( + f"Command {command} invoked by {ctx.message.author} with error " + f"{e.__class__.__name__}: {e}" + ) elif isinstance(e, NoPrivateMessage): await ctx.send("Sorry, this command can't be used in a private message!") elif isinstance(e, BotMissingPermissions): @@ -79,26 +85,35 @@ class ErrorHandler: ) elif isinstance(e, CommandInvokeError): if isinstance(e.original, ResponseCodeError): - if e.original.response.status == 404: + status = e.original.response.status + + if status == 404: await ctx.send("There does not seem to be anything matching your query.") - elif e.original.response.status == 400: + elif status == 400: content = await e.original.response.json() - log.debug("API gave bad request on command. Response: %r.", content) + log.debug(f"API responded with 400 for command {command}: %r.", content) await ctx.send("According to the API, your request is malformed.") - elif 500 <= e.original.response.status < 600: + elif 500 <= status < 600: await ctx.send("Sorry, there seems to be an internal issue with the API.") + log.warning(f"API responded with {status} for command {command}") else: - await ctx.send( - "Got an unexpected status code from the " - f"API (`{e.original.response.code}`)." - ) + await ctx.send(f"Got an unexpected status code from the API (`{status}`).") + log.warning(f"Unexpected API response for command {command}: {status}") else: - await ctx.send( - f"Sorry, an unexpected error occurred. Please let us know!\n\n```{e}```" - ) - raise e.original + await self.handle_unexpected_error(ctx, e.original) else: - raise e + await self.handle_unexpected_error(ctx, e) + + @staticmethod + async def handle_unexpected_error(ctx: Context, e: CommandError): + await ctx.send( + f"Sorry, an unexpected error occurred. Please let us know!\n\n" + f"```{e.__class__.__name__}: {e}```" + ) + log.error( + f"Error executing command invoked by {ctx.message.author}: {ctx.message.content}" + ) + raise e def setup(bot: Bot): -- cgit v1.2.3 From df3f385acfc48e7a97147e32bc03f6f5f69610e3 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 14 Sep 2019 16:23:56 -0700 Subject: Fix cog error handler check when command is None --- 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 59b6c0573..994950c84 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -43,7 +43,7 @@ class ErrorHandler: else: help_command = (self.bot.get_command("help"),) - if hasattr(command, "on_error") or hasattr(command.instance, f"_{command.cog_name}__error"): + if hasattr(command, "on_error") or hasattr(ctx.cog, f"_{command.cog_name}__error"): log.debug(f"Command {command} has a local error handler; ignoring.") return -- cgit v1.2.3 From 14a67c187a38b9748fee375e15cfc9f6aa10fc6f Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Sun, 15 Sep 2019 01:23:49 +0200 Subject: Fix AntiSpam sending duplicate messages to API https://github.com/python-discord/bot/issues/412 https://github.com/python-discord/bot/issues/410 The AntiSpam cog had a bug that caused it to send the same messages in more than one request to the deleted messages API endpoint. Since the API rejects duplicate messages, all requests containing a duplicate message were rejected, even if the request contained new messages as well. This commit fixes that by gathering up all the messages of a single spam event into a single DeletionContext and sending that instead. This commit also prevents the bot fomr being bricked by a single misconfigured antispam configuration. Instead of raising a bare exception, it will now log the validation error and alert moderation on server that antispam has been disabled. closes #412, closes #410 --- bot/cogs/antispam.py | 192 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 143 insertions(+), 49 deletions(-) diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index 02d5d64ce..22f9794f3 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -1,6 +1,9 @@ +import asyncio import logging +from dataclasses import dataclass, field from datetime import datetime, timedelta -from typing import List +from operator import itemgetter +from typing import Dict, Iterable, List, Set from discord import Colour, Member, Message, Object, TextChannel from discord.ext.commands import Bot @@ -33,18 +36,102 @@ RULE_FUNCTION_MAPPING = { } +@dataclass +class DeletionContext: + """Represents a Deletion Context for a single spam event.""" + + channel: TextChannel + members: Dict[int, Member] = field(default_factory=dict) + rules: Set[str] = field(default_factory=set) + messages: Dict[int, Message] = field(default_factory=dict) + + def add(self, rule_name: str, members: Iterable[Member], messages: Iterable[Message]) -> None: + """Adds new rule violation events to the deletion context.""" + self.rules.add(rule_name) + + for member in members: + if member.id not in self.members: + self.members[member.id] = member + + for message in messages: + if message.id not in self.messages: + self.messages[message.id] = message + + async def upload_messages(self, actor_id: int, modlog: ModLog) -> None: + """Method that takes care of uploading the queue and posting modlog alert.""" + triggered_by_users = ", ".join(f"{m.display_name}#{m.discriminator} (`{m.id}`)" for m in self.members.values()) + + mod_alert_message = ( + f"**Triggered by:** {triggered_by_users}\n" + f"**Channel:** {self.channel.mention}\n" + f"**Rules:** {', '.join(rule for rule in self.rules)}\n" + ) + + # For multiple messages or those with excessive newlines, use the logs API + if len(self.messages) > 1 or 'newlines' in self.rules: + url = await modlog.upload_log(self.messages.values(), actor_id) + mod_alert_message += f"A complete log of the offending messages can be found [here]({url})" + else: + mod_alert_message += "Message:\n" + [message] = self.messages.values() + content = message[0].clean_content + remaining_chars = 2040 - len(mod_alert_message) + + if len(content) > remaining_chars: + content = content[:remaining_chars] + "..." + + mod_alert_message += f"{content}" + + *_, last_message = self.messages.values() + await modlog.send_log_message( + icon_url=Icons.filtering, + colour=Colour(Colours.soft_red), + title=f"Spam detected!", + text=mod_alert_message, + thumbnail=last_message.author.avatar_url_as(static_format="png"), + channel_id=Channels.mod_alerts, + ping_everyone=AntiSpamConfig.ping_everyone + ) + + class AntiSpam: - def __init__(self, bot: Bot): + """Cog that controls our anti-spam measures.""" + + def __init__(self, bot: Bot, validation_errors: bool) -> None: self.bot = bot + self.validation_errors = validation_errors role_id = AntiSpamConfig.punishment['role_id'] self.muted_role = Object(role_id) self.expiration_date_converter = ExpirationDate() + self.message_deletion_queue = dict() + self.queue_consumption_tasks = dict() + @property def mod_log(self) -> ModLog: + """Allows for easy access of the ModLog cog.""" return self.bot.get_cog("ModLog") - async def on_message(self, message: Message): + async def on_ready(self): + """Unloads the cog and alerts admins if configuration validation failed.""" + if self.validation_errors: + body = "**The following errors were encountered:**\n" + body += "\n".join(f"- {error}" for error in self.validation_errors.values()) + body += "\n\n**The cog has been unloaded.**" + + await self.mod_log.send_log_message( + title=f"Error: AntiSpam configuration validation failed!", + text=body, + ping_everyone=True, + icon_url=Icons.token_removed, + colour=Colour.red() + ) + + self.bot.remove_cog(self.__class__.__name__) + return + + async def on_message(self, message: Message) -> None: + """Applies the antispam rules to each received message.""" if ( not message.guild or message.guild.id != GuildConfig.id @@ -57,7 +144,7 @@ class AntiSpam: # Fetch the rule configuration with the highest rule interval. max_interval_config = max( AntiSpamConfig.rules.values(), - key=lambda config: config['interval'] + key=itemgetter('interval') ) max_interval = max_interval_config['interval'] @@ -65,6 +152,7 @@ class AntiSpam: earliest_relevant_at = datetime.utcnow() - timedelta(seconds=max_interval) relevant_messages = [ msg async for msg in message.channel.history(after=earliest_relevant_at, reverse=False) + if not msg.author.bot ] for rule_name in AntiSpamConfig.rules: @@ -85,59 +173,48 @@ class AntiSpam: if result is not None: reason, members, relevant_messages = result full_reason = f"`{rule_name}` rule: {reason}" + + # If there's no spam event going on for this channel, start a new Message Deletion Context + if message.channel.id not in self.message_deletion_queue: + log.trace(f"Creating queue for channel `{message.channel.id}`") + self.message_deletion_queue[message.channel.id] = DeletionContext(channel=message.channel) + self.queue_consumption_tasks = self.bot.loop.create_task( + self._process_deletion_context(message.channel.id) + ) + + # Add the relevant of this trigger to the Deletion Context + self.message_deletion_queue[message.channel.id].add( + rule_name=rule_name, + members=members, + messages=relevant_messages + ) + for member in members: # Fire it off as a background task to ensure # that the sleep doesn't block further tasks self.bot.loop.create_task( - self.punish(message, member, full_reason, relevant_messages, rule_name) + self.punish(message, member, full_reason) ) await self.maybe_delete_messages(message.channel, relevant_messages) break - async def punish(self, msg: Message, member: Member, reason: str, messages: List[Message], rule_name: str): - # Sanity check to ensure we're not lagging behind - if self.muted_role not in member.roles: + async def punish(self, msg: Message, member: Member, reason: str) -> None: + """Punishes the given member for triggering an antispam rule.""" + if not any(role.id == self.muted_role.id for role in member.roles): remove_role_after = AntiSpamConfig.punishment['remove_after'] - mod_alert_message = ( - f"**Triggered by:** {member.display_name}#{member.discriminator} (`{member.id}`)\n" - f"**Channel:** {msg.channel.mention}\n" - f"**Reason:** {reason}\n" - ) - - # For multiple messages or those with excessive newlines, use the logs API - if len(messages) > 1 or rule_name == 'newlines': - url = await self.mod_log.upload_log(messages, msg.guild.me.id) - mod_alert_message += f"A complete log of the offending messages can be found [here]({url})" - else: - mod_alert_message += "Message:\n" - content = messages[0].clean_content - remaining_chars = 2040 - len(mod_alert_message) - - if len(content) > remaining_chars: - content = content[:remaining_chars] + "..." - - mod_alert_message += f"{content}" - - # Return the mod log message Context that we can use to post the infraction - mod_log_ctx = await self.mod_log.send_log_message( - icon_url=Icons.filtering, - colour=Colour(Colours.soft_red), - title=f"Spam detected!", - text=mod_alert_message, - thumbnail=msg.author.avatar_url_as(static_format="png"), - channel_id=Channels.mod_alerts, - ping_everyone=AntiSpamConfig.ping_everyone - ) + # We need context, let's get it + context = await self.bot.get_context(msg) # Since we're going to invoke the tempmute command directly, we need to manually call the converter. - dt_remove_role_after = await self.expiration_date_converter.convert(mod_log_ctx, f"{remove_role_after}S") - await mod_log_ctx.invoke(Moderation.tempmute, member, dt_remove_role_after, reason=reason) + dt_remove_role_after = await self.expiration_date_converter.convert(context, f"{remove_role_after}S") + await context.invoke(Moderation.tempmute, member, dt_remove_role_after, reason=reason) + + async def maybe_delete_messages(self, channel: TextChannel, messages: List[Message]) -> None: + """Cleans the messages if cleaning is configured.""" - async def maybe_delete_messages(self, channel: TextChannel, messages: List[Message]): - # Is deletion of offending messages actually enabled? if AntiSpamConfig.clean_offending: # If we have more than one message, we can use bulk delete. @@ -152,24 +229,41 @@ class AntiSpam: self.mod_log.ignore(Event.message_delete, messages[0].id) await messages[0].delete() + async def _process_deletion_context(self, context_id: int) -> None: + """Processes the Deletion Context queue.""" + log.trace("Sleeping before processing message deletion queue.") + await asyncio.sleep(10) -def validate_config(): + if context_id not in self.message_deletion_queue: + log.error(f"Started processing deletion queue for context `{context_id}`, but it was not found!") + return + + deletion_context = self.message_deletion_queue.pop(context_id) + await deletion_context.upload_messages(self.bot.user.id, self.mod_log) + + +def validate_config() -> bool: + """Validates the antispam configs.""" + validation_errors = {} for name, config in AntiSpamConfig.rules.items(): if name not in RULE_FUNCTION_MAPPING: - raise ValueError( + log.error( f"Unrecognized antispam rule `{name}`. " f"Valid rules are: {', '.join(RULE_FUNCTION_MAPPING)}" ) - + validation_errors[name] = f"`{name}` is not recognized as an antispam rule." for required_key in ('interval', 'max'): if required_key not in config: - raise ValueError( + log.error( f"`{required_key}` is required but was not " f"set in rule `{name}`'s configuration." ) + validation_errors[name] = f"Key `{required_key}` is required but not set for rule `{name}`" + return validation_errors -def setup(bot: Bot): - validate_config() - bot.add_cog(AntiSpam(bot)) +def setup(bot: Bot) -> None: + """Setup for the cog.""" + validation_errors = validate_config() + bot.add_cog(AntiSpam(bot, validation_errors)) log.info("Cog loaded: AntiSpam") -- cgit v1.2.3 From 76c9515501c09a8adb19dcff8f1d61e2a6dd188f Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Sun, 15 Sep 2019 02:00:04 +0200 Subject: Fix deleting already deleted message in antispam Since we're in an async context, it can happen that a message was already deleted before the antispam cog could get to it. To prevent processing from stopping dead because of a NotFound exception, I added a try-except and log message to handle that In addition, corrected a small mistake: trying to indice a single Message object. Corrected. --- bot/cogs/antispam.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index 22f9794f3..69367b40b 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta from operator import itemgetter from typing import Dict, Iterable, List, Set -from discord import Colour, Member, Message, Object, TextChannel +from discord import Colour, Member, Message, NotFound, Object, TextChannel from discord.ext.commands import Bot from bot import rules @@ -74,7 +74,7 @@ class DeletionContext: else: mod_alert_message += "Message:\n" [message] = self.messages.values() - content = message[0].clean_content + content = message.clean_content remaining_chars = 2040 - len(mod_alert_message) if len(content) > remaining_chars: @@ -227,7 +227,10 @@ class AntiSpam: # Delete the message directly instead. else: self.mod_log.ignore(Event.message_delete, messages[0].id) - await messages[0].delete() + try: + await messages[0].delete() + except NotFound: + log.info(f"Tried to delete message `{messages[0].id}`, but message could not be found.") async def _process_deletion_context(self, context_id: int) -> None: """Processes the Deletion Context queue.""" -- cgit v1.2.3 From d1626f6263d40acca8a80257ba085107c6c8633a Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 14 Sep 2019 17:00:28 -0700 Subject: Actually fix cog error handler check when command is None --- bot/cogs/error_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py index 994950c84..1f0700f28 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -43,7 +43,8 @@ class ErrorHandler: else: help_command = (self.bot.get_command("help"),) - if hasattr(command, "on_error") or hasattr(ctx.cog, f"_{command.cog_name}__error"): + cog_has_handler = command and hasattr(ctx.cog, f"_{command.cog_name}__error") + if hasattr(command, "on_error") or cog_has_handler: log.debug(f"Command {command} has a local error handler; ignoring.") return -- cgit v1.2.3 From 45a4fb55498230177ae8cadb3e308adcfa755443 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 14 Sep 2019 17:41:11 -0700 Subject: Generate InChannelCheckFailure's message inside the exception The exception now expects channel IDs to be passed to it. --- bot/decorators.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/bot/decorators.py b/bot/decorators.py index 1ba2cd59e..923d21938 100644 --- a/bot/decorators.py +++ b/bot/decorators.py @@ -18,7 +18,11 @@ log = logging.getLogger(__name__) class InChannelCheckFailure(CheckFailure): - pass + def __init__(self, *channels: int): + self.channels = channels + channels_str = ', '.join(f"<#{c_id}>" for c_id in channels) + + super().__init__(f"Sorry, but you may only use this command within {channels_str}.") def in_channel(*channels: int, bypass_roles: typing.Container[int] = None): @@ -41,10 +45,7 @@ def in_channel(*channels: int, bypass_roles: typing.Container[int] = None): 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}." - ) + raise InChannelCheckFailure(*channels) return commands.check(predicate) -- cgit v1.2.3 From 4a5ea8cd1fc13de8c419ef48b42dc11f5bba6705 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 14 Sep 2019 18:20:22 -0700 Subject: Ignore handled errors by checking for a "handled" attribute --- bot/cogs/error_handler.py | 5 ++--- bot/cogs/moderation.py | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py index 1f0700f28..033a49d39 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -43,9 +43,8 @@ class ErrorHandler: else: help_command = (self.bot.get_command("help"),) - cog_has_handler = command and hasattr(ctx.cog, f"_{command.cog_name}__error") - if hasattr(command, "on_error") or cog_has_handler: - log.debug(f"Command {command} has a local error handler; ignoring.") + if hasattr(e, "handled"): + log.trace(f"Command {command} had its error already handled locally; ignoring.") return # Try to look for a tag with the command's name if the command isn't found. diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 63c0e4417..fb791c933 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -1378,6 +1378,7 @@ class Moderation(Scheduler): if isinstance(error, BadUnionArgument): if User in error.converters: await ctx.send(str(error.errors[0])) + error.handled = True async def respect_role_hierarchy(self, ctx: Context, target: UserTypes, infr_type: str) -> bool: """ -- cgit v1.2.3 From c020004cb75414865583d1c200cc9a6df1a49cbb Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 14 Sep 2019 18:42:35 -0700 Subject: Remove most local error handlers & handle InChannelCheckFailure globally * Add error handler to ignore InChannelCheckFailure in the verification cog * Raise InChannelCheckFailure instead of MissingPermissions in !user * Send message instead of raising BadArgument in !user to prevent help message from being shown in such case * Clean up !user command --- bot/cogs/error_handler.py | 3 +++ bot/cogs/information.py | 57 +++++++++++++---------------------------------- bot/cogs/snekbox.py | 31 +++----------------------- bot/cogs/utils.py | 12 ++-------- bot/cogs/verification.py | 8 ++++++- 5 files changed, 30 insertions(+), 81 deletions(-) diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py index 033a49d39..cfcba6f26 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -18,6 +18,7 @@ from discord.ext.commands import Bot, Context from bot.api import ResponseCodeError from bot.constants import Channels +from bot.decorators import InChannelCheckFailure log = logging.getLogger(__name__) @@ -78,6 +79,8 @@ class ErrorHandler: f"{ctx.message.author} is missing permissions to invoke command {command}: " f"{e.missing_perms}" ) + elif isinstance(e, InChannelCheckFailure): + await ctx.send(e) elif isinstance(e, (CheckFailure, CommandOnCooldown, DisabledCommand)): log.debug( f"Command {command} invoked by {ctx.message.author} with error " diff --git a/bot/cogs/information.py b/bot/cogs/information.py index a2585f395..320750a24 100644 --- a/bot/cogs/information.py +++ b/bot/cogs/information.py @@ -1,16 +1,13 @@ import logging -import random import textwrap from discord import CategoryChannel, Colour, Embed, Member, TextChannel, VoiceChannel -from discord.ext.commands import ( - BadArgument, Bot, CommandError, Context, MissingPermissions, command -) +from discord.ext.commands import Bot, Context, command from bot.constants import ( - Channels, Emojis, Keys, MODERATION_ROLES, NEGATIVE_REPLIES, STAFF_ROLES + Channels, Emojis, Keys, MODERATION_ROLES, STAFF_ROLES ) -from bot.decorators import with_role +from bot.decorators import InChannelCheckFailure, with_role from bot.utils.checks import with_role_check from bot.utils.time import time_since @@ -131,30 +128,25 @@ class Information: Returns info about a user. """ - # Do a role check if this is being executed on - # someone other than the caller - if user and user != ctx.author: - if not with_role_check(ctx, *MODERATION_ROLES): - raise BadArgument("You do not have permission to use this command on users other than yourself.") + if user is None: + user = ctx.author + + # Do a role check if this is being executed on someone other than the caller + if user != ctx.author and not with_role_check(ctx, *MODERATION_ROLES): + await ctx.send("You may not use this command on users other than yourself.") + return - # Non-moderators may only do this in #bot-commands and can't see - # hidden infractions. + # Non-moderators may only do this in #bot-commands and can't see hidden infractions. if not with_role_check(ctx, *STAFF_ROLES): if not ctx.channel.id == Channels.bot: - raise MissingPermissions("You can't do that here!") + raise InChannelCheckFailure(Channels.bot) # Hide hidden infractions for users without a moderation role hidden = False - # Validates hidden input - hidden = str(hidden) - - if user is None: - user = ctx.author - # User information created = time_since(user.created_at, max_units=3) - name = f"{user.name}#{user.discriminator}" + name = str(user) if user.nick: name = f"{user.nick} ({name})" @@ -162,15 +154,13 @@ class Information: joined = time_since(user.joined_at, precision="days") # You're welcome, Volcyyyyyyyyyyyyyyyy - roles = ", ".join( - role.mention for role in user.roles if role.name != "@everyone" - ) + roles = ", ".join(role.mention for role in user.roles if role.name != "@everyone") # Infractions infractions = await self.bot.api_client.get( 'bot/infractions', params={ - 'hidden': hidden, + 'hidden': str(hidden), 'user__id': str(user.id) } ) @@ -209,23 +199,6 @@ class Information: await ctx.send(embed=embed) - @user_info.error - async def user_info_command_error(self, ctx: Context, error: CommandError): - embed = Embed(colour=Colour.red()) - - if isinstance(error, BadArgument): - embed.title = random.choice(NEGATIVE_REPLIES) - embed.description = str(error) - await ctx.send(embed=embed) - - elif isinstance(error, MissingPermissions): - embed.title = random.choice(NEGATIVE_REPLIES) - embed.description = f"Sorry, but you may only use this command within <#{Channels.bot}>." - await ctx.send(embed=embed) - - else: - log.exception(f"Unhandled error: {error}") - def setup(bot): bot.add_cog(Information(bot)) diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index 05834e421..c8705ac6f 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -1,18 +1,14 @@ import datetime import logging -import random import re import textwrap from signal import Signals from typing import Optional, Tuple -from discord import Colour, Embed -from discord.ext.commands import ( - Bot, CommandError, Context, NoPrivateMessage, command, guild_only -) +from discord.ext.commands import Bot, Context, command, guild_only -from bot.constants import Channels, ERROR_REPLIES, NEGATIVE_REPLIES, STAFF_ROLES, URLs -from bot.decorators import InChannelCheckFailure, in_channel +from bot.constants import Channels, STAFF_ROLES, URLs +from bot.decorators import in_channel from bot.utils.messages import wait_for_deletion @@ -224,27 +220,6 @@ class Snekbox: finally: del self.jobs[ctx.author.id] - @eval_command.error - async def eval_command_error(self, ctx: Context, error: CommandError): - embed = Embed(colour=Colour.red()) - - if isinstance(error, NoPrivateMessage): - embed.title = random.choice(NEGATIVE_REPLIES) - embed.description = "You're not allowed to use this command in private messages." - await ctx.send(embed=embed) - - elif isinstance(error, InChannelCheckFailure): - embed.title = random.choice(NEGATIVE_REPLIES) - embed.description = str(error) - await ctx.send(embed=embed) - - else: - original_error = getattr(error, 'original', "no original error") - log.error(f"Unhandled error in snekbox eval: {error} ({original_error})") - embed.title = random.choice(ERROR_REPLIES) - embed.description = "Some unhandled error occurred. Sorry for that!" - await ctx.send(embed=embed) - def setup(bot): bot.add_cog(Snekbox(bot)) diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py index 0c6d9d2ba..98208723a 100644 --- a/bot/cogs/utils.py +++ b/bot/cogs/utils.py @@ -1,5 +1,4 @@ import logging -import random import re import unicodedata from email.parser import HeaderParser @@ -8,8 +7,8 @@ from io import StringIO from discord import Colour, Embed from discord.ext.commands import AutoShardedBot, Context, command -from bot.constants import Channels, NEGATIVE_REPLIES, STAFF_ROLES -from bot.decorators import InChannelCheckFailure, in_channel +from bot.constants import Channels, STAFF_ROLES +from bot.decorators import in_channel log = logging.getLogger(__name__) @@ -133,13 +132,6 @@ class Utils: await ctx.send(embed=embed) - async def __error(self, ctx, error): - embed = Embed(colour=Colour.red()) - if isinstance(error, InChannelCheckFailure): - embed.title = random.choice(NEGATIVE_REPLIES) - embed.description = str(error) - await ctx.send(embed=embed) - def setup(bot): bot.add_cog(Utils(bot)) diff --git a/bot/cogs/verification.py b/bot/cogs/verification.py index 56fcd63eb..6b42c9213 100644 --- a/bot/cogs/verification.py +++ b/bot/cogs/verification.py @@ -5,7 +5,7 @@ from discord.ext.commands import Bot, Context, command from bot.cogs.modlog import ModLog from bot.constants import Channels, Event, Roles -from bot.decorators import in_channel, without_role +from bot.decorators import InChannelCheckFailure, in_channel, without_role log = logging.getLogger(__name__) @@ -151,6 +151,12 @@ class Verification: f"{ctx.author.mention} Unsubscribed from <#{Channels.announcements}> notifications." ) + @staticmethod + async def __error(ctx: Context, error): + if isinstance(error, InChannelCheckFailure): + # Do nothing; just ignore this error + error.handled = True + @staticmethod def __global_check(ctx: Context): """ -- cgit v1.2.3 From 33f04eb28b720e69ad9def01961ffca8e55393fb Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 11:26:18 +0200 Subject: Tag Django images as `latest`. --- scripts/deploy-azure.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/deploy-azure.sh b/scripts/deploy-azure.sh index af69ab46b..9ffe01ab8 100644 --- a/scripts/deploy-azure.sh +++ b/scripts/deploy-azure.sh @@ -5,8 +5,8 @@ cd .. # Build and deploy on django branch, only if not a pull request if [[ ($BUILD_SOURCEBRANCHNAME == 'django') && ($SYSTEM_PULLREQUEST_PULLREQUESTID == '') ]]; then echo "Building image" - docker build -t pythondiscord/bot:django . + docker build -t pythondiscord/bot:latest . echo "Pushing image" - docker push pythondiscord/bot:django + docker push pythondiscord/bot:latest fi -- cgit v1.2.3 From d8f3d10a5298095d5b9dffe1f063ad69c8498883 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 12:07:07 +0200 Subject: Validate bot.cogs.antispam configuration on CI. --- bot/cogs/antispam.py | 6 ++++-- tests/cogs/test_antispam.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 tests/cogs/test_antispam.py diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index 69367b40b..482965b9b 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -1,5 +1,6 @@ import asyncio import logging +from collections.abc import Mapping from dataclasses import dataclass, field from datetime import datetime, timedelta from operator import itemgetter @@ -245,16 +246,17 @@ class AntiSpam: await deletion_context.upload_messages(self.bot.user.id, self.mod_log) -def validate_config() -> bool: +def validate_config(rules: Mapping = AntiSpamConfig.rules) -> dict: """Validates the antispam configs.""" validation_errors = {} - for name, config in AntiSpamConfig.rules.items(): + for name, config in rules.items(): if name not in RULE_FUNCTION_MAPPING: log.error( f"Unrecognized antispam rule `{name}`. " f"Valid rules are: {', '.join(RULE_FUNCTION_MAPPING)}" ) validation_errors[name] = f"`{name}` is not recognized as an antispam rule." + continue for required_key in ('interval', 'max'): if required_key not in config: log.error( diff --git a/tests/cogs/test_antispam.py b/tests/cogs/test_antispam.py new file mode 100644 index 000000000..67900b275 --- /dev/null +++ b/tests/cogs/test_antispam.py @@ -0,0 +1,30 @@ +import pytest + +from bot.cogs import antispam + + +def test_default_antispam_config_is_valid(): + validation_errors = antispam.validate_config() + assert not validation_errors + + +@pytest.mark.parametrize( + ('config', 'expected'), + ( + ( + {'invalid-rule': {}}, + {'invalid-rule': "`invalid-rule` is not recognized as an antispam rule."} + ), + ( + {'burst': {'interval': 10}}, + {'burst': "Key `max` is required but not set for rule `burst`"} + ), + ( + {'burst': {'max': 10}}, + {'burst': "Key `interval` is required but not set for rule `burst`"} + ) + ) +) +def test_invalid_antispam_config_returns_validation_errors(config, expected): + validation_errors = antispam.validate_config(config) + assert validation_errors == expected -- cgit v1.2.3 From b3279e4251d9e7caa25e32db2bca170275c130ef Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 15 Sep 2019 12:30:58 +0200 Subject: The DockerHub deployment should now run on 'master' --- scripts/deploy-azure.sh | 4 ++-- scripts/deploy.sh | 32 -------------------------------- 2 files changed, 2 insertions(+), 34 deletions(-) delete mode 100644 scripts/deploy.sh diff --git a/scripts/deploy-azure.sh b/scripts/deploy-azure.sh index 9ffe01ab8..ed4b719e2 100644 --- a/scripts/deploy-azure.sh +++ b/scripts/deploy-azure.sh @@ -2,8 +2,8 @@ cd .. -# Build and deploy on django branch, only if not a pull request -if [[ ($BUILD_SOURCEBRANCHNAME == 'django') && ($SYSTEM_PULLREQUEST_PULLREQUESTID == '') ]]; then +# Build and deploy on master branch, only if not a pull request +if [[ ($BUILD_SOURCEBRANCHNAME == 'master') && ($SYSTEM_PULLREQUEST_PULLREQUESTID == '') ]]; then echo "Building image" docker build -t pythondiscord/bot:latest . diff --git a/scripts/deploy.sh b/scripts/deploy.sh deleted file mode 100644 index 070d0ec26..000000000 --- a/scripts/deploy.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - -# Build and deploy on master branch -if [[ $CI_COMMIT_REF_SLUG == 'master' ]]; then - echo "Connecting to docker hub" - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - - changed_lines=$(git diff HEAD~1 HEAD docker/base.Dockerfile | wc -l) - - if [ $changed_lines != '0' ]; then - echo "base.Dockerfile was changed" - - echo "Building bot base" - docker build -t pythondiscord/bot-base:latest -f docker/base.Dockerfile . - - echo "Pushing image to Docker Hub" - docker push pythondiscord/bot-base:latest - else - echo "base.Dockerfile was not changed, not building" - fi - - echo "Building image" - docker build -t pythondiscord/bot:latest -f docker/bot.Dockerfile . - - echo "Pushing image" - docker push pythondiscord/bot:latest - - echo "Deploying container" - curl -H "token: $AUTODEPLOY_TOKEN" $AUTODEPLOY_WEBHOOK -else - echo "Skipping deploy" -fi -- cgit v1.2.3 From 82e8ca20bb1f61162d1b55c6e354c68ea4cdfcf1 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 12:57:54 +0200 Subject: Add tests for `bot.utils.checks`. --- tests/utils/__init__.py | 0 tests/utils/test_checks.py | 67 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 tests/utils/__init__.py create mode 100644 tests/utils/test_checks.py diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/utils/test_checks.py b/tests/utils/test_checks.py new file mode 100644 index 000000000..915d074b3 --- /dev/null +++ b/tests/utils/test_checks.py @@ -0,0 +1,67 @@ +from unittest.mock import MagicMock + +from bot.utils import checks + + +def test_with_role_check_without_guild(): + context = MagicMock() + context.guild = None + + assert not checks.with_role_check(context) + + +def test_with_role_check_with_guild_without_required_role(): + context = MagicMock() + context.guild = True + context.author.roles = [] + + assert not checks.with_role_check(context) + + +def test_with_role_check_with_guild_with_required_role(): + context = MagicMock() + context.guild = True + role = MagicMock() + role.id = 42 + context.author.roles = (role,) + + assert checks.with_role_check(context, role.id) + + +def test_without_role_check_without_guild(): + context = MagicMock() + context.guild = None + + assert not checks.without_role_check(context) + + +def test_without_role_check_with_unwanted_role(): + context = MagicMock() + context.guild = True + role = MagicMock() + role.id = 42 + context.author.roles = (role,) + + assert not checks.without_role_check(context, role.id) + + +def test_without_role_check_without_unwanted_role(): + context = MagicMock() + context.guild = True + role = MagicMock() + role.id = 42 + context.author.roles = (role,) + + assert checks.without_role_check(context, role.id + 10) + + +def test_in_channel_check_for_correct_channel(): + context = MagicMock() + context.channel.id = 42 + assert checks.in_channel_check(context, context.channel.id) + + +def test_in_channel_check_for_incorrect_channel(): + context = MagicMock() + context.channel.id = 42 + assert not checks.in_channel_check(context, context.channel.id + 10) -- cgit v1.2.3 From 3385689dee24c08bb66b8bedba57caa617023f6b Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 15 Sep 2019 14:11:56 +0200 Subject: Resolves a breaking bug in the Dockerfile. We were using a pipenv run script to launch the bot, but pipenv run scripts assume that the run command will be run inside of a virtual environment. So, the default behaviour when we try to use a run command and no venv exists is to create a venv. Because we were installing all the packages to the local environment by passing the '--system' flag to our install, this would make the bot fail with ImportErrors. This commit fixes it so that the Dockerfile will run the bot using the system Python instead of the pipenv run script. --- Dockerfile | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 864b4e557..aa6333380 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,15 +14,7 @@ RUN apk add --no-cache \ zlib-dev ENV \ - LIBRARY_PATH=/lib:/usr/lib \ - PIPENV_HIDE_EMOJIS=1 \ - PIPENV_HIDE_EMOJIS=1 \ - PIPENV_IGNORE_VIRTUALENVS=1 \ - PIPENV_IGNORE_VIRTUALENVS=1 \ - PIPENV_NOSPIN=1 \ - PIPENV_NOSPIN=1 \ - PIPENV_VENV_IN_PROJECT=1 \ - PIPENV_VENV_IN_PROJECT=1 + LIBRARY_PATH=/lib:/usr/lib RUN pip install -U pipenv @@ -32,4 +24,4 @@ COPY . . RUN pipenv install --deploy --system ENTRYPOINT ["/sbin/tini", "--"] -CMD ["pipenv", "run", "start"] +CMD ["python3", "-m", "bot"] -- cgit v1.2.3 From 3da45c2a8ac967c9c0ed1525e04686914eb50e7d Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 12:43:11 +0200 Subject: Add tests for `bot.converters`. --- tests/test_converters.py | 93 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 tests/test_converters.py diff --git a/tests/test_converters.py b/tests/test_converters.py new file mode 100644 index 000000000..3cf774c80 --- /dev/null +++ b/tests/test_converters.py @@ -0,0 +1,93 @@ +import asyncio +from datetime import datetime +from unittest.mock import MagicMock + +import pytest +from discord.ext.commands import BadArgument + +from bot.converters import ( + ExpirationDate, + TagContentConverter, + TagNameConverter, + ValidPythonIdentifier, +) + + +@pytest.mark.parametrize( + ('value', 'expected'), + ( + # sorry aliens + ('2199-01-01T00:00:00', datetime(2199, 1, 1)), + ) +) +def test_expiration_date_converter_for_valid(value: str, expected: datetime): + converter = ExpirationDate() + assert asyncio.run(converter.convert(None, value)) == expected + + +@pytest.mark.parametrize( + ('value', 'expected'), + ( + ('hello', 'hello'), + (' h ello ', 'h ello') + ) +) +def test_tag_content_converter_for_valid(value: str, expected: str): + assert asyncio.run(TagContentConverter.convert(None, value)) == expected + + +@pytest.mark.parametrize( + ('value', 'expected'), + ( + ('', "Tag contents should not be empty, or filled with whitespace."), + (' ', "Tag contents should not be empty, or filled with whitespace.") + ) +) +def test_tag_content_converter_for_invalid(value: str, expected: str): + context = MagicMock() + context.author = 'bob' + + with pytest.raises(BadArgument, match=expected): + asyncio.run(TagContentConverter.convert(context, value)) + + +@pytest.mark.parametrize( + ('value', 'expected'), + ( + ('tracebacks', 'tracebacks'), + ('Tracebacks', 'tracebacks'), + (' Tracebacks ', 'tracebacks'), + ) +) +def test_tag_name_converter_for_valid(value: str, expected: str): + assert asyncio.run(TagNameConverter.convert(None, value)) == expected + + +@pytest.mark.parametrize( + ('value', 'expected'), + ( + ('👋', "Don't be ridiculous, you can't use that character!"), + ('', "Tag names should not be empty, or filled with whitespace."), + (' ', "Tag names should not be empty, or filled with whitespace."), + ('42', "Tag names can't be numbers."), + # Escape question mark as this is evaluated as regular expression. + ('x' * 128, r"Are you insane\? That's way too long!"), + ) +) +def test_tag_name_converter_for_invalid(value: str, expected: str): + context = MagicMock() + context.author = 'bob' + + with pytest.raises(BadArgument, match=expected): + asyncio.run(TagNameConverter.convert(context, value)) + + +@pytest.mark.parametrize('value', ('foo', 'lemon')) +def test_valid_python_identifier_for_valid(value: str): + assert asyncio.run(ValidPythonIdentifier.convert(None, value)) == value + + +@pytest.mark.parametrize('value', ('nested.stuff', '#####')) +def test_valid_python_identifier_for_invalid(value: str): + with pytest.raises(BadArgument, match=f'`{value}` is not a valid Python identifier'): + asyncio.run(ValidPythonIdentifier.convert(None, value)) -- cgit v1.2.3 From bf9a6c250eb28dd64e8b449a9641f09c9b5bd2d7 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 14:29:08 +0200 Subject: Typehint the result of `validate_config`. Co-Authored-By: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> --- bot/cogs/antispam.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index 482965b9b..e980de364 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -246,7 +246,7 @@ class AntiSpam: await deletion_context.upload_messages(self.bot.user.id, self.mod_log) -def validate_config(rules: Mapping = AntiSpamConfig.rules) -> dict: +def validate_config(rules: Mapping = AntiSpamConfig.rules) -> Dict[str, str]: """Validates the antispam configs.""" validation_errors = {} for name, config in rules.items(): -- cgit v1.2.3 From aaa24e14e27389deaa33f2776766805b195079cc Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 15 Sep 2019 14:39:43 +0200 Subject: Temporarily pointing config at django.pydis.com --- config-default.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config-default.yml b/config-default.yml index b31f79272..599fc5093 100644 --- a/config-default.yml +++ b/config-default.yml @@ -234,8 +234,8 @@ keys: urls: # PyDis site vars - site: &DOMAIN "pythondiscord.com" - site_api: &API !JOIN ["api.", *DOMAIN] + site: &DOMAIN "django.pythondiscord.com" + site_api: &API !JOIN ["api", *DOMAIN] site_paste: &PASTE !JOIN ["paste.", *DOMAIN] site_staff: &STAFF !JOIN ["staff.", *DOMAIN] site_schema: &SCHEMA "https://" -- cgit v1.2.3 From ca1f7dca0413a7b425654d32bdee1ddef6222d3d Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 15 Sep 2019 15:03:17 +0200 Subject: Adding the snekbox URL to the default config, fixing typo. --- config-default.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config-default.yml b/config-default.yml index 599fc5093..bae4b16c0 100644 --- a/config-default.yml +++ b/config-default.yml @@ -235,7 +235,7 @@ keys: urls: # PyDis site vars site: &DOMAIN "django.pythondiscord.com" - site_api: &API !JOIN ["api", *DOMAIN] + site_api: &API !JOIN ["api.", *DOMAIN] site_paste: &PASTE !JOIN ["paste.", *DOMAIN] site_staff: &STAFF !JOIN ["staff.", *DOMAIN] site_schema: &SCHEMA "https://" @@ -261,7 +261,7 @@ urls: paste_service: !JOIN [*SCHEMA, *PASTE, "/{key}"] # Snekbox - snekbox_eval_api: "http://localhost:8060/eval" + snekbox_eval_api: "https://snekbox.pythondiscord.com/eval" # Env vars deploy: !ENV "DEPLOY_URL" -- cgit v1.2.3 From ab21ed98d7373b80c52f007c8becb93a5edcd03a Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 15:10:34 +0200 Subject: Use `@pytest.fixture` for creating contexts. --- tests/utils/test_checks.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/tests/utils/test_checks.py b/tests/utils/test_checks.py index 915d074b3..7121acebd 100644 --- a/tests/utils/test_checks.py +++ b/tests/utils/test_checks.py @@ -1,25 +1,29 @@ from unittest.mock import MagicMock +import pytest + from bot.utils import checks -def test_with_role_check_without_guild(): - context = MagicMock() +@pytest.fixture() +def context(): + return MagicMock() + + +def test_with_role_check_without_guild(context): context.guild = None assert not checks.with_role_check(context) -def test_with_role_check_with_guild_without_required_role(): - context = MagicMock() +def test_with_role_check_with_guild_without_required_role(context): context.guild = True context.author.roles = [] assert not checks.with_role_check(context) -def test_with_role_check_with_guild_with_required_role(): - context = MagicMock() +def test_with_role_check_with_guild_with_required_role(context): context.guild = True role = MagicMock() role.id = 42 @@ -28,15 +32,13 @@ def test_with_role_check_with_guild_with_required_role(): assert checks.with_role_check(context, role.id) -def test_without_role_check_without_guild(): - context = MagicMock() +def test_without_role_check_without_guild(context): context.guild = None assert not checks.without_role_check(context) -def test_without_role_check_with_unwanted_role(): - context = MagicMock() +def test_without_role_check_with_unwanted_role(context): context.guild = True role = MagicMock() role.id = 42 @@ -45,8 +47,7 @@ def test_without_role_check_with_unwanted_role(): assert not checks.without_role_check(context, role.id) -def test_without_role_check_without_unwanted_role(): - context = MagicMock() +def test_without_role_check_without_unwanted_role(context): context.guild = True role = MagicMock() role.id = 42 @@ -55,13 +56,11 @@ def test_without_role_check_without_unwanted_role(): assert checks.without_role_check(context, role.id + 10) -def test_in_channel_check_for_correct_channel(): - context = MagicMock() +def test_in_channel_check_for_correct_channel(context): context.channel.id = 42 assert checks.in_channel_check(context, context.channel.id) -def test_in_channel_check_for_incorrect_channel(): - context = MagicMock() +def test_in_channel_check_for_incorrect_channel(context): context.channel.id = 42 assert not checks.in_channel_check(context, context.channel.id + 10) -- cgit v1.2.3 From 1c8b07bc2262f08af26aec00633de73dac5a4ddb Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 15:19:52 +0200 Subject: Add basic tests for `bot.pagination`. --- tests/test_pagination.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tests/test_pagination.py diff --git a/tests/test_pagination.py b/tests/test_pagination.py new file mode 100644 index 000000000..11d6541ae --- /dev/null +++ b/tests/test_pagination.py @@ -0,0 +1,29 @@ +from unittest import TestCase + +import pytest + +from bot import pagination + + +class LinePaginatorTests(TestCase): + def setUp(self): + self.paginator = pagination.LinePaginator(prefix='', suffix='', max_size=30) + + def test_add_line_raises_on_too_long_lines(self): + message = f"Line exceeds maximum page size {self.paginator.max_size - 2}" + with pytest.raises(RuntimeError, match=message): + self.paginator.add_line('x' * self.paginator.max_size) + + def test_add_line_works_on_small_lines(self): + self.paginator.add_line('x' * (self.paginator.max_size - 3)) + + +class ImagePaginatorTests(TestCase): + def setUp(self): + self.paginator = pagination.ImagePaginator() + + def test_add_image_appends_image(self): + image = 'lemon' + self.paginator.add_image(image) + + assert self.paginator.images == [image] -- cgit v1.2.3 From e7342735d00205ca09771a5c434dba5e1d185ba7 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 15 Sep 2019 15:34:22 +0200 Subject: Bot Test Server default config, for testing. --- config-default.yml | 127 ++++++++++--------- config-prod.yml | 360 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 423 insertions(+), 64 deletions(-) create mode 100644 config-prod.yml diff --git a/config-default.yml b/config-default.yml index bae4b16c0..eace9caff 100644 --- a/config-default.yml +++ b/config-default.yml @@ -1,5 +1,5 @@ bot: - prefix: "!" + prefix: "." token: !ENV "BOT_TOKEN" cooldowns: @@ -10,7 +10,6 @@ bot: # Maximum number of messages to traverse for clean commands message_limit: 10000 - style: colours: soft_red: 0xcd6d6d @@ -19,8 +18,8 @@ style: emojis: defcon_disabled: "<:defcondisabled:470326273952972810>" - defcon_enabled: "<:defconenabled:470326274213150730>" - defcon_updated: "<:defconsettingsupdated:470326274082996224>" + defcon_enabled: "<:defconenabled:470326274213150730>" + defcon_updated: "<:defconsettingsupdated:470326274082996224>" green_chevron: "<:greenchevron:418104310329769993>" red_chevron: "<:redchevron:418112778184818698>" @@ -42,7 +41,7 @@ style: crown_green: "https://cdn.discordapp.com/emojis/469964154719961088.png" crown_red: "https://cdn.discordapp.com/emojis/469964154879344640.png" - defcon_denied: "https://cdn.discordapp.com/emojis/472475292078964738.png" + defcon_denied: "https://cdn.discordapp.com/emojis/472475292078964738.png" defcon_disabled: "https://cdn.discordapp.com/emojis/470326273952972810.png" defcon_enabled: "https://cdn.discordapp.com/emojis/470326274213150730.png" defcon_updated: "https://cdn.discordapp.com/emojis/472472638342561793.png" @@ -68,8 +67,8 @@ style: user_unban: "https://cdn.discordapp.com/emojis/469952898692808704.png" user_update: "https://cdn.discordapp.com/emojis/469952898684551168.png" - user_mute: "https://cdn.discordapp.com/emojis/472472640100106250.png" - user_unmute: "https://cdn.discordapp.com/emojis/472472639206719508.png" + user_mute: "https://cdn.discordapp.com/emojis/472472640100106250.png" + user_unmute: "https://cdn.discordapp.com/emojis/472472639206719508.png" user_verified: "https://cdn.discordapp.com/emojis/470326274519334936.png" user_warn: "https://cdn.discordapp.com/emojis/470326274238447633.png" @@ -83,74 +82,74 @@ style: questionmark: "https://cdn.discordapp.com/emojis/512367613339369475.png" guild: - id: 267624335836053506 + id: 476190141161930753 categories: - python_help: 356013061213126657 + python_help: 476196174789869577 channels: - admins: &ADMINS 365960823622991872 - announcements: 354619224620138496 - big_brother_logs: &BBLOGS 468507907357409333 - bot: 267659945086812160 - checkpoint_test: 422077681434099723 - defcon: 464469101889454091 - devlog: &DEVLOG 409308876241108992 - devtest: &DEVTEST 414574275865870337 - help_0: 303906576991780866 - help_1: 303906556754395136 - help_2: 303906514266226689 - help_3: 439702951246692352 - help_4: 451312046647148554 - help_5: 454941769734422538 - help_6: 587375753306570782 - help_7: 587375768556797982 - helpers: 385474242440986624 - message_log: &MESSAGE_LOG 467752170159079424 - mod_alerts: 473092532147060736 - modlog: &MODLOG 282638479504965634 - off_topic_0: 291284109232308226 - off_topic_1: 463035241142026251 - off_topic_2: 463035268514185226 - python: 267624335836053506 - reddit: 458224812528238616 - staff_lounge: &STAFF_LOUNGE 464905259261755392 - talent_pool: &TALENT_POOL 534321732593647616 - userlog: 528976905546760203 - user_event_a: &USER_EVENT_A 592000283102674944 - verification: 352442727016693763 + admins: &ADMINS 476196003733569547 + announcements: 476196024512413698 + big_brother_logs: &BBLOGS 476196047631417345 + bot: 476196062214750219 + checkpoint_test: 476196079562653698 + defcon: 476196101284954122 + devlog: &DEVLOG 476196115432210443 + devtest: &DEVTEST 476196128933543937 + help_0: 476196221845897270 + help_1: 476196242926469121 + help_2: 476196266594926593 + help_3: 476196281421660160 + help_4: 476196292398153738 + help_5: 476196300933824532 + help_6: 621711690140221440 + help_7: 621711714811117568 + helpers: 476196888295505940 + message_log: &MESSAGE_LOG 476197264667181057 + mod_alerts: 476197283256336385 + modlog: &MODLOG 476197299169525780 + off_topic_0: 476196547324018688 + off_topic_1: 476196563216105472 + off_topic_2: 476196574343593985 + python: 476190141161930755 + reddit: 476197119762366464 + staff_lounge: &STAFF_LOUNGE 476197226348019712 + talent_pool: &TALENT_POOL 609530835476938757 + userlog: 609531966387388446 + user_event_a: &USER_EVENT_A 609531030164078613 + verification: 476197158928777237 ignored: [*ADMINS, *MESSAGE_LOG, *MODLOG] roles: - admin: &ADMIN_ROLE 267628507062992896 - announcements: 463658397560995840 - champion: 430492892331769857 - contributor: 295488872404484098 - core_developer: 587606783669829632 - jammer: 423054537079783434 - moderator: &MOD_ROLE 267629731250176001 - muted: &MUTED_ROLE 277914926603829249 - owner: &OWNER_ROLE 267627879762755584 - verified: 352427296948486144 - helpers: 267630620367257601 - rockstars: &ROCKSTARS_ROLE 458226413825294336 - team_leader: 501324292341104650 + admin: &ADMIN_ROLE 476190234653229056 + announcements: 476190253548306433 + champion: 476190284447875086 + contributor: 476190302659543061 + core_developer: 622459804367061015 + jammer: 476190341566038027 + moderator: &MOD_ROLE 476190357927886848 + muted: &MUTED_ROLE 476190376949186560 + owner: &OWNER_ROLE 476190391595433985 + verified: 476190408871772171 + helpers: 476190429960732672 + rockstars: &ROCKSTARS_ROLE 503859559815708682 + team_leader: 609532800139264018 webhooks: - talent_pool: 569145364800602132 - big_brother: 569133704568373283 + talent_pool: 609534369178189844 + big_brother: 609535034474496063 filter: # What do we filter? - filter_zalgo: false - filter_invites: true - filter_domains: true + filter_zalgo: true + filter_invites: true + filter_domains: true watch_rich_embeds: true - watch_words: true - watch_tokens: true + watch_words: true + watch_tokens: true # Notify user on filter? # Notifications are not expected for "watchlist" type filters @@ -159,7 +158,7 @@ filter: notify_user_domains: false # Filter configuration - ping_everyone: true # Ping @everyone when we send a mod-alert? + ping_everyone: false # Ping @everyone when we send a mod-alert? guild_invite_whitelist: - 280033776820813825 # Functional Programming @@ -236,9 +235,9 @@ urls: # PyDis site vars site: &DOMAIN "django.pythondiscord.com" site_api: &API !JOIN ["api.", *DOMAIN] - site_paste: &PASTE !JOIN ["paste.", *DOMAIN] - site_staff: &STAFF !JOIN ["staff.", *DOMAIN] + site_paste: &PASTE "https://paste.pythondiscord.com" site_schema: &SCHEMA "https://" + site_staff: &STAFF !JOIN ["staff.", *DOMAIN] site_bigbrother_api: !JOIN [*SCHEMA, *API, "/bot/bigbrother"] site_docs_api: !JOIN [*SCHEMA, *API, "/bot/docs"] @@ -258,7 +257,7 @@ urls: site_tags_api: !JOIN [*SCHEMA, *API, "/bot/tags"] site_user_api: !JOIN [*SCHEMA, *API, "/bot/users"] site_user_complete_api: !JOIN [*SCHEMA, *API, "/bot/users/complete"] - paste_service: !JOIN [*SCHEMA, *PASTE, "/{key}"] + paste_service: !JOIN [*PASTE, "/{key}"] # Snekbox snekbox_eval_api: "https://snekbox.pythondiscord.com/eval" @@ -278,7 +277,7 @@ urls: anti_spam: # Clean messages that violate a rule. clean_offending: true - ping_everyone: true + ping_everyone: false punishment: role_id: *MUTED_ROLE diff --git a/config-prod.yml b/config-prod.yml new file mode 100644 index 000000000..c9fc3b954 --- /dev/null +++ b/config-prod.yml @@ -0,0 +1,360 @@ +bot: + prefix: "!" + token: !ENV "BOT_TOKEN" + + cooldowns: + # Per channel, per tag. + tags: 60 + + clean: + # Maximum number of messages to traverse for clean commands + message_limit: 10000 + + +style: + colours: + soft_red: 0xcd6d6d + soft_green: 0x68c290 + soft_orange: 0xf9cb54 + + emojis: + defcon_disabled: "<:defcondisabled:470326273952972810>" + defcon_enabled: "<:defconenabled:470326274213150730>" + defcon_updated: "<:defconsettingsupdated:470326274082996224>" + + green_chevron: "<:greenchevron:418104310329769993>" + red_chevron: "<:redchevron:418112778184818698>" + white_chevron: "<:whitechevron:418110396973711363>" + bb_message: "<:bbmessage:476273120999636992>" + + status_online: "<:status_online:470326272351010816>" + status_idle: "<:status_idle:470326266625785866>" + status_dnd: "<:status_dnd:470326272082313216>" + status_offline: "<:status_offline:470326266537705472>" + + bullet: "\u2022" + pencil: "\u270F" + new: "\U0001F195" + cross_mark: "\u274C" + + icons: + crown_blurple: "https://cdn.discordapp.com/emojis/469964153289965568.png" + crown_green: "https://cdn.discordapp.com/emojis/469964154719961088.png" + crown_red: "https://cdn.discordapp.com/emojis/469964154879344640.png" + + defcon_denied: "https://cdn.discordapp.com/emojis/472475292078964738.png" + defcon_disabled: "https://cdn.discordapp.com/emojis/470326273952972810.png" + defcon_enabled: "https://cdn.discordapp.com/emojis/470326274213150730.png" + defcon_updated: "https://cdn.discordapp.com/emojis/472472638342561793.png" + + filtering: "https://cdn.discordapp.com/emojis/472472638594482195.png" + + guild_update: "https://cdn.discordapp.com/emojis/469954765141442561.png" + + hash_blurple: "https://cdn.discordapp.com/emojis/469950142942806017.png" + hash_green: "https://cdn.discordapp.com/emojis/469950144918585344.png" + hash_red: "https://cdn.discordapp.com/emojis/469950145413251072.png" + + message_bulk_delete: "https://cdn.discordapp.com/emojis/469952898994929668.png" + message_delete: "https://cdn.discordapp.com/emojis/472472641320648704.png" + message_edit: "https://cdn.discordapp.com/emojis/472472638976163870.png" + + sign_in: "https://cdn.discordapp.com/emojis/469952898181234698.png" + sign_out: "https://cdn.discordapp.com/emojis/469952898089091082.png" + + token_removed: "https://cdn.discordapp.com/emojis/470326273298792469.png" + + user_ban: "https://cdn.discordapp.com/emojis/469952898026045441.png" + user_unban: "https://cdn.discordapp.com/emojis/469952898692808704.png" + user_update: "https://cdn.discordapp.com/emojis/469952898684551168.png" + + user_mute: "https://cdn.discordapp.com/emojis/472472640100106250.png" + user_unmute: "https://cdn.discordapp.com/emojis/472472639206719508.png" + user_verified: "https://cdn.discordapp.com/emojis/470326274519334936.png" + + user_warn: "https://cdn.discordapp.com/emojis/470326274238447633.png" + + pencil: "https://cdn.discordapp.com/emojis/470326272401211415.png" + + remind_blurple: "https://cdn.discordapp.com/emojis/477907609215827968.png" + remind_green: "https://cdn.discordapp.com/emojis/477907607785570310.png" + remind_red: "https://cdn.discordapp.com/emojis/477907608057937930.png" + + questionmark: "https://cdn.discordapp.com/emojis/512367613339369475.png" + +guild: + id: 267624335836053506 + + categories: + python_help: 356013061213126657 + + channels: + admins: &ADMINS 365960823622991872 + announcements: 354619224620138496 + big_brother_logs: &BBLOGS 468507907357409333 + bot: 267659945086812160 + checkpoint_test: 422077681434099723 + defcon: 464469101889454091 + devlog: &DEVLOG 409308876241108992 + devtest: &DEVTEST 414574275865870337 + help_0: 303906576991780866 + help_1: 303906556754395136 + help_2: 303906514266226689 + help_3: 439702951246692352 + help_4: 451312046647148554 + help_5: 454941769734422538 + help_6: 587375753306570782 + help_7: 587375768556797982 + helpers: 385474242440986624 + message_log: &MESSAGE_LOG 467752170159079424 + mod_alerts: 473092532147060736 + modlog: &MODLOG 282638479504965634 + off_topic_0: 291284109232308226 + off_topic_1: 463035241142026251 + off_topic_2: 463035268514185226 + python: 267624335836053506 + reddit: 458224812528238616 + staff_lounge: &STAFF_LOUNGE 464905259261755392 + talent_pool: &TALENT_POOL 534321732593647616 + userlog: 528976905546760203 + user_event_a: &USER_EVENT_A 592000283102674944 + verification: 352442727016693763 + + ignored: [*ADMINS, *MESSAGE_LOG, *MODLOG] + + roles: + admin: &ADMIN_ROLE 267628507062992896 + announcements: 463658397560995840 + champion: 430492892331769857 + contributor: 295488872404484098 + core_developer: 587606783669829632 + jammer: 423054537079783434 + moderator: &MOD_ROLE 267629731250176001 + muted: &MUTED_ROLE 277914926603829249 + owner: &OWNER_ROLE 267627879762755584 + verified: 352427296948486144 + helpers: 267630620367257601 + rockstars: &ROCKSTARS_ROLE 458226413825294336 + team_leader: 501324292341104650 + + webhooks: + talent_pool: 569145364800602132 + big_brother: 569133704568373283 + + +filter: + + # What do we filter? + filter_zalgo: false + filter_invites: true + filter_domains: true + watch_rich_embeds: true + watch_words: true + watch_tokens: true + + # Notify user on filter? + # Notifications are not expected for "watchlist" type filters + notify_user_zalgo: false + notify_user_invites: true + notify_user_domains: false + + # Filter configuration + ping_everyone: true # Ping @everyone when we send a mod-alert? + + guild_invite_whitelist: + - 280033776820813825 # Functional Programming + - 267624335836053506 # Python Discord + - 440186186024222721 # Python Discord: ModLog Emojis + - 273944235143593984 # STEM + - 348658686962696195 # RLBot + - 531221516914917387 # Pallets + - 249111029668249601 # Gentoo + - 327254708534116352 # Adafruit + - 544525886180032552 # kennethreitz.org + - 590806733924859943 # Discord Hack Week + - 423249981340778496 # Kivy + + domain_blacklist: + - pornhub.com + - liveleak.com + + word_watchlist: + - goo+ks* + - ky+s+ + - ki+ke+s* + - beaner+s? + - coo+ns* + - nig+lets* + - slant-eyes* + - towe?l-?head+s* + - chi*n+k+s* + - spick*s* + - kill* +(?:yo)?urself+ + - jew+s* + - suicide + - rape + - (re+)tar+(d+|t+)(ed)? + - ta+r+d+ + - cunts* + - trann*y + - shemale + + token_watchlist: + - fa+g+s* + - 卐 + - 卍 + - cuck(?!oo+) + - nigg+(?:e*r+|a+h*?|u+h+)s? + - fag+o+t+s* + + # Censor doesn't apply to these + channel_whitelist: + - *ADMINS + - *MODLOG + - *MESSAGE_LOG + - *DEVLOG + - *BBLOGS + - *STAFF_LOUNGE + - *DEVTEST + - *TALENT_POOL + - *USER_EVENT_A + + role_whitelist: + - *ADMIN_ROLE + - *MOD_ROLE + - *OWNER_ROLE + - *ROCKSTARS_ROLE + + +keys: + deploy_bot: !ENV "DEPLOY_BOT_KEY" + deploy_site: !ENV "DEPLOY_SITE" + site_api: !ENV "BOT_API_KEY" + + +urls: + # PyDis site vars + site: &DOMAIN "pythondiscord.com" + site_api: &API !JOIN ["api.", *DOMAIN] + site_paste: &PASTE !JOIN ["paste.", *DOMAIN] + site_staff: &STAFF !JOIN ["staff.", *DOMAIN] + site_schema: &SCHEMA "https://" + + site_bigbrother_api: !JOIN [*SCHEMA, *API, "/bot/bigbrother"] + site_docs_api: !JOIN [*SCHEMA, *API, "/bot/docs"] + site_superstarify_api: !JOIN [*SCHEMA, *API, "/bot/superstarify"] + site_infractions: !JOIN [*SCHEMA, *API, "/bot/infractions"] + site_infractions_user: !JOIN [*SCHEMA, *API, "/bot/infractions/user/{user_id}"] + site_infractions_type: !JOIN [*SCHEMA, *API, "/bot/infractions/type/{infraction_type}"] + site_infractions_by_id: !JOIN [*SCHEMA, *API, "/bot/infractions/id/{infraction_id}"] + site_infractions_user_type_current: !JOIN [*SCHEMA, *API, "/bot/infractions/user/{user_id}/{infraction_type}/current"] + site_infractions_user_type: !JOIN [*SCHEMA, *API, "/bot/infractions/user/{user_id}/{infraction_type}"] + site_logs_api: !JOIN [*SCHEMA, *API, "/bot/logs"] + site_logs_view: !JOIN [*SCHEMA, *STAFF, "/bot/logs"] + site_off_topic_names_api: !JOIN [*SCHEMA, *API, "/bot/off-topic-names"] + site_reminders_api: !JOIN [*SCHEMA, *API, "/bot/reminders"] + site_reminders_user_api: !JOIN [*SCHEMA, *API, "/bot/reminders/user"] + site_settings_api: !JOIN [*SCHEMA, *API, "/bot/settings"] + site_tags_api: !JOIN [*SCHEMA, *API, "/bot/tags"] + site_user_api: !JOIN [*SCHEMA, *API, "/bot/users"] + site_user_complete_api: !JOIN [*SCHEMA, *API, "/bot/users/complete"] + paste_service: !JOIN [*SCHEMA, *PASTE, "/{key}"] + + # Snekbox + snekbox_eval_api: "https://snekbox.pythondiscord.com/eval" + + # Env vars + deploy: !ENV "DEPLOY_URL" + status: !ENV "STATUS_URL" + + # Discord API URLs + discord_api: &DISCORD_API "https://discordapp.com/api/v7/" + discord_invite_api: !JOIN [*DISCORD_API, "invites"] + + # Misc URLs + bot_avatar: "https://raw.githubusercontent.com/discord-python/branding/master/logos/logo_circle/logo_circle.png" + gitlab_bot_repo: "https://gitlab.com/python-discord/projects/bot" + +anti_spam: + # Clean messages that violate a rule. + clean_offending: true + ping_everyone: true + + punishment: + role_id: *MUTED_ROLE + remove_after: 600 + + rules: + attachments: + interval: 10 + max: 3 + + burst: + interval: 10 + max: 7 + + burst_shared: + interval: 10 + max: 20 + + chars: + interval: 5 + max: 3_000 + + duplicates: + interval: 10 + max: 3 + + discord_emojis: + interval: 10 + max: 20 + + links: + interval: 10 + max: 10 + + mentions: + interval: 10 + max: 5 + + newlines: + interval: 10 + max: 100 + max_consecutive: 10 + + role_mentions: + interval: 10 + max: 3 + + +reddit: + request_delay: 60 + subreddits: + - 'r/Python' + + +wolfram: + # Max requests per day. + user_limit_day: 10 + guild_limit_day: 67 + key: !ENV "WOLFRAM_API_KEY" + + +big_brother: + log_delay: 15 + header_message_limit: 15 + + +free: + # Seconds to elapse for a channel + # to be considered inactive. + activity_timeout: 600 + cooldown_rate: 1 + cooldown_per: 60.0 + +redirect_output: + delete_invocation: true + delete_delay: 15 + +config: + required_keys: ['bot.token'] -- cgit v1.2.3 From 1e57d2a9c7d8d8bdf2beeac4dc062c9e7ffe547b Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 15:48:56 +0200 Subject: Ship `DEBUG` log messages to the site. --- bot/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/api.py b/bot/api.py index 9a0ebaa26..3acde242e 100644 --- a/bot/api.py +++ b/bot/api.py @@ -124,7 +124,7 @@ class APILoggingHandler(logging.StreamHandler): # 1. Do not log anything below `DEBUG`. This is only applicable # for the monkeypatched `TRACE` logging level, which has a # lower numeric value than `DEBUG`. - record.levelno > logging.DEBUG + record.levelno >= logging.DEBUG # 2. Ignore logging messages which are sent by this logging # handler itself. This is required because if we were to # not ignore messages emitted by this handler, we would -- cgit v1.2.3 From dc13880991442ef5614ef960ea8a90b1386ed955 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 15 Sep 2019 15:53:33 +0200 Subject: Setting the config to work with the pydis server. --- config-default.yml | 125 ++++++++++--------- config-prod.yml | 360 ----------------------------------------------------- 2 files changed, 63 insertions(+), 422 deletions(-) delete mode 100644 config-prod.yml diff --git a/config-default.yml b/config-default.yml index eace9caff..01bdcd1e7 100644 --- a/config-default.yml +++ b/config-default.yml @@ -10,6 +10,7 @@ bot: # Maximum number of messages to traverse for clean commands message_limit: 10000 + style: colours: soft_red: 0xcd6d6d @@ -18,8 +19,8 @@ style: emojis: defcon_disabled: "<:defcondisabled:470326273952972810>" - defcon_enabled: "<:defconenabled:470326274213150730>" - defcon_updated: "<:defconsettingsupdated:470326274082996224>" + defcon_enabled: "<:defconenabled:470326274213150730>" + defcon_updated: "<:defconsettingsupdated:470326274082996224>" green_chevron: "<:greenchevron:418104310329769993>" red_chevron: "<:redchevron:418112778184818698>" @@ -41,7 +42,7 @@ style: crown_green: "https://cdn.discordapp.com/emojis/469964154719961088.png" crown_red: "https://cdn.discordapp.com/emojis/469964154879344640.png" - defcon_denied: "https://cdn.discordapp.com/emojis/472475292078964738.png" + defcon_denied: "https://cdn.discordapp.com/emojis/472475292078964738.png" defcon_disabled: "https://cdn.discordapp.com/emojis/470326273952972810.png" defcon_enabled: "https://cdn.discordapp.com/emojis/470326274213150730.png" defcon_updated: "https://cdn.discordapp.com/emojis/472472638342561793.png" @@ -67,8 +68,8 @@ style: user_unban: "https://cdn.discordapp.com/emojis/469952898692808704.png" user_update: "https://cdn.discordapp.com/emojis/469952898684551168.png" - user_mute: "https://cdn.discordapp.com/emojis/472472640100106250.png" - user_unmute: "https://cdn.discordapp.com/emojis/472472639206719508.png" + user_mute: "https://cdn.discordapp.com/emojis/472472640100106250.png" + user_unmute: "https://cdn.discordapp.com/emojis/472472639206719508.png" user_verified: "https://cdn.discordapp.com/emojis/470326274519334936.png" user_warn: "https://cdn.discordapp.com/emojis/470326274238447633.png" @@ -82,74 +83,74 @@ style: questionmark: "https://cdn.discordapp.com/emojis/512367613339369475.png" guild: - id: 476190141161930753 + id: 267624335836053506 categories: - python_help: 476196174789869577 + python_help: 356013061213126657 channels: - admins: &ADMINS 476196003733569547 - announcements: 476196024512413698 - big_brother_logs: &BBLOGS 476196047631417345 - bot: 476196062214750219 - checkpoint_test: 476196079562653698 - defcon: 476196101284954122 - devlog: &DEVLOG 476196115432210443 - devtest: &DEVTEST 476196128933543937 - help_0: 476196221845897270 - help_1: 476196242926469121 - help_2: 476196266594926593 - help_3: 476196281421660160 - help_4: 476196292398153738 - help_5: 476196300933824532 - help_6: 621711690140221440 - help_7: 621711714811117568 - helpers: 476196888295505940 - message_log: &MESSAGE_LOG 476197264667181057 - mod_alerts: 476197283256336385 - modlog: &MODLOG 476197299169525780 - off_topic_0: 476196547324018688 - off_topic_1: 476196563216105472 - off_topic_2: 476196574343593985 - python: 476190141161930755 - reddit: 476197119762366464 - staff_lounge: &STAFF_LOUNGE 476197226348019712 - talent_pool: &TALENT_POOL 609530835476938757 - userlog: 609531966387388446 - user_event_a: &USER_EVENT_A 609531030164078613 - verification: 476197158928777237 + admins: &ADMINS 365960823622991872 + announcements: 354619224620138496 + big_brother_logs: &BBLOGS 468507907357409333 + bot: 267659945086812160 + checkpoint_test: 422077681434099723 + defcon: 464469101889454091 + devlog: &DEVLOG 409308876241108992 + devtest: &DEVTEST 414574275865870337 + help_0: 303906576991780866 + help_1: 303906556754395136 + help_2: 303906514266226689 + help_3: 439702951246692352 + help_4: 451312046647148554 + help_5: 454941769734422538 + help_6: 587375753306570782 + help_7: 587375768556797982 + helpers: 385474242440986624 + message_log: &MESSAGE_LOG 467752170159079424 + mod_alerts: 473092532147060736 + modlog: &MODLOG 282638479504965634 + off_topic_0: 291284109232308226 + off_topic_1: 463035241142026251 + off_topic_2: 463035268514185226 + python: 267624335836053506 + reddit: 458224812528238616 + staff_lounge: &STAFF_LOUNGE 464905259261755392 + talent_pool: &TALENT_POOL 534321732593647616 + userlog: 528976905546760203 + user_event_a: &USER_EVENT_A 592000283102674944 + verification: 352442727016693763 ignored: [*ADMINS, *MESSAGE_LOG, *MODLOG] roles: - admin: &ADMIN_ROLE 476190234653229056 - announcements: 476190253548306433 - champion: 476190284447875086 - contributor: 476190302659543061 - core_developer: 622459804367061015 - jammer: 476190341566038027 - moderator: &MOD_ROLE 476190357927886848 - muted: &MUTED_ROLE 476190376949186560 - owner: &OWNER_ROLE 476190391595433985 - verified: 476190408871772171 - helpers: 476190429960732672 - rockstars: &ROCKSTARS_ROLE 503859559815708682 - team_leader: 609532800139264018 + admin: &ADMIN_ROLE 267628507062992896 + announcements: 463658397560995840 + champion: 430492892331769857 + contributor: 295488872404484098 + core_developer: 587606783669829632 + jammer: 423054537079783434 + moderator: &MOD_ROLE 267629731250176001 + muted: &MUTED_ROLE 277914926603829249 + owner: &OWNER_ROLE 267627879762755584 + verified: 352427296948486144 + helpers: 267630620367257601 + rockstars: &ROCKSTARS_ROLE 458226413825294336 + team_leader: 501324292341104650 webhooks: - talent_pool: 609534369178189844 - big_brother: 609535034474496063 + talent_pool: 569145364800602132 + big_brother: 569133704568373283 filter: # What do we filter? - filter_zalgo: true - filter_invites: true - filter_domains: true + filter_zalgo: false + filter_invites: true + filter_domains: true watch_rich_embeds: true - watch_words: true - watch_tokens: true + watch_words: true + watch_tokens: true # Notify user on filter? # Notifications are not expected for "watchlist" type filters @@ -158,7 +159,7 @@ filter: notify_user_domains: false # Filter configuration - ping_everyone: false # Ping @everyone when we send a mod-alert? + ping_everyone: true # Ping @everyone when we send a mod-alert? guild_invite_whitelist: - 280033776820813825 # Functional Programming @@ -235,9 +236,9 @@ urls: # PyDis site vars site: &DOMAIN "django.pythondiscord.com" site_api: &API !JOIN ["api.", *DOMAIN] - site_paste: &PASTE "https://paste.pythondiscord.com" - site_schema: &SCHEMA "https://" + site_paste: &PASTE !JOIN ["paste.", *DOMAIN] site_staff: &STAFF !JOIN ["staff.", *DOMAIN] + site_schema: &SCHEMA "https://" site_bigbrother_api: !JOIN [*SCHEMA, *API, "/bot/bigbrother"] site_docs_api: !JOIN [*SCHEMA, *API, "/bot/docs"] @@ -257,7 +258,7 @@ urls: site_tags_api: !JOIN [*SCHEMA, *API, "/bot/tags"] site_user_api: !JOIN [*SCHEMA, *API, "/bot/users"] site_user_complete_api: !JOIN [*SCHEMA, *API, "/bot/users/complete"] - paste_service: !JOIN [*PASTE, "/{key}"] + paste_service: !JOIN [*SCHEMA, *PASTE, "/{key}"] # Snekbox snekbox_eval_api: "https://snekbox.pythondiscord.com/eval" @@ -277,7 +278,7 @@ urls: anti_spam: # Clean messages that violate a rule. clean_offending: true - ping_everyone: false + ping_everyone: true punishment: role_id: *MUTED_ROLE diff --git a/config-prod.yml b/config-prod.yml deleted file mode 100644 index c9fc3b954..000000000 --- a/config-prod.yml +++ /dev/null @@ -1,360 +0,0 @@ -bot: - prefix: "!" - token: !ENV "BOT_TOKEN" - - cooldowns: - # Per channel, per tag. - tags: 60 - - clean: - # Maximum number of messages to traverse for clean commands - message_limit: 10000 - - -style: - colours: - soft_red: 0xcd6d6d - soft_green: 0x68c290 - soft_orange: 0xf9cb54 - - emojis: - defcon_disabled: "<:defcondisabled:470326273952972810>" - defcon_enabled: "<:defconenabled:470326274213150730>" - defcon_updated: "<:defconsettingsupdated:470326274082996224>" - - green_chevron: "<:greenchevron:418104310329769993>" - red_chevron: "<:redchevron:418112778184818698>" - white_chevron: "<:whitechevron:418110396973711363>" - bb_message: "<:bbmessage:476273120999636992>" - - status_online: "<:status_online:470326272351010816>" - status_idle: "<:status_idle:470326266625785866>" - status_dnd: "<:status_dnd:470326272082313216>" - status_offline: "<:status_offline:470326266537705472>" - - bullet: "\u2022" - pencil: "\u270F" - new: "\U0001F195" - cross_mark: "\u274C" - - icons: - crown_blurple: "https://cdn.discordapp.com/emojis/469964153289965568.png" - crown_green: "https://cdn.discordapp.com/emojis/469964154719961088.png" - crown_red: "https://cdn.discordapp.com/emojis/469964154879344640.png" - - defcon_denied: "https://cdn.discordapp.com/emojis/472475292078964738.png" - defcon_disabled: "https://cdn.discordapp.com/emojis/470326273952972810.png" - defcon_enabled: "https://cdn.discordapp.com/emojis/470326274213150730.png" - defcon_updated: "https://cdn.discordapp.com/emojis/472472638342561793.png" - - filtering: "https://cdn.discordapp.com/emojis/472472638594482195.png" - - guild_update: "https://cdn.discordapp.com/emojis/469954765141442561.png" - - hash_blurple: "https://cdn.discordapp.com/emojis/469950142942806017.png" - hash_green: "https://cdn.discordapp.com/emojis/469950144918585344.png" - hash_red: "https://cdn.discordapp.com/emojis/469950145413251072.png" - - message_bulk_delete: "https://cdn.discordapp.com/emojis/469952898994929668.png" - message_delete: "https://cdn.discordapp.com/emojis/472472641320648704.png" - message_edit: "https://cdn.discordapp.com/emojis/472472638976163870.png" - - sign_in: "https://cdn.discordapp.com/emojis/469952898181234698.png" - sign_out: "https://cdn.discordapp.com/emojis/469952898089091082.png" - - token_removed: "https://cdn.discordapp.com/emojis/470326273298792469.png" - - user_ban: "https://cdn.discordapp.com/emojis/469952898026045441.png" - user_unban: "https://cdn.discordapp.com/emojis/469952898692808704.png" - user_update: "https://cdn.discordapp.com/emojis/469952898684551168.png" - - user_mute: "https://cdn.discordapp.com/emojis/472472640100106250.png" - user_unmute: "https://cdn.discordapp.com/emojis/472472639206719508.png" - user_verified: "https://cdn.discordapp.com/emojis/470326274519334936.png" - - user_warn: "https://cdn.discordapp.com/emojis/470326274238447633.png" - - pencil: "https://cdn.discordapp.com/emojis/470326272401211415.png" - - remind_blurple: "https://cdn.discordapp.com/emojis/477907609215827968.png" - remind_green: "https://cdn.discordapp.com/emojis/477907607785570310.png" - remind_red: "https://cdn.discordapp.com/emojis/477907608057937930.png" - - questionmark: "https://cdn.discordapp.com/emojis/512367613339369475.png" - -guild: - id: 267624335836053506 - - categories: - python_help: 356013061213126657 - - channels: - admins: &ADMINS 365960823622991872 - announcements: 354619224620138496 - big_brother_logs: &BBLOGS 468507907357409333 - bot: 267659945086812160 - checkpoint_test: 422077681434099723 - defcon: 464469101889454091 - devlog: &DEVLOG 409308876241108992 - devtest: &DEVTEST 414574275865870337 - help_0: 303906576991780866 - help_1: 303906556754395136 - help_2: 303906514266226689 - help_3: 439702951246692352 - help_4: 451312046647148554 - help_5: 454941769734422538 - help_6: 587375753306570782 - help_7: 587375768556797982 - helpers: 385474242440986624 - message_log: &MESSAGE_LOG 467752170159079424 - mod_alerts: 473092532147060736 - modlog: &MODLOG 282638479504965634 - off_topic_0: 291284109232308226 - off_topic_1: 463035241142026251 - off_topic_2: 463035268514185226 - python: 267624335836053506 - reddit: 458224812528238616 - staff_lounge: &STAFF_LOUNGE 464905259261755392 - talent_pool: &TALENT_POOL 534321732593647616 - userlog: 528976905546760203 - user_event_a: &USER_EVENT_A 592000283102674944 - verification: 352442727016693763 - - ignored: [*ADMINS, *MESSAGE_LOG, *MODLOG] - - roles: - admin: &ADMIN_ROLE 267628507062992896 - announcements: 463658397560995840 - champion: 430492892331769857 - contributor: 295488872404484098 - core_developer: 587606783669829632 - jammer: 423054537079783434 - moderator: &MOD_ROLE 267629731250176001 - muted: &MUTED_ROLE 277914926603829249 - owner: &OWNER_ROLE 267627879762755584 - verified: 352427296948486144 - helpers: 267630620367257601 - rockstars: &ROCKSTARS_ROLE 458226413825294336 - team_leader: 501324292341104650 - - webhooks: - talent_pool: 569145364800602132 - big_brother: 569133704568373283 - - -filter: - - # What do we filter? - filter_zalgo: false - filter_invites: true - filter_domains: true - watch_rich_embeds: true - watch_words: true - watch_tokens: true - - # Notify user on filter? - # Notifications are not expected for "watchlist" type filters - notify_user_zalgo: false - notify_user_invites: true - notify_user_domains: false - - # Filter configuration - ping_everyone: true # Ping @everyone when we send a mod-alert? - - guild_invite_whitelist: - - 280033776820813825 # Functional Programming - - 267624335836053506 # Python Discord - - 440186186024222721 # Python Discord: ModLog Emojis - - 273944235143593984 # STEM - - 348658686962696195 # RLBot - - 531221516914917387 # Pallets - - 249111029668249601 # Gentoo - - 327254708534116352 # Adafruit - - 544525886180032552 # kennethreitz.org - - 590806733924859943 # Discord Hack Week - - 423249981340778496 # Kivy - - domain_blacklist: - - pornhub.com - - liveleak.com - - word_watchlist: - - goo+ks* - - ky+s+ - - ki+ke+s* - - beaner+s? - - coo+ns* - - nig+lets* - - slant-eyes* - - towe?l-?head+s* - - chi*n+k+s* - - spick*s* - - kill* +(?:yo)?urself+ - - jew+s* - - suicide - - rape - - (re+)tar+(d+|t+)(ed)? - - ta+r+d+ - - cunts* - - trann*y - - shemale - - token_watchlist: - - fa+g+s* - - 卐 - - 卍 - - cuck(?!oo+) - - nigg+(?:e*r+|a+h*?|u+h+)s? - - fag+o+t+s* - - # Censor doesn't apply to these - channel_whitelist: - - *ADMINS - - *MODLOG - - *MESSAGE_LOG - - *DEVLOG - - *BBLOGS - - *STAFF_LOUNGE - - *DEVTEST - - *TALENT_POOL - - *USER_EVENT_A - - role_whitelist: - - *ADMIN_ROLE - - *MOD_ROLE - - *OWNER_ROLE - - *ROCKSTARS_ROLE - - -keys: - deploy_bot: !ENV "DEPLOY_BOT_KEY" - deploy_site: !ENV "DEPLOY_SITE" - site_api: !ENV "BOT_API_KEY" - - -urls: - # PyDis site vars - site: &DOMAIN "pythondiscord.com" - site_api: &API !JOIN ["api.", *DOMAIN] - site_paste: &PASTE !JOIN ["paste.", *DOMAIN] - site_staff: &STAFF !JOIN ["staff.", *DOMAIN] - site_schema: &SCHEMA "https://" - - site_bigbrother_api: !JOIN [*SCHEMA, *API, "/bot/bigbrother"] - site_docs_api: !JOIN [*SCHEMA, *API, "/bot/docs"] - site_superstarify_api: !JOIN [*SCHEMA, *API, "/bot/superstarify"] - site_infractions: !JOIN [*SCHEMA, *API, "/bot/infractions"] - site_infractions_user: !JOIN [*SCHEMA, *API, "/bot/infractions/user/{user_id}"] - site_infractions_type: !JOIN [*SCHEMA, *API, "/bot/infractions/type/{infraction_type}"] - site_infractions_by_id: !JOIN [*SCHEMA, *API, "/bot/infractions/id/{infraction_id}"] - site_infractions_user_type_current: !JOIN [*SCHEMA, *API, "/bot/infractions/user/{user_id}/{infraction_type}/current"] - site_infractions_user_type: !JOIN [*SCHEMA, *API, "/bot/infractions/user/{user_id}/{infraction_type}"] - site_logs_api: !JOIN [*SCHEMA, *API, "/bot/logs"] - site_logs_view: !JOIN [*SCHEMA, *STAFF, "/bot/logs"] - site_off_topic_names_api: !JOIN [*SCHEMA, *API, "/bot/off-topic-names"] - site_reminders_api: !JOIN [*SCHEMA, *API, "/bot/reminders"] - site_reminders_user_api: !JOIN [*SCHEMA, *API, "/bot/reminders/user"] - site_settings_api: !JOIN [*SCHEMA, *API, "/bot/settings"] - site_tags_api: !JOIN [*SCHEMA, *API, "/bot/tags"] - site_user_api: !JOIN [*SCHEMA, *API, "/bot/users"] - site_user_complete_api: !JOIN [*SCHEMA, *API, "/bot/users/complete"] - paste_service: !JOIN [*SCHEMA, *PASTE, "/{key}"] - - # Snekbox - snekbox_eval_api: "https://snekbox.pythondiscord.com/eval" - - # Env vars - deploy: !ENV "DEPLOY_URL" - status: !ENV "STATUS_URL" - - # Discord API URLs - discord_api: &DISCORD_API "https://discordapp.com/api/v7/" - discord_invite_api: !JOIN [*DISCORD_API, "invites"] - - # Misc URLs - bot_avatar: "https://raw.githubusercontent.com/discord-python/branding/master/logos/logo_circle/logo_circle.png" - gitlab_bot_repo: "https://gitlab.com/python-discord/projects/bot" - -anti_spam: - # Clean messages that violate a rule. - clean_offending: true - ping_everyone: true - - punishment: - role_id: *MUTED_ROLE - remove_after: 600 - - rules: - attachments: - interval: 10 - max: 3 - - burst: - interval: 10 - max: 7 - - burst_shared: - interval: 10 - max: 20 - - chars: - interval: 5 - max: 3_000 - - duplicates: - interval: 10 - max: 3 - - discord_emojis: - interval: 10 - max: 20 - - links: - interval: 10 - max: 10 - - mentions: - interval: 10 - max: 5 - - newlines: - interval: 10 - max: 100 - max_consecutive: 10 - - role_mentions: - interval: 10 - max: 3 - - -reddit: - request_delay: 60 - subreddits: - - 'r/Python' - - -wolfram: - # Max requests per day. - user_limit_day: 10 - guild_limit_day: 67 - key: !ENV "WOLFRAM_API_KEY" - - -big_brother: - log_delay: 15 - header_message_limit: 15 - - -free: - # Seconds to elapse for a channel - # to be considered inactive. - activity_timeout: 600 - cooldown_rate: 1 - cooldown_per: 60.0 - -redirect_output: - delete_invocation: true - delete_delay: 15 - -config: - required_keys: ['bot.token'] -- cgit v1.2.3 From 6af4044c9a4c12a957e579c82871e7a2917781d9 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 15 Sep 2019 17:11:53 +0200 Subject: Changing the prefix and domain back --- config-default.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config-default.yml b/config-default.yml index 01bdcd1e7..c9fc3b954 100644 --- a/config-default.yml +++ b/config-default.yml @@ -1,5 +1,5 @@ bot: - prefix: "." + prefix: "!" token: !ENV "BOT_TOKEN" cooldowns: @@ -234,7 +234,7 @@ keys: urls: # PyDis site vars - site: &DOMAIN "django.pythondiscord.com" + site: &DOMAIN "pythondiscord.com" site_api: &API !JOIN ["api.", *DOMAIN] site_paste: &PASTE !JOIN ["paste.", *DOMAIN] site_staff: &STAFF !JOIN ["staff.", *DOMAIN] -- cgit v1.2.3 From 1dd55ae6055bbe320588a7f64de1a2bdd5ebaca3 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 18:07:58 +0200 Subject: Add tests for `bot.cogs.token_remover`. --- tests/cogs/test_token_remover.py | 133 +++++++++++++++++++++++++++++++++++++++ tests/helpers.py | 10 +++ tox.ini | 2 +- 3 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 tests/cogs/test_token_remover.py create mode 100644 tests/helpers.py diff --git a/tests/cogs/test_token_remover.py b/tests/cogs/test_token_remover.py new file mode 100644 index 000000000..9d46b3a05 --- /dev/null +++ b/tests/cogs/test_token_remover.py @@ -0,0 +1,133 @@ +import asyncio +from unittest.mock import MagicMock + +import pytest +from discord import Colour + +from bot.cogs.token_remover import ( + DELETION_MESSAGE_TEMPLATE, + TokenRemover, + setup as setup_cog, +) +from bot.constants import Channels, Colours, Event, Icons +from tests.helpers import AsyncMock + + +@pytest.fixture() +def token_remover(): + bot = MagicMock() + bot.get_cog.return_value = MagicMock() + bot.get_cog.return_value.send_log_message = AsyncMock() + return TokenRemover(bot=bot) + + +@pytest.fixture() +def message(): + message = MagicMock() + message.author.__str__.return_value = 'lemon' + message.author.bot = False + message.author.avatar_url_as.return_value = 'picture-lemon.png' + message.author.id = 42 + message.author.mention = '@lemon' + message.channel.send = AsyncMock() + message.channel.mention = '#lemonade-stand' + message.content = '' + message.delete = AsyncMock() + message.id = 555 + return message + + +@pytest.mark.parametrize( + ('content', 'expected'), + ( + ('MTIz', True), # 123 + ('YWJj', False), # abc + ) +) +def test_is_valid_user_id(content: str, expected: bool): + assert TokenRemover.is_valid_user_id(content) is expected + + +@pytest.mark.parametrize( + ('content', 'expected'), + ( + ('DN9r_A', True), # stolen from dapi, thanks to the author of the 'token' tag! + ('MTIz', False), # 123 + ) +) +def test_is_valid_timestamp(content: str, expected: bool): + assert TokenRemover.is_valid_timestamp(content) is expected + + +def test_mod_log_property(token_remover): + token_remover.bot.get_cog.return_value = 'lemon' + assert token_remover.mod_log == 'lemon' + token_remover.bot.get_cog.assert_called_once_with('ModLog') + + +def test_ignores_bot_messages(token_remover, message): + message.author.bot = True + coroutine = token_remover.on_message(message) + assert asyncio.run(coroutine) is None + + +@pytest.mark.parametrize('content', ('', 'lemon wins')) +def test_ignores_messages_without_tokens(token_remover, message, content): + message.content = content + coroutine = token_remover.on_message(message) + assert asyncio.run(coroutine) is None + + +@pytest.mark.parametrize('content', ('foo.bar.baz', 'x.y.')) +def test_ignores_invalid_tokens(token_remover, message, content): + message.content = content + coroutine = token_remover.on_message(message) + assert asyncio.run(coroutine) is None + + +@pytest.mark.parametrize( + 'content, censored_token', + ( + ('MTIz.DN9R_A.xyz', 'MTIz.DN9R_A.xxx'), + ) +) +def test_censors_valid_tokens( + token_remover, message, content, censored_token, caplog +): + message.content = content + coroutine = token_remover.on_message(message) + assert asyncio.run(coroutine) is None # still no rval + + # asyncio logs some stuff about its reactor, discard it + [_, record] = caplog.records + assert record.message == ( + "Censored a seemingly valid token sent by lemon (`42`) in #lemonade-stand, " + f"token was `{censored_token}`" + ) + + message.delete.assert_called_once_with() + message.channel.send.assert_called_once_with( + DELETION_MESSAGE_TEMPLATE.format(mention='@lemon') + ) + token_remover.bot.get_cog.assert_called_with('ModLog') + message.author.avatar_url_as.assert_called_once_with(static_format='png') + + mod_log = token_remover.bot.get_cog.return_value + mod_log.ignore.assert_called_once_with(Event.message_delete, message.id) + mod_log.send_log_message.assert_called_once_with( + icon_url=Icons.token_removed, + colour=Colour(Colours.soft_red), + title="Token removed!", + text=record.message, + thumbnail='picture-lemon.png', + channel_id=Channels.mod_alerts + ) + + +def test_setup(caplog): + bot = MagicMock() + setup_cog(bot) + [record] = caplog.records + + bot.add_cog.assert_called_once() + assert record.message == "Cog loaded: TokenRemover" diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 000000000..57c6fcc1a --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,10 @@ +from unittest.mock import MagicMock + + +__all__ = ('AsyncMock',) + + +# TODO: Remove me on 3.8 +class AsyncMock(MagicMock): + async def __call__(self, *args, **kwargs): + return super(AsyncMock, self).__call__(*args, **kwargs) diff --git a/tox.ini b/tox.ini index c84827570..21097cd97 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [flake8] max-line-length=120 -application_import_names=bot +application_import_names=bot,tests exclude=.cache,.venv ignore=B311,W503,E226,S311,T000 import-order-style=pycharm -- cgit v1.2.3 From 4d2e3b1afcb2ad15ff3e091929f42907effa4496 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 17:26:41 +0200 Subject: Validate `bot/resources/stars.json` in tests. --- tests/test_resources.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/test_resources.py diff --git a/tests/test_resources.py b/tests/test_resources.py new file mode 100644 index 000000000..2b17aea64 --- /dev/null +++ b/tests/test_resources.py @@ -0,0 +1,18 @@ +import json +import mimetypes +from pathlib import Path +from urllib.parse import urlparse + + +def test_stars_valid(): + """Validates that `bot/resources/stars.json` contains valid images.""" + + path = Path('bot', 'resources', 'stars.json') + content = path.read_text() + data = json.loads(content) + + for url in data.values(): + assert urlparse(url).scheme == 'https' + + mimetype, _ = mimetypes.guess_type(url) + assert mimetype in ('image/jpeg', 'image/png') -- cgit v1.2.3 From 8d04b381dc41fde83fb4144cbf83ba3e0664fe82 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 19:28:48 +0200 Subject: Implement `!otn search`. Closes #408. --- bot/cogs/off_topic_names.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/bot/cogs/off_topic_names.py b/bot/cogs/off_topic_names.py index 5a61425be..8f5f9c2e5 100644 --- a/bot/cogs/off_topic_names.py +++ b/bot/cogs/off_topic_names.py @@ -1,4 +1,5 @@ import asyncio +import difflib import logging from datetime import datetime, timedelta @@ -141,6 +142,27 @@ class OffTopicNames: embed.description = "Hmmm, seems like there's nothing here yet." await ctx.send(embed=embed) + @otname_group.command(name='search', aliases=('s',)) + @with_role(*MODERATION_ROLES) + async def search_command(self, ctx, *, query: str): + """ + Search for an off-topic name. + """ + + result = await self.bot.api_client.get('bot/off-topic-channel-names') + matches = difflib.get_close_matches(query, result, n=10, cutoff=0.35) + lines = sorted(f"• {name}" for name in matches) + embed = Embed( + title=f"Query results", + colour=Colour.blue() + ) + + if matches: + await LinePaginator.paginate(lines, ctx, embed, max_size=400, empty=False) + else: + embed.description = "Nothing found." + await ctx.send(embed=embed) + def setup(bot: Bot): bot.add_cog(OffTopicNames(bot)) -- cgit v1.2.3 From 80ca36bc8e97ee578584e30be284fc0f033ad8a9 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Sun, 15 Sep 2019 20:52:43 +0200 Subject: Update site links to new URL scheme Some links still had the URL scheme of the old Flask website, I updated them to point to the correct pages on the new website. --- bot/cogs/filtering.py | 2 +- bot/cogs/moderation.py | 2 +- bot/cogs/site.py | 15 +++++++++------ bot/cogs/superstarify/__init__.py | 2 +- bot/cogs/verification.py | 4 ++-- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py index 418297fc4..77f6eece5 100644 --- a/bot/cogs/filtering.py +++ b/bot/cogs/filtering.py @@ -59,7 +59,7 @@ class Filtering: "user_notification": Filter.notify_user_invites, "notification_msg": ( f"Per Rule 10, your invite link has been removed. {_staff_mistake_str}\n\n" - r"Our server rules can be found here: " + r"Our server rules can be found here: " ) }, "filter_domains": { diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index fb791c933..532a44f4d 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -28,7 +28,7 @@ INFRACTION_ICONS = { "Kick": Icons.sign_out, "Ban": Icons.user_ban } -RULES_URL = "https://pythondiscord.com/about/rules" +RULES_URL = "https://pythondiscord.com/pages/rules" APPEALABLE_INFRACTIONS = ("Ban", "Mute") diff --git a/bot/cogs/site.py b/bot/cogs/site.py index b5e63fb41..b540827bf 100644 --- a/bot/cogs/site.py +++ b/bot/cogs/site.py @@ -46,15 +46,18 @@ class Site: async def site_resources(self, ctx: Context): """Info about the site's Resources page.""" - url = f"{PAGES_URL}/resources" + learning_url = f"{PAGES_URL}/resources" + tools_url = f"{PAGES_URL}/tools" - embed = Embed(title="Resources") - embed.set_footer(text=url) + embed = Embed(title="Resources & Tools") + embed.set_footer(text=f"{learning_url} | {tools_url}") embed.colour = Colour.blurple() embed.description = ( - f"The [Resources page]({url}) on our website contains a " + f"The [Resources page]({learning_url}) on our website contains a " "list of hand-selected goodies that we regularly recommend " - "to both beginners and experts." + f"to both beginners and experts. The [Tools page]({tools_url}) " + "contains a couple of the most popular tools for programming in " + "Python." ) await ctx.send(embed=embed) @@ -111,7 +114,7 @@ class Site: # Rules were not submitted. Return the default description. rules_embed.description = ( "The rules and guidelines that apply to this community can be found on" - " our [rules page](https://pythondiscord.com/about/rules). We expect" + f" our [rules page]({PAGES_URL}/rules). We expect" " all members of the community to have read and understood these." ) diff --git a/bot/cogs/superstarify/__init__.py b/bot/cogs/superstarify/__init__.py index cccd91304..b2e31db3e 100644 --- a/bot/cogs/superstarify/__init__.py +++ b/bot/cogs/superstarify/__init__.py @@ -15,7 +15,7 @@ from bot.decorators import with_role from bot.utils.moderation import post_infraction log = logging.getLogger(__name__) -NICKNAME_POLICY_URL = "https://pythondiscord.com/about/rules#nickname-policy" +NICKNAME_POLICY_URL = "https://pythondiscord.com/pages/rules/#wiki-toc-nickname-policy" class Superstarify: diff --git a/bot/cogs/verification.py b/bot/cogs/verification.py index 6b42c9213..efbcda166 100644 --- a/bot/cogs/verification.py +++ b/bot/cogs/verification.py @@ -14,8 +14,8 @@ Hello! Welcome to the server, and thanks for verifying yourself! For your records, these are the documents you accepted: -`1)` Our rules, here: -`2)` Our privacy policy, here: - you can find information on how to have \ +`1)` Our rules, here: +`2)` Our privacy policy, here: - you can find information on how to have \ your information removed here as well. Feel free to review them at any point! -- cgit v1.2.3 From 43cc15121482de120dcc1158153a24d5cadf27fa Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 21:11:09 +0200 Subject: Add tests for `bot.cogs.security`. --- tests/cogs/test_security.py | 54 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/cogs/test_security.py diff --git a/tests/cogs/test_security.py b/tests/cogs/test_security.py new file mode 100644 index 000000000..1efb460fe --- /dev/null +++ b/tests/cogs/test_security.py @@ -0,0 +1,54 @@ +import logging +from unittest.mock import MagicMock + +import pytest +from discord.ext.commands import NoPrivateMessage + +from bot.cogs import security + + +@pytest.fixture() +def cog(): + bot = MagicMock() + return security.Security(bot) + + +@pytest.fixture() +def context(): + return MagicMock() + + +def test_check_additions(cog): + cog.bot.check.assert_any_call(cog.check_on_guild) + cog.bot.check.assert_any_call(cog.check_not_bot) + + +def test_check_not_bot_for_humans(cog, context): + context.author.bot = False + assert cog.check_not_bot(context) + + +def test_check_not_bot_for_robots(cog, context): + context.author.bot = True + assert not cog.check_not_bot(context) + + +def test_check_on_guild_outside_of_guild(cog, context): + context.guild = None + + with pytest.raises(NoPrivateMessage, match="This command cannot be used in private messages."): + cog.check_on_guild(context) + + +def test_check_on_guild_on_guild(cog, context): + context.guild = "lemon's lemonade stand" + assert cog.check_on_guild(context) + + +def test_security_cog_load(caplog): + bot = MagicMock() + security.setup(bot) + bot.add_cog.assert_called_once() + [record] = caplog.records + assert record.message == "Cog loaded: Security" + assert record.levelno == logging.INFO -- cgit v1.2.3 From 6032466412e6bed5d431968158e40cb5097d3915 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 15 Sep 2019 22:51:08 +0200 Subject: Changing the #dev-logs ID to new channel. We retired the old #dev-logs channel (for security reasons) and have made a new one for public consumption. This commit changes the ID to match the new channel. --- config-default.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-default.yml b/config-default.yml index c9fc3b954..be18b9475 100644 --- a/config-default.yml +++ b/config-default.yml @@ -95,7 +95,7 @@ guild: bot: 267659945086812160 checkpoint_test: 422077681434099723 defcon: 464469101889454091 - devlog: &DEVLOG 409308876241108992 + devlog: &DEVLOG 622895325144940554 devtest: &DEVTEST 414574275865870337 help_0: 303906576991780866 help_1: 303906556754395136 -- cgit v1.2.3 From 1af9ffd4185766b8955f65ba92eab5123c41d97e Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 15 Sep 2019 23:22:26 +0200 Subject: Replaces all GitLab refs with GitHub. There were some GitLab references in various parts of the code, which were causing a problem with displaying icons in the bot connection embeds and other minor aesthetic issues. This commit replaces all links to GitLab with their GitHub equivalent, resolving these bugs. --- bot/cogs/cogs.py | 8 ++++---- bot/cogs/logging.py | 4 ++-- bot/constants.py | 2 +- config-default.yml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bot/cogs/cogs.py b/bot/cogs/cogs.py index ebdbf5ad8..7283aae6d 100644 --- a/bot/cogs/cogs.py +++ b/bot/cogs/cogs.py @@ -60,7 +60,7 @@ class Cogs: embed.set_author( name="Python Bot (Cogs)", - url=URLs.gitlab_bot_repo, + url=URLs.github_bot_repo, icon_url=URLs.bot_avatar ) @@ -113,7 +113,7 @@ class Cogs: embed.set_author( name="Python Bot (Cogs)", - url=URLs.gitlab_bot_repo, + url=URLs.github_bot_repo, icon_url=URLs.bot_avatar ) @@ -168,7 +168,7 @@ class Cogs: embed.set_author( name="Python Bot (Cogs)", - url=URLs.gitlab_bot_repo, + url=URLs.github_bot_repo, icon_url=URLs.bot_avatar ) @@ -269,7 +269,7 @@ class Cogs: embed.colour = Colour.blurple() embed.set_author( name="Python Bot (Cogs)", - url=URLs.gitlab_bot_repo, + url=URLs.github_bot_repo, icon_url=URLs.bot_avatar ) diff --git a/bot/cogs/logging.py b/bot/cogs/logging.py index 6b8462f3b..b31db60d9 100644 --- a/bot/cogs/logging.py +++ b/bot/cogs/logging.py @@ -23,8 +23,8 @@ class Logging: embed = Embed(description="Connected!") embed.set_author( name="Python Bot", - url="https://gitlab.com/discord-python/projects/bot", - icon_url="https://gitlab.com/python-discord/branding/raw/master/logos/logo_circle/logo_circle.png" + url="https://github.com/python-discord/bot", + icon_url="https://github.com/python-discord/branding/blob/master/logos/logo_circle/logo_circle.png" ) if not DEBUG_MODE: diff --git a/bot/constants.py b/bot/constants.py index 4e14a85a8..d5b73bd1d 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -412,7 +412,7 @@ class URLs(metaclass=YAMLGetter): # Misc endpoints bot_avatar: str deploy: str - gitlab_bot_repo: str + github_bot_repo: str status: str # Site endpoints diff --git a/config-default.yml b/config-default.yml index be18b9475..fd83e69a4 100644 --- a/config-default.yml +++ b/config-default.yml @@ -273,7 +273,7 @@ urls: # Misc URLs bot_avatar: "https://raw.githubusercontent.com/discord-python/branding/master/logos/logo_circle/logo_circle.png" - gitlab_bot_repo: "https://gitlab.com/python-discord/projects/bot" + github_bot_repo: "https://github.com/python-discord/bot" anti_spam: # Clean messages that violate a rule. -- cgit v1.2.3 From 70b7ef7ae65888da3e5a36fad76f51726b03fc7e Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Mon, 16 Sep 2019 16:59:21 +0200 Subject: Update discord.py version to 1.2.3 I have updated the discord.py version to 1.2.3. This includes changes throughout the entire code base, including: - All cogs now inherit from `discord.ext.commands.Cog`; - All of our ABCs now have `bot.utils.CogABCMeta` as a metaclass; - All event handlers in cogs are now decorated with `Cog.listener()`; - Some method names have changes, including: - get_message => fetch_message - get_webhook_info => fetch_webhook - A few occurences of `get_channel` have been replaced by the new coroutine `fetch_channel`; - I've had to patch a few lines of code to account for small differences between the versions, like `on_member_update` attribute names in ModLog and the fact the way we used `Context.invoke` a couple of times has stopped working. In addition, I've added a patch for a bug in discord.py (with the help of @Scragly). This discord.py version has a bug which causes the edited timestamp not to be processed for edited messages. It's already fixed on GitHub, but a bug fix release has not been released to PyPI. In the meantime, I've added a patch in `bot.patches.message_edited_at` and included conditional loading in `__main__`. Finally, I noticed a small bug in `bot.cogs.filtering` that I fixed; I replaces `return` with `continue` to make sure filtering for edited messages doesn't stop after the `rich_embed_filter`. --- Pipfile | 2 +- Pipfile.lock | 96 +++++++++++----------------------- bot/__main__.py | 14 +++-- bot/cogs/alias.py | 4 +- bot/cogs/antispam.py | 16 ++++-- bot/cogs/bot.py | 14 ++--- bot/cogs/clean.py | 4 +- bot/cogs/cogs.py | 4 +- bot/cogs/defcon.py | 14 ++--- bot/cogs/doc.py | 3 +- bot/cogs/error_handler.py | 5 +- bot/cogs/eval.py | 4 +- bot/cogs/filtering.py | 10 ++-- bot/cogs/free.py | 4 +- bot/cogs/help.py | 6 +-- bot/cogs/information.py | 9 ++-- bot/cogs/jams.py | 2 +- bot/cogs/logging.py | 10 ++-- bot/cogs/moderation.py | 7 +-- bot/cogs/modlog.py | 24 +++++++-- bot/cogs/off_topic_names.py | 12 ++--- bot/cogs/reddit.py | 7 +-- bot/cogs/reminders.py | 5 +- bot/cogs/security.py | 4 +- bot/cogs/site.py | 4 +- bot/cogs/snekbox.py | 4 +- bot/cogs/superstarify/__init__.py | 6 ++- bot/cogs/sync/cog.py | 11 +++- bot/cogs/tags.py | 9 ++-- bot/cogs/token_remover.py | 5 +- bot/cogs/utils.py | 4 +- bot/cogs/verification.py | 5 +- bot/cogs/watchchannels/bigbrother.py | 4 +- bot/cogs/watchchannels/talentpool.py | 4 +- bot/cogs/watchchannels/watchchannel.py | 30 +++++------ bot/cogs/wolfram.py | 4 +- bot/patches/__init__.py | 6 +++ bot/patches/message_edited_at.py | 32 ++++++++++++ bot/utils/__init__.py | 9 ++++ bot/utils/scheduling.py | 6 ++- 40 files changed, 240 insertions(+), 183 deletions(-) create mode 100644 bot/patches/__init__.py create mode 100644 bot/patches/message_edited_at.py diff --git a/Pipfile b/Pipfile index 739507ac3..eaef3bd65 100644 --- a/Pipfile +++ b/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -discord-py = {git = "https://github.com/Rapptz/discord.py.git",extras = ["voice"],ref = "860d6a9ace8248dfeec18b8b159e7b757d9f56bb",editable = true} +discord-py = "~=1.2" aiodns = "*" logmatic-python = "*" aiohttp = "*" diff --git a/Pipfile.lock b/Pipfile.lock index f655943b4..3c98e2b93 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "987e3fc1840e8050f159daa9c23a2c67bd18d17914d4295eb469a42c778daa10" + "sha256": "c1933af105f88f5f2541b1796b92f91d1fcf7a1a947abfe1d8edb016710a56df" }, "pipfile-spec": 6, "requires": { @@ -34,31 +34,31 @@ }, "aiohttp": { "hashes": [ - "sha256:0419705a36b43c0ac6f15469f9c2a08cad5c939d78bd12a5c23ea167c8253b2b", - "sha256:1812fc4bc6ac1bde007daa05d2d0f61199324e0cc893b11523e646595047ca08", - "sha256:2214b5c0153f45256d5d52d1e0cafe53f9905ed035a142191727a5fb620c03dd", - "sha256:275909137f0c92c61ba6bb1af856a522d5546f1de8ea01e4e726321c697754ac", - "sha256:3983611922b561868428ea1e7269e757803713f55b53502423decc509fef1650", - "sha256:51afec6ffa50a9da4cdef188971a802beb1ca8e8edb40fa429e5e529db3475fa", - "sha256:589f2ec8a101a0f340453ee6945bdfea8e1cd84c8d88e5be08716c34c0799d95", - "sha256:789820ddc65e1f5e71516adaca2e9022498fa5a837c79ba9c692a9f8f916c330", - "sha256:7a968a0bdaaf9abacc260911775611c9a602214a23aeb846f2eb2eeaa350c4dc", - "sha256:7aeefbed253f59ea39e70c5848de42ed85cb941165357fc7e87ab5d8f1f9592b", - "sha256:7b2eb55c66512405103485bd7d285a839d53e7fdc261ab20e5bcc51d7aaff5de", - "sha256:87bc95d3d333bb689c8d755b4a9d7095a2356108002149523dfc8e607d5d32a4", - "sha256:9d80e40db208e29168d3723d1440ecbb06054d349c5ece6a2c5a611490830dd7", - "sha256:a1b442195c2a77d33e4dbee67c9877ccbdd3a1f686f91eb479a9577ed8cc326b", - "sha256:ab3d769413b322d6092f169f316f7b21cd261a7589f7e31db779d5731b0480d8", - "sha256:b066d3dec5d0f5aee6e34e5765095dc3d6d78ef9839640141a2b20816a0642bd", - "sha256:b24e7845ae8de3e388ef4bcfcf7f96b05f52c8e633b33cf8003a6b1d726fc7c2", - "sha256:c59a953c3f8524a7c86eaeaef5bf702555be12f5668f6384149fe4bb75c52698", - "sha256:cf2cc6c2c10d242790412bea7ccf73726a9a44b4c4b073d2699ef3b48971fd95", - "sha256:e0c9c8d4150ae904f308ff27b35446990d2b1dfc944702a21925937e937394c6", - "sha256:f1839db4c2b08a9c8f9788112644f8a8557e8e0ecc77b07091afabb941dc55d0", - "sha256:f3df52362be39908f9c028a65490fae0475e4898b43a03d8aa29d1e765b45e07" + "sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55", + "sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed", + "sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10", + "sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5", + "sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1", + "sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939", + "sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390", + "sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa", + "sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc", + "sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5", + "sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d", + "sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf", + "sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6", + "sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72", + "sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12", + "sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366", + "sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4", + "sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300", + "sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d", + "sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303", + "sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6", + "sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889" ], "index": "pypi", - "version": "==3.4.4" + "version": "==3.5.4" }, "aiormq": { "hashes": [ @@ -167,12 +167,11 @@ "version": "==4.0.7" }, "discord-py": { - "editable": true, - "extras": [ - "voice" + "hashes": [ + "sha256:4684733fa137cc7def18087ae935af615212e423e3dbbe3e84ef01d7ae8ed17d" ], - "git": "https://github.com/Rapptz/discord.py.git", - "ref": "860d6a9ace8248dfeec18b8b159e7b757d9f56bb" + "index": "pypi", + "version": "==1.2.3" }, "docutils": { "hashes": [ @@ -375,7 +374,8 @@ }, "pycparser": { "hashes": [ - "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" + "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3", + "sha256:d51e69d7e2bda15beda04993a07d49598a09de7651375270ca60e234d10b7343" ], "version": "==2.19" }, @@ -386,42 +386,6 @@ ], "version": "==2.4.2" }, - "pynacl": { - "hashes": [ - "sha256:04e30e5bdeeb2d5b34107f28cd2f5bbfdc6c616f3be88fc6f53582ff1669eeca", - "sha256:0bfa0d94d2be6874e40f896e0a67e290749151e7de767c5aefbad1121cad7512", - "sha256:11aa4e141b2456ce5cecc19c130e970793fa3a2c2e6fbb8ad65b28f35aa9e6b6", - "sha256:13bdc1fe084ff9ac7653ae5a924cae03bf4bb07c6667c9eb5b6eb3c570220776", - "sha256:14339dc233e7a9dda80a3800e64e7ff89d0878ba23360eea24f1af1b13772cac", - "sha256:1d33e775fab3f383167afb20b9927aaf4961b953d76eeb271a5703a6d756b65b", - "sha256:2a42b2399d0428619e58dac7734838102d35f6dcdee149e0088823629bf99fbb", - "sha256:2dce05ac8b3c37b9e2f65eab56c544885607394753e9613fd159d5e2045c2d98", - "sha256:63cfccdc6217edcaa48369191ae4dca0c390af3c74f23c619e954973035948cd", - "sha256:6453b0dae593163ffc6db6f9c9c1597d35c650598e2c39c0590d1757207a1ac2", - "sha256:73a5a96fb5fbf2215beee2353a128d382dbca83f5341f0d3c750877a236569ef", - "sha256:8abb4ef79161a5f58848b30ab6fb98d8c466da21fdd65558ce1d7afc02c70b5f", - "sha256:8ac1167195b32a8755de06efd5b2d2fe76fc864517dab66aaf65662cc59e1988", - "sha256:8f505f42f659012794414fa57c498404e64db78f1d98dfd40e318c569f3c783b", - "sha256:9c8a06556918ee8e3ab48c65574f318f5a0a4d31437fc135da7ee9d4f9080415", - "sha256:a1e25fc5650cf64f01c9e435033e53a4aca9de30eb9929d099f3bb078e18f8f2", - "sha256:be71cd5fce04061e1f3d39597f93619c80cdd3558a6c9ba99a546f144a8d8101", - "sha256:c5b1a7a680218dee9da0f1b5e24072c46b3c275d35712bc1d505b85bb03441c0", - "sha256:cb785db1a9468841a1265c9215c60fe5d7af2fb1b209e3316a152704607fc582", - "sha256:cf6877124ae6a0698404e169b3ba534542cfbc43f939d46b927d956daf0a373a", - "sha256:d0eb5b2795b7ee2cbcfcadacbe95a13afbda048a262bd369da9904fecb568975", - "sha256:d3a934e2b9f20abac009d5b6951067cfb5486889cb913192b4d8288b216842f1", - "sha256:d795f506bcc9463efb5ebb0f65ed77921dcc9e0a50499dedd89f208445de9ecb", - "sha256:d8aaf7e5d6b0e0ef7d6dbf7abeb75085713d0100b4eb1a4e4e857de76d77ac45", - "sha256:de2aaca8386cf4d70f1796352f2346f48ddb0bed61dc43a3ce773ba12e064031", - "sha256:e0d38fa0a75f65f556fb912f2c6790d1fa29b7dd27a1d9cc5591b281321eaaa9", - "sha256:eb2acabbd487a46b38540a819ef67e477a674481f84a82a7ba2234b9ba46f752", - "sha256:eeee629828d0eb4f6d98ac41e9a3a6461d114d1d0aa111a8931c049359298da0", - "sha256:f5836463a3c0cca300295b229b6c7003c415a9d11f8f9288ddbd728e2746524c", - "sha256:f5ce9e26d25eb0b2d96f3ef0ad70e1d3ae89b5d60255c462252a3e456a48c053", - "sha256:fabf73d5d0286f9e078774f3435601d2735c94ce9e514ac4fb945701edead7e4" - ], - "version": "==1.2.1" - }, "pyparsing": { "hashes": [ "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", diff --git a/bot/__main__.py b/bot/__main__.py index b1a6a5fcd..f25693734 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -2,10 +2,11 @@ import asyncio import logging import socket +import discord from aiohttp import AsyncResolver, ClientSession, TCPConnector -from discord import Game from discord.ext.commands import Bot, when_mentioned_or +from bot import patches from bot.api import APIClient, APILoggingHandler from bot.constants import Bot as BotConfig, DEBUG_MODE @@ -14,9 +15,9 @@ log = logging.getLogger('bot') bot = Bot( command_prefix=when_mentioned_or(BotConfig.prefix), - activity=Game(name="Commands: !help"), + activity=discord.Game(name="Commands: !help"), case_insensitive=True, - max_messages=10_000 + max_messages=10_000, ) # Global aiohttp session for all cogs @@ -71,6 +72,11 @@ bot.load_extension("bot.cogs.utils") bot.load_extension("bot.cogs.watchchannels") bot.load_extension("bot.cogs.wolfram") +# Apply `message_edited_at` patch if discord.py did not yet release a bug fix. +if not hasattr(discord.message.Message, '_handle_edited_timestamp'): + patches.message_edited_at.apply_patch() + bot.run(BotConfig.token) -bot.http_session.close() # Close the aiohttp session when the bot finishes running +# This calls a coroutine, so it doesn't do anything at the moment. +# bot.http_session.close() # Close the aiohttp session when the bot finishes running diff --git a/bot/cogs/alias.py b/bot/cogs/alias.py index a44c47331..3d0c9d826 100644 --- a/bot/cogs/alias.py +++ b/bot/cogs/alias.py @@ -4,7 +4,7 @@ from typing import Union from discord import Colour, Embed, Member, User from discord.ext.commands import ( - Command, Context, clean_content, command, group + Cog, Command, Context, clean_content, command, group ) from bot.cogs.watchchannels.watchchannel import proxy_user @@ -14,7 +14,7 @@ from bot.pagination import LinePaginator log = logging.getLogger(__name__) -class Alias: +class Alias(Cog): """ Aliases for more used commands """ diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index e980de364..7b97881fd 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -7,10 +7,9 @@ from operator import itemgetter from typing import Dict, Iterable, List, Set from discord import Colour, Member, Message, NotFound, Object, TextChannel -from discord.ext.commands import Bot +from discord.ext.commands import Bot, Cog from bot import rules -from bot.cogs.moderation import Moderation from bot.cogs.modlog import ModLog from bot.constants import ( AntiSpam as AntiSpamConfig, Channels, @@ -95,7 +94,7 @@ class DeletionContext: ) -class AntiSpam: +class AntiSpam(Cog): """Cog that controls our anti-spam measures.""" def __init__(self, bot: Bot, validation_errors: bool) -> None: @@ -113,6 +112,7 @@ class AntiSpam: """Allows for easy access of the ModLog cog.""" return self.bot.get_cog("ModLog") + @Cog.listener() async def on_ready(self): """Unloads the cog and alerts admins if configuration validation failed.""" if self.validation_errors: @@ -131,6 +131,7 @@ class AntiSpam: self.bot.remove_cog(self.__class__.__name__) return + @Cog.listener() async def on_message(self, message: Message) -> None: """Applies the antispam rules to each received message.""" if ( @@ -152,7 +153,7 @@ class AntiSpam: # Store history messages since `interval` seconds ago in a list to prevent unnecessary API calls. earliest_relevant_at = datetime.utcnow() - timedelta(seconds=max_interval) relevant_messages = [ - msg async for msg in message.channel.history(after=earliest_relevant_at, reverse=False) + msg async for msg in message.channel.history(after=earliest_relevant_at, oldest_first=False) if not msg.author.bot ] @@ -211,7 +212,12 @@ class AntiSpam: # Since we're going to invoke the tempmute command directly, we need to manually call the converter. dt_remove_role_after = await self.expiration_date_converter.convert(context, f"{remove_role_after}S") - await context.invoke(Moderation.tempmute, member, dt_remove_role_after, reason=reason) + await context.invoke( + self.bot.get_command('tempmute'), + member, + dt_remove_role_after, + reason=reason + ) async def maybe_delete_messages(self, channel: TextChannel, messages: List[Message]) -> None: """Cleans the messages if cleaning is configured.""" diff --git a/bot/cogs/bot.py b/bot/cogs/bot.py index 4a0f208f4..e88b1d9b5 100644 --- a/bot/cogs/bot.py +++ b/bot/cogs/bot.py @@ -4,7 +4,7 @@ import re import time from discord import Embed, Message, RawMessageUpdateEvent -from discord.ext.commands import Bot, Context, command, group +from discord.ext.commands import Bot, Cog, Context, command, group from bot.constants import ( Channels, Guild, MODERATION_ROLES, @@ -16,7 +16,7 @@ from bot.utils.messages import wait_for_deletion log = logging.getLogger(__name__) -class Bot: +class Bot(Cog): """ Bot information commands """ @@ -48,14 +48,14 @@ class Bot: @group(invoke_without_command=True, name="bot", hidden=True) @with_role(Roles.verified) - async def bot_group(self, ctx: Context): + async def botinfo_group(self, ctx: Context): """ Bot informational commands """ await ctx.invoke(self.bot.get_command("help"), "bot") - @bot_group.command(name='about', aliases=('info',), hidden=True) + @botinfo_group.command(name='about', aliases=('info',), hidden=True) @with_role(Roles.verified) async def about_command(self, ctx: Context): """ @@ -236,6 +236,7 @@ class Bot: return msg.content[:3] in not_backticks + @Cog.listener() async def on_message(self, msg: Message): """ Detect poorly formatted Python code and send the user @@ -357,6 +358,7 @@ class Bot: f"The message that was posted was:\n\n{msg.content}\n\n" ) + @Cog.listener() async def on_raw_message_edit(self, payload: RawMessageUpdateEvent): if ( # Checks to see if the message was called out by the bot @@ -370,14 +372,14 @@ class Bot: # Retrieve channel and message objects for use later channel = self.bot.get_channel(int(payload.data.get("channel_id"))) - user_message = await channel.get_message(payload.message_id) + user_message = await channel.fetch_message(payload.message_id) # Checks to see if the user has corrected their codeblock. If it's fixed, has_fixed_codeblock will be None has_fixed_codeblock = self.codeblock_stripping(payload.data.get("content"), self.has_bad_ticks(user_message)) # If the message is fixed, delete the bot message and the entry from the id dictionary if has_fixed_codeblock is None: - bot_message = await channel.get_message(self.codeblock_message_ids[payload.message_id]) + bot_message = await channel.fetch_message(self.codeblock_message_ids[payload.message_id]) await bot_message.delete() del self.codeblock_message_ids[payload.message_id] log.trace("User's incorrect code block has been fixed. Removing bot formatting message.") diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index 1f3e1caa9..20c24dafc 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -4,7 +4,7 @@ import re from typing import Optional from discord import Colour, Embed, Message, User -from discord.ext.commands import Bot, Context, group +from discord.ext.commands import Bot, Cog, Context, group from bot.cogs.modlog import ModLog from bot.constants import ( @@ -16,7 +16,7 @@ from bot.decorators import with_role log = logging.getLogger(__name__) -class Clean: +class Clean(Cog): """ A cog that allows messages to be deleted in bulk, while applying various filters. diff --git a/bot/cogs/cogs.py b/bot/cogs/cogs.py index 7283aae6d..ec497b966 100644 --- a/bot/cogs/cogs.py +++ b/bot/cogs/cogs.py @@ -2,7 +2,7 @@ import logging import os from discord import Colour, Embed -from discord.ext.commands import Bot, Context, group +from discord.ext.commands import Bot, Cog, Context, group from bot.constants import ( Emojis, MODERATION_ROLES, Roles, URLs @@ -15,7 +15,7 @@ log = logging.getLogger(__name__) KEEP_LOADED = ["bot.cogs.cogs", "bot.cogs.modlog"] -class Cogs: +class Cogs(Cog): """ Cog management commands """ diff --git a/bot/cogs/defcon.py b/bot/cogs/defcon.py index c67fa2807..8fab00712 100644 --- a/bot/cogs/defcon.py +++ b/bot/cogs/defcon.py @@ -2,10 +2,10 @@ import logging from datetime import datetime, timedelta from discord import Colour, Embed, Member -from discord.ext.commands import Bot, Context, group +from discord.ext.commands import Bot, Cog, Context, group from bot.cogs.modlog import ModLog -from bot.constants import Channels, Colours, Emojis, Event, Icons, Keys, Roles +from bot.constants import Channels, Colours, Emojis, Event, Icons, Roles from bot.decorators import with_role log = logging.getLogger(__name__) @@ -24,21 +24,23 @@ will be resolved soon. In the meantime, please feel free to peruse the resources BASE_CHANNEL_TOPIC = "Python Discord Defense Mechanism" -class Defcon: +class Defcon(Cog): """Time-sensitive server defense mechanisms""" days = None # type: timedelta enabled = False # type: bool def __init__(self, bot: Bot): self.bot = bot + self.channel = None self.days = timedelta(days=0) - self.headers = {"X-API-KEY": Keys.site_api} @property def mod_log(self) -> ModLog: return self.bot.get_cog("ModLog") + @Cog.listener() async def on_ready(self): + self.channel = await self.bot.fetch_channel(Channels.defcon) try: response = await self.bot.api_client.get('bot/bot-settings/defcon') data = response['data'] @@ -62,6 +64,7 @@ class Defcon: await self.update_channel_topic() + @Cog.listener() async def on_member_join(self, member: Member): if self.enabled and self.days.days > 0: now = datetime.utcnow() @@ -278,8 +281,7 @@ class Defcon: new_topic = f"{BASE_CHANNEL_TOPIC}\n(Status: Disabled)" self.mod_log.ignore(Event.guild_channel_update, Channels.defcon) - defcon_channel = self.bot.guilds[0].get_channel(Channels.defcon) - await defcon_channel.edit(topic=new_topic) + await self.channel.edit(topic=new_topic) def setup(bot: Bot): diff --git a/bot/cogs/doc.py b/bot/cogs/doc.py index aa49b0c25..ebf2c1d65 100644 --- a/bot/cogs/doc.py +++ b/bot/cogs/doc.py @@ -120,12 +120,13 @@ class InventoryURL(commands.Converter): return url -class Doc: +class Doc(commands.Cog): def __init__(self, bot): self.base_urls = {} self.bot = bot self.inventories = {} + @commands.Cog.listener() async def on_ready(self): await self.refresh_inventory() diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py index cfcba6f26..e2d8c3a8f 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -14,7 +14,7 @@ from discord.ext.commands import ( NoPrivateMessage, UserInputError, ) -from discord.ext.commands import Bot, Context +from discord.ext.commands import Bot, Cog, Context from bot.api import ResponseCodeError from bot.constants import Channels @@ -23,12 +23,13 @@ from bot.decorators import InChannelCheckFailure log = logging.getLogger(__name__) -class ErrorHandler: +class ErrorHandler(Cog): """Handles errors emitted from commands.""" def __init__(self, bot: Bot): self.bot = bot + @Cog.listener() async def on_command_error(self, ctx: Context, e: CommandError): command = ctx.command parent = None diff --git a/bot/cogs/eval.py b/bot/cogs/eval.py index 8e97a35a2..c52c04df1 100644 --- a/bot/cogs/eval.py +++ b/bot/cogs/eval.py @@ -8,7 +8,7 @@ import traceback from io import StringIO import discord -from discord.ext.commands import Bot, group +from discord.ext.commands import Bot, Cog, group from bot.constants import Roles from bot.decorators import with_role @@ -17,7 +17,7 @@ from bot.interpreter import Interpreter log = logging.getLogger(__name__) -class CodeEval: +class CodeEval(Cog): """ Owner and admin feature that evaluates code and returns the result to the channel. diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py index 77f6eece5..dc4de7ff1 100644 --- a/bot/cogs/filtering.py +++ b/bot/cogs/filtering.py @@ -5,7 +5,7 @@ from typing import Optional, Union import discord.errors from dateutil.relativedelta import relativedelta from discord import Colour, DMChannel, Member, Message, TextChannel -from discord.ext.commands import Bot +from discord.ext.commands import Bot, Cog from bot.cogs.modlog import ModLog from bot.constants import ( @@ -29,7 +29,7 @@ URL_RE = r"(https?://[^\s]+)" ZALGO_RE = r"[\u0300-\u036F\u0489]" -class Filtering: +class Filtering(Cog): """ Filtering out invites, blacklisting domains, and warning us of certain regular expressions @@ -96,14 +96,16 @@ class Filtering: def mod_log(self) -> ModLog: return self.bot.get_cog("ModLog") + @Cog.listener() async def on_message(self, msg: Message): await self._filter_message(msg) + @Cog.listener() async def on_message_edit(self, before: Message, after: Message): if not before.edited_at: delta = relativedelta(after.edited_at, before.created_at).microseconds else: - delta = None + delta = relativedelta(after.edited_at, before.edited_at).microseconds await self._filter_message(after, delta) async def _filter_message(self, msg: Message, delta: Optional[int] = None): @@ -142,7 +144,7 @@ class Filtering: # If the edit delta is less than 0.001 seconds, then we're probably dealing # with a double filter trigger. if delta is not None and delta < 100: - return + continue # Does the filter only need the message content or the full message? if _filter["content_only"]: diff --git a/bot/cogs/free.py b/bot/cogs/free.py index fd6009bb8..92a9ca041 100644 --- a/bot/cogs/free.py +++ b/bot/cogs/free.py @@ -2,7 +2,7 @@ import logging from datetime import datetime from discord import Colour, Embed, Member, utils -from discord.ext.commands import Context, command +from discord.ext.commands import Cog, Context, command from bot.constants import Categories, Channels, Free, STAFF_ROLES from bot.decorators import redirect_output @@ -15,7 +15,7 @@ RATE = Free.cooldown_rate PER = Free.cooldown_per -class Free: +class Free(Cog): """Tries to figure out which help channels are free.""" PYTHON_HELP_ID = Categories.python_help diff --git a/bot/cogs/help.py b/bot/cogs/help.py index 20ed08f07..31e729003 100644 --- a/bot/cogs/help.py +++ b/bot/cogs/help.py @@ -6,7 +6,7 @@ from contextlib import suppress from discord import Colour, Embed, HTTPException from discord.ext import commands -from discord.ext.commands import CheckFailure +from discord.ext.commands import CheckFailure, Cog as DiscordCog from fuzzywuzzy import fuzz, process from bot import constants @@ -107,7 +107,7 @@ class HelpSession: self.query = ctx.bot self.description = self.query.description self.author = ctx.author - self.destination = ctx.author if ctx.bot.pm_help else ctx.channel + self.destination = ctx.channel # set the config for the session self._cleanup = cleanup @@ -649,7 +649,7 @@ class HelpSession: await self.message.delete() -class Help: +class Help(DiscordCog): """ Custom Embed Pagination Help feature """ diff --git a/bot/cogs/information.py b/bot/cogs/information.py index 320750a24..c4aff73b8 100644 --- a/bot/cogs/information.py +++ b/bot/cogs/information.py @@ -2,11 +2,9 @@ import logging import textwrap from discord import CategoryChannel, Colour, Embed, Member, TextChannel, VoiceChannel -from discord.ext.commands import Bot, Context, command +from discord.ext.commands import Bot, Cog, Context, command -from bot.constants import ( - Channels, Emojis, Keys, MODERATION_ROLES, STAFF_ROLES -) +from bot.constants import Channels, Emojis, MODERATION_ROLES, STAFF_ROLES from bot.decorators import InChannelCheckFailure, with_role from bot.utils.checks import with_role_check from bot.utils.time import time_since @@ -14,7 +12,7 @@ from bot.utils.time import time_since log = logging.getLogger(__name__) -class Information: +class Information(Cog): """ A cog with commands for generating embeds with server information, such as server statistics @@ -23,7 +21,6 @@ class Information: def __init__(self, bot: Bot): self.bot = bot - self.headers = {"X-API-Key": Keys.site_api} @with_role(*MODERATION_ROLES) @command(name="roles") diff --git a/bot/cogs/jams.py b/bot/cogs/jams.py index bca1fb607..dd14111ce 100644 --- a/bot/cogs/jams.py +++ b/bot/cogs/jams.py @@ -10,7 +10,7 @@ from bot.decorators import with_role log = logging.getLogger(__name__) -class CodeJams: +class CodeJams(commands.Cog): """ Manages the code-jam related parts of our server """ diff --git a/bot/cogs/logging.py b/bot/cogs/logging.py index b31db60d9..64bbed46e 100644 --- a/bot/cogs/logging.py +++ b/bot/cogs/logging.py @@ -1,7 +1,7 @@ import logging from discord import Embed -from discord.ext.commands import Bot +from discord.ext.commands import Bot, Cog from bot.constants import Channels, DEBUG_MODE @@ -9,7 +9,7 @@ from bot.constants import Channels, DEBUG_MODE log = logging.getLogger(__name__) -class Logging: +class Logging(Cog): """ Debug logging module """ @@ -17,6 +17,7 @@ class Logging: def __init__(self, bot: Bot): self.bot = bot + @Cog.listener() async def on_ready(self): log.info("Bot connected!") @@ -24,7 +25,10 @@ class Logging: embed.set_author( name="Python Bot", url="https://github.com/python-discord/bot", - icon_url="https://github.com/python-discord/branding/blob/master/logos/logo_circle/logo_circle.png" + icon_url=( + "https://raw.githubusercontent.com/" + "python-discord/branding/master/logos/logo_circle/logo_circle_large.png" + ) ) if not DEBUG_MODE: diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 532a44f4d..fcbadd235 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -8,7 +8,7 @@ from discord import ( Colour, Embed, Forbidden, Guild, HTTPException, Member, NotFound, Object, User ) from discord.ext.commands import ( - BadArgument, BadUnionArgument, Bot, Context, command, group + BadArgument, BadUnionArgument, Bot, Cog, Context, command, group ) from bot import constants @@ -46,7 +46,7 @@ def proxy_user(user_id: str) -> Object: UserTypes = Union[Member, User, proxy_user] -class Moderation(Scheduler): +class Moderation(Scheduler, Cog): """ Server moderation tools. """ @@ -60,6 +60,7 @@ class Moderation(Scheduler): def mod_log(self) -> ModLog: return self.bot.get_cog("ModLog") + @Cog.listener() async def on_ready(self): # Schedule expiration for previous infractions infractions = await self.bot.api_client.get( @@ -1348,7 +1349,7 @@ class Moderation(Scheduler): """ # sometimes `user` is a `discord.Object`, so let's make it a proper user. - user = await self.bot.get_user_info(user.id) + user = await self.bot.fetch_user(user.id) try: await user.send(embed=embed) diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py index 808ba667b..978646f46 100644 --- a/bot/cogs/modlog.py +++ b/bot/cogs/modlog.py @@ -11,7 +11,7 @@ from discord import ( RawMessageUpdateEvent, Role, TextChannel, User, VoiceChannel ) from discord.abc import GuildChannel -from discord.ext.commands import Bot +from discord.ext.commands import Bot, Cog from bot.constants import ( Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, URLs @@ -24,11 +24,11 @@ GUILD_CHANNEL = Union[CategoryChannel, TextChannel, VoiceChannel] CHANNEL_CHANGES_UNSUPPORTED = ("permissions",) CHANNEL_CHANGES_SUPPRESSED = ("_overwrites", "position") -MEMBER_CHANGES_SUPPRESSED = ("activity", "status") +MEMBER_CHANGES_SUPPRESSED = ("status", "activities", "_client_status") ROLE_CHANGES_UNSUPPORTED = ("colour", "permissions") -class ModLog: +class ModLog(Cog, name="ModLog"): """ Logging for server events and staff actions """ @@ -122,6 +122,7 @@ class ModLog: return await self.bot.get_context(log_message) # Optionally return for use with antispam + @Cog.listener() async def on_guild_channel_create(self, channel: GUILD_CHANNEL): if channel.guild.id != GuildConstant.id: return @@ -146,6 +147,7 @@ class ModLog: await self.send_log_message(Icons.hash_green, Colour(Colours.soft_green), title, message) + @Cog.listener() async def on_guild_channel_delete(self, channel: GUILD_CHANNEL): if channel.guild.id != GuildConstant.id: return @@ -167,6 +169,7 @@ class ModLog: title, message ) + @Cog.listener() async def on_guild_channel_update(self, before: GUILD_CHANNEL, after: GuildChannel): if before.guild.id != GuildConstant.id: return @@ -225,6 +228,7 @@ class ModLog: "Channel updated", message ) + @Cog.listener() async def on_guild_role_create(self, role: Role): if role.guild.id != GuildConstant.id: return @@ -234,6 +238,7 @@ class ModLog: "Role created", f"`{role.id}`" ) + @Cog.listener() async def on_guild_role_delete(self, role: Role): if role.guild.id != GuildConstant.id: return @@ -243,6 +248,7 @@ class ModLog: "Role removed", f"{role.name} (`{role.id}`)" ) + @Cog.listener() async def on_guild_role_update(self, before: Role, after: Role): if before.guild.id != GuildConstant.id: return @@ -294,6 +300,7 @@ class ModLog: "Role updated", message ) + @Cog.listener() async def on_guild_update(self, before: Guild, after: Guild): if before.id != GuildConstant.id: return @@ -343,6 +350,7 @@ class ModLog: thumbnail=after.icon_url_as(format="png") ) + @Cog.listener() async def on_member_ban(self, guild: Guild, member: Union[Member, User]): if guild.id != GuildConstant.id: return @@ -358,6 +366,7 @@ class ModLog: channel_id=Channels.modlog ) + @Cog.listener() async def on_member_join(self, member: Member): if member.guild.id != GuildConstant.id: return @@ -378,6 +387,7 @@ class ModLog: channel_id=Channels.userlog ) + @Cog.listener() async def on_member_remove(self, member: Member): if member.guild.id != GuildConstant.id: return @@ -393,6 +403,7 @@ class ModLog: channel_id=Channels.userlog ) + @Cog.listener() async def on_member_unban(self, guild: Guild, member: User): if guild.id != GuildConstant.id: return @@ -408,6 +419,7 @@ class ModLog: channel_id=Channels.modlog ) + @Cog.listener() async def on_member_update(self, before: Member, after: Member): if before.guild.id != GuildConstant.id: return @@ -497,6 +509,7 @@ class ModLog: channel_id=Channels.userlog ) + @Cog.listener() async def on_message_delete(self, message: Message): channel = message.channel author = message.author @@ -551,6 +564,7 @@ class ModLog: channel_id=Channels.message_log ) + @Cog.listener() async def on_raw_message_delete(self, event: RawMessageDeleteEvent): if event.guild_id != GuildConstant.id or event.channel_id in GuildConstant.ignored: return @@ -590,6 +604,7 @@ class ModLog: channel_id=Channels.message_log ) + @Cog.listener() async def on_message_edit(self, before: Message, after: Message): if ( not before.guild @@ -663,10 +678,11 @@ class ModLog: channel_id=Channels.message_log, timestamp_override=after.edited_at ) + @Cog.listener() async def on_raw_message_edit(self, event: RawMessageUpdateEvent): try: channel = self.bot.get_channel(int(event.data["channel_id"])) - message = await channel.get_message(event.message_id) + message = await channel.fetch_message(event.message_id) except NotFound: # Was deleted before we got the event return diff --git a/bot/cogs/off_topic_names.py b/bot/cogs/off_topic_names.py index 5a61425be..f05ac7fef 100644 --- a/bot/cogs/off_topic_names.py +++ b/bot/cogs/off_topic_names.py @@ -3,9 +3,9 @@ import logging from datetime import datetime, timedelta from discord import Colour, Embed -from discord.ext.commands import BadArgument, Bot, Context, Converter, group +from discord.ext.commands import BadArgument, Bot, Cog, Context, Converter, group -from bot.constants import Channels, Keys, MODERATION_ROLES +from bot.constants import Channels, MODERATION_ROLES from bot.decorators import with_role from bot.pagination import LinePaginator @@ -37,7 +37,7 @@ class OffTopicName(Converter): return argument.translate(table) -async def update_names(bot: Bot, headers: dict): +async def update_names(bot: Bot): """ The background updater task that performs a channel name update daily. @@ -69,21 +69,21 @@ async def update_names(bot: Bot, headers: dict): ) -class OffTopicNames: +class OffTopicNames(Cog): """Commands related to managing the off-topic category channel names.""" def __init__(self, bot: Bot): self.bot = bot - self.headers = {"X-API-KEY": Keys.site_api} self.updater_task = None def __cleanup(self): if self.updater_task is not None: self.updater_task.cancel() + @Cog.listener() async def on_ready(self): if self.updater_task is None: - coro = update_names(self.bot, self.headers) + coro = update_names(self.bot) self.updater_task = self.bot.loop.create_task(coro) @group(name='otname', aliases=('otnames', 'otn'), invoke_without_command=True) diff --git a/bot/cogs/reddit.py b/bot/cogs/reddit.py index b5bd26e3d..4c561b7e8 100644 --- a/bot/cogs/reddit.py +++ b/bot/cogs/reddit.py @@ -5,7 +5,7 @@ import textwrap from datetime import datetime, timedelta from discord import Colour, Embed, TextChannel -from discord.ext.commands import Bot, Context, group +from discord.ext.commands import Bot, Cog, Context, group from bot.constants import Channels, ERROR_REPLIES, Reddit as RedditConfig, STAFF_ROLES from bot.converters import Subreddit @@ -15,7 +15,7 @@ from bot.pagination import LinePaginator log = logging.getLogger(__name__) -class Reddit: +class Reddit(Cog): """ Track subreddit posts and show detailed statistics about them. """ @@ -279,8 +279,9 @@ class Reddit: max_lines=15 ) + @Cog.listener() async def on_ready(self): - self.reddit_channel = self.bot.get_channel(Channels.reddit) + self.reddit_channel = await self.bot.fetch_channel(Channels.reddit) if self.reddit_channel is not None: if self.new_posts_task is None: diff --git a/bot/cogs/reminders.py b/bot/cogs/reminders.py index 03ea00de8..c6ae984ea 100644 --- a/bot/cogs/reminders.py +++ b/bot/cogs/reminders.py @@ -7,7 +7,7 @@ from operator import itemgetter from dateutil.relativedelta import relativedelta from discord import Colour, Embed -from discord.ext.commands import Bot, Context, group +from discord.ext.commands import Bot, Cog, Context, group from bot.constants import Channels, Icons, NEGATIVE_REPLIES, POSITIVE_REPLIES, STAFF_ROLES from bot.converters import ExpirationDate @@ -22,12 +22,13 @@ WHITELISTED_CHANNELS = (Channels.bot,) MAXIMUM_REMINDERS = 5 -class Reminders(Scheduler): +class Reminders(Scheduler, Cog): def __init__(self, bot: Bot): self.bot = bot super().__init__() + @Cog.listener() async def on_ready(self): # Get all the current reminders for re-scheduling response = await self.bot.api_client.get( diff --git a/bot/cogs/security.py b/bot/cogs/security.py index 9523766af..e02e91530 100644 --- a/bot/cogs/security.py +++ b/bot/cogs/security.py @@ -1,11 +1,11 @@ import logging -from discord.ext.commands import Bot, Context, NoPrivateMessage +from discord.ext.commands import Bot, Cog, Context, NoPrivateMessage log = logging.getLogger(__name__) -class Security: +class Security(Cog): """ Security-related helpers """ diff --git a/bot/cogs/site.py b/bot/cogs/site.py index b540827bf..4d5b2e811 100644 --- a/bot/cogs/site.py +++ b/bot/cogs/site.py @@ -1,7 +1,7 @@ import logging from discord import Colour, Embed -from discord.ext.commands import Bot, Context, group +from discord.ext.commands import Bot, Cog, Context, group from bot.constants import Channels, STAFF_ROLES, URLs from bot.decorators import redirect_output @@ -12,7 +12,7 @@ log = logging.getLogger(__name__) PAGES_URL = f"{URLs.site_schema}{URLs.site}/pages" -class Site: +class Site(Cog): """Commands for linking to different parts of the site.""" def __init__(self, bot: Bot): diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index c8705ac6f..d36c0795d 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -5,7 +5,7 @@ import textwrap from signal import Signals from typing import Optional, Tuple -from discord.ext.commands import Bot, Context, command, guild_only +from discord.ext.commands import Bot, Cog, Context, command, guild_only from bot.constants import Channels, STAFF_ROLES, URLs from bot.decorators import in_channel @@ -36,7 +36,7 @@ RAW_CODE_REGEX = re.compile( MAX_PASTE_LEN = 1000 -class Snekbox: +class Snekbox(Cog): """ Safe evaluation of Python code using Snekbox """ diff --git a/bot/cogs/superstarify/__init__.py b/bot/cogs/superstarify/__init__.py index b2e31db3e..e9743a2f5 100644 --- a/bot/cogs/superstarify/__init__.py +++ b/bot/cogs/superstarify/__init__.py @@ -4,7 +4,7 @@ from datetime import datetime from discord import Colour, Embed, Member from discord.errors import Forbidden -from discord.ext.commands import Bot, Context, command +from discord.ext.commands import Bot, Cog, Context, command from bot.cogs.moderation import Moderation from bot.cogs.modlog import ModLog @@ -18,7 +18,7 @@ log = logging.getLogger(__name__) NICKNAME_POLICY_URL = "https://pythondiscord.com/pages/rules/#wiki-toc-nickname-policy" -class Superstarify: +class Superstarify(Cog): """ A set of commands to moderate terrible nicknames. """ @@ -34,6 +34,7 @@ class Superstarify: def modlog(self) -> ModLog: return self.bot.get_cog("ModLog") + @Cog.listener() async def on_member_update(self, before: Member, after: Member): """ This event will trigger when someone changes their name. @@ -91,6 +92,7 @@ class Superstarify: "to DM them, and a discord.errors.Forbidden error was incurred." ) + @Cog.listener() async def on_member_join(self, member: Member): """ This event will trigger when someone (re)joins the server. diff --git a/bot/cogs/sync/cog.py b/bot/cogs/sync/cog.py index ec6c5f447..9a3a48bba 100644 --- a/bot/cogs/sync/cog.py +++ b/bot/cogs/sync/cog.py @@ -3,7 +3,7 @@ from typing import Callable, Iterable from discord import Guild, Member, Role from discord.ext import commands -from discord.ext.commands import Bot, Context +from discord.ext.commands import Bot, Cog, Context from bot import constants from bot.api import ResponseCodeError @@ -12,7 +12,7 @@ from bot.cogs.sync import syncers log = logging.getLogger(__name__) -class Sync: +class Sync(Cog): """Captures relevant events and sends them to the site.""" # The server to synchronize events on. @@ -29,6 +29,7 @@ class Sync: def __init__(self, bot: Bot) -> None: self.bot = bot + @Cog.listener() async def on_ready(self) -> None: """Syncs the roles/users of the guild with the database.""" guild = self.bot.get_guild(self.SYNC_SERVER_ID) @@ -47,6 +48,7 @@ class Sync: f"deleted `{total_deleted}`." ) + @Cog.listener() async def on_guild_role_create(self, role: Role) -> None: """Adds newly create role to the database table over the API.""" await self.bot.api_client.post( @@ -60,10 +62,12 @@ class Sync: } ) + @Cog.listener() async def on_guild_role_delete(self, role: Role) -> None: """Deletes role from the database when it's deleted from the guild.""" await self.bot.api_client.delete(f'bot/roles/{role.id}') + @Cog.listener() async def on_guild_role_update(self, before: Role, after: Role) -> None: """Syncs role with the database if any of the stored attributes were updated.""" if ( @@ -83,6 +87,7 @@ class Sync: } ) + @Cog.listener() async def on_member_join(self, member: Member) -> None: """ Adds a new user or updates existing user to the database when a member joins the guild. @@ -118,6 +123,7 @@ class Sync: # If we got `404`, the user is new. Create them. await self.bot.api_client.post('bot/users', json=packed) + @Cog.listener() async def on_member_remove(self, member: Member) -> None: """Updates the user information when a member leaves the guild.""" await self.bot.api_client.put( @@ -132,6 +138,7 @@ class Sync: } ) + @Cog.listener() async def on_member_update(self, before: Member, after: Member) -> None: """Updates the user information if any of relevant attributes have changed.""" if ( diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py index 7b1003148..8e9ba5da3 100644 --- a/bot/cogs/tags.py +++ b/bot/cogs/tags.py @@ -2,9 +2,9 @@ import logging import time from discord import Colour, Embed -from discord.ext.commands import Bot, Context, group +from discord.ext.commands import Bot, Cog, Context, group -from bot.constants import Channels, Cooldowns, Keys, MODERATION_ROLES, Roles +from bot.constants import Channels, Cooldowns, MODERATION_ROLES, Roles from bot.converters import TagContentConverter, TagNameConverter from bot.decorators import with_role from bot.pagination import LinePaginator @@ -19,7 +19,7 @@ TEST_CHANNELS = ( ) -class Tags: +class Tags(Cog): """ Save new tags and fetch existing tags. """ @@ -27,7 +27,6 @@ class Tags: def __init__(self, bot: Bot): self.bot = bot self.tag_cooldowns = {} - self.headers = {"Authorization": f"Token {Keys.site_api}"} @group(name='tags', aliases=('tag', 't'), hidden=True, invoke_without_command=True) async def tags_group(self, ctx: Context, *, tag_name: TagNameConverter = None): @@ -82,7 +81,7 @@ class Tags: "time": time.time(), "channel": ctx.channel.id } - await ctx.send(embed=Embed.from_data(tag['embed'])) + await ctx.send(embed=Embed.from_dict(tag['embed'])) else: tags = await self.bot.api_client.get('bot/tags') diff --git a/bot/cogs/token_remover.py b/bot/cogs/token_remover.py index b2c4cd522..64bf126d6 100644 --- a/bot/cogs/token_remover.py +++ b/bot/cogs/token_remover.py @@ -6,7 +6,7 @@ import struct from datetime import datetime from discord import Colour, Message -from discord.ext.commands import Bot +from discord.ext.commands import Bot, Cog from discord.utils import snowflake_time from bot.cogs.modlog import ModLog @@ -34,7 +34,7 @@ TOKEN_RE = re.compile( ) -class TokenRemover: +class TokenRemover(Cog): """Scans messages for potential discord.py bot tokens and removes them.""" def __init__(self, bot: Bot): @@ -44,6 +44,7 @@ class TokenRemover: def mod_log(self) -> ModLog: return self.bot.get_cog("ModLog") + @Cog.listener() async def on_message(self, msg: Message): if msg.author.bot: return diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py index 98208723a..08e77a24e 100644 --- a/bot/cogs/utils.py +++ b/bot/cogs/utils.py @@ -5,7 +5,7 @@ from email.parser import HeaderParser from io import StringIO from discord import Colour, Embed -from discord.ext.commands import AutoShardedBot, Context, command +from discord.ext.commands import AutoShardedBot, Cog, Context, command from bot.constants import Channels, STAFF_ROLES from bot.decorators import in_channel @@ -13,7 +13,7 @@ from bot.decorators import in_channel log = logging.getLogger(__name__) -class Utils: +class Utils(Cog): """ A selection of utilities which don't have a clear category. """ diff --git a/bot/cogs/verification.py b/bot/cogs/verification.py index efbcda166..14c3f39e3 100644 --- a/bot/cogs/verification.py +++ b/bot/cogs/verification.py @@ -1,7 +1,7 @@ import logging from discord import Message, NotFound, Object -from discord.ext.commands import Bot, Context, command +from discord.ext.commands import Bot, Cog, Context, command from bot.cogs.modlog import ModLog from bot.constants import Channels, Event, Roles @@ -28,7 +28,7 @@ If you'd like to unsubscribe from the announcement notifications, simply send `! """ -class Verification: +class Verification(Cog): """ User verification and role self-management """ @@ -40,6 +40,7 @@ class Verification: def mod_log(self) -> ModLog: return self.bot.get_cog("ModLog") + @Cog.listener() async def on_message(self, message: Message): if message.author.bot: return # They're a bot, ignore diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py index e7b3d70bc..338b6c4ad 100644 --- a/bot/cogs/watchchannels/bigbrother.py +++ b/bot/cogs/watchchannels/bigbrother.py @@ -3,7 +3,7 @@ from collections import ChainMap from typing import Union from discord import User -from discord.ext.commands import Context, group +from discord.ext.commands import Cog, Context, group from bot.constants import Channels, Roles, Webhooks from bot.decorators import with_role @@ -13,7 +13,7 @@ from .watchchannel import WatchChannel, proxy_user log = logging.getLogger(__name__) -class BigBrother(WatchChannel): +class BigBrother(WatchChannel, Cog, name="Big Brother"): """Monitors users by relaying their messages to a watch channel to assist with moderation.""" def __init__(self, bot) -> None: diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py index 47d207d05..4452d7a59 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -4,7 +4,7 @@ from collections import ChainMap from typing import Union from discord import Color, Embed, Member, User -from discord.ext.commands import Context, group +from discord.ext.commands import Cog, Context, group from bot.api import ResponseCodeError from bot.constants import Channels, Guild, Roles, Webhooks @@ -16,7 +16,7 @@ log = logging.getLogger(__name__) STAFF_ROLES = Roles.owner, Roles.admin, Roles.moderator, Roles.helpers # <- In constants after the merge? -class TalentPool(WatchChannel): +class TalentPool(WatchChannel, Cog, name="Talentpool"): """Relays messages of helper candidates to a watch channel to observe them.""" def __init__(self, bot) -> None: diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py index 3a24e3f21..c34b0d5bb 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/cogs/watchchannels/watchchannel.py @@ -3,20 +3,20 @@ import datetime import logging import re import textwrap -from abc import ABC, abstractmethod +from abc import abstractmethod from collections import defaultdict, deque from dataclasses import dataclass from typing import Optional import discord -from discord import Color, Embed, Message, Object, errors -from discord.ext.commands import BadArgument, Bot, Context +from discord import Color, Embed, HTTPException, Message, Object, errors +from discord.ext.commands import BadArgument, Bot, Cog, 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 -from bot.utils import messages +from bot.utils import CogABCMeta, messages from bot.utils.time import time_since log = logging.getLogger(__name__) @@ -47,7 +47,7 @@ class MessageHistory: message_count: int = 0 -class WatchChannel(ABC): +class WatchChannel(metaclass=CogABCMeta): """ABC with functionality for relaying users' messages to a certain channel.""" @abstractmethod @@ -98,21 +98,14 @@ class WatchChannel(ABC): """Starts the watch channel by getting the channel, webhook, and user cache ready.""" await self.bot.wait_until_ready() - # After updating d.py, this block can be replaced by `fetch_channel` with a try-except - for attempt in range(1, self.retries+1): - self.channel = self.bot.get_channel(self.destination) - if self.channel is None: - if attempt < self.retries: - await asyncio.sleep(self.retry_delay) - else: - break - else: - self.log.error(f"Failed to retrieve the text channel with id {self.destination}") + try: + self.channel = await self.bot.fetch_channel(self.destination) + except HTTPException: + self.log.exception(f"Failed to retrieve the text channel with id `{self.destination}`") - # `get_webhook_info` has been renamed to `fetch_webhook` in newer versions of d.py try: - self.webhook = await self.bot.get_webhook_info(self.webhook_id) - except (discord.HTTPException, discord.NotFound, discord.Forbidden): + self.webhook = await self.bot.fetch_webhook(self.webhook_id) + except discord.HTTPException: self.log.exception(f"Failed to fetch webhook with id `{self.webhook_id}`") if self.channel is None or self.webhook is None: @@ -169,6 +162,7 @@ class WatchChannel(ABC): return True + @Cog.listener() async def on_message(self, msg: Message) -> None: """Queues up messages sent by watched users.""" if msg.author.id in self.watched_users: diff --git a/bot/cogs/wolfram.py b/bot/cogs/wolfram.py index 7dd613083..e88efa033 100644 --- a/bot/cogs/wolfram.py +++ b/bot/cogs/wolfram.py @@ -7,7 +7,7 @@ import discord from dateutil.relativedelta import relativedelta from discord import Embed from discord.ext import commands -from discord.ext.commands import BucketType, Context, check, group +from discord.ext.commands import BucketType, Cog, Context, check, group from bot.constants import Colours, STAFF_ROLES, Wolfram from bot.pagination import ImagePaginator @@ -163,7 +163,7 @@ async def get_pod_pages(ctx, bot, query: str) -> Optional[List[Tuple]]: return pages -class Wolfram: +class Wolfram(Cog): """ Commands for interacting with the Wolfram|Alpha API. """ diff --git a/bot/patches/__init__.py b/bot/patches/__init__.py new file mode 100644 index 000000000..fd38ea8cf --- /dev/null +++ b/bot/patches/__init__.py @@ -0,0 +1,6 @@ +"""Subpackage that contains patches for discord.py""" +from . import message_edited_at + +__all__ = [ + message_edited_at, +] diff --git a/bot/patches/message_edited_at.py b/bot/patches/message_edited_at.py new file mode 100644 index 000000000..528373a9b --- /dev/null +++ b/bot/patches/message_edited_at.py @@ -0,0 +1,32 @@ +""" +# message_edited_at patch + +Date: 2019-09-16 +Author: Scragly +Added by: Ves Zappa + +Due to a bug in our current version of discord.py (1.2.3), the edited_at timestamp of +`discord.Messages` are not being handled correctly. This patch fixes that until a new +release of discord.py is released (and we've updated to it). +""" +import logging + +from discord import message, utils + +log = logging.getLogger(__name__) + + +def _handle_edited_timestamp(self, value): + """Helper function that takes care of parsing the edited timestamp.""" + self._edited_timestamp = utils.parse_time(value) + + +def apply_patch(): + """Applies the `edited_at` patch to the `discord.message.Message` class.""" + message.Message._handle_edited_timestamp = _handle_edited_timestamp + message.Message._HANDLERS['edited_timestamp'] = message.Message._handle_edited_timestamp + log.info("Patch applied: message_edited_at") + + +if __name__ == "__main__": + apply_patch() diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py index 4c99d50e8..d5ae0a7c5 100644 --- a/bot/utils/__init__.py +++ b/bot/utils/__init__.py @@ -1,3 +1,12 @@ +from abc import ABCMeta + +from discord.ext.commands import CogMeta + + +class CogABCMeta(CogMeta, ABCMeta): + """Metaclass for ABCs meant to be implemented as Cogs.""" + pass + class CaseInsensitiveDict(dict): """ diff --git a/bot/utils/scheduling.py b/bot/utils/scheduling.py index ded6401b0..f03865013 100644 --- a/bot/utils/scheduling.py +++ b/bot/utils/scheduling.py @@ -1,13 +1,15 @@ import asyncio import contextlib import logging -from abc import ABC, abstractmethod +from abc import abstractmethod from typing import Dict +from bot.utils import CogABCMeta + log = logging.getLogger(__name__) -class Scheduler(ABC): +class Scheduler(metaclass=CogABCMeta): def __init__(self): -- cgit v1.2.3 From 230bd00695ac6be5b0dd0907ead776d48e153da2 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Tue, 17 Sep 2019 05:05:52 +1000 Subject: Adjust to new cog method names. --- bot/cogs/moderation.py | 6 ++++-- bot/cogs/off_topic_names.py | 2 +- bot/cogs/verification.py | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index fcbadd235..c631dd69d 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -1375,13 +1375,15 @@ class Moderation(Scheduler, Cog): # endregion - async def __error(self, ctx: Context, error) -> None: + @staticmethod + async def cog_command_error(ctx: Context, error) -> None: if isinstance(error, BadUnionArgument): if User in error.converters: await ctx.send(str(error.errors[0])) error.handled = True - async def respect_role_hierarchy(self, ctx: Context, target: UserTypes, infr_type: str) -> bool: + @staticmethod + async def respect_role_hierarchy(ctx: Context, target: UserTypes, infr_type: str) -> bool: """ Check if the highest role of the invoking member is greater than that of the target member. If this check fails, a warning is sent to the invoking ctx. diff --git a/bot/cogs/off_topic_names.py b/bot/cogs/off_topic_names.py index f05ac7fef..cadc1bf92 100644 --- a/bot/cogs/off_topic_names.py +++ b/bot/cogs/off_topic_names.py @@ -76,7 +76,7 @@ class OffTopicNames(Cog): self.bot = bot self.updater_task = None - def __cleanup(self): + def cog_unload(self): if self.updater_task is not None: self.updater_task.cancel() diff --git a/bot/cogs/verification.py b/bot/cogs/verification.py index 14c3f39e3..c42d4d67e 100644 --- a/bot/cogs/verification.py +++ b/bot/cogs/verification.py @@ -153,13 +153,13 @@ class Verification(Cog): ) @staticmethod - async def __error(ctx: Context, error): + async def cog_command_error(ctx: Context, error): if isinstance(error, InChannelCheckFailure): # Do nothing; just ignore this error error.handled = True @staticmethod - def __global_check(ctx: Context): + def bot_check(ctx: Context): """ Block any command within the verification channel that is not !accept. """ -- cgit v1.2.3 From 164a409f232162b7c7c637c175503ba74eedd5a8 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Mon, 16 Sep 2019 22:30:21 +0200 Subject: Fix InfractionSearchQuery I missed a `get_user_info` in InfractionSearchQuery in bot.converts. This method is now `fetch_user` in Discord.py 1.2.3. --- bot/converters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/converters.py b/bot/converters.py index 30ea7ca0f..4bd9aba13 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -71,7 +71,7 @@ class InfractionSearchQuery(Converter): async def convert(ctx, arg): try: maybe_snowflake = arg.strip("<@!>") - return await ctx.bot.get_user_info(maybe_snowflake) + return await ctx.bot.fetch_user(maybe_snowflake) except (discord.NotFound, discord.HTTPException): return arg -- cgit v1.2.3 From 39a28ca7eb2ba7966677b5283bce1ebdf8974ee1 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 23:55:11 +0200 Subject: Add coverage reporting to tests. --- .gitignore | 6 ++++++ Pipfile | 1 + azure-pipelines.yml | 16 +++++++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index be4f43c7f..09ca151fb 100644 --- a/.gitignore +++ b/.gitignore @@ -114,3 +114,9 @@ log.* # Custom user configuration config.yml + +# JUnit XML reports from pytest +junit.xml + +# Coverage XML artifacts +coverage.xml diff --git a/Pipfile b/Pipfile index eaef3bd65..273db04d2 100644 --- a/Pipfile +++ b/Pipfile @@ -32,6 +32,7 @@ pre-commit = "~=1.18" safety = "*" dodgy = "*" pytest = "*" +pytest-cov = "*" [requires] python_version = "3.7" diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 19df35c11..242513ab4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -38,9 +38,23 @@ jobs: - script: python -m flake8 displayName: 'Run linter' - - script: BOT_TOKEN=foobar python -m pytest tests + - script: BOT_TOKEN=foobar python -m pytest --junitxml=junit.xml --cov=bot --cov-branch --cov-report=term --cov-report=xml tests displayName: Run tests + - task: PublishCodeCoverageResults@1 + displayName: 'Publish Coverage Results' + condition: succeededOrFailed() + inputs: + codeCoverageTool: Cobertura + summaryFileLocation: coverage.xml + + - task: PublishTestResults@2 + displayName: 'Publish Test Results' + condition: succeededOrFailed() + inputs: + testResultsFiles: junit.xml + testRunTitle: 'Bot Test results' + - job: build displayName: 'Build Containers' dependsOn: 'test' -- cgit v1.2.3 From 8346c3663f8a1adb9a8e9d0a97216aa267958a51 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Tue, 17 Sep 2019 23:15:47 +1000 Subject: Update lock to new coverage dependancies. --- Pipfile.lock | 68 +++++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 3c98e2b93..e5978198f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c1933af105f88f5f2541b1796b92f91d1fcf7a1a947abfe1d8edb016710a56df" + "sha256": "f21c27a5c4493b65a36a78721c2cb597c3eed7fcbd28f3bf731453f2c3cccb56" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,11 @@ "default": { "aio-pika": { "hashes": [ - "sha256:31e08189841a8350db5bec70608b4d2fbacb89c0a555a18ec47511716a9bfc41", - "sha256:adfe0acf34356ccd9654a9a1c46f7e8db1dc4497a774c0e54bf2d3af14571bd0" + "sha256:29f27a8092169924c9eefb0c5e428d216706618dc9caa75ddb7759638e16cf26", + "sha256:4f77ba9b6e7bc27fc88c49638bc3657ae5d4a2539e17fa0c2b25b370547b1b50" ], "index": "pypi", - "version": "==6.1.1" + "version": "==6.1.2" }, "aiodns": { "hashes": [ @@ -152,11 +152,11 @@ }, "dateparser": { "hashes": [ - "sha256:42d51be54e74a8e80a4d76d1fa6e4edd997098fce24ad2d94a2eab5ef247193e", - "sha256:78124c458c461ea7198faa3c038f6381f37588b84bb42740e91a4cbd260b1d09" + "sha256:983d84b5e3861cb0aa240cad07f12899bb10b62328aae188b9007e04ce37d665", + "sha256:e1eac8ef28de69a554d5fcdb60b172d526d61924b1a40afbbb08df459a36006b" ], "index": "pypi", - "version": "==0.7.1" + "version": "==0.7.2" }, "deepdiff": { "hashes": [ @@ -374,8 +374,7 @@ }, "pycparser": { "hashes": [ - "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3", - "sha256:d51e69d7e2bda15beda04993a07d49598a09de7651375270ca60e234d10b7343" + "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" ], "version": "==2.19" }, @@ -635,6 +634,43 @@ ], "version": "==7.0" }, + "coverage": { + "hashes": [ + "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", + "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", + "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", + "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", + "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", + "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", + "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", + "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", + "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", + "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", + "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", + "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", + "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", + "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", + "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", + "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", + "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", + "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", + "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", + "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", + "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", + "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", + "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", + "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", + "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", + "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", + "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", + "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", + "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", + "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", + "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", + "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025" + ], + "version": "==4.5.4" + }, "dodgy": { "hashes": [ "sha256:65e13cf878d7aff129f1461c13cb5fd1bb6dfe66bb5327e09379c3877763280c" @@ -719,11 +755,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:652234b6ab8f2506ae58e528b6fbcc668831d3cc758e1bc01ef438d328b68cdb", - "sha256:6f264986fb88042bc1f0535fa9a557e6a376cfe5679dc77caac7fe8b5d43d05f" + "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", + "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" ], "markers": "python_version < '3.8'", - "version": "==0.22" + "version": "==0.23" }, "mccabe": { "hashes": [ @@ -804,6 +840,14 @@ "index": "pypi", "version": "==5.1.2" }, + "pytest-cov": { + "hashes": [ + "sha256:2b097cde81a302e1047331b48cadacf23577e431b61e9c6f49a1170bbe3d3da6", + "sha256:e00ea4fdde970725482f1f35630d12f074e121a23801aabf2ae154ec6bdd343a" + ], + "index": "pypi", + "version": "==2.7.1" + }, "pyyaml": { "hashes": [ "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", -- cgit v1.2.3 From f23cc37bbd4e44c6bf5fa1e25218b2030a41ab46 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Tue, 17 Sep 2019 23:17:35 +1000 Subject: Remove duplicate coverage.xml gitignore --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 09ca151fb..cda3aeb9f 100644 --- a/.gitignore +++ b/.gitignore @@ -117,6 +117,3 @@ config.yml # JUnit XML reports from pytest junit.xml - -# Coverage XML artifacts -coverage.xml -- cgit v1.2.3 From 0d27d5d0edf17fec789b752700e9e4a753f45df0 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 16:48:10 +0200 Subject: Validate configuration against typehints. --- azure-pipelines.yml | 2 +- bot/constants.py | 7 +------ config-default.yml | 6 ------ tests/test_constants.py | 23 +++++++++++++++++++++++ 4 files changed, 25 insertions(+), 13 deletions(-) create mode 100644 tests/test_constants.py diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 242513ab4..4dcad685c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -38,7 +38,7 @@ jobs: - script: python -m flake8 displayName: 'Run linter' - - script: BOT_TOKEN=foobar python -m pytest --junitxml=junit.xml --cov=bot --cov-branch --cov-report=term --cov-report=xml tests + - script: BOT_API_KEY=foo BOT_TOKEN=bar WOLFRAM_API_KEY=baz python -m pytest --junitxml=junit.xml --cov=bot --cov-branch --cov-report=term --cov-report=xml tests displayName: Run tests - task: PublishCodeCoverageResults@1 diff --git a/bot/constants.py b/bot/constants.py index d5b73bd1d..e1c47889c 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -347,9 +347,9 @@ class Channels(metaclass=YAMLGetter): message_log: int mod_alerts: int modlog: int + off_topic_0: int off_topic_1: int off_topic_2: int - off_topic_3: int python: int reddit: int talent_pool: int @@ -394,8 +394,6 @@ class Guild(metaclass=YAMLGetter): class Keys(metaclass=YAMLGetter): section = "keys" - deploy_bot: str - deploy_site: str site_api: str @@ -411,14 +409,11 @@ class URLs(metaclass=YAMLGetter): # Misc endpoints bot_avatar: str - deploy: str github_bot_repo: str - status: str # Site endpoints site: str site_api: str - site_clean_api: str site_superstarify_api: str site_logs_api: str site_logs_view: str diff --git a/config-default.yml b/config-default.yml index fd83e69a4..403de21ad 100644 --- a/config-default.yml +++ b/config-default.yml @@ -227,8 +227,6 @@ filter: keys: - deploy_bot: !ENV "DEPLOY_BOT_KEY" - deploy_site: !ENV "DEPLOY_SITE" site_api: !ENV "BOT_API_KEY" @@ -263,10 +261,6 @@ urls: # Snekbox snekbox_eval_api: "https://snekbox.pythondiscord.com/eval" - # Env vars - deploy: !ENV "DEPLOY_URL" - status: !ENV "STATUS_URL" - # Discord API URLs discord_api: &DISCORD_API "https://discordapp.com/api/v7/" discord_invite_api: !JOIN [*DISCORD_API, "invites"] diff --git a/tests/test_constants.py b/tests/test_constants.py new file mode 100644 index 000000000..e4a29d994 --- /dev/null +++ b/tests/test_constants.py @@ -0,0 +1,23 @@ +import inspect + +import pytest + +from bot import constants + + +@pytest.mark.parametrize( + 'section', + ( + cls + for (name, cls) in inspect.getmembers(constants) + if hasattr(cls, 'section') and isinstance(cls, type) + ) +) +def test_section_configuration_matches_typespec(section): + for (name, annotation) in section.__annotations__.items(): + value = getattr(section, name) + + if getattr(annotation, '_name', None) in ('Dict', 'List'): + pytest.skip("Cannot validate containers yet") + + assert isinstance(value, annotation) -- cgit v1.2.3 From ad711f04a789811c1aade6b49639474c592c044c Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 16:04:54 +0200 Subject: Add basic tests for `bot.api`. --- tests/helpers.py | 22 +++++++++++- tests/test_api.py | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 tests/test_api.py diff --git a/tests/helpers.py b/tests/helpers.py index 57c6fcc1a..f8fbb5e60 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,10 +1,30 @@ +import asyncio +import functools + from unittest.mock import MagicMock -__all__ = ('AsyncMock',) +__all__ = ('AsyncMock', 'async_test') # TODO: Remove me on 3.8 class AsyncMock(MagicMock): async def __call__(self, *args, **kwargs): return super(AsyncMock, self).__call__(*args, **kwargs) + + +def async_test(wrapped): + """ + Run a test case via asyncio. + + Example: + + >>> @async_test + ... async def lemon_wins(): + ... assert True + """ + + @functools.wraps(wrapped) + def wrapper(*args, **kwargs): + return asyncio.run(wrapped(*args, **kwargs)) + return wrapper diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 000000000..ce69ef187 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,106 @@ +import logging +from unittest.mock import MagicMock, patch + +import pytest + +from bot import api +from tests.helpers import async_test + + +def test_loop_is_not_running_by_default(): + assert not api.loop_is_running() + + +@async_test +async def test_loop_is_running_in_async_test(): + assert api.loop_is_running() + + +@pytest.fixture() +def error_api_response(): + response = MagicMock() + response.status = 999 + return response + + +@pytest.fixture() +def api_log_handler(): + return api.APILoggingHandler(None) + + +@pytest.fixture() +def debug_log_record(): + return logging.LogRecord( + name='my.logger', level=logging.DEBUG, + pathname='my/logger.py', lineno=666, + msg="Lemon wins", args=(), + exc_info=None + ) + + +def test_response_code_error_default_initialization(error_api_response): + error = api.ResponseCodeError(response=error_api_response) + assert error.status is error_api_response.status + assert not error.response_json + assert not error.response_text + assert error.response is error_api_response + + +def test_response_code_error_default_representation(error_api_response): + error = api.ResponseCodeError(response=error_api_response) + assert str(error) == f"Status: {error_api_response.status} Response: " + + +def test_response_code_error_representation_with_nonempty_response_json(error_api_response): + error = api.ResponseCodeError( + response=error_api_response, + response_json={'hello': 'world'} + ) + assert str(error) == f"Status: {error_api_response.status} Response: {{'hello': 'world'}}" + + +def test_response_code_error_representation_with_nonempty_response_text(error_api_response): + error = api.ResponseCodeError( + response=error_api_response, + response_text='Lemon will eat your soul' + ) + assert str(error) == f"Status: {error_api_response.status} Response: Lemon will eat your soul" + + +@patch('bot.api.APILoggingHandler.ship_off') +def test_emit_appends_to_queue_with_stopped_event_loop( + ship_off_patch, api_log_handler, debug_log_record +): + # This is a coroutine so returns something we should await, + # but asyncio complains about that. To ease testing, we patch + # `ship_off` to just return a regular value instead. + ship_off_patch.return_value = 42 + api_log_handler.emit(debug_log_record) + + assert api_log_handler.queue == [42] + + +def test_emit_ignores_less_than_debug(debug_log_record, api_log_handler): + debug_log_record.levelno = logging.DEBUG - 5 + api_log_handler.emit(debug_log_record) + assert not api_log_handler.queue + + +def test_schedule_queued_tasks_for_empty_queue(api_log_handler, caplog): + api_log_handler.schedule_queued_tasks() + # Logs when tasks are scheduled + assert not caplog.records + + +@patch('asyncio.create_task') +def test_schedule_queued_tasks_for_nonempty_queue(create_task_patch, api_log_handler, caplog): + api_log_handler.queue = [555] + api_log_handler.schedule_queued_tasks() + assert not api_log_handler.queue + create_task_patch.assert_called_once_with(555) + + [record] = caplog.records + assert record.message == "Scheduled 1 pending logging tasks." + assert record.levelno == logging.DEBUG + assert record.name == 'bot.api' + assert record.__dict__['via_handler'] -- cgit v1.2.3 From 3ab9c2f8d26e023dc56541c00073deaa39293592 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Wed, 18 Sep 2019 01:29:42 +1000 Subject: Recombine import groups. --- tests/helpers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/helpers.py b/tests/helpers.py index f8fbb5e60..2908294f7 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,6 +1,5 @@ import asyncio import functools - from unittest.mock import MagicMock -- cgit v1.2.3 From cfb6b634f8330f29576a3e7afc9b1d199c7651bf Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Tue, 17 Sep 2019 18:33:12 +0200 Subject: Enhance off-topic names search feature https://github.com/python-discord/bot/issues/435 This commit is meant to enhance the search feature in three separate, but related ways: 1. By changing the type annotation of the query to OffTopicName, we will use the same character translation table for the query as we did when storing the off-topic name, leading to better matches. 2. By adding a membership test, `query in name`, we are better able to search for off-topic names using a substring. 3. Given point 1 and point 2, we can increase the cut-off value we use for `difflib.get_close_matches` so we reduce the number of matches that bear little resemblance to the query in our human eyes. This commit closes #435 --- bot/cogs/off_topic_names.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bot/cogs/off_topic_names.py b/bot/cogs/off_topic_names.py index cb8a03374..1f6ed80b5 100644 --- a/bot/cogs/off_topic_names.py +++ b/bot/cogs/off_topic_names.py @@ -144,20 +144,21 @@ class OffTopicNames(Cog): @otname_group.command(name='search', aliases=('s',)) @with_role(*MODERATION_ROLES) - async def search_command(self, ctx, *, query: str): + async def search_command(self, ctx, *, query: OffTopicName): """ Search for an off-topic name. """ result = await self.bot.api_client.get('bot/off-topic-channel-names') - matches = difflib.get_close_matches(query, result, n=10, cutoff=0.35) - lines = sorted(f"• {name}" for name in matches) + in_matches = {name for name in result if query in name} + close_matches = difflib.get_close_matches(query, result, n=10, cutoff=0.70) + lines = sorted(f"• {name}" for name in in_matches.union(close_matches)) embed = Embed( title=f"Query results", colour=Colour.blue() ) - if matches: + if lines: await LinePaginator.paginate(lines, ctx, embed, max_size=400, empty=False) else: embed.description = "Nothing found." -- cgit v1.2.3 From 6815bca50816678f6069516ef564ecb4a4d01d83 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Wed, 18 Sep 2019 08:55:56 +0200 Subject: Escape markdown in codeblock suggestion embed https://github.com/python-discord/bot/issues/434 If the content we prepare for the codeblock suggestion embed defined in the `bot` cog contains markdown characters (e.g., `__`, `**`), this will cause Discord to apply markdown, since both the codeblock syntax example as well as the codeblock result example will contain the characters, making it a matched formatting pair. This will hide those characters, which often have a function in the code, and break the example of a formatted codeblock. To deal with that, I've added a regex substitution that substitutes markdown characters by an escaped version of that markdown character. Example: `'__hello__'` will become `'\_\_hello\_\_'` I've added this substitution to both paths in the `on_message` event listener, since we can't substitute at an earlier place due to it generating `SyntaxErrors` with the `ast.parse` check. This closes #434 --- bot/cogs/bot.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/bot/cogs/bot.py b/bot/cogs/bot.py index e88b1d9b5..577865a65 100644 --- a/bot/cogs/bot.py +++ b/bot/cogs/bot.py @@ -6,15 +6,14 @@ import time from discord import Embed, Message, RawMessageUpdateEvent from discord.ext.commands import Bot, Cog, Context, command, group -from bot.constants import ( - Channels, Guild, MODERATION_ROLES, - Roles, URLs, -) +from bot.constants import Channels, DEBUG_MODE, Guild, MODERATION_ROLES, Roles, URLs from bot.decorators import with_role from bot.utils.messages import wait_for_deletion log = logging.getLogger(__name__) +RE_MARKDOWN = re.compile(r'([*_~`|>])') + class Bot(Cog): """ @@ -255,7 +254,7 @@ class Bot(Cog): if parse_codeblock: on_cooldown = (time.time() - self.channel_cooldowns.get(msg.channel.id, 0)) < 300 - if not on_cooldown: + if not on_cooldown or DEBUG_MODE: try: if self.has_bad_ticks(msg): ticks = msg.content[:3] @@ -280,13 +279,14 @@ class Bot(Cog): current_length += len(line) lines_walked += 1 content = content[:current_length] + "#..." - + content_escaped_markdown = RE_MARKDOWN.sub(r'\\\1', content) howto = ( "It looks like you are trying to paste code into this channel.\n\n" "You seem to be using the wrong symbols to indicate where the codeblock should start. " f"The correct symbols would be \\`\\`\\`, not `{ticks}`.\n\n" "**Here is an example of how it should look:**\n" - f"\\`\\`\\`python\n{content}\n\\`\\`\\`\n\n**This will result in the following:**\n" + f"\\`\\`\\`python\n{content_escaped_markdown}\n\\`\\`\\`\n\n" + "**This will result in the following:**\n" f"```python\n{content}\n```" ) @@ -322,13 +322,15 @@ class Bot(Cog): lines_walked += 1 content = content[:current_length] + "#..." + content_escaped_markdown = RE_MARKDOWN.sub(r'\\\1', content) howto += ( "It looks like you're trying to paste code into this channel.\n\n" "Discord has support for Markdown, which allows you to post code with full " "syntax highlighting. Please use these whenever you paste code, as this " "helps improve the legibility and makes it easier for us to help you.\n\n" f"**To do this, use the following method:**\n" - f"\\`\\`\\`python\n{content}\n\\`\\`\\`\n\n**This will result in the following:**\n" + f"\\`\\`\\`python\n{content_escaped_markdown}\n\\`\\`\\`\n\n" + "**This will result in the following:**\n" f"```python\n{content}\n```" ) -- cgit v1.2.3 From 17d6d80a78b8ab66d2f82060cb8334ccd8c62bca Mon Sep 17 00:00:00 2001 From: Daniel Brown Date: Wed, 18 Sep 2019 10:54:49 -0500 Subject: Infraction Date Humanization - Changed the date returned on infraction searches to use the `"%c"` strftime format instead of the standard ISO format. Signed-off-by: Daniel Brown --- bot/cogs/moderation.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index c631dd69d..898f053f5 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -1260,6 +1260,11 @@ class Moderation(Scheduler, Cog): active = infraction_object["active"] user_id = infraction_object["user"] hidden = infraction_object["hidden"] + created = datetime.fromisoformat(infraction_object["inserted_at"].strftime("%c")) + if not infraction_object["expires_at"]: + expires = "*Permanent*" + else: + expires = datetime.fromisoformat(infraction_object["expires_at"].strftime("%c")) lines = textwrap.dedent(f""" {"**===============**" if active else "==============="} @@ -1268,8 +1273,8 @@ class Moderation(Scheduler, Cog): Type: **{infraction_object["type"]}** Shadow: {hidden} Reason: {infraction_object["reason"] or "*None*"} - Created: {infraction_object["inserted_at"]} - Expires: {infraction_object["expires_at"] or "*Permanent*"} + Created: {created} + Expires: {expires} Actor: {actor.mention if actor else actor_id} ID: `{infraction_object["id"]}` {"**===============**" if active else "==============="} -- cgit v1.2.3 From f846416be2cd2fe05d0689f11bb30d3a67b4a571 Mon Sep 17 00:00:00 2001 From: Daniel Brown Date: Wed, 18 Sep 2019 15:02:51 -0500 Subject: Infraction Date Humanization - Changed to use the format `"%Y-%m-%d %H:%M"`, which will turn out looking like `2019-09-18 13:59` Signed-off-by: Daniel Brown --- bot/cogs/moderation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 898f053f5..f16a13a3e 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -1260,11 +1260,11 @@ class Moderation(Scheduler, Cog): active = infraction_object["active"] user_id = infraction_object["user"] hidden = infraction_object["hidden"] - created = datetime.fromisoformat(infraction_object["inserted_at"].strftime("%c")) + created = datetime.fromisoformat(infraction_object["inserted_at"].strftime("%Y-%m-%d %H:%M")) if not infraction_object["expires_at"]: expires = "*Permanent*" else: - expires = datetime.fromisoformat(infraction_object["expires_at"].strftime("%c")) + expires = datetime.fromisoformat(infraction_object["expires_at"].strftime("%Y-%m-%d %H:%M")) lines = textwrap.dedent(f""" {"**===============**" if active else "==============="} -- cgit v1.2.3 From 58f7e94746de7394cef7d7b5a193d43740fbe49c Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 22:10:09 +0200 Subject: Add basic tests for `bot.cogs.information`. --- tests/cogs/test_information.py | 163 +++++++++++++++++++++++++++++++++++++++++ tests/conftest.py | 32 ++++++++ 2 files changed, 195 insertions(+) create mode 100644 tests/cogs/test_information.py create mode 100644 tests/conftest.py diff --git a/tests/cogs/test_information.py b/tests/cogs/test_information.py new file mode 100644 index 000000000..85b2d092e --- /dev/null +++ b/tests/cogs/test_information.py @@ -0,0 +1,163 @@ +import asyncio +import logging +import textwrap +from datetime import datetime +from unittest.mock import MagicMock, patch + +import pytest +from discord import ( + CategoryChannel, + Colour, + TextChannel, + VoiceChannel, +) + +from bot.cogs import information +from bot.constants import Emojis +from bot.decorators import InChannelCheckFailure +from tests.helpers import AsyncMock + + +@pytest.fixture() +def cog(simple_bot): + return information.Information(simple_bot) + + +def role(name: str, id_: int): + r = MagicMock() + r.name = name + r.id = id_ + r.mention = f'&{name}' + return r + + +def member(status: str): + m = MagicMock() + m.status = status + return m + + +@pytest.fixture() +def ctx(moderator_role, simple_ctx): + simple_ctx.author.roles = [moderator_role] + simple_ctx.guild.created_at = datetime(2001, 1, 1) + simple_ctx.send = AsyncMock() + return simple_ctx + + +def test_roles_info_command(cog, ctx): + everyone_role = MagicMock() + everyone_role.name = '@everyone' # should be excluded in the output + ctx.author.roles.append(everyone_role) + ctx.guild.roles = ctx.author.roles + + cog.roles_info.can_run = AsyncMock() + cog.roles_info.can_run.return_value = True + + coroutine = cog.roles_info.callback(cog, ctx) + + assert asyncio.run(coroutine) is None # no rval + ctx.send.assert_called_once() + _, kwargs = ctx.send.call_args + embed = kwargs.pop('embed') + assert embed.title == "Role information" + assert embed.colour == Colour.blurple() + assert embed.description == f"`{ctx.guild.roles[0].id}` - {ctx.guild.roles[0].mention}\n" + assert embed.footer.text == "Total roles: 1" + + +# There is no argument passed in here that we can use to test, +# so the return value would change constantly. +@patch('bot.cogs.information.time_since') +def test_server_info_command(time_since_patch, cog, ctx, moderator_role): + time_since_patch.return_value = '2 days ago' + + ctx.guild.created_at = datetime(2001, 1, 1) + ctx.guild.features = ('lemons', 'apples') + ctx.guild.region = 'The Moon' + ctx.guild.roles = [moderator_role] + ctx.guild.channels = [ + TextChannel( + state={}, + guild=ctx.guild, + data={'id': 42, 'name': 'lemons-offering', 'position': 22, 'type': 'text'} + ), + CategoryChannel( + state={}, + guild=ctx.guild, + data={'id': 5125, 'name': 'the-lemon-collection', 'position': 22, 'type': 'category'} + ), + VoiceChannel( + state={}, + guild=ctx.guild, + data={'id': 15290, 'name': 'listen-to-lemon', 'position': 22, 'type': 'voice'} + ) + ] + ctx.guild.members = [ + member('online'), member('online'), + member('idle'), + member('dnd'), member('dnd'), member('dnd'), member('dnd'), + member('offline'), member('offline'), member('offline') + ] + ctx.guild.member_count = 1_234 + ctx.guild.icon_url = 'a-lemon.png' + + coroutine = cog.server_info.callback(cog, ctx) + assert asyncio.run(coroutine) is None # no rval + + time_since_patch.assert_called_once_with(ctx.guild.created_at, precision='days') + _, kwargs = ctx.send.call_args + embed = kwargs.pop('embed') + assert embed.colour == Colour.blurple() + assert embed.description == textwrap.dedent(f""" + **Server information** + Created: {time_since_patch.return_value} + Voice region: {ctx.guild.region} + Features: {', '.join(ctx.guild.features)} + + **Counts** + Members: {ctx.guild.member_count:,} + Roles: {len(ctx.guild.roles)} + Text: 1 + Voice: 1 + Channel categories: 1 + + **Members** + {Emojis.status_online} 2 + {Emojis.status_idle} 1 + {Emojis.status_dnd} 4 + {Emojis.status_offline} 3 + """) + assert embed.thumbnail.url == 'a-lemon.png' + + +def test_user_info_on_other_users_from_non_moderator(ctx, cog): + ctx.author = MagicMock() + ctx.author.__eq__.return_value = False + ctx.author.roles = [] + coroutine = cog.user_info.callback(cog, ctx, user='scragly') # skip checks, pass args + + assert asyncio.run(coroutine) is None # no rval + ctx.send.assert_called_once_with( + "You may not use this command on users other than yourself." + ) + + +def test_user_info_in_wrong_channel_from_non_moderator(ctx, cog): + ctx.author = MagicMock() + ctx.author.__eq__.return_value = False + ctx.author.roles = [] + + coroutine = cog.user_info.callback(cog, ctx) + message = 'Sorry, but you may only use this command within <#267659945086812160>.' + with pytest.raises(InChannelCheckFailure, match=message): + assert asyncio.run(coroutine) is None # no rval + + +def test_setup(simple_bot, caplog): + information.setup(simple_bot) + simple_bot.add_cog.assert_called_once() + [record] = caplog.records + + assert record.message == "Cog loaded: Information" + assert record.levelno == logging.INFO diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..d3de4484d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,32 @@ +from unittest.mock import MagicMock + +import pytest + +from bot.constants import Roles +from tests.helpers import AsyncMock + + +@pytest.fixture() +def moderator_role(): + mock = MagicMock() + mock.id = Roles.moderator + mock.name = 'Moderator' + mock.mention = f'&{mock.name}' + return mock + + +@pytest.fixture() +def simple_bot(): + mock = MagicMock() + mock._before_invoke = AsyncMock() + mock._after_invoke = AsyncMock() + mock.can_run = AsyncMock() + mock.can_run.return_value = True + return mock + + +@pytest.fixture() +def simple_ctx(simple_bot): + mock = MagicMock() + mock.bot = simple_bot + return mock -- cgit v1.2.3 From cb2d892c37f68aa72b3078905d26030ea50368f2 Mon Sep 17 00:00:00 2001 From: Daniel Brown Date: Wed, 18 Sep 2019 15:30:46 -0500 Subject: Infraction Date Humanization - Changed the if statement to use `is None` for code clarity. Signed-off-by: Daniel Brown --- bot/cogs/moderation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index f16a13a3e..2c7253c7b 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -1261,7 +1261,7 @@ class Moderation(Scheduler, Cog): user_id = infraction_object["user"] hidden = infraction_object["hidden"] created = datetime.fromisoformat(infraction_object["inserted_at"].strftime("%Y-%m-%d %H:%M")) - if not infraction_object["expires_at"]: + if infraction_object["expires_at"] is None: expires = "*Permanent*" else: expires = datetime.fromisoformat(infraction_object["expires_at"].strftime("%Y-%m-%d %H:%M")) -- cgit v1.2.3 From 5640a23b4b2af23a4a767d506105730ca06f5d0b Mon Sep 17 00:00:00 2001 From: Daniel Brown Date: Wed, 18 Sep 2019 16:01:03 -0500 Subject: Infraction Date Humanization - Corrected an error that would have made the code bug out. Moved a closing parentheses to the correct spot. Signed-off-by: Daniel Brown --- bot/cogs/moderation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 2c7253c7b..2d6c40a46 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -1260,11 +1260,11 @@ class Moderation(Scheduler, Cog): active = infraction_object["active"] user_id = infraction_object["user"] hidden = infraction_object["hidden"] - created = datetime.fromisoformat(infraction_object["inserted_at"].strftime("%Y-%m-%d %H:%M")) + created = datetime.fromisoformat(infraction_object["inserted_at"]).strftime("%Y-%m-%d %H:%M") if infraction_object["expires_at"] is None: expires = "*Permanent*" else: - expires = datetime.fromisoformat(infraction_object["expires_at"].strftime("%Y-%m-%d %H:%M")) + expires = datetime.fromisoformat(infraction_object["expires_at"]).strftime("%Y-%m-%d %H:%M") lines = textwrap.dedent(f""" {"**===============**" if active else "==============="} -- cgit v1.2.3 From 7f70a4eb064458b56892c046a34cce598a2053e2 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 17:18:00 +0200 Subject: Add tests for `bot.rules.attachments`. This also fixes an issue with the `attachments` rule not respecting the most recent message sent by a user. --- bot/rules/attachments.py | 4 ++-- tests/rules/__init__.py | 0 tests/rules/test_attachments.py | 52 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 tests/rules/__init__.py create mode 100644 tests/rules/test_attachments.py diff --git a/bot/rules/attachments.py b/bot/rules/attachments.py index 47b927101..80a15d440 100644 --- a/bot/rules/attachments.py +++ b/bot/rules/attachments.py @@ -11,14 +11,14 @@ async def apply( config: Dict[str, int] ) -> Optional[Tuple[str, Iterable[Member], Iterable[Message]]]: - relevant_messages = tuple( + relevant_messages = [last_message] + [ msg for msg in recent_messages if ( msg.author == last_message.author and len(msg.attachments) > 0 ) - ) + ] total_recent_attachments = sum(len(msg.attachments) for msg in relevant_messages) if total_recent_attachments > config['max']: diff --git a/tests/rules/__init__.py b/tests/rules/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/rules/test_attachments.py b/tests/rules/test_attachments.py new file mode 100644 index 000000000..6f025b3cb --- /dev/null +++ b/tests/rules/test_attachments.py @@ -0,0 +1,52 @@ +import asyncio +from dataclasses import dataclass +from typing import Any, List + +import pytest + +from bot.rules import attachments + + +# Using `MagicMock` sadly doesn't work for this usecase +# since it's __eq__ compares the MagicMock's ID. We just +# want to compare the actual attributes we set. +@dataclass +class FakeMessage: + author: str + attachments: List[Any] + + +def msg(total_attachments: int): + return FakeMessage(author='lemon', attachments=list(range(total_attachments))) + + +@pytest.mark.parametrize( + 'messages', + ( + (msg(0), msg(0), msg(0)), + (msg(2), msg(2)), + (msg(0),), + ) +) +def test_allows_messages_without_too_many_attachments(messages): + last_message, *recent_messages = messages + coro = attachments.apply(last_message, recent_messages, {'max': 5}) + assert asyncio.run(coro) is None + + +@pytest.mark.parametrize( + ('messages', 'relevant_messages', 'total'), + ( + ((msg(4), msg(0), msg(6)), [msg(4), msg(6)], 10), + ((msg(6),), [msg(6)], 6), + ((msg(1),) * 6, [msg(1)] * 6, 6), + ) +) +def test_disallows_messages_with_too_many_attachments(messages, relevant_messages, total): + last_message, *recent_messages = messages + coro = attachments.apply(last_message, recent_messages, {'max': 5}) + assert asyncio.run(coro) == ( + f"sent {total} attachments in 5s", + ('lemon',), + relevant_messages + ) -- cgit v1.2.3 From e70c96248bd7b548412811a4f1ffe88bed41f815 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 19 Sep 2019 12:37:47 +0200 Subject: Fix date formatting bug in infraction search The infraction search feature did not work because of a small bug with the date formatting: `datetime.fromisoformat` does not like the Z at the end of the datestring the database sends back, so we need to chop it off. I've applied the same method for doing that as already in use in other parts of the bot codebase. --- bot/cogs/moderation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 2d6c40a46..fea86c33e 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -1260,11 +1260,11 @@ class Moderation(Scheduler, Cog): active = infraction_object["active"] user_id = infraction_object["user"] hidden = infraction_object["hidden"] - created = datetime.fromisoformat(infraction_object["inserted_at"]).strftime("%Y-%m-%d %H:%M") + created = datetime.fromisoformat(infraction_object["inserted_at"][:-1]).strftime("%Y-%m-%d %H:%M") if infraction_object["expires_at"] is None: expires = "*Permanent*" else: - expires = datetime.fromisoformat(infraction_object["expires_at"]).strftime("%Y-%m-%d %H:%M") + expires = datetime.fromisoformat(infraction_object["expires_at"][:-1]).strftime("%Y-%m-%d %H:%M") lines = textwrap.dedent(f""" {"**===============**" if active else "==============="} -- cgit v1.2.3