diff options
31 files changed, 288 insertions, 98 deletions
diff --git a/bot/converters.py b/bot/converters.py index 4d019691e..4a4d3b544 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -273,7 +273,7 @@ class Snowflake(IDConverter):          snowflake = int(arg)          try: -            time = snowflake_time(snowflake) +            time = snowflake_time(snowflake).replace(tzinfo=None)          except (OverflowError, OSError) as e:              # Not sure if this can ever even happen, but let's be safe.              raise BadArgument(f"{error}: {e}") diff --git a/bot/exts/filters/antispam.py b/bot/exts/filters/antispam.py index 78ad57b48..37ac70508 100644 --- a/bot/exts/filters/antispam.py +++ b/bot/exts/filters/antispam.py @@ -106,7 +106,7 @@ class DeletionContext:              colour=Colour(Colours.soft_red),              title="Spam detected!",              text=mod_alert_message, -            thumbnail=first_message.author.avatar_url_as(static_format="png"), +            thumbnail=first_message.author.display_avatar.url,              channel_id=Channels.mod_alerts,              ping_everyone=AntiSpamConfig.ping_everyone          ) @@ -178,7 +178,9 @@ class AntiSpam(Cog):          self.cache.append(message)          earliest_relevant_at = datetime.utcnow() - timedelta(seconds=self.max_interval) -        relevant_messages = list(takewhile(lambda msg: msg.created_at > earliest_relevant_at, self.cache)) +        relevant_messages = list( +            takewhile(lambda msg: msg.created_at.replace(tzinfo=None) > earliest_relevant_at, self.cache) +        )          for rule_name in AntiSpamConfig.rules:              rule_config = AntiSpamConfig.rules[rule_name] @@ -187,7 +189,9 @@ class AntiSpam(Cog):              # Create a list of messages that were sent in the interval that the rule cares about.              latest_interesting_stamp = datetime.utcnow() - timedelta(seconds=rule_config['interval'])              messages_for_rule = list( -                takewhile(lambda msg: msg.created_at > latest_interesting_stamp, relevant_messages) +                takewhile( +                    lambda msg: msg.created_at.replace(tzinfo=None) > latest_interesting_stamp, relevant_messages +                )              )              result = await rule_function(message, messages_for_rule, rule_config) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 78b7a8d94..7faf063b9 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -223,7 +223,7 @@ class Filtering(Cog):                  title="Username filtering alert",                  text=log_string,                  channel_id=Channels.mod_alerts, -                thumbnail=member.avatar_url +                thumbnail=member.display_avatar.url              )              # Update time when alert sent @@ -383,7 +383,7 @@ class Filtering(Cog):              colour=Colour(Colours.soft_red),              title=f"{_filter['type'].title()} triggered!",              text=message, -            thumbnail=msg.author.avatar_url_as(static_format="png"), +            thumbnail=msg.author.display_avatar.url,              channel_id=Channels.mod_alerts,              ping_everyone=ping_everyone,              additional_embeds=stats.additional_embeds, diff --git a/bot/exts/filters/token_remover.py b/bot/exts/filters/token_remover.py index f68d4b987..520283ba3 100644 --- a/bot/exts/filters/token_remover.py +++ b/bot/exts/filters/token_remover.py @@ -109,7 +109,7 @@ class TokenRemover(Cog):              colour=Colour(Colours.soft_red),              title="Token removed!",              text=log_message + "\n" + userid_message, -            thumbnail=msg.author.avatar_url_as(static_format="png"), +            thumbnail=msg.author.display_avatar.url,              channel_id=Channels.mod_alerts,              ping_everyone=mention_everyone,          ) diff --git a/bot/exts/filters/webhook_remover.py b/bot/exts/filters/webhook_remover.py index 40cb4e141..96334317c 100644 --- a/bot/exts/filters/webhook_remover.py +++ b/bot/exts/filters/webhook_remover.py @@ -63,7 +63,7 @@ class WebhookRemover(Cog):              colour=Colour(Colours.soft_red),              title="Discord webhook URL removed!",              text=message, -            thumbnail=msg.author.avatar_url_as(static_format="png"), +            thumbnail=msg.author.display_avatar.url,              channel_id=Channels.mod_alerts          ) diff --git a/bot/exts/fun/duck_pond.py b/bot/exts/fun/duck_pond.py index 2b5592530..c51656343 100644 --- a/bot/exts/fun/duck_pond.py +++ b/bot/exts/fun/duck_pond.py @@ -94,7 +94,7 @@ class DuckPond(Cog):                  webhook=self.webhook,                  content=message.clean_content,                  username=message.author.display_name, -                avatar_url=message.author.avatar_url +                avatar_url=message.author.display_avatar.url              )          if message.attachments: @@ -109,7 +109,7 @@ class DuckPond(Cog):                      webhook=self.webhook,                      embed=e,                      username=message.author.display_name, -                    avatar_url=message.author.avatar_url +                    avatar_url=message.author.display_avatar.url                  )              except discord.HTTPException:                  log.exception("Failed to send an attachment to the webhook") diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index f27483af8..1b3e28e79 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -315,7 +315,7 @@ class Information(Cog):          for field_name, field_content in fields:              embed.add_field(name=field_name, value=field_content, inline=False) -        embed.set_thumbnail(url=user.avatar_url_as(static_format="png")) +        embed.set_thumbnail(url=user.display_avatar.url)          embed.colour = user.colour if user.colour != Colour.default() else Colour.blurple()          return embed diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 56051d0e5..80ba10112 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -111,7 +111,7 @@ class Defcon(Cog):          if self.threshold:              now = datetime.utcnow() -            if now - member.created_at < relativedelta_to_timedelta(self.threshold): +            if now - member.created_at.replace(tzinfo=None) < relativedelta_to_timedelta(self.threshold):                  log.info(f"Rejecting user {member}: Account is too new")                  message_sent = False @@ -137,7 +137,7 @@ class Defcon(Cog):                  await self.mod_log.send_log_message(                      Icons.defcon_denied, Colours.soft_red, "Entry denied", -                    message, member.avatar_url_as(static_format="png") +                    message, member.display_avatar.url                  )      @group(name='defcon', aliases=('dc',), invoke_without_command=True) @@ -185,7 +185,12 @@ class Defcon(Cog):          role = ctx.guild.default_role          permissions = role.permissions -        permissions.update(send_messages=False, add_reactions=False, connect=False) +        permissions.update( +            send_messages=False, +            add_reactions=False, +            send_messages_in_threads=False, +            connect=False +        )          await role.edit(reason="DEFCON shutdown", permissions=permissions)          await ctx.send(f"{Action.SERVER_SHUTDOWN.value.emoji} Server shut down.") @@ -196,7 +201,12 @@ class Defcon(Cog):          role = ctx.guild.default_role          permissions = role.permissions -        permissions.update(send_messages=True, add_reactions=True, connect=True) +        permissions.update( +            send_messages=True, +            add_reactions=True, +            send_messages_in_threads=True, +            connect=True +        )          await role.edit(reason="DEFCON unshutdown", permissions=permissions)          await ctx.send(f"{Action.SERVER_OPEN.value.emoji} Server reopened.") diff --git a/bot/exts/moderation/incidents.py b/bot/exts/moderation/incidents.py index 4470b6dd6..097fa36f1 100644 --- a/bot/exts/moderation/incidents.py +++ b/bot/exts/moderation/incidents.py @@ -94,7 +94,7 @@ async def make_embed(incident: discord.Message, outcome: Signal, actioned_by: di          timestamp=datetime.utcnow(),          colour=colour,      ) -    embed.set_footer(text=footer, icon_url=actioned_by.avatar_url) +    embed.set_footer(text=footer, icon_url=actioned_by.display_avatar.url)      if incident.attachments:          attachment = incident.attachments[0]  # User-sent messages can only contain one attachment @@ -253,7 +253,7 @@ class Incidents(Cog):              await webhook.send(                  embed=embed,                  username=sub_clyde(incident.author.name), -                avatar_url=incident.author.avatar_url, +                avatar_url=incident.author.display_avatar.url,                  file=attachment_file,              )          except Exception: diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index fc915016c..d4e96b10b 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -253,7 +253,7 @@ class InfractionScheduler:              icon_url=icon,              colour=Colours.soft_red,              title=f"Infraction {log_title}: {' '.join(infr_type.split('_'))}", -            thumbnail=user.avatar_url_as(static_format="png"), +            thumbnail=user.display_avatar.url,              text=textwrap.dedent(f"""                  Member: {messages.format_user(user)}                  Actor: {ctx.author.mention}{dm_log_text}{expiry_log_text} @@ -347,7 +347,7 @@ class InfractionScheduler:              icon_url=_utils.INFRACTION_ICONS[infr_type][1],              colour=Colours.soft_green,              title=f"Infraction {log_title}: {' '.join(infr_type.split('_'))}", -            thumbnail=user.avatar_url_as(static_format="png"), +            thumbnail=user.display_avatar.url,              text="\n".join(f"{k}: {v}" for k, v in log_text.items()),              footer=footer,              content=log_content, @@ -464,7 +464,7 @@ class InfractionScheduler:              log_title = "expiration failed" if "Failure" in log_text else "expired"              user = self.bot.get_user(user_id) -            avatar = user.avatar_url_as(static_format="png") if user else None +            avatar = user.display_avatar.url if user else None              # Move reason to end so when reason is too long, this is not gonna cut out required items.              log_text["Reason"] = log_text.pop("Reason") diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index a50339ee2..b1c8b64dc 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -196,7 +196,7 @@ class ModManagement(commands.Cog):          if user:              user_text = messages.format_user(user) -            thumbnail = user.avatar_url_as(static_format="png") +            thumbnail = user.display_avatar.url          else:              user_text = f"<@{user_id}>"              thumbnail = None diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index fbb3684e7..7d80d4ba5 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -8,7 +8,7 @@ from itertools import zip_longest  import discord  from dateutil.relativedelta import relativedelta  from deepdiff import DeepDiff -from discord import Colour +from discord import Colour, Message, Thread  from discord.abc import GuildChannel  from discord.ext.commands import Cog, Context  from discord.utils import escape_markdown @@ -394,7 +394,7 @@ class ModLog(Cog, name="ModLog"):          await self.send_log_message(              Icons.user_ban, Colours.soft_red,              "User banned", format_user(member), -            thumbnail=member.avatar_url_as(static_format="png"), +            thumbnail=member.display_avatar.url,              channel_id=Channels.user_log          ) @@ -415,7 +415,7 @@ class ModLog(Cog, name="ModLog"):          await self.send_log_message(              Icons.sign_in, Colours.soft_green,              "User joined", message, -            thumbnail=member.avatar_url_as(static_format="png"), +            thumbnail=member.display_avatar.url,              channel_id=Channels.user_log          ) @@ -432,7 +432,7 @@ class ModLog(Cog, name="ModLog"):          await self.send_log_message(              Icons.sign_out, Colours.soft_red,              "User left", format_user(member), -            thumbnail=member.avatar_url_as(static_format="png"), +            thumbnail=member.display_avatar.url,              channel_id=Channels.user_log          ) @@ -449,7 +449,7 @@ class ModLog(Cog, name="ModLog"):          await self.send_log_message(              Icons.user_unban, Colour.blurple(),              "User unbanned", format_user(member), -            thumbnail=member.avatar_url_as(static_format="png"), +            thumbnail=member.display_avatar.url,              channel_id=Channels.mod_log          ) @@ -515,21 +515,39 @@ class ModLog(Cog, name="ModLog"):              colour=Colour.blurple(),              title="Member updated",              text=message, -            thumbnail=after.avatar_url_as(static_format="png"), +            thumbnail=after.display_avatar.url,              channel_id=Channels.user_log          ) +    def is_message_blacklisted(self, message: Message) -> bool: +        """Return true if the message is in a blacklisted thread or channel.""" +        # Ignore bots or DMs +        if message.author.bot or not message.guild: +            return True + +        return self.is_raw_message_blacklisted(message.guild.id, message.channel.id) + +    def is_raw_message_blacklisted(self, guild_id: t.Optional[int], channel_id: int) -> bool: +        """Return true if the message constructed from raw parameter is in a blacklisted thread or channel.""" +        # Ignore DMs or messages outside of the main guild +        if not guild_id or guild_id != GuildConstant.id: +            return True + +        channel = self.bot.get_channel(channel_id) + +        # Look at the parent channel of a thread +        if isinstance(channel, Thread): +            return channel.parent.id in GuildConstant.modlog_blacklist + +        return channel.id in GuildConstant.modlog_blacklist +      @Cog.listener()      async def on_message_delete(self, message: discord.Message) -> None:          """Log message delete event to message change log."""          channel = message.channel          author = message.author -        # Ignore DMs. -        if not message.guild: -            return - -        if message.guild.id != GuildConstant.id or channel.id in GuildConstant.modlog_blacklist: +        if self.is_message_blacklisted(message):              return          self._cached_deletes.append(message.id) @@ -584,7 +602,7 @@ class ModLog(Cog, name="ModLog"):      @Cog.listener()      async def on_raw_message_delete(self, event: discord.RawMessageDeleteEvent) -> None:          """Log raw message delete event to message change log.""" -        if event.guild_id != GuildConstant.id or event.channel_id in GuildConstant.modlog_blacklist: +        if self.is_raw_message_blacklisted(event.guild_id, event.channel_id):              return          await asyncio.sleep(1)  # Wait here in case the normal event was fired @@ -625,12 +643,7 @@ class ModLog(Cog, name="ModLog"):      @Cog.listener()      async def on_message_edit(self, msg_before: discord.Message, msg_after: discord.Message) -> None:          """Log message edit event to message change log.""" -        if ( -            not msg_before.guild -            or msg_before.guild.id != GuildConstant.id -            or msg_before.channel.id in GuildConstant.modlog_blacklist -            or msg_before.author.bot -        ): +        if self.is_message_blacklisted(msg_before):              return          self._cached_edits.append(msg_before.id) @@ -707,12 +720,7 @@ class ModLog(Cog, name="ModLog"):          except discord.NotFound:  # Was deleted before we got the event              return -        if ( -            not message.guild -            or message.guild.id != GuildConstant.id -            or message.channel.id in GuildConstant.modlog_blacklist -            or message.author.bot -        ): +        if self.is_message_blacklisted(message):              return          await asyncio.sleep(1)  # Wait here in case the normal event was fired @@ -752,6 +760,64 @@ class ModLog(Cog, name="ModLog"):          )      @Cog.listener() +    async def on_thread_update(self, before: Thread, after: Thread) -> None: +        """Log thread archiving, un-archiving and name edits.""" +        if before.name != after.name: +            await self.send_log_message( +                Icons.hash_blurple, +                Colour.blurple(), +                "Thread name edited", +                ( +                    f"Thread {after.mention} (`{after.id}`) from {after.parent.mention} (`{after.parent.id}`): " +                    f"`{before.name}` -> `{after.name}`" +                ) +            ) +            return + +        if not before.archived and after.archived: +            colour = Colour.red() +            action = "archived" +            icon = Icons.hash_red +        elif before.archived and not after.archived: +            colour = Colour.green() +            action = "un-archived" +            icon = Icons.hash_green +        else: +            return + +        await self.send_log_message( +            icon, +            colour, +            f"Thread {action}", +            f"Thread {after.mention} (`{after.id}`) from {after.parent.mention} (`{after.parent.id}`) was {action}" +        ) + +    @Cog.listener() +    async def on_thread_delete(self, thread: Thread) -> None: +        """Log thread deletion.""" +        await self.send_log_message( +            Icons.hash_red, +            Colour.red(), +            "Thread deleted", +            f"Thread {thread.mention} (`{thread.id}`) from {thread.parent.mention} (`{thread.parent.id}`) deleted" +        ) + +    @Cog.listener() +    async def on_thread_join(self, thread: Thread) -> None: +        """Log thread creation.""" +        # If we are in the thread already we can most probably assume we already logged it? +        # We don't really have a better way of doing this since the API doesn't make any difference between the two +        if thread.me: +            return + +        await self.send_log_message( +            Icons.hash_green, +            Colour.green(), +            "Thread created", +            f"Thread {thread.mention} (`{thread.id}`) from {thread.parent.mention} (`{thread.parent.id}`) created" +        ) + +    @Cog.listener()      async def on_voice_state_update(          self,          member: discord.Member, @@ -820,7 +886,7 @@ class ModLog(Cog, name="ModLog"):              colour=colour,              title="Voice state updated",              text=message, -            thumbnail=member.avatar_url_as(static_format="png"), +            thumbnail=member.display_avatar.url,              channel_id=Channels.voice_log          ) diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index 133ebaba5..511520252 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -5,9 +5,10 @@ from datetime import datetime, timedelta, timezone  from typing import Optional, OrderedDict, Union  from async_rediscache import RedisCache -from discord import Guild, PermissionOverwrite, TextChannel, VoiceChannel +from discord import Guild, PermissionOverwrite, TextChannel, Thread, VoiceChannel  from discord.ext import commands, tasks  from discord.ext.commands import Context +from discord.utils import MISSING  from bot import constants  from bot.bot import Bot @@ -48,7 +49,16 @@ class SilenceNotifier(tasks.Loop):      """Loop notifier for posting notices to `alert_channel` containing added channels."""      def __init__(self, alert_channel: TextChannel): -        super().__init__(self._notifier, seconds=1, minutes=0, hours=0, count=None, reconnect=True, loop=None) +        super().__init__( +            self._notifier, +            seconds=1, +            minutes=0, +            hours=0, +            count=None, +            reconnect=True, +            loop=None, +            time=MISSING +        )          self._silenced_channels = {}          self._alert_channel = alert_channel @@ -173,6 +183,12 @@ class Silence(commands.Cog):          channel_info = f"#{channel} ({channel.id})"          log.debug(f"{ctx.author} is silencing channel {channel_info}.") +        # Since threads don't have specific overrides, we cannot silence them individually. +        # The parent channel has to be muted or the thread should be archived. +        if isinstance(channel, Thread): +            await ctx.send(":x: Threads cannot be silenced.") +            return +          if not await self._set_silence_overwrites(channel, kick=kick):              log.info(f"Tried to silence channel {channel_info} but the channel was already silenced.")              await self.send_message(MSG_SILENCE_FAIL, ctx.channel, channel, alert_target=False) @@ -223,7 +239,13 @@ class Silence(commands.Cog):          if isinstance(channel, TextChannel):              role = self._everyone_role              overwrite = channel.overwrites_for(role) -            prev_overwrites = dict(send_messages=overwrite.send_messages, add_reactions=overwrite.add_reactions) +            prev_overwrites = dict( +                send_messages=overwrite.send_messages, +                add_reactions=overwrite.add_reactions, +                create_private_threads=overwrite.create_private_threads, +                create_public_threads=overwrite.create_public_threads, +                send_messages_in_threads=overwrite.send_messages_in_threads +            )          else:              role = self._verified_voice_role @@ -323,7 +345,15 @@ class Silence(commands.Cog):          # Check if old overwrites were not stored          if prev_overwrites is None:              log.info(f"Missing previous overwrites for #{channel} ({channel.id}); defaulting to None.") -            overwrite.update(send_messages=None, add_reactions=None, speak=None, connect=None) +            overwrite.update( +                send_messages=None, +                add_reactions=None, +                create_private_threads=None, +                create_public_threads=None, +                send_messages_in_threads=None, +                speak=None, +                connect=None +            )          else:              overwrite.update(**json.loads(prev_overwrites)) diff --git a/bot/exts/moderation/voice_gate.py b/bot/exts/moderation/voice_gate.py index 88733176f..8fdc7c76b 100644 --- a/bot/exts/moderation/voice_gate.py +++ b/bot/exts/moderation/voice_gate.py @@ -165,7 +165,10 @@ class VoiceGate(Cog):              return          checks = { -            "joined_at": ctx.author.joined_at > datetime.utcnow() - timedelta(days=GateConf.minimum_days_member), +            "joined_at": ( +                ctx.author.joined_at.replace(tzinfo=None) > datetime.utcnow() +                - timedelta(days=GateConf.minimum_days_member) +            ),              "total_messages": data["total_messages"] < GateConf.minimum_messages,              "voice_banned": data["voice_banned"],              "activity_blocks": data["activity_blocks"] < GateConf.minimum_activity_blocks diff --git a/bot/exts/moderation/watchchannels/_watchchannel.py b/bot/exts/moderation/watchchannels/_watchchannel.py index 8a64e83ff..8f97130ca 100644 --- a/bot/exts/moderation/watchchannels/_watchchannel.py +++ b/bot/exts/moderation/watchchannels/_watchchannel.py @@ -250,7 +250,7 @@ class WatchChannel(metaclass=CogABCMeta):              await self.webhook_send(                  cleaned_content,                  username=msg.author.display_name, -                avatar_url=msg.author.avatar_url +                avatar_url=msg.author.display_avatar.url              )          if msg.attachments: @@ -264,7 +264,7 @@ class WatchChannel(metaclass=CogABCMeta):                  await self.webhook_send(                      embed=e,                      username=msg.author.display_name, -                    avatar_url=msg.author.avatar_url +                    avatar_url=msg.author.display_avatar.url                  )              except discord.HTTPException as exc:                  self.log.exception( @@ -301,7 +301,7 @@ class WatchChannel(metaclass=CogABCMeta):          embed = Embed(description=f"{msg.author.mention} {message_jump}")          embed.set_footer(text=textwrap.shorten(footer, width=256, placeholder="...")) -        await self.webhook_send(embed=embed, username=msg.author.display_name, avatar_url=msg.author.avatar_url) +        await self.webhook_send(embed=embed, username=msg.author.display_name, avatar_url=msg.author.display_avatar.url)      async def list_watched_users(          self, ctx: Context, oldest_first: bool = False, update_cache: bool = True diff --git a/bot/exts/utils/bot.py b/bot/exts/utils/bot.py index 8f0094bc9..788692777 100644 --- a/bot/exts/utils/bot.py +++ b/bot/exts/utils/bot.py @@ -1,6 +1,7 @@ +from contextlib import suppress  from typing import Optional -from discord import Embed, TextChannel +from discord import Embed, Forbidden, TextChannel, Thread  from discord.ext.commands import Cog, Context, command, group, has_any_role  from bot.bot import Bot @@ -16,6 +17,20 @@ class BotCog(Cog, name="Bot"):      def __init__(self, bot: Bot):          self.bot = bot +    @Cog.listener() +    async def on_thread_join(self, thread: Thread) -> None: +        """ +        Try to join newly created threads. + +        Despite the event name being misleading, this is dispatched when new threads are created. +        """ +        if thread.me: +            # We have already joined this thread +            return + +        with suppress(Forbidden): +            await thread.join() +      @group(invoke_without_command=True, name="bot", hidden=True)      async def botinfo_group(self, ctx: Context) -> None:          """Bot informational commands.""" diff --git a/bot/exts/utils/clean.py b/bot/exts/utils/clean.py index fa9b7e219..a2e2d3eed 100644 --- a/bot/exts/utils/clean.py +++ b/bot/exts/utils/clean.py @@ -105,7 +105,7 @@ class Clean(Cog):          elif regex:              predicate = predicate_regex          # Delete messages that match regex          else: -            predicate = None                     # Delete all messages +            predicate = lambda *_: True          # Delete all messages          # Default to using the invoking context's channel          if not channels: diff --git a/bot/exts/utils/ping.py b/bot/exts/utils/ping.py index cf0e3265e..43d371d87 100644 --- a/bot/exts/utils/ping.py +++ b/bot/exts/utils/ping.py @@ -32,7 +32,7 @@ class Latency(commands.Cog):          """          # datetime.datetime objects do not have the "milliseconds" attribute.          # It must be converted to seconds before converting to milliseconds. -        bot_ping = (datetime.utcnow() - ctx.message.created_at).total_seconds() * 1000 +        bot_ping = (datetime.utcnow() - ctx.message.created_at.replace(tzinfo=None)).total_seconds() * 1000          if bot_ping <= 0:              bot_ping = "Your clock is out of sync, could not calculate ping."          else: diff --git a/bot/utils/checks.py b/bot/utils/checks.py index 972a5ef38..e7f2cfbda 100644 --- a/bot/utils/checks.py +++ b/bot/utils/checks.py @@ -126,7 +126,7 @@ def cooldown_with_role_bypass(rate: int, per: float, type: BucketType = BucketTy      bypass = set(bypass_roles)      # this handles the actual cooldown logic -    buckets = CooldownMapping(Cooldown(rate, per, type)) +    buckets = CooldownMapping(Cooldown(rate, per), type)      # will be called after the command has been parse but before it has been invoked, ensures that      # the cooldown won't be updated if the user screws up their input to the command @@ -141,7 +141,7 @@ def cooldown_with_role_bypass(rate: int, per: float, type: BucketType = BucketTy          bucket = buckets.get_bucket(ctx.message)          retry_after = bucket.update_rate_limit(current)          if retry_after: -            raise CommandOnCooldown(bucket, retry_after) +            raise CommandOnCooldown(bucket, retry_after, type)      def wrapper(command: Command) -> Command:          # NOTE: this could be changed if a subclass of Command were to be used. I didn't see the need for it diff --git a/bot/utils/messages.py b/bot/utils/messages.py index 053750cc3..e55c07062 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -121,7 +121,7 @@ async def send_attachments(      """      webhook_send_kwargs = {          'username': message.author.display_name, -        'avatar_url': message.author.avatar_url, +        'avatar_url': message.author.display_avatar.url,      }      webhook_send_kwargs.update(kwargs)      webhook_send_kwargs['username'] = sub_clyde(webhook_send_kwargs['username']) diff --git a/poetry.lock b/poetry.lock index 5e3f575d3..16c599bd1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -264,19 +264,23 @@ murmur = ["mmh3"]  [[package]]  name = "discord.py" -version = "1.7.3" +version = "2.0.0a0"  description = "A Python wrapper for the Discord API"  category = "main"  optional = false -python-versions = ">=3.5.3" +python-versions = ">=3.8.0"  [package.dependencies]  aiohttp = ">=3.6.0,<3.8.0"  [package.extras] -docs = ["sphinx (==3.0.3)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport"] +docs = ["sphinx (==4.0.2)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport"] +speed = ["orjson (>=3.5.4)"]  voice = ["PyNaCl (>=1.3.0,<1.5)"] +[package.source] +type = "url" +url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip"  [[package]]  name = "distlib"  version = "0.3.3" @@ -346,7 +350,7 @@ sgmllib3k = "*"  [[package]]  name = "filelock" -version = "3.3.0" +version = "3.3.1"  description = "A platform independent file lock."  category = "dev"  optional = false @@ -1110,7 +1114,7 @@ multidict = ">=4.0"  [metadata]  lock-version = "1.1"  python-versions = "3.9.*" -content-hash = "24a2142956e96706dced0172955c0338cb48fb4c067451301613014e23a82d62" +content-hash = "e37923739c35ef349d57e324579acfe304cc7e6fc20ddc54205fc89f171ae94f"  [metadata.files]  aio-pika = [ @@ -1334,10 +1338,7 @@ deepdiff = [      {file = "deepdiff-4.3.2-py3-none-any.whl", hash = "sha256:59fc1e3e7a28dd0147b0f2b00e3e27181f0f0ef4286b251d5f214a5bcd9a9bc4"},      {file = "deepdiff-4.3.2.tar.gz", hash = "sha256:91360be1d9d93b1d9c13ae9c5048fa83d9cff17a88eb30afaa0d7ff2d0fee17d"},  ] -"discord.py" = [ -    {file = "discord.py-1.7.3-py3-none-any.whl", hash = "sha256:c6f64db136de0e18e090f6752ea68bdd4ab0a61b82dfe7acecefa22d6477bb0c"}, -    {file = "discord.py-1.7.3.tar.gz", hash = "sha256:462cd0fe307aef8b29cbfa8dd613e548ae4b2cb581d46da9ac0d46fb6ea19408"}, -] +"discord.py" = []  distlib = [      {file = "distlib-0.3.3-py2.py3-none-any.whl", hash = "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31"},      {file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"}, @@ -1361,8 +1362,8 @@ feedparser = [      {file = "feedparser-6.0.8.tar.gz", hash = "sha256:5ce0410a05ab248c8c7cfca3a0ea2203968ee9ff4486067379af4827a59f9661"},  ]  filelock = [ -    {file = "filelock-3.3.0-py3-none-any.whl", hash = "sha256:bbc6a0382fe8ec4744ecdf6683a2e07f65eb10ff1aff53fc02a202565446cde0"}, -    {file = "filelock-3.3.0.tar.gz", hash = "sha256:8c7eab13dc442dc249e95158bcc12dec724465919bdc9831fdbf0660f03d1785"}, +    {file = "filelock-3.3.1-py3-none-any.whl", hash = "sha256:2b5eb3589e7fdda14599e7eb1a50e09b4cc14f34ed98b8ba56d33bfaafcbef2f"}, +    {file = "filelock-3.3.1.tar.gz", hash = "sha256:34a9f35f95c441e7b38209775d6e0337f9a3759f3565f6c5798f19618527c76f"},  ]  flake8 = [      {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, @@ -1470,8 +1471,6 @@ lxml = [      {file = "lxml-4.6.3-cp27-cp27m-win_amd64.whl", hash = "sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee"},      {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f"},      {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4"}, -    {file = "lxml-4.6.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:64812391546a18896adaa86c77c59a4998f33c24788cadc35789e55b727a37f4"}, -    {file = "lxml-4.6.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c1a40c06fd5ba37ad39caa0b3144eb3772e813b5fb5b084198a985431c2f1e8d"},      {file = "lxml-4.6.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51"},      {file = "lxml-4.6.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f90ba11136bfdd25cae3951af8da2e95121c9b9b93727b1b896e3fa105b2f586"},      {file = "lxml-4.6.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:4c61b3a0db43a1607d6264166b230438f85bfed02e8cff20c22e564d0faff354"}, diff --git a/pyproject.toml b/pyproject.toml index 515514c7b..e227ffaa6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ license = "MIT"  [tool.poetry.dependencies]  python = "3.9.*" +"discord.py" = {url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip"}  aio-pika = "~=6.1"  aiodns = "~=2.0"  aiohttp = "~=3.7" @@ -17,7 +18,6 @@ beautifulsoup4 = "~=4.9"  colorama = { version = "~=0.4.3", markers = "sys_platform == 'win32'" }  coloredlogs = "~=14.0"  deepdiff = "~=4.0" -"discord.py" = "~=1.7.3"  emoji = "~=0.6"  feedparser = "~=6.0.2"  rapidfuzz = "~=1.4" diff --git a/tests/base.py b/tests/base.py index ab9287e9a..5e304ea9d 100644 --- a/tests/base.py +++ b/tests/base.py @@ -103,4 +103,4 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):          with self.assertRaises(commands.MissingPermissions) as cm:              await cmd.can_run(ctx) -        self.assertCountEqual(permissions.keys(), cm.exception.missing_perms) +        self.assertCountEqual(permissions.keys(), cm.exception.missing_permissions) diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py index 2b0549b98..462f718e6 100644 --- a/tests/bot/exts/backend/test_error_handler.py +++ b/tests/bot/exts/backend/test_error_handler.py @@ -107,7 +107,7 @@ class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase):          """Should send error with `ctx.send` when error is `CommandOnCooldown`."""          self.ctx.reset_mock()          cog = ErrorHandler(self.bot) -        error = errors.CommandOnCooldown(10, 9) +        error = errors.CommandOnCooldown(10, 9, type=None)          self.assertIsNone(await cog.on_command_error(self.ctx, error))          self.ctx.send.assert_awaited_once_with(error) diff --git a/tests/bot/exts/filters/test_token_remover.py b/tests/bot/exts/filters/test_token_remover.py index 05e790723..4db27269a 100644 --- a/tests/bot/exts/filters/test_token_remover.py +++ b/tests/bot/exts/filters/test_token_remover.py @@ -26,7 +26,7 @@ class TokenRemoverTests(unittest.IsolatedAsyncioTestCase):          self.msg.guild.get_member.return_value.bot = False          self.msg.guild.get_member.return_value.__str__.return_value = "Woody"          self.msg.author.__str__ = MagicMock(return_value=self.msg.author.name) -        self.msg.author.avatar_url_as.return_value = "picture-lemon.png" +        self.msg.author.display_avatar.url = "picture-lemon.png"      def test_extract_user_id_valid(self):          """Should consider user IDs valid if they decode into an integer ID.""" @@ -376,7 +376,7 @@ class TokenRemoverTests(unittest.IsolatedAsyncioTestCase):              colour=Colour(constants.Colours.soft_red),              title="Token removed!",              text=log_msg + "\n" + userid_log_message, -            thumbnail=self.msg.author.avatar_url_as.return_value, +            thumbnail=self.msg.author.display_avatar.url,              channel_id=constants.Channels.mod_alerts,              ping_everyone=True,          ) diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index d8250befb..4b50c3fd9 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -84,7 +84,7 @@ class InformationCogTests(unittest.IsolatedAsyncioTestCase):          self.assertEqual(dummy_embed.fields[0].value, str(dummy_role.id))          self.assertEqual(dummy_embed.fields[1].value, f"#{dummy_role.colour.value:0>6x}") -        self.assertEqual(dummy_embed.fields[2].value, "0.63 0.48 218") +        self.assertEqual(dummy_embed.fields[2].value, "0.65 0.64 242")          self.assertEqual(dummy_embed.fields[3].value, "1")          self.assertEqual(dummy_embed.fields[4].value, "10")          self.assertEqual(dummy_embed.fields[5].value, "0") @@ -435,10 +435,9 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):          ctx = helpers.MockContext()          user = helpers.MockMember(id=217, colour=0) -        user.avatar_url_as.return_value = "avatar url" +        user.display_avatar.url = "avatar url"          embed = await self.cog.create_user_embed(ctx, user) -        user.avatar_url_as.assert_called_once_with(static_format="png")          self.assertEqual(embed.thumbnail.url, "avatar url") diff --git a/tests/bot/exts/moderation/test_incidents.py b/tests/bot/exts/moderation/test_incidents.py index c98edf08a..ccc842050 100644 --- a/tests/bot/exts/moderation/test_incidents.py +++ b/tests/bot/exts/moderation/test_incidents.py @@ -3,7 +3,7 @@ import enum  import logging  import typing as t  import unittest -from unittest.mock import AsyncMock, MagicMock, call, patch +from unittest.mock import AsyncMock, MagicMock, Mock, call, patch  import aiohttp  import discord @@ -372,7 +372,7 @@ class TestArchive(TestIncidents):          # Define our own `incident` to be archived          incident = MockMessage(              content="this is an incident", -            author=MockUser(name="author_name", avatar_url="author_avatar"), +            author=MockUser(name="author_name", display_avatar=Mock(url="author_avatar")),              id=123,          )          built_embed = MagicMock(discord.Embed, id=123)  # We patch `make_embed` to return this diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index ef8394be8..92ce3418a 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -431,7 +431,13 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase):          asyncio.run(self.cog._async_init())  # Populate instance attributes.          self.text_channel = MockTextChannel() -        self.text_overwrite = PermissionOverwrite(send_messages=True, add_reactions=False) +        self.text_overwrite = PermissionOverwrite( +            send_messages=True, +            add_reactions=False, +            create_private_threads=True, +            create_public_threads=False, +            send_messages_in_threads=True +        )          self.text_channel.overwrites_for.return_value = self.text_overwrite          self.voice_channel = MockVoiceChannel() @@ -502,9 +508,39 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase):      async def test_skipped_already_silenced(self):          """Permissions were not set and `False` was returned for an already silenced channel."""          subtests = ( -            (False, MockTextChannel(), PermissionOverwrite(send_messages=False, add_reactions=False)), -            (True, MockTextChannel(), PermissionOverwrite(send_messages=True, add_reactions=True)), -            (True, MockTextChannel(), PermissionOverwrite(send_messages=False, add_reactions=False)), +            ( +                False, +                MockTextChannel(), +                PermissionOverwrite( +                    send_messages=False, +                    add_reactions=False, +                    create_private_threads=False, +                    create_public_threads=False, +                    send_messages_in_threads=False +                ) +            ), +            ( +                True, +                MockTextChannel(), +                PermissionOverwrite( +                    send_messages=True, +                    add_reactions=True, +                    create_private_threads=True, +                    create_public_threads=True, +                    send_messages_in_threads=True +                ) +            ), +            ( +                True, +                MockTextChannel(), +                PermissionOverwrite( +                    send_messages=False, +                    add_reactions=False, +                    create_private_threads=False, +                    create_public_threads=False, +                    send_messages_in_threads=False +                ) +            ),              (False, MockVoiceChannel(), PermissionOverwrite(connect=False, speak=False)),              (True, MockVoiceChannel(), PermissionOverwrite(connect=True, speak=True)),              (True, MockVoiceChannel(), PermissionOverwrite(connect=False, speak=False)), @@ -552,11 +588,16 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase):          await self.cog._set_silence_overwrites(self.text_channel)          new_overwrite_dict = dict(self.text_overwrite) -        # Remove 'send_messages' & 'add_reactions' keys because they were changed by the method. -        del prev_overwrite_dict['send_messages'] -        del prev_overwrite_dict['add_reactions'] -        del new_overwrite_dict['send_messages'] -        del new_overwrite_dict['add_reactions'] +        # Remove related permission keys because they were changed by the method. +        for perm_name in ( +                "send_messages", +                "add_reactions", +                "create_private_threads", +                "create_public_threads", +                "send_messages_in_threads" +        ): +            del prev_overwrite_dict[perm_name] +            del new_overwrite_dict[perm_name]          self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict) @@ -594,7 +635,10 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase):      async def test_cached_previous_overwrites(self):          """Channel's previous overwrites were cached.""" -        overwrite_json = '{"send_messages": true, "add_reactions": false}' +        overwrite_json = ( +            '{"send_messages": true, "add_reactions": false, "create_private_threads": true, ' +            '"create_public_threads": false, "send_messages_in_threads": true}' +        )          await self.cog._set_silence_overwrites(self.text_channel)          self.cog.previous_overwrites.set.assert_awaited_once_with(self.text_channel.id, overwrite_json) diff --git a/tests/bot/utils/test_checks.py b/tests/bot/utils/test_checks.py index 883465e0b..4ae11d5d3 100644 --- a/tests/bot/utils/test_checks.py +++ b/tests/bot/utils/test_checks.py @@ -32,6 +32,7 @@ class ChecksTests(unittest.IsolatedAsyncioTestCase):      async def test_has_no_roles_check_without_guild(self):          """`has_no_roles_check` should return `False` when `Context.guild` is None."""          self.ctx.channel = MagicMock(DMChannel) +        self.ctx.guild = None          self.assertFalse(await checks.has_no_roles_check(self.ctx))      async def test_has_no_roles_check_returns_false_with_unwanted_role(self): diff --git a/tests/helpers.py b/tests/helpers.py index 83b9b2363..9d4988d23 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -39,7 +39,7 @@ class HashableMixin(discord.mixins.EqualityComparable):  class ColourMixin: -    """A mixin for Mocks that provides the aliasing of color->colour like discord.py does.""" +    """A mixin for Mocks that provides the aliasing of (accent_)color->(accent_)colour like discord.py does."""      @property      def color(self) -> discord.Colour: @@ -49,6 +49,14 @@ class ColourMixin:      def color(self, color: discord.Colour) -> None:          self.colour = color +    @property +    def accent_color(self) -> discord.Colour: +        return self.accent_colour + +    @accent_color.setter +    def accent_color(self, color: discord.Colour) -> None: +        self.accent_colour = color +  class CustomMockMixin:      """ @@ -242,7 +250,13 @@ class MockMember(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin  # Create a User instance to get a realistic Mock of `discord.User` -user_instance = discord.User(data=unittest.mock.MagicMock(), state=unittest.mock.MagicMock()) +_user_data_mock = collections.defaultdict(unittest.mock.MagicMock, { +    "accent_color": 0 +}) +user_instance = discord.User( +    data=unittest.mock.MagicMock(get=unittest.mock.Mock(side_effect=_user_data_mock.get)), +    state=unittest.mock.MagicMock() +)  class MockUser(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin): @@ -428,7 +442,12 @@ message_instance = discord.Message(state=state, channel=channel, data=message_da  # Create a Context instance to get a realistic MagicMock of `discord.ext.commands.Context` -context_instance = Context(message=unittest.mock.MagicMock(), prefix=unittest.mock.MagicMock()) +context_instance = Context( +    message=unittest.mock.MagicMock(), +    prefix="$", +    bot=MockBot(), +    view=None +)  context_instance.invoked_from_error_handler = None @@ -537,7 +556,7 @@ class MockReaction(CustomMockMixin, unittest.mock.MagicMock):          self.__str__.return_value = str(self.emoji) -webhook_instance = discord.Webhook(data=unittest.mock.MagicMock(), adapter=unittest.mock.MagicMock()) +webhook_instance = discord.Webhook(data=unittest.mock.MagicMock(), session=unittest.mock.MagicMock())  class MockAsyncWebhook(CustomMockMixin, unittest.mock.MagicMock): @@ -5,7 +5,7 @@ import-order-style=pycharm  application_import_names=bot,tests  exclude=.cache,.venv,.git,constants.py  ignore= -    B311,W503,E226,S311,T000 +    B311,W503,E226,S311,T000,E731      # Missing Docstrings      D100,D104,D105,D107,      # Docstring Whitespace  |