aboutsummaryrefslogtreecommitdiffstats
path: root/bot/cogs/moderation.py
diff options
context:
space:
mode:
authorGravatar sco1 <[email protected]>2019-09-18 14:00:57 -0700
committerGravatar sco1 <[email protected]>2019-09-18 14:10:06 -0700
commit2ac0c6df978f20a488b3eb026753c8a972cb2554 (patch)
tree43fc70b14b517139dea5324e762ba92a9e211e28 /bot/cogs/moderation.py
parentDocstring linting chunk 7 (diff)
parentMerge pull request #436 from python-discord/enhance-offtopicnames-search (diff)
Merge branch 'master' into flake8-plugins
Diffstat (limited to '')
-rw-r--r--bot/cogs/moderation.py353
1 files changed, 175 insertions, 178 deletions
diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py
index 28956e636..d407a90fe 100644
--- a/bot/cogs/moderation.py
+++ b/bot/cogs/moderation.py
@@ -2,13 +2,13 @@ import asyncio
import logging
import textwrap
from datetime import datetime
-from typing import Union
+from typing import Dict, Union
from discord import (
- Colour, Embed, Forbidden, Guild, HTTPException, Member, Object, User
+ Colour, Embed, Forbidden, Guild, HTTPException, Member, NotFound, Object, User
)
from discord.ext.commands import (
- BadArgument, BadUnionArgument, Bot, Context, command, group
+ BadArgument, BadUnionArgument, Bot, Cog, Context, command, group
)
from bot import constants
@@ -17,7 +17,7 @@ from bot.constants import Colours, Event, Icons, MODERATION_ROLES
from bot.converters import ExpirationDate, InfractionSearchQuery
from bot.decorators import with_role
from bot.pagination import LinePaginator
-from bot.utils.moderation import post_infraction
+from bot.utils.moderation import already_has_active_infraction, post_infraction
from bot.utils.scheduling import Scheduler, create_task
from bot.utils.time import wait_until
@@ -28,7 +28,7 @@ INFRACTION_ICONS = {
"Kick": Icons.sign_out,
"Ban": Icons.user_ban
}
-RULES_URL = "https://pythondiscord.com/about/rules"
+RULES_URL = "https://pythondiscord.com/pages/rules"
APPEALABLE_INFRACTIONS = ("Ban", "Mute")
@@ -47,7 +47,7 @@ def proxy_user(user_id: str) -> Object:
UserTypes = Union[Member, User, proxy_user]
-class Moderation(Scheduler):
+class Moderation(Scheduler, Cog):
"""Server moderation tools."""
def __init__(self, bot: Bot):
@@ -60,8 +60,10 @@ class Moderation(Scheduler):
"""Get currently loaded ModLog cog instance."""
return self.bot.get_cog("ModLog")
+ @Cog.listener()
async def on_ready(self) -> None:
"""Schedule expiration for previous infractions."""
+ # Schedule expiration for previous infractions
infractions = await self.bot.api_client.get(
'bot/infractions', params={'active': 'true'}
)
@@ -74,21 +76,12 @@ class Moderation(Scheduler):
@with_role(*MODERATION_ROLES)
@command()
async def warn(self, ctx: Context, user: UserTypes, *, reason: str = None) -> None:
- """
- Create a warning infraction in the database for a user.
-
- **`user`:** Accepts user mention, ID, etc.
- **`reason`:** The reason for the warning.
- """
- response_object = await post_infraction(ctx, user, type="warning", reason=reason)
- if response_object is None:
+ """Create a warning infraction in the database for a user."""
+ infraction = await post_infraction(ctx, user, type="warning", reason=reason)
+ if infraction is None:
return
- notified = await self.notify_infraction(
- user=user,
- infr_type="Warning",
- reason=reason
- )
+ notified = await self.notify_infraction(user=user, infr_type="Warning", reason=reason)
dm_result = ":incoming_envelope: " if notified else ""
action = f"{dm_result}:ok_hand: warned {user.mention}"
@@ -117,7 +110,7 @@ class Moderation(Scheduler):
Reason: {reason}
"""),
content=log_content,
- footer=f"ID {response_object['infraction']['id']}"
+ footer=f"ID {infraction['id']}"
)
@with_role(*MODERATION_ROLES)
@@ -129,15 +122,11 @@ class Moderation(Scheduler):
# Warning is sent to ctx by the helper method
return
- response_object = await post_infraction(ctx, user, type="kick", reason=reason)
- if response_object is None:
+ infraction = await post_infraction(ctx, user, type="kick", reason=reason)
+ if infraction is None:
return
- notified = await self.notify_infraction(
- user=user,
- infr_type="Kick",
- reason=reason
- )
+ notified = await self.notify_infraction(user=user, infr_type="Kick", reason=reason)
self.mod_log.ignore(Event.member_remove, user.id)
@@ -171,7 +160,7 @@ class Moderation(Scheduler):
Reason: {reason}
"""),
content=log_content,
- footer=f"ID {response_object['infraction']['id']}"
+ footer=f"ID {infraction['id']}"
)
@with_role(*MODERATION_ROLES)
@@ -183,22 +172,11 @@ class Moderation(Scheduler):
# Warning is sent to ctx by the helper method
return
- active_bans = await self.bot.api_client.get(
- 'bot/infractions',
- params={
- 'active': 'true',
- 'type': 'ban',
- 'user__id': str(user.id)
- }
- )
- if active_bans:
- return await ctx.send(
- ":x: According to my records, this user is already banned. "
- f"See infraction **#{active_bans[0]['id']}**."
- )
+ if await already_has_active_infraction(ctx=ctx, user=user, type="ban"):
+ return
- response_object = await post_infraction(ctx, user, type="ban", reason=reason)
- if response_object is None:
+ infraction = await post_infraction(ctx, user, type="ban", reason=reason)
+ if infraction is None:
return
notified = await self.notify_infraction(
@@ -242,29 +220,18 @@ class Moderation(Scheduler):
Reason: {reason}
"""),
content=log_content,
- footer=f"ID {response_object['infraction']['id']}"
+ footer=f"ID {infraction['id']}"
)
@with_role(*MODERATION_ROLES)
@command()
async def mute(self, ctx: Context, user: Member, *, reason: str = None) -> None:
"""Create a permanent mute infraction for a user with the provided reason."""
- active_mutes = await self.bot.api_client.get(
- 'bot/infractions',
- params={
- 'active': 'true',
- 'type': 'mute',
- 'user__id': str(user.id)
- }
- )
- if active_mutes:
- return await ctx.send(
- ":x: According to my records, this user is already muted. "
- f"See infraction **#{active_mutes[0]['id']}**."
- )
+ if await already_has_active_infraction(ctx=ctx, user=user, type="mute"):
+ return
- response_object = await post_infraction(ctx, user, type="mute", reason=reason)
- if response_object is None:
+ infraction = await post_infraction(ctx, user, type="mute", reason=reason)
+ if infraction is None:
return
self.mod_log.ignore(Event.member_update, user.id)
@@ -304,7 +271,7 @@ class Moderation(Scheduler):
Reason: {reason}
"""),
content=log_content,
- footer=f"ID {response_object['infraction']['id']}"
+ footer=f"ID {infraction['id']}"
)
# endregion
@@ -312,34 +279,20 @@ class Moderation(Scheduler):
@with_role(*MODERATION_ROLES)
@command()
- async def tempmute(
- self, ctx: Context, user: Member, expiration: ExpirationDate,
- *, reason: str = None
- ) -> None:
+ async def tempmute(self, ctx: Context, user: Member, duration: ExpirationDate, *, reason: str = None) -> None:
"""
Create a temporary mute infraction for a user with the provided expiration and reason.
Duration strings are parsed per: http://strftime.org/
"""
- active_mutes = await self.bot.api_client.get(
- 'bot/infractions',
- params={
- 'active': 'true',
- 'type': 'mute',
- 'user__id': str(user.id)
- }
- )
- if active_mutes:
- return await ctx.send(
- ":x: According to my records, this user is already muted. "
- f"See infraction **#{active_mutes[0]['id']}**."
- )
+ expiration = duration
- infraction = await post_infraction(
- ctx, user,
- type="mute", reason=reason,
- expires_at=expiration
- )
+ if await already_has_active_infraction(ctx=ctx, user=user, type="mute"):
+ return
+
+ infraction = await post_infraction(ctx, user, type="mute", reason=reason, expires_at=expiration)
+ if infraction is None:
+ return
self.mod_log.ignore(Event.member_update, user.id)
await user.add_roles(self._muted_role, reason=reason)
@@ -392,44 +345,30 @@ class Moderation(Scheduler):
@with_role(*MODERATION_ROLES)
@command()
- async def tempban(
- self, ctx: Context, user: UserTypes, expiry: ExpirationDate, *, reason: str = None
- ) -> None:
+ async def tempban(self, ctx: Context, user: UserTypes, duration: ExpirationDate, *, reason: str = None) -> None:
"""
Create a temporary ban infraction for a user with the provided expiration and reason.
Duration strings are parsed per: http://strftime.org/
"""
+ expiration = duration
+
if not await self.respect_role_hierarchy(ctx, user, 'tempban'):
# Ensure ctx author has a higher top role than the target user
# Warning is sent to ctx by the helper method
return
- active_bans = await self.bot.api_client.get(
- 'bot/infractions',
- params={
- 'active': 'true',
- 'type': 'ban',
- 'user__id': str(user.id)
- }
- )
- if active_bans:
- return await ctx.send(
- ":x: According to my records, this user is already banned. "
- f"See infraction **#{active_bans[0]['id']}**."
- )
+ if await already_has_active_infraction(ctx=ctx, user=user, type="ban"):
+ return
- infraction = await post_infraction(
- ctx, user, type="ban",
- reason=reason, expires_at=expiry
- )
+ infraction = await post_infraction(ctx, user, type="ban", reason=reason, expires_at=expiration)
if infraction is None:
return
notified = await self.notify_infraction(
user=user,
infr_type="Ban",
- expires_at=expiry,
+ expires_at=expiration,
reason=reason
)
@@ -491,11 +430,8 @@ class Moderation(Scheduler):
This does not send the user a notification
"""
- response_object = await post_infraction(
- ctx, user, type="warning", reason=reason, hidden=True
- )
-
- if response_object is None:
+ infraction = await post_infraction(ctx, user, type="warning", reason=reason, hidden=True)
+ if infraction is None:
return
if reason is None:
@@ -513,7 +449,7 @@ class Moderation(Scheduler):
Actor: {ctx.message.author}
Reason: {reason}
"""),
- footer=f"ID {response_object['infraction']['id']}"
+ footer=f"ID {infraction['id']}"
)
@with_role(*MODERATION_ROLES)
@@ -529,8 +465,8 @@ class Moderation(Scheduler):
# Warning is sent to ctx by the helper method
return
- response_object = await post_infraction(ctx, user, type="kick", reason=reason, hidden=True)
- if response_object is None:
+ infraction = await post_infraction(ctx, user, type="kick", reason=reason, hidden=True)
+ if infraction is None:
return
self.mod_log.ignore(Event.member_remove, user.id)
@@ -564,7 +500,7 @@ class Moderation(Scheduler):
Reason: {reason}
"""),
content=log_content,
- footer=f"ID {response_object['infraction']['id']}"
+ footer=f"ID {infraction['id']}"
)
@with_role(*MODERATION_ROLES)
@@ -580,8 +516,11 @@ class Moderation(Scheduler):
# Warning is sent to ctx by the helper method
return
- response_object = await post_infraction(ctx, user, type="ban", reason=reason, hidden=True)
- if response_object is None:
+ if await already_has_active_infraction(ctx=ctx, user=user, type="ban"):
+ return
+
+ infraction = await post_infraction(ctx, user, type="ban", reason=reason, hidden=True)
+ if infraction is None:
return
self.mod_log.ignore(Event.member_ban, user.id)
@@ -616,7 +555,7 @@ class Moderation(Scheduler):
Reason: {reason}
"""),
content=log_content,
- footer=f"ID {response_object['infraction']['id']}"
+ footer=f"ID {infraction['id']}"
)
@with_role(*MODERATION_ROLES)
@@ -627,8 +566,11 @@ class Moderation(Scheduler):
This does not send the user a notification.
"""
- response_object = await post_infraction(ctx, user, type="mute", reason=reason, hidden=True)
- if response_object is None:
+ if await already_has_active_infraction(ctx=ctx, user=user, type="mute"):
+ return
+
+ infraction = await post_infraction(ctx, user, type="mute", reason=reason, hidden=True)
+ if infraction is None:
return
self.mod_log.ignore(Event.member_update, user.id)
@@ -649,7 +591,7 @@ class Moderation(Scheduler):
Actor: {ctx.message.author}
Reason: {reason}
"""),
- footer=f"ID {response_object['infraction']['id']}"
+ footer=f"ID {infraction['id']}"
)
# endregion
@@ -658,7 +600,7 @@ class Moderation(Scheduler):
@with_role(*MODERATION_ROLES)
@command(hidden=True, aliases=["shadowtempmute, stempmute"])
async def shadow_tempmute(
- self, ctx: Context, user: Member, duration: str, *, reason: str = None
+ self, ctx: Context, user: Member, duration: ExpirationDate, *, reason: str = None
) -> None:
"""
Create a temporary mute infraction for a user with the provided reason.
@@ -667,19 +609,25 @@ class Moderation(Scheduler):
This does not send the user a notification.
"""
- response_object = await post_infraction(
- ctx, user, type="mute", reason=reason, duration=duration, hidden=True
- )
- if response_object is None:
+ expiration = duration
+
+ if await already_has_active_infraction(ctx=ctx, user=user, type="mute"):
+ return
+
+ infraction = await post_infraction(ctx, user, type="mute", reason=reason, expires_at=expiration, hidden=True)
+ if infraction is None:
return
self.mod_log.ignore(Event.member_update, user.id)
await user.add_roles(self._muted_role, reason=reason)
- infraction_object = response_object["infraction"]
- infraction_expiration = infraction_object["expires_at"]
+ infraction_expiration = (
+ datetime
+ .fromisoformat(infraction["expires_at"][:-1])
+ .strftime('%c')
+ )
- self.schedule_expiration(ctx.bot.loop, infraction_object)
+ self.schedule_task(ctx.bot.loop, infraction["id"], infraction)
if reason is None:
await ctx.send(f":ok_hand: muted {user.mention} until {infraction_expiration}.")
@@ -697,16 +645,15 @@ class Moderation(Scheduler):
Member: {user.mention} (`{user.id}`)
Actor: {ctx.message.author}
Reason: {reason}
- Duration: {duration}
Expires: {infraction_expiration}
"""),
- footer=f"ID {response_object['infraction']['id']}"
+ footer=f"ID {infraction['id']}"
)
@with_role(*MODERATION_ROLES)
@command(hidden=True, aliases=["shadowtempban, stempban"])
async def shadow_tempban(
- self, ctx: Context, user: UserTypes, duration: str, *, reason: str = None
+ self, ctx: Context, user: UserTypes, duration: ExpirationDate, *, reason: str = None
) -> None:
"""
Create a temporary ban infraction for a user with the provided reason.
@@ -715,15 +662,18 @@ class Moderation(Scheduler):
This does not send the user a notification.
"""
+ expiration = duration
+
if not await self.respect_role_hierarchy(ctx, user, 'shadowtempban'):
# Ensure ctx author has a higher top role than the target user
# Warning is sent to ctx by the helper method
return
- response_object = await post_infraction(
- ctx, user, type="ban", reason=reason, duration=duration, hidden=True
- )
- if response_object is None:
+ if await already_has_active_infraction(ctx=ctx, user=user, type="ban"):
+ return
+
+ infraction = await post_infraction(ctx, user, type="ban", reason=reason, expires_at=expiration, hidden=True)
+ if infraction is None:
return
self.mod_log.ignore(Event.member_ban, user.id)
@@ -735,10 +685,13 @@ class Moderation(Scheduler):
except Forbidden:
action_result = False
- infraction_object = response_object["infraction"]
- infraction_expiration = infraction_object["expires_at"]
+ infraction_expiration = (
+ datetime
+ .fromisoformat(infraction["expires_at"][:-1])
+ .strftime('%c')
+ )
- self.schedule_expiration(ctx.bot.loop, infraction_object)
+ self.schedule_task(ctx.bot.loop, infraction["id"], infraction)
if reason is None:
await ctx.send(f":ok_hand: banned {user.mention} until {infraction_expiration}.")
@@ -764,11 +717,10 @@ class Moderation(Scheduler):
Member: {user.mention} (`{user.id}`)
Actor: {ctx.message.author}
Reason: {reason}
- Duration: {duration}
Expires: {infraction_expiration}
"""),
content=log_content,
- footer=f"ID {response_object['infraction']['id']}"
+ footer=f"ID {infraction['id']}"
)
# endregion
@@ -776,7 +728,7 @@ class Moderation(Scheduler):
@with_role(*MODERATION_ROLES)
@command()
- async def unmute(self, ctx: Context, user: Member) -> None:
+ async def unmute(self, ctx: Context, user: UserTypes) -> None:
"""Deactivates the active mute infraction for a user."""
try:
# check the current active infraction
@@ -793,14 +745,15 @@ class Moderation(Scheduler):
if not response:
# no active infraction
- return await ctx.send(
+ await ctx.send(
f":x: There is no active mute infraction for user {user.mention}."
)
+ return
- infraction = response[0]
- await self._deactivate_infraction(infraction)
- if infraction["expires_at"] is not None:
- self.cancel_expiration(infraction["id"])
+ for infraction in response:
+ await self._deactivate_infraction(infraction)
+ if infraction["expires_at"] is not None:
+ self.cancel_expiration(infraction["id"])
notified = await self.notify_pardon(
user=user,
@@ -820,19 +773,31 @@ class Moderation(Scheduler):
await ctx.send(f"{dm_emoji}:ok_hand: Un-muted {user.mention}.")
+ embed_text = textwrap.dedent(
+ f"""
+ Member: {user.mention} (`{user.id}`)
+ Actor: {ctx.message.author}
+ DM: {dm_status}
+ """
+ )
+
+ if len(response) > 1:
+ footer = f"Infraction IDs: {', '.join(str(infr['id']) for infr in response)}"
+ title = "Member unmuted"
+ embed_text += "Note: User had multiple **active** mute infractions in the database."
+ else:
+ infraction = response[0]
+ footer = f"Infraction ID: {infraction['id']}"
+ title = "Member unmuted"
+
# Send a log message to the mod log
await self.mod_log.send_log_message(
icon_url=Icons.user_unmute,
colour=Colour(Colours.soft_green),
- title="Member unmuted",
+ title=title,
thumbnail=user.avatar_url_as(static_format="png"),
- text=textwrap.dedent(f"""
- Member: {user.mention} (`{user.id}`)
- Actor: {ctx.message.author}
- Intended expiry: {infraction['expires_at']}
- DM: {dm_status}
- """),
- footer=infraction["id"],
+ text=embed_text,
+ footer=footer,
content=log_content
)
except Exception:
@@ -861,14 +826,29 @@ class Moderation(Scheduler):
if not response:
# no active infraction
- return await ctx.send(
+ await ctx.send(
f":x: There is no active ban infraction for user {user.mention}."
)
+ return
- infraction = response[0]
- await self._deactivate_infraction(infraction)
- if infraction["expires_at"] is not None:
- self.cancel_expiration(infraction["id"])
+ for infraction in response:
+ await self._deactivate_infraction(infraction)
+ if infraction["expires_at"] is not None:
+ self.cancel_expiration(infraction["id"])
+
+ embed_text = textwrap.dedent(
+ f"""
+ Member: {user.mention} (`{user.id}`)
+ Actor: {ctx.message.author}
+ """
+ )
+
+ if len(response) > 1:
+ footer = f"Infraction IDs: {', '.join(str(infr['id']) for infr in response)}"
+ embed_text += "Note: User had multiple **active** ban infractions in the database."
+ else:
+ infraction = response[0]
+ footer = f"Infraction ID: {infraction['id']}"
await ctx.send(f":ok_hand: Un-banned {user.mention}.")
@@ -878,11 +858,8 @@ class Moderation(Scheduler):
colour=Colour(Colours.soft_green),
title="Member unbanned",
thumbnail=user.avatar_url_as(static_format="png"),
- text=textwrap.dedent(f"""
- Member: {user.mention} (`{user.id}`)
- Actor: {ctx.message.author}
- Intended expiry: {infraction['expires_at']}
- """)
+ text=embed_text,
+ footer=footer,
)
except Exception:
log.exception("There was an error removing an infraction.")
@@ -1008,7 +985,8 @@ class Moderation(Scheduler):
except Exception:
log.exception("There was an error updating an infraction.")
- return await ctx.send(":x: There was an error updating the infraction.")
+ await ctx.send(":x: There was an error updating the infraction.")
+ return
# Get information about the infraction's user
user_id = updated_infraction['user']
@@ -1105,7 +1083,9 @@ class Moderation(Scheduler):
# endregion
# region: Utility functions
- def schedule_expiration(self, loop: asyncio.AbstractEventLoop, infraction_object: dict) -> None:
+ def schedule_expiration(
+ self, loop: asyncio.AbstractEventLoop, infraction_object: Dict[str, Union[str, int, bool]]
+ ) -> None:
"""Schedules a task to expire a temporary infraction."""
infraction_id = infraction_object["id"]
if infraction_id in self.scheduled_tasks:
@@ -1125,7 +1105,7 @@ class Moderation(Scheduler):
log.debug(f"Unscheduled {infraction_id}.")
del self.scheduled_tasks[infraction_id]
- async def _scheduled_task(self, infraction_object: dict) -> None:
+ async def _scheduled_task(self, infraction_object: Dict[str, Union[str, int, bool]]) -> None:
"""
Marks an infraction expired after the delay from time of scheduling to time of expiration.
@@ -1153,7 +1133,7 @@ class Moderation(Scheduler):
icon_url=Icons.user_unmute
)
- async def _deactivate_infraction(self, infraction_object: dict) -> None:
+ async def _deactivate_infraction(self, infraction_object: Dict[str, Union[str, int, bool]]) -> None:
"""
A co-routine which marks an infraction as inactive on the website.
@@ -1173,14 +1153,17 @@ class Moderation(Scheduler):
log.warning(f"Failed to un-mute user: {user_id} (not found)")
elif infraction_type == "ban":
user: Object = Object(user_id)
- await guild.unban(user)
+ try:
+ await guild.unban(user)
+ except NotFound:
+ log.info(f"Tried to unban user `{user_id}`, but Discord does not have an active ban registered.")
await self.bot.api_client.patch(
'bot/infractions/' + str(infraction_object['id']),
json={"active": False}
)
- def _infraction_to_string(self, infraction_object: dict) -> str:
+ def _infraction_to_string(self, infraction_object: Dict[str, Union[str, int, bool]]) -> str:
"""Convert the infraction object to a string representation."""
actor_id = infraction_object["actor"]
guild: Guild = self.bot.get_guild(constants.Guild.id)
@@ -1188,6 +1171,11 @@ class Moderation(Scheduler):
active = infraction_object["active"]
user_id = infraction_object["user"]
hidden = infraction_object["hidden"]
+ created = datetime.fromisoformat(infraction_object["inserted_at"]).strftime("%Y-%m-%d %H:%M")
+ if infraction_object["expires_at"] is None:
+ expires = "*Permanent*"
+ else:
+ expires = datetime.fromisoformat(infraction_object["expires_at"]).strftime("%Y-%m-%d %H:%M")
lines = textwrap.dedent(f"""
{"**===============**" if active else "==============="}
@@ -1196,8 +1184,8 @@ class Moderation(Scheduler):
Type: **{infraction_object["type"]}**
Shadow: {hidden}
Reason: {infraction_object["reason"] or "*None*"}
- Created: {infraction_object["inserted_at"]}
- Expires: {infraction_object["expires_at"] or "*Permanent*"}
+ Created: {created}
+ Expires: {expires}
Actor: {actor.mention if actor else actor_id}
ID: `{infraction_object["id"]}`
{"**===============**" if active else "==============="}
@@ -1206,13 +1194,16 @@ class Moderation(Scheduler):
return lines.strip()
async def notify_infraction(
- self, user: Union[User, Member], infr_type: str,
- expires_at: Union[datetime, str] = 'N/A', reason: str = "No reason provided."
+ self,
+ user: Union[User, Member],
+ infr_type: str,
+ expires_at: Union[datetime, str] = 'N/A',
+ reason: str = "No reason provided."
) -> bool:
"""
Attempt to notify a user, via DM, of their fresh infraction.
- Optionally returns a boolean indicator of whether the DM was successful.
+ Returns a boolean indicator of whether the DM was successful.
"""
if isinstance(expires_at, datetime):
expires_at = expires_at.strftime('%c')
@@ -1237,8 +1228,11 @@ class Moderation(Scheduler):
return await self.send_private_embed(user, embed)
async def notify_pardon(
- self, user: Union[User, Member], title: str, content: str,
- icon_url: str = Icons.user_verified
+ self,
+ user: Union[User, Member],
+ title: str,
+ content: str,
+ icon_url: str = Icons.user_verified
) -> bool:
"""
Attempt to notify a user, via DM, of their expired infraction.
@@ -1261,7 +1255,7 @@ class Moderation(Scheduler):
Returns a boolean indicator of DM success.
"""
# sometimes `user` is a `discord.Object`, so let's make it a proper user.
- user = await self.bot.get_user_info(user.id)
+ user = await self.bot.fetch_user(user.id)
try:
await user.send(embed=embed)
@@ -1288,13 +1282,16 @@ class Moderation(Scheduler):
# endregion
- async def __error(self, ctx: Context, error: Exception) -> None:
+ @staticmethod
+ async def cog_command_error(ctx: Context, error: Exception) -> None:
"""Send a notification to the invoking context on a Union failure."""
if isinstance(error, BadUnionArgument):
if User in error.converters:
await ctx.send(str(error.errors[0]))
+ error.handled = True
- async def respect_role_hierarchy(self, ctx: Context, target: UserTypes, infr_type: str) -> bool:
+ @staticmethod
+ async def respect_role_hierarchy(ctx: Context, target: UserTypes, infr_type: str) -> bool:
"""
Check if the highest role of the invoking member is greater than that of the target member.