diff options
author | 2022-03-18 06:35:46 +0000 | |
---|---|---|
committer | 2022-03-18 06:35:46 +0000 | |
commit | a7d20320791fda9f0d8c3b92b2bb211a75aca70d (patch) | |
tree | eaa62d107e1697b5a3dd35669e66b781a5058548 | |
parent | apply decorator on superstarify too (diff) | |
parent | Merge pull request #2087 from python-discord/fix-issue-1930 (diff) |
Merge branch 'main' into bug/infr-duration
-rw-r--r-- | bot/constants.py | 8 | ||||
-rw-r--r-- | bot/exts/fun/duck_pond.py | 20 | ||||
-rw-r--r-- | bot/exts/help_channels/_channel.py | 11 | ||||
-rw-r--r-- | bot/exts/help_channels/_cog.py | 23 | ||||
-rw-r--r-- | bot/exts/help_channels/_message.py | 93 | ||||
-rw-r--r-- | bot/exts/info/code_snippets.py | 3 | ||||
-rw-r--r-- | bot/exts/info/help.py | 26 | ||||
-rw-r--r-- | bot/exts/moderation/infraction/_scheduler.py | 22 | ||||
-rw-r--r-- | bot/exts/moderation/infraction/_utils.py | 49 | ||||
-rw-r--r-- | bot/exts/moderation/infraction/management.py | 29 | ||||
-rw-r--r-- | bot/exts/moderation/infraction/superstarify.py | 22 | ||||
-rw-r--r-- | bot/exts/moderation/watchchannels/bigbrother.py | 2 | ||||
-rw-r--r-- | bot/resources/tags/regex.md | 15 | ||||
-rw-r--r-- | config-default.yml | 17 | ||||
-rw-r--r-- | tests/bot/exts/moderation/infraction/test_utils.py | 36 |
15 files changed, 242 insertions, 134 deletions
diff --git a/bot/constants.py b/bot/constants.py index 77c01bfa3..4531b547d 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -429,8 +429,6 @@ class Channels(metaclass=YAMLGetter): off_topic_1: int off_topic_2: int - black_formatter: int - bot_commands: int discord_bots: int esoteric: int @@ -620,10 +618,12 @@ class HelpChannels(metaclass=YAMLGetter): max_available: int max_total_channels: int name_prefix: str - notify: bool notify_channel: int notify_minutes: int - notify_roles: List[int] + notify_none_remaining: bool + notify_none_remaining_roles: List[int] + notify_running_low: bool + notify_running_low_threshold: int class RedirectOutput(metaclass=YAMLGetter): diff --git a/bot/exts/fun/duck_pond.py b/bot/exts/fun/duck_pond.py index c51656343..8a41a3116 100644 --- a/bot/exts/fun/duck_pond.py +++ b/bot/exts/fun/duck_pond.py @@ -2,7 +2,7 @@ import asyncio from typing import Union import discord -from discord import Color, Embed, Message, RawReactionActionEvent, TextChannel, errors +from discord import Color, Embed, Message, RawReactionActionEvent, errors from discord.ext.commands import Cog, Context, command from bot import constants @@ -46,17 +46,6 @@ class DuckPond(Cog): return True return False - @staticmethod - def is_helper_viewable(channel: TextChannel) -> bool: - """Check if helpers can view a specific channel.""" - guild = channel.guild - helper_role = guild.get_role(constants.Roles.helpers) - # check channel overwrites for both the Helper role and @everyone and - # return True for channels that they have permissions to view. - helper_overwrites = channel.overwrites_for(helper_role) - default_overwrites = channel.overwrites_for(guild.default_role) - return default_overwrites.view_channel is None or helper_overwrites.view_channel is True - async def has_green_checkmark(self, message: Message) -> bool: """Check if the message has a green checkmark reaction.""" for reaction in message.reactions: @@ -165,12 +154,15 @@ class DuckPond(Cog): if not self._payload_has_duckpond_emoji(payload.emoji): return - channel = discord.utils.get(self.bot.get_all_channels(), id=payload.channel_id) + await self.bot.wait_until_guild_available() + guild = self.bot.get_guild(payload.guild_id) + channel = guild.get_channel_or_thread(payload.channel_id) if channel is None: return # Was the message sent in a channel Helpers can see? - if not self.is_helper_viewable(channel): + helper_role = guild.get_role(constants.Roles.helpers) + if not channel.permissions_for(helper_role).view_channel: return try: diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index ff9e6a347..d9cebf215 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -24,7 +24,7 @@ class ClosingReason(Enum): """All possible closing reasons for help channels.""" COMMAND = "command" - LATEST_MESSSAGE = "auto.latest_message" + LATEST_MESSAGE = "auto.latest_message" CLAIMANT_TIMEOUT = "auto.claimant_timeout" OTHER_TIMEOUT = "auto.other_timeout" DELETED = "auto.deleted" @@ -77,7 +77,7 @@ async def get_closing_time(channel: discord.TextChannel, init_done: bool) -> t.T # Use the greatest offset to avoid the possibility of prematurely closing the channel. time = Arrow.fromdatetime(msg.created_at) + timedelta(minutes=idle_minutes_claimant) - reason = ClosingReason.DELETED if is_empty else ClosingReason.LATEST_MESSSAGE + reason = ClosingReason.DELETED if is_empty else ClosingReason.LATEST_MESSAGE return time, reason claimant_time = Arrow.utcfromtimestamp(claimant_time) @@ -182,9 +182,10 @@ async def ensure_cached_claimant(channel: discord.TextChannel) -> None: if _message._match_bot_embed(message, _message.DORMANT_MSG): log.info("Hit the dormant message embed before finding a claimant in %s (%d).", channel, channel.id) break - user_id = CLAIMED_BY_RE.match(message.embeds[0].description).group("user_id") - await _caches.claimants.set(channel.id, int(user_id)) - return + # Only set the claimant if the first embed matches the claimed channel embed regex + if match := CLAIMED_BY_RE.match(message.embeds[0].description): + await _caches.claimants.set(channel.id, int(match.group("user_id"))) + return await bot.instance.get_channel(constants.Channels.helpers).send( f"I couldn't find a claimant for {channel.mention} in that last 1000 messages. " diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index f276a7993..a93acffb6 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -78,7 +78,10 @@ class HelpChannels(commands.Cog): self.channel_queue: asyncio.Queue[discord.TextChannel] = None self.name_queue: t.Deque[str] = None - self.last_notification: t.Optional[arrow.Arrow] = None + # Notifications + # Using a very old date so that we don't have to use Optional typing. + self.last_none_remaining_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') + self.last_running_low_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') self.dynamic_message: t.Optional[int] = None self.available_help_channels: t.Set[discord.TextChannel] = set() @@ -252,13 +255,21 @@ class HelpChannels(commands.Cog): if not channel: log.info("Couldn't create a candidate channel; waiting to get one from the queue.") - notify_channel = self.bot.get_channel(constants.HelpChannels.notify_channel) - last_notification = await _message.notify(notify_channel, self.last_notification) + last_notification = await _message.notify_none_remaining(self.last_none_remaining_notification) + if last_notification: - self.last_notification = last_notification - self.bot.stats.incr("help.out_of_channel_alerts") + self.last_none_remaining_notification = last_notification + + channel = await self.wait_for_dormant_channel() # Blocks until a new channel is available + + else: + last_notification = await _message.notify_running_low( + self.channel_queue.qsize(), + self.last_running_low_notification + ) - channel = await self.wait_for_dormant_channel() + if last_notification: + self.last_running_low_notification = last_notification return channel diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 241dd606c..7ceed9b4d 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -124,52 +124,93 @@ async def dm_on_open(message: discord.Message) -> None: ) -async def notify(channel: discord.TextChannel, last_notification: t.Optional[Arrow]) -> t.Optional[Arrow]: +async def notify_none_remaining(last_notification: Arrow) -> t.Optional[Arrow]: """ - Send a message in `channel` notifying about a lack of available help channels. + Send a pinging message in `channel` notifying about there being no dormant channels remaining. If a notification was sent, return the time at which the message was sent. Otherwise, return None. Configuration: - - * `HelpChannels.notify` - toggle notifications - * `HelpChannels.notify_minutes` - minimum interval between notifications - * `HelpChannels.notify_roles` - roles mentioned in notifications + * `HelpChannels.notify_minutes` - minimum interval between notifications + * `HelpChannels.notify_none_remaining` - toggle none_remaining notifications + * `HelpChannels.notify_none_remaining_roles` - roles mentioned in notifications """ - if not constants.HelpChannels.notify: - return + if not constants.HelpChannels.notify_none_remaining: + return None + + if (arrow.utcnow() - last_notification).total_seconds() < (constants.HelpChannels.notify_minutes * 60): + log.trace("Did not send none_remaining notification as it hasn't been enough time since the last one.") + return None log.trace("Notifying about lack of channels.") - if last_notification: - elapsed = (arrow.utcnow() - last_notification).seconds - minimum_interval = constants.HelpChannels.notify_minutes * 60 - should_send = elapsed >= minimum_interval - else: - should_send = True + mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_none_remaining_roles) + allowed_roles = [discord.Object(id_) for id_ in constants.HelpChannels.notify_none_remaining_roles] - if not should_send: - log.trace("Notification not sent because it's too recent since the previous one.") - return + channel = bot.instance.get_channel(constants.HelpChannels.notify_channel) + if channel is None: + log.trace("Did not send none_remaining notification as the notification channel couldn't be gathered.") try: - log.trace("Sending notification message.") - - mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_roles) - allowed_roles = [discord.Object(id_) for id_ in constants.HelpChannels.notify_roles] - - message = await channel.send( + await channel.send( f"{mentions} A new available help channel is needed but there " - f"are no more dormant ones. Consider freeing up some in-use channels manually by " + "are no more dormant ones. Consider freeing up some in-use channels manually by " f"using the `{constants.Bot.prefix}dormant` command within the channels.", allowed_mentions=discord.AllowedMentions(everyone=False, roles=allowed_roles) ) - - return Arrow.fromdatetime(message.created_at) except Exception: # Handle it here cause this feature isn't critical for the functionality of the system. log.exception("Failed to send notification about lack of dormant channels!") + else: + bot.instance.stats.incr("help.out_of_channel_alerts") + return arrow.utcnow() + + +async def notify_running_low(number_of_channels_left: int, last_notification: Arrow) -> t.Optional[Arrow]: + """ + Send a non-pinging message in `channel` notifying about there being a low amount of dormant channels. + + This will include the number of dormant channels left `number_of_channels_left` + + If a notification was sent, return the time at which the message was sent. + Otherwise, return None. + + Configuration: + * `HelpChannels.notify_minutes` - minimum interval between notifications + * `HelpChannels.notify_running_low` - toggle running_low notifications + * `HelpChannels.notify_running_low_threshold` - minimum amount of channels to trigger running_low notifications + """ + if not constants.HelpChannels.notify_running_low: + return None + + if number_of_channels_left > constants.HelpChannels.notify_running_low_threshold: + log.trace("Did not send notify_running_low notification as the threshold was not met.") + return None + + if (arrow.utcnow() - last_notification).total_seconds() < (constants.HelpChannels.notify_minutes * 60): + log.trace("Did not send notify_running_low notification as it hasn't been enough time since the last one.") + return None + + log.trace("Notifying about getting close to no dormant channels.") + + channel = bot.instance.get_channel(constants.HelpChannels.notify_channel) + if channel is None: + log.trace("Did not send notify_running notification as the notification channel couldn't be gathered.") + + try: + if number_of_channels_left == 1: + message = f"There is only {number_of_channels_left} dormant channel left. " + else: + message = f"There are only {number_of_channels_left} dormant channels left. " + message += "Consider participating in some help channels so that we don't run out." + await channel.send(message) + except Exception: + # Handle it here cause this feature isn't critical for the functionality of the system. + log.exception("Failed to send notification about running low of dormant channels!") + else: + bot.instance.stats.incr("help.running_low_alerts") + return arrow.utcnow() async def pin(message: discord.Message) -> None: diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index 07b1b8a2d..f2f29020f 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -246,6 +246,9 @@ class CodeSnippets(Cog): if message.author.bot: return + if message.guild is None: + return + message_to_send = await self._parse_snippets(message.content) destination = message.channel diff --git a/bot/exts/info/help.py b/bot/exts/info/help.py index 06799fb71..864e7edd2 100644 --- a/bot/exts/info/help.py +++ b/bot/exts/info/help.py @@ -113,12 +113,28 @@ class CommandView(ui.View): If the command has a parent, a button is added to the view to show that parent's help embed. """ - def __init__(self, help_command: CustomHelpCommand, command: Command): + def __init__(self, help_command: CustomHelpCommand, command: Command, context: Context): + self.context = context super().__init__() if command.parent: self.children.append(GroupButton(help_command, command, emoji="↩️")) + async def interaction_check(self, interaction: Interaction) -> bool: + """ + Ensures that the button only works for the user who spawned the help command. + + Also allows moderators to access buttons even when not the author of message. + """ + if interaction.user is not None: + if any(role.id in constants.MODERATION_ROLES for role in interaction.user.roles): + return True + + elif interaction.user.id == self.context.author.id: + return True + + return False + class GroupView(CommandView): """ @@ -130,8 +146,8 @@ class GroupView(CommandView): MAX_BUTTONS_IN_ROW = 5 MAX_ROWS = 5 - def __init__(self, help_command: CustomHelpCommand, group: Group, subcommands: list[Command]): - super().__init__(help_command, group) + def __init__(self, help_command: CustomHelpCommand, group: Group, subcommands: list[Command], context: Context): + super().__init__(help_command, group, context) # Don't add buttons if only a portion of the subcommands can be shown. if len(subcommands) + len(self.children) > self.MAX_ROWS * self.MAX_BUTTONS_IN_ROW: log.trace(f"Attempted to add navigation buttons for `{group.qualified_name}`, but there was no space.") @@ -302,7 +318,7 @@ class CustomHelpCommand(HelpCommand): embed.description = command_details # If the help is invoked in the context of an error, don't show subcommand navigation. - view = CommandView(self, command) if not self.context.command_failed else None + view = CommandView(self, command, self.context) if not self.context.command_failed else None return embed, view async def send_command_help(self, command: Command) -> None: @@ -347,7 +363,7 @@ class CustomHelpCommand(HelpCommand): embed.description += f"\n**Subcommands:**\n{command_details}" # If the help is invoked in the context of an error, don't show subcommand navigation. - view = GroupView(self, group, commands_) if not self.context.command_failed else None + view = GroupView(self, group, commands_, self.context) if not self.context.command_failed else None return embed, view async def send_group_help(self, group: Group) -> None: diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 47b639421..2fc54856f 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -166,15 +166,12 @@ class InfractionScheduler: # apply kick/ban infractions first, this would mean that we'd make it # impossible for us to deliver a DM. See python-discord/bot#982. if not infraction["hidden"] and infr_type in {"ban", "kick"}: - dm_result = f"{constants.Emojis.failmail} " - dm_log_text = "\nDM: **Failed**" - - # Accordingly update whether the user was successfully notified via DM. - if await _utils.notify_infraction( - self.bot, user, infraction["id"], infr_type.replace("_", " ").title(), expiry, user_reason, icon - ): + if await _utils.notify_infraction(infraction, user, user_reason): dm_result = ":incoming_envelope: " dm_log_text = "\nDM: Sent" + else: + dm_result = f"{constants.Emojis.failmail} " + dm_log_text = "\nDM: **Failed**" end_msg = "" if is_mod_channel(ctx.channel): @@ -236,15 +233,12 @@ class InfractionScheduler: # If we need to DM and haven't already tried to if not infraction["hidden"] and infr_type not in {"ban", "kick"}: - dm_result = f"{constants.Emojis.failmail} " - dm_log_text = "\nDM: **Failed**" - - # Accordingly update whether the user was successfully notified via DM. - if await _utils.notify_infraction( - self.bot, user, infraction["id"], infr_type.replace("_", " ").title(), expiry, user_reason, icon - ): + if await _utils.notify_infraction(infraction, user, user_reason): dm_result = ":incoming_envelope: " dm_log_text = "\nDM: Sent" + else: + dm_result = f"{constants.Emojis.failmail} " + dm_log_text = "\nDM: **Failed**" # Send a confirmation message to the invoking context. log.trace(f"Sending infraction #{id_} confirmation message.") diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 4df833ffb..c1be18362 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -1,15 +1,17 @@ import typing as t from datetime import datetime +import arrow import discord from discord.ext.commands import Context +import bot from bot.api import ResponseCodeError -from bot.bot import Bot from bot.constants import Colours, Icons from bot.converters import MemberOrUser from bot.errors import InvalidInfractedUserError from bot.log import get_logger +from bot.utils import time log = get_logger(__name__) @@ -43,6 +45,7 @@ LONGEST_EXTRAS = max(len(INFRACTION_APPEAL_SERVER_FOOTER), len(INFRACTION_APPEAL INFRACTION_DESCRIPTION_TEMPLATE = ( "**Type:** {type}\n" "**Expires:** {expires}\n" + "**Duration:** {duration}\n" "**Reason:** {reason}\n" ) @@ -159,20 +162,44 @@ async def send_active_infraction_message(ctx: Context, infraction: Infraction) - async def notify_infraction( - bot: Bot, + infraction: Infraction, user: MemberOrUser, - infr_id: id, - infr_type: str, - expires_at: t.Optional[str] = None, - reason: t.Optional[str] = None, - icon_url: str = Icons.token_removed + reason: t.Optional[str] = None ) -> bool: - """DM a user about their new infraction and return True if the DM is successful.""" + """ + DM a user about their new infraction and return True if the DM is successful. + + `reason` can be used to override what is in `infraction`. Otherwise, this data will + be retrieved from `infraction`. + """ + infr_id = infraction["id"] + infr_type = infraction["type"].replace("_", " ").title() + icon_url = INFRACTION_ICONS[infraction["type"]][0] + + if infraction["expires_at"] is None: + expires_at = "Never" + duration = "Permanent" + else: + expiry = arrow.get(infraction["expires_at"]) + expires_at = time.format_relative(expiry) + duration = time.humanize_delta(infraction["inserted_at"], expiry, max_units=2) + + if infraction["active"]: + remaining = time.humanize_delta(expiry, arrow.utcnow(), max_units=2) + if duration != remaining: + duration += f" ({remaining} remaining)" + else: + expires_at += " (Inactive)" + log.trace(f"Sending {user} a DM about their {infr_type} infraction.") + if reason is None: + reason = infraction["reason"] + text = INFRACTION_DESCRIPTION_TEMPLATE.format( type=infr_type.title(), - expires=expires_at or "N/A", + expires=expires_at, + duration=duration, reason=reason or "No reason provided." ) @@ -180,7 +207,7 @@ async def notify_infraction( if len(text) > 4096 - LONGEST_EXTRAS: text = f"{text[:4093-LONGEST_EXTRAS]}..." - text += INFRACTION_APPEAL_SERVER_FOOTER if infr_type.lower() == 'ban' else INFRACTION_APPEAL_MODMAIL_FOOTER + text += INFRACTION_APPEAL_SERVER_FOOTER if infraction["type"] == 'ban' else INFRACTION_APPEAL_MODMAIL_FOOTER embed = discord.Embed( description=text, @@ -193,7 +220,7 @@ async def notify_infraction( dm_sent = await send_private_embed(user, embed) if dm_sent: - await bot.api_client.patch( + await bot.instance.api_client.patch( f"bot/infractions/{infr_id}", json={"dm_sent": True} ) diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index 842cdabcd..62d349519 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -11,6 +11,7 @@ from bot.bot import Bot from bot.converters import Expiry, Infraction, MemberOrUser, Snowflake, UnambiguousUser, allowed_strings from bot.decorators import ensure_future_timestamp from bot.errors import InvalidInfraction +from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction.infractions import Infractions from bot.exts.moderation.modlog import ModLog from bot.log import get_logger @@ -40,12 +41,10 @@ class ModManagement(commands.Cog): """Get currently loaded Infractions cog instance.""" return self.bot.get_cog("Infractions") - # region: Edit infraction commands - @commands.group(name='infraction', aliases=('infr', 'infractions', 'inf', 'i'), invoke_without_command=True) async def infraction_group(self, ctx: Context, infraction: Infraction = None) -> None: """ - Infraction manipulation commands. + Infraction management commands. If `infraction` is passed then this command fetches that infraction. The `Infraction` converter supports 'l', 'last' and 'recent' to get the most recent infraction made by `ctx.author`. @@ -60,6 +59,30 @@ class ModManagement(commands.Cog): ) await self.send_infraction_list(ctx, embed, [infraction]) + @infraction_group.command(name="resend", aliases=("send", "rs", "dm")) + async def infraction_resend(self, ctx: Context, infraction: Infraction) -> None: + """Resend a DM to a user about a given infraction of theirs.""" + if infraction["hidden"]: + await ctx.send(f"{constants.Emojis.failmail} You may not resend hidden infractions.") + return + + member_id = infraction["user"]["id"] + member = await get_or_fetch_member(ctx.guild, member_id) + if not member: + await ctx.send(f"{constants.Emojis.failmail} Cannot find member `{member_id}` in the guild.") + return + + id_ = infraction["id"] + reason = infraction["reason"] or "No reason provided." + reason += "\n\n**This is a re-sent message for a previously applied infraction which may have been edited.**" + + if await _utils.notify_infraction(infraction, member, reason): + await ctx.send(f":incoming_envelope: Resent DM for infraction `{id_}`.") + else: + await ctx.send(f"{constants.Emojis.failmail} Failed to resend DM for infraction `{id_}`.") + + # region: Edit infraction commands + @infraction_group.command(name="append", aliases=("amend", "add", "a")) async def infraction_append( self, diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index af676a0de..c4a7e5081 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -64,6 +64,12 @@ class Superstarify(InfractionScheduler, Cog): if after.display_name == forced_nick: return # Nick change was triggered by this event. Ignore. + reason = ( + "You have tried to change your nickname on the **Python Discord** server " + f"from **{before.display_name}** to **{after.display_name}**, but as you " + "are currently in superstar-prison, you do not have permission to do so." + ) + log.info( f"{after.display_name} ({after.id}) tried to escape superstar prison. " f"Changing the nick back to {before.display_name}." @@ -73,21 +79,7 @@ class Superstarify(InfractionScheduler, Cog): reason=f"Superstarified member tried to escape the prison: {infr_id}" ) - notified = await _utils.notify_infraction( - bot=self.bot, - user=after, - infr_id=infr_id, - infr_type="Superstarify", - expires_at=time.discord_timestamp(infraction["expires_at"]), - reason=( - "You have tried to change your nickname on the **Python Discord** server " - f"from **{before.display_name}** to **{after.display_name}**, but as you " - "are currently in superstar-prison, you do not have permission to do so." - ), - icon_url=_utils.INFRACTION_ICONS["superstar"][0] - ) - - if not notified: + if not await _utils.notify_infraction(infraction, after, reason): log.info("Failed to DM user about why they cannot change their nickname.") @Cog.listener() diff --git a/bot/exts/moderation/watchchannels/bigbrother.py b/bot/exts/moderation/watchchannels/bigbrother.py index ab37b1b80..31b106a20 100644 --- a/bot/exts/moderation/watchchannels/bigbrother.py +++ b/bot/exts/moderation/watchchannels/bigbrother.py @@ -22,7 +22,7 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"): destination=Channels.big_brother_logs, webhook_id=Webhooks.big_brother, api_endpoint='bot/infractions', - api_default_params={'active': 'true', 'type': 'watch', 'ordering': '-inserted_at'}, + api_default_params={'active': 'true', 'type': 'watch', 'ordering': '-inserted_at', 'limit': 10_000}, logger=log ) diff --git a/bot/resources/tags/regex.md b/bot/resources/tags/regex.md new file mode 100644 index 000000000..35fee45a9 --- /dev/null +++ b/bot/resources/tags/regex.md @@ -0,0 +1,15 @@ +**Regular expressions** +Regular expressions (regex) are a tool for finding patterns in strings. The standard library's `re` module defines functions for using regex patterns. + +**Example** +We can use regex to pull out all the numbers in a sentence: +```py +>>> import re +>>> x = "On Oct 18 1963 a cat was launched aboard rocket #47" +>>> regex_pattern = r"[0-9]{1,3}" # Matches 1-3 digits +>>> re.findall(regex_pattern, foo) +['18', '196', '3', '47'] # Notice the year is cut off +``` +**See Also** +• [The re docs](https://docs.python.org/3/library/re.html) - for functions that use regex +• [regex101.com](https://regex101.com) - an interactive site for testing your regular expression diff --git a/config-default.yml b/config-default.yml index 583733fda..dae923158 100644 --- a/config-default.yml +++ b/config-default.yml @@ -513,19 +513,16 @@ help_channels: # Prefix for help channel names name_prefix: 'help-' - # Notify if more available channels are needed but there are no more dormant ones - notify: true + notify_channel: *HELPERS # Channel in which to send notifications messages + notify_minutes: 15 # Minimum interval between none_remaining or running_low notifications - # Channel in which to send notifications - notify_channel: *HELPERS - - # Minimum interval between helper notifications - notify_minutes: 15 - - # Mention these roles in notifications - notify_roles: + notify_none_remaining: true # Pinging notification for the Helper role when no dormant channels remain + notify_none_remaining_roles: # Mention these roles in the none_remaining notification - *HELPERS_ROLE + notify_running_low: true # Non-pinging notification which is triggered when the channel count is equal or less than the threshold + notify_running_low_threshold: 4 # The amount of channels at which a running_low notification will be sent + redirect_output: delete_delay: 15 diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 350274ecd..ff81ddd65 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -15,7 +15,10 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): """Tests Moderation utils.""" def setUp(self): - self.bot = MockBot() + patcher = patch("bot.instance", new=MockBot()) + self.bot = patcher.start() + self.addCleanup(patcher.stop) + self.member = MockMember(id=1234) self.user = MockUser(id=1234) self.ctx = MockContext(bot=self.bot, author=self.member) @@ -123,8 +126,9 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): else: self.ctx.send.assert_not_awaited() + @unittest.skip("Current time needs to be patched so infraction duration is correct.") @patch("bot.exts.moderation.infraction._utils.send_private_embed") - async def test_notify_infraction(self, send_private_embed_mock): + async def test_send_infraction_embed(self, send_private_embed_mock): """ Should send an embed of a certain format as a DM and return `True` if DM successful. @@ -132,7 +136,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): """ test_cases = [ { - "args": (self.bot, self.user, 0, "ban", "2020-02-26 09:20 (23 hours and 59 minutes)"), + "args": (dict(id=0, type="ban", reason=None, expires_at=datetime(2020, 2, 26, 9, 20)), self.user), "expected_output": Embed( title=utils.INFRACTION_TITLE, description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( @@ -145,12 +149,12 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): ).set_author( name=utils.INFRACTION_AUTHOR_NAME, url=utils.RULES_URL, - icon_url=Icons.token_removed + icon_url=Icons.user_ban ), "send_result": True }, { - "args": (self.bot, self.user, 0, "warning", None, "Test reason."), + "args": (dict(id=0, type="warning", reason="Test reason.", expires_at=None), self.user), "expected_output": Embed( title=utils.INFRACTION_TITLE, description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( @@ -163,14 +167,14 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): ).set_author( name=utils.INFRACTION_AUTHOR_NAME, url=utils.RULES_URL, - icon_url=Icons.token_removed + icon_url=Icons.user_warn ), "send_result": False }, # Note that this test case asserts that the DM that *would* get sent to the user is formatted # correctly, even though that message is deliberately never sent. { - "args": (self.bot, self.user, 0, "note", None, None, Icons.defcon_denied), + "args": (dict(id=0, type="note", reason=None, expires_at=None), self.user), "expected_output": Embed( title=utils.INFRACTION_TITLE, description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( @@ -183,20 +187,12 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): ).set_author( name=utils.INFRACTION_AUTHOR_NAME, url=utils.RULES_URL, - icon_url=Icons.defcon_denied + icon_url=Icons.user_warn ), "send_result": False }, { - "args": ( - self.bot, - self.user, - 0, - "mute", - "2020-02-26 09:20 (23 hours and 59 minutes)", - "Test", - Icons.defcon_denied - ), + "args": (dict(id=0, type="mute", reason="Test", expires_at=datetime(2020, 2, 26, 9, 20)), self.user), "expected_output": Embed( title=utils.INFRACTION_TITLE, description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( @@ -209,12 +205,12 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): ).set_author( name=utils.INFRACTION_AUTHOR_NAME, url=utils.RULES_URL, - icon_url=Icons.defcon_denied + icon_url=Icons.user_mute ), "send_result": False }, { - "args": (self.bot, self.user, 0, "mute", None, "foo bar" * 4000, Icons.defcon_denied), + "args": (dict(id=0, type="mute", reason="foo bar" * 4000, expires_at=None), self.user), "expected_output": Embed( title=utils.INFRACTION_TITLE, description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( @@ -227,7 +223,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): ).set_author( name=utils.INFRACTION_AUTHOR_NAME, url=utils.RULES_URL, - icon_url=Icons.defcon_denied + icon_url=Icons.user_mute ), "send_result": True } |