aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/converters.py2
-rw-r--r--bot/exts/filters/antispam.py10
-rw-r--r--bot/exts/filters/filtering.py4
-rw-r--r--bot/exts/filters/token_remover.py2
-rw-r--r--bot/exts/filters/webhook_remover.py2
-rw-r--r--bot/exts/fun/duck_pond.py4
-rw-r--r--bot/exts/info/information.py2
-rw-r--r--bot/exts/moderation/defcon.py18
-rw-r--r--bot/exts/moderation/incidents.py4
-rw-r--r--bot/exts/moderation/infraction/_scheduler.py6
-rw-r--r--bot/exts/moderation/infraction/management.py2
-rw-r--r--bot/exts/moderation/modlog.py116
-rw-r--r--bot/exts/moderation/silence.py38
-rw-r--r--bot/exts/moderation/voice_gate.py5
-rw-r--r--bot/exts/moderation/watchchannels/_watchchannel.py6
-rw-r--r--bot/exts/utils/bot.py17
-rw-r--r--bot/exts/utils/clean.py2
-rw-r--r--bot/exts/utils/ping.py2
-rw-r--r--bot/utils/checks.py4
-rw-r--r--bot/utils/messages.py2
-rw-r--r--poetry.lock25
-rw-r--r--pyproject.toml2
-rw-r--r--tests/base.py2
-rw-r--r--tests/bot/exts/backend/test_error_handler.py2
-rw-r--r--tests/bot/exts/filters/test_token_remover.py4
-rw-r--r--tests/bot/exts/info/test_information.py5
-rw-r--r--tests/bot/exts/moderation/test_incidents.py4
-rw-r--r--tests/bot/exts/moderation/test_silence.py64
-rw-r--r--tests/bot/utils/test_checks.py1
-rw-r--r--tests/helpers.py27
-rw-r--r--tox.ini2
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):
diff --git a/tox.ini b/tox.ini
index b8293a3b6..9472c32f9 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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