aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/constants.py2
-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--tests/bot/exts/moderation/infraction/test_utils.py36
6 files changed, 95 insertions, 65 deletions
diff --git a/bot/constants.py b/bot/constants.py
index b775848fb..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
diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py
index d51009358..8107b502a 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 a464f7c87..36e818ec6 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 disnake
from disnake.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 = disnake.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 875e8ef34..25420cd7a 100644
--- a/bot/exts/moderation/infraction/management.py
+++ b/bot/exts/moderation/infraction/management.py
@@ -10,6 +10,7 @@ from bot import constants
from bot.bot import Bot
from bot.converters import Expiry, Infraction, MemberOrUser, Snowflake, UnambiguousUser, allowed_strings
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
@@ -39,12 +40,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`.
@@ -59,6 +58,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 1d357d441..41ba52580 100644
--- a/bot/exts/moderation/infraction/superstarify.py
+++ b/bot/exts/moderation/infraction/superstarify.py
@@ -63,6 +63,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}."
@@ -72,21 +78,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/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py
index 6601b9d25..eaa0e701e 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
}