aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Mark <[email protected]>2022-03-18 06:35:46 +0000
committerGravatar GitHub <[email protected]>2022-03-18 06:35:46 +0000
commita7d20320791fda9f0d8c3b92b2bb211a75aca70d (patch)
treeeaa62d107e1697b5a3dd35669e66b781a5058548
parentapply decorator on superstarify too (diff)
parentMerge pull request #2087 from python-discord/fix-issue-1930 (diff)
Merge branch 'main' into bug/infr-duration
-rw-r--r--bot/constants.py8
-rw-r--r--bot/exts/fun/duck_pond.py20
-rw-r--r--bot/exts/help_channels/_channel.py11
-rw-r--r--bot/exts/help_channels/_cog.py23
-rw-r--r--bot/exts/help_channels/_message.py93
-rw-r--r--bot/exts/info/code_snippets.py3
-rw-r--r--bot/exts/info/help.py26
-rw-r--r--bot/exts/moderation/infraction/_scheduler.py22
-rw-r--r--bot/exts/moderation/infraction/_utils.py49
-rw-r--r--bot/exts/moderation/infraction/management.py29
-rw-r--r--bot/exts/moderation/infraction/superstarify.py22
-rw-r--r--bot/exts/moderation/watchchannels/bigbrother.py2
-rw-r--r--bot/resources/tags/regex.md15
-rw-r--r--config-default.yml17
-rw-r--r--tests/bot/exts/moderation/infraction/test_utils.py36
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
}