From f1894e5adcb5711ea849752a2c94b65ba57fb0ce Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 15 Jul 2021 15:22:56 -0700 Subject: Remove unnecessary config constant It's only being used as an anchor in the YAML file. There is no need to have it in Python if no Python code references it. --- bot/constants.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 500803f33..6ff0ceebe 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -435,8 +435,6 @@ class Channels(metaclass=YAMLGetter): off_topic_1: int off_topic_2: int - black_formatter: int - bot_commands: int discord_py: int esoteric: int -- cgit v1.2.3 From af3c1459ba491e748339545687a8939b4dd70e43 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 15 Jul 2021 15:25:12 -0700 Subject: Add util function to send an infraction using an Infraction dict There was some redundant pre-processing of arguments happening before calling `notify_infraction`. --- bot/exts/moderation/infraction/_scheduler.py | 18 +++------- bot/exts/moderation/infraction/_utils.py | 38 +++++++++++++++++++++- bot/exts/moderation/infraction/superstarify.py | 4 +-- tests/bot/exts/moderation/infraction/test_utils.py | 4 +-- 4 files changed, 45 insertions(+), 19 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 8286d3635..19402d01d 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -162,20 +162,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"]: - dm_result = f"{constants.Emojis.failmail} " - dm_log_text = "\nDM: **Failed**" - - # Sometimes user is a discord.Object; make it a proper user. - try: - if not isinstance(user, (discord.Member, discord.User)): - user = await self.bot.fetch_user(user.id) - except discord.HTTPException as e: - log.error(f"Failed to DM {user.id}: could not fetch user (status {e.status})") + if await _utils.notify_infraction(infraction, user, user_reason): + dm_result = ":incoming_envelope: " + dm_log_text = "\nDM: Sent" else: - # Accordingly display whether the user was successfully notified via DM. - if await _utils.notify_infraction(user, infr_type.replace("_", " ").title(), expiry, user_reason, icon): - dm_result = ":incoming_envelope: " - dm_log_text = "\nDM: Sent" + dm_result = f"{constants.Emojis.failmail} " + dm_log_text = "\nDM: **Failed**" end_msg = "" if infraction["actor"] == self.bot.user.id: diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index adbc641fa..a6f180c8c 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -5,9 +5,11 @@ from datetime import datetime import discord from discord.ext.commands import Context +import bot from bot.api import ResponseCodeError from bot.constants import Colours, Icons from bot.errors import InvalidInfractedUserError +from bot.utils import time log = logging.getLogger(__name__) @@ -152,7 +154,7 @@ async def get_active_infraction( log.trace(f"{user} does not have active infractions of type {infr_type}.") -async def notify_infraction( +async def send_infraction_embed( user: UserObject, infr_type: str, expires_at: t.Optional[str] = None, @@ -188,6 +190,40 @@ async def notify_infraction( return await send_private_embed(user, embed) +async def notify_infraction( + infraction: Infraction, + user: t.Optional[UserSnowflake] = None, + reason: t.Optional[str] = None +) -> bool: + """ + DM a user about their new infraction and return True if the DM is successful. + + `user` and `reason` can be used to override what is in `infraction`. Otherwise, this data will + be retrieved from `infraction`. + + Also return False if the user needs to be fetched but fails to be fetched. + """ + if user is None: + user = discord.Object(infraction["user"]) + + # Sometimes user is a discord.Object; make it a proper user. + try: + if not isinstance(user, (discord.Member, discord.User)): + user = await bot.instance.fetch_user(user.id) + except discord.HTTPException as e: + log.error(f"Failed to DM {user.id}: could not fetch user (status {e.status})") + return False + + type_ = infraction["type"].replace("_", " ").title() + icon = INFRACTION_ICONS[infraction["type"]][0] + expiry = time.format_infraction_with_duration(infraction["expires_at"]) + + if reason is None: + reason = infraction["reason"] + + return await send_infraction_embed(user, type_, expiry, reason, icon) + + async def notify_pardon( user: UserObject, title: str, diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 07e79b9fe..6dd9924ad 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -70,15 +70,13 @@ class Superstarify(InfractionScheduler, Cog): ) notified = await _utils.notify_infraction( + infraction=infraction, user=after, - infr_type="Superstarify", - expires_at=format_infraction(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: diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 50a717bb5..d35120992 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -124,7 +124,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.ctx.send.assert_not_awaited() @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. @@ -230,7 +230,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): send_private_embed_mock.reset_mock() send_private_embed_mock.return_value = case["send_result"] - result = await utils.notify_infraction(*case["args"]) + result = await utils.send_infraction_embed(*case["args"]) self.assertEqual(case["send_result"], result) -- cgit v1.2.3 From 780074f24d110534fd0c1d1975cb351420b61b0a Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 15 Jul 2021 15:25:43 -0700 Subject: Add command to resend infraction embed Resolve #1664 --- bot/exts/moderation/infraction/management.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index b3783cd60..813559030 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -11,6 +11,7 @@ from discord.utils import escape_markdown from bot import constants from bot.bot import Bot from bot.converters import Expiry, Infraction, Snowflake, UserMention, allowed_strings, proxy_user +from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction.infractions import Infractions from bot.exts.moderation.modlog import ModLog from bot.pagination import LinePaginator @@ -38,13 +39,22 @@ 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) -> None: - """Infraction manipulation commands.""" + """Infraction management commands.""" await ctx.send_help(ctx.command) + @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.""" + id_ = infraction["id"] + if await _utils.notify_infraction(infraction): + 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, -- cgit v1.2.3 From fa797cea8d8b5e2115e80802c0b4f4ef2d51609f Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 15 Jul 2021 15:40:03 -0700 Subject: Fix superstarify reason displaying the incorrect nickname Because the edit was happening before the reason string was formatted, the edit updated the state of the user object, causing the nickname to be the superstarified one rather than the one the user was attempting to use. --- bot/exts/moderation/infraction/superstarify.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 6dd9924ad..160c1ad19 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -60,6 +60,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}." @@ -69,17 +75,7 @@ class Superstarify(InfractionScheduler, Cog): reason=f"Superstarified member tried to escape the prison: {infraction['id']}" ) - notified = await _utils.notify_infraction( - infraction=infraction, - user=after, - 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." - ), - ) - - 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() -- cgit v1.2.3 From 44451dc6e12e074376f6693c64f686f2379c00c3 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 15 Jul 2021 19:07:53 -0700 Subject: Disallow resending hidden infractions --- bot/exts/moderation/infraction/management.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index 813559030..aeadee9d0 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -47,6 +47,10 @@ class ModManagement(commands.Cog): @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 + id_ = infraction["id"] if await _utils.notify_infraction(infraction): await ctx.send(f":incoming_envelope: Resent DM for infraction `{id_}`.") -- cgit v1.2.3 From d914046dc5b661d144537715dbf80ae6b361b0e8 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 15 Jul 2021 19:33:33 -0700 Subject: Clarify that a resent infraction DM is not a new infraction. --- bot/exts/moderation/infraction/management.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index aeadee9d0..08d7e0b6d 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -52,7 +52,10 @@ class ModManagement(commands.Cog): return id_ = infraction["id"] - if await _utils.notify_infraction(infraction): + 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, reason=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_}`.") -- cgit v1.2.3 From 940c9048cc95c114d2ea657e3efc3a9d9468d831 Mon Sep 17 00:00:00 2001 From: MarkKoz <1515135+MarkKoz@users.noreply.github.com> Date: Tue, 22 Feb 2022 18:36:26 -0800 Subject: Fix Member fetch in resend infraction command --- bot/exts/moderation/infraction/management.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index c813d1fdc..c12dff928 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -65,9 +65,10 @@ class ModManagement(commands.Cog): await ctx.send(f"{constants.Emojis.failmail} You may not resend hidden infractions.") return - member = await get_or_fetch_member(ctx.guild, infraction["user"]) + 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 `{infraction['user']}`.") + await ctx.send(f"{constants.Emojis.failmail} Cannot find member `{member_id}` in the guild.") return id_ = infraction["id"] -- cgit v1.2.3 From 56f91ef04267bb0fe2e48650ced5b786c50f8499 Mon Sep 17 00:00:00 2001 From: MarkKoz <1515135+MarkKoz@users.noreply.github.com> Date: Sat, 5 Mar 2022 16:28:57 -0800 Subject: Add more expiration details to infraction DMs Separate the expiration timestamp and the duration. Explicitly indicate if an infraction is permanent or expired. Include the time remaining as a humanised delta. --- bot/exts/moderation/infraction/_utils.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index eec539fee..d5c6cd817 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -1,6 +1,7 @@ import typing as t from datetime import datetime +import arrow import discord from discord.ext.commands import Context @@ -44,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" ) @@ -173,7 +175,23 @@ async def notify_infraction( infr_id = infraction["id"] infr_type = infraction["type"].replace("_", " ").title() icon_url = INFRACTION_ICONS[infraction["type"]][0] - expires_at = time.format_with_duration(infraction["expires_at"]) + + if infraction["expires_at"] is None: + expires_at = "Never" + duration = "Permanent" + else: + expiry = arrow.get(infraction["expires_at"]) + now = arrow.utcnow() + + expires_at = time.format_relative(expiry) + duration = time.humanize_delta(infraction["inserted_at"], expiry, max_units=2) + + if expiry < now: + expires_at += " (Expired)" + else: + remaining = time.humanize_delta(expiry, now, max_units=2) + if duration != remaining: + duration += f" ({remaining} remaining)" log.trace(f"Sending {user} a DM about their {infr_type} infraction.") @@ -182,7 +200,8 @@ async def notify_infraction( 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." ) -- cgit v1.2.3 From 60c5762436dcacef5c67dfc266d3bbecf7349cfb Mon Sep 17 00:00:00 2001 From: MarkKoz <1515135+MarkKoz@users.noreply.github.com> Date: Sat, 5 Mar 2022 16:52:06 -0800 Subject: Fix detection of expired infractions in DMs --- bot/exts/moderation/infraction/_utils.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index e319f9d71..36e818ec6 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -181,17 +181,15 @@ async def notify_infraction( duration = "Permanent" else: expiry = arrow.get(infraction["expires_at"]) - now = arrow.utcnow() - expires_at = time.format_relative(expiry) duration = time.humanize_delta(infraction["inserted_at"], expiry, max_units=2) - if expiry < now: - expires_at += " (Expired)" - else: - remaining = time.humanize_delta(expiry, now, 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.") -- cgit v1.2.3