diff options
| -rw-r--r-- | bot/cogs/moderation/infractions.py | 48 | ||||
| -rw-r--r-- | bot/cogs/moderation/management.py | 28 | ||||
| -rw-r--r-- | bot/cogs/moderation/modlog.py | 94 | ||||
| -rw-r--r-- | bot/cogs/moderation/superstarify.py | 17 | ||||
| -rw-r--r-- | bot/cogs/moderation/utils.py | 4 | 
5 files changed, 96 insertions, 95 deletions
| diff --git a/bot/cogs/moderation/infractions.py b/bot/cogs/moderation/infractions.py index 4c903debd..0b5d2bfb0 100644 --- a/bot/cogs/moderation/infractions.py +++ b/bot/cogs/moderation/infractions.py @@ -1,34 +1,36 @@  import logging  import textwrap -from typing import Awaitable, Dict, Optional, Union +import typing as t  import dateutil.parser -from discord import Colour, Forbidden, HTTPException, Member, NotFound, Object, User -from discord.ext.commands import BadUnionArgument, Bot, Cog, Context, command +import discord +from discord import Member +from discord.ext import commands +from discord.ext.commands import Context, command  from bot import constants  from bot.api import ResponseCodeError  from bot.constants import Colours, Event  from bot.converters import Duration  from bot.decorators import respect_role_hierarchy +from bot.utils import time  from bot.utils.checks import with_role_check  from bot.utils.scheduling import Scheduler -from bot.utils.time import format_infraction, wait_until  from . import utils  from .modlog import ModLog  from .utils import MemberObject  log = logging.getLogger(__name__) -MemberConverter = Union[Member, User, utils.proxy_user] +MemberConverter = t.Union[utils.UserTypes, utils.proxy_user] -class Infractions(Scheduler, Cog): +class Infractions(Scheduler, commands.Cog):      """Server moderation tools.""" -    def __init__(self, bot: Bot): +    def __init__(self, bot: commands.Bot):          self.bot = bot -        self._muted_role = Object(constants.Roles.muted) +        self._muted_role = discord.Object(constants.Roles.muted)          super().__init__()      @property @@ -36,7 +38,7 @@ class Infractions(Scheduler, Cog):          """Get currently loaded ModLog cog instance."""          return self.bot.get_cog("ModLog") -    @Cog.listener() +    @commands.Cog.listener()      async def on_ready(self) -> None:          """Schedule expiration for previous infractions."""          infractions = await self.bot.api_client.get( @@ -211,7 +213,7 @@ class Infractions(Scheduler, Cog):          _id = infraction["id"]          expiry = dateutil.parser.isoparse(infraction["expires_at"]).replace(tzinfo=None) -        await wait_until(expiry) +        await time.wait_until(expiry)          log.debug(f"Marking infraction {_id} as inactive (expired).")          await self.deactivate_infraction(infraction) @@ -220,7 +222,7 @@ class Infractions(Scheduler, Cog):          self,          infraction: utils.Infraction,          send_log: bool = True -    ) -> Dict[str, str]: +    ) -> t.Dict[str, str]:          """          Deactivate an active infraction and return a dictionary of lines to send in a mod log. @@ -263,21 +265,21 @@ class Infractions(Scheduler, Cog):                      log.info(f"Failed to unmute user {user_id}: user not found")                      log_text["Failure"] = "User was not found in the guild."              elif _type == "ban": -                user = Object(user_id) +                user = discord.Object(user_id)                  self.mod_log.ignore(Event.member_unban, user_id)                  try:                      await guild.unban(user, reason=reason) -                except NotFound: +                except discord.NotFound:                      log.info(f"Failed to unban user {user_id}: no active ban found on Discord")                      log_text["Failure"] = "No active ban found on Discord."              else:                  raise ValueError(                      f"Attempted to deactivate an unsupported infraction #{_id} ({_type})!"                  ) -        except Forbidden: +        except discord.Forbidden:              log.warning(f"Failed to deactivate infraction #{_id} ({_type}): bot lacks permissions")              log_text["Failure"] = f"The bot lacks permissions to do this (role hierarchy?)" -        except HTTPException as e: +        except discord.HTTPException as e:              log.exception(f"Failed to deactivate infraction #{_id} ({_type})")              log_text["Failure"] = f"HTTPException with code {e.code}." @@ -307,7 +309,7 @@ class Infractions(Scheduler, Cog):              await self.mod_log.send_log_message(                  icon_url=utils.INFRACTION_ICONS[_type][1], -                colour=Colour(Colours.soft_green), +                colour=Colours.soft_green,                  title=f"Infraction {log_title}: {_type}",                  text="\n".join(f"{k}: {v}" for k, v in log_text.items()),                  footer=f"ID: {_id}", @@ -320,7 +322,7 @@ class Infractions(Scheduler, Cog):          ctx: Context,          infraction: utils.Infraction,          user: MemberObject, -        action_coro: Optional[Awaitable] = None +        action_coro: t.Optional[t.Awaitable] = None      ) -> None:          """Apply an infraction to the user, log the infraction, and optionally notify the user."""          infr_type = infraction["type"] @@ -329,7 +331,7 @@ class Infractions(Scheduler, Cog):          expiry = infraction["expires_at"]          if expiry: -            expiry = format_infraction(expiry) +            expiry = time.format_infraction(expiry)          # Default values for the confirmation message and mod log.          confirm_msg = f":ok_hand: applied" @@ -360,7 +362,7 @@ class Infractions(Scheduler, Cog):                  if expiry:                      # Schedule the expiration of the infraction.                      self.schedule_task(ctx.bot.loop, infraction["id"], infraction) -            except Forbidden: +            except discord.Forbidden:                  # Accordingly display that applying the infraction failed.                  confirm_msg = f":x: failed to apply"                  expiry_msg = "" @@ -373,7 +375,7 @@ class Infractions(Scheduler, Cog):          # Send a log message to the mod log.          await self.mod_log.send_log_message(              icon_url=icon, -            colour=Colour(Colours.soft_red), +            colour=Colours.soft_red,              title=f"Infraction {log_title}: {infr_type}",              thumbnail=user.avatar_url_as(static_format="png"),              text=textwrap.dedent(f""" @@ -464,7 +466,7 @@ class Infractions(Scheduler, Cog):          # Send a log message to the mod log.          await self.mod_log.send_log_message(              icon_url=utils.INFRACTION_ICONS[infr_type][1], -            colour=Colour(Colours.soft_green), +            colour=Colours.soft_green,              title=f"Infraction {log_title}: {infr_type}",              thumbnail=user.avatar_url_as(static_format="png"),              text="\n".join(f"{k}: {v}" for k, v in log_text.items()), @@ -482,7 +484,7 @@ class Infractions(Scheduler, Cog):      # This cannot be static (must have a __func__ attribute).      async def cog_command_error(self, 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: +        if isinstance(error, commands.BadUnionArgument): +            if discord.User in error.converters:                  await ctx.send(str(error.errors[0]))                  error.handled = True diff --git a/bot/cogs/moderation/management.py b/bot/cogs/moderation/management.py index f159082aa..567c4e2df 100644 --- a/bot/cogs/moderation/management.py +++ b/bot/cogs/moderation/management.py @@ -12,13 +12,13 @@ from bot.converters import Duration, InfractionSearchQuery  from bot.pagination import LinePaginator  from bot.utils import time  from bot.utils.checks import with_role_check +from . import utils  from .infractions import Infractions  from .modlog import ModLog -from .utils import Infraction, proxy_user  log = logging.getLogger(__name__) -UserConverter = t.Union[discord.User, proxy_user] +UserConverter = t.Union[discord.User, utils.proxy_user]  def permanent_duration(expires_at: str) -> str: @@ -191,7 +191,7 @@ class ModManagement(commands.Cog):          self,          ctx: Context,          embed: discord.Embed, -        infractions: t.Iterable[Infraction] +        infractions: t.Iterable[utils.Infraction]      ) -> None:          """Send a paginated embed of infractions for the specified user."""          if not infractions: @@ -212,31 +212,31 @@ class ModManagement(commands.Cog):              max_size=1000          ) -    def infraction_to_string(self, infraction_object: Infraction) -> str: +    def infraction_to_string(self, infraction: utils.Infraction) -> str:          """Convert the infraction object to a string representation.""" -        actor_id = infraction_object["actor"] +        actor_id = infraction["actor"]          guild = self.bot.get_guild(constants.Guild.id)          actor = guild.get_member(actor_id) -        active = infraction_object["active"] -        user_id = infraction_object["user"] -        hidden = infraction_object["hidden"] -        created = time.format_infraction(infraction_object["inserted_at"]) -        if infraction_object["expires_at"] is None: +        active = infraction["active"] +        user_id = infraction["user"] +        hidden = infraction["hidden"] +        created = time.format_infraction(infraction["inserted_at"]) +        if infraction["expires_at"] is None:              expires = "*Permanent*"          else: -            expires = time.format_infraction(infraction_object["expires_at"]) +            expires = time.format_infraction(infraction["expires_at"])          lines = textwrap.dedent(f"""              {"**===============**" if active else "==============="}              Status: {"__**Active**__" if active else "Inactive"}              User: {self.bot.get_user(user_id)} (`{user_id}`) -            Type: **{infraction_object["type"]}** +            Type: **{infraction["type"]}**              Shadow: {hidden} -            Reason: {infraction_object["reason"] or "*None*"} +            Reason: {infraction["reason"] or "*None*"}              Created: {created}              Expires: {expires}              Actor: {actor.mention if actor else actor_id} -            ID: `{infraction_object["id"]}` +            ID: `{infraction["id"]}`              {"**===============**" if active else "==============="}          """) diff --git a/bot/cogs/moderation/modlog.py b/bot/cogs/moderation/modlog.py index e929b2aab..86eab55de 100644 --- a/bot/cogs/moderation/modlog.py +++ b/bot/cogs/moderation/modlog.py @@ -1,26 +1,22 @@  import asyncio  import logging +import typing as t  from datetime import datetime -from typing import List, Optional, Union +import discord  from dateutil.relativedelta import relativedelta  from deepdiff import DeepDiff -from discord import ( -    Asset, CategoryChannel, Colour, Embed, File, Guild, -    Member, Message, NotFound, RawMessageDeleteEvent, -    RawMessageUpdateEvent, Role, TextChannel, User, VoiceChannel -) +from discord import Colour  from discord.abc import GuildChannel  from discord.ext.commands import Bot, Cog, Context -from bot.constants import ( -    Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, URLs -) +from bot.constants import Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, URLs  from bot.utils.time import humanize_delta +from .utils import UserTypes  log = logging.getLogger(__name__) -GUILD_CHANNEL = Union[CategoryChannel, TextChannel, VoiceChannel] +GUILD_CHANNEL = t.Union[discord.CategoryChannel, discord.TextChannel, discord.VoiceChannel]  CHANNEL_CHANGES_UNSUPPORTED = ("permissions",)  CHANNEL_CHANGES_SUPPRESSED = ("_overwrites", "position") @@ -38,7 +34,7 @@ class ModLog(Cog, name="ModLog"):          self._cached_deletes = []          self._cached_edits = [] -    async def upload_log(self, messages: List[Message], actor_id: int) -> str: +    async def upload_log(self, messages: t.List[discord.Message], actor_id: int) -> str:          """          Uploads the log data to the database via an API endpoint for uploading logs. @@ -74,22 +70,22 @@ class ModLog(Cog, name="ModLog"):      async def send_log_message(          self, -        icon_url: Optional[str], -        colour: Colour, -        title: Optional[str], +        icon_url: t.Optional[str], +        colour: t.Union[discord.Colour, int], +        title: t.Optional[str],          text: str, -        thumbnail: Optional[Union[str, Asset]] = None, +        thumbnail: t.Optional[t.Union[str, discord.Asset]] = None,          channel_id: int = Channels.modlog,          ping_everyone: bool = False, -        files: Optional[List[File]] = None, -        content: Optional[str] = None, -        additional_embeds: Optional[List[Embed]] = None, -        additional_embeds_msg: Optional[str] = None, -        timestamp_override: Optional[datetime] = None, -        footer: Optional[str] = None, +        files: t.Optional[t.List[discord.File]] = None, +        content: t.Optional[str] = None, +        additional_embeds: t.Optional[t.List[discord.Embed]] = None, +        additional_embeds_msg: t.Optional[str] = None, +        timestamp_override: t.Optional[datetime] = None, +        footer: t.Optional[str] = None,      ) -> Context:          """Generate log embed and send to logging channel.""" -        embed = Embed(description=text) +        embed = discord.Embed(description=text)          if title and icon_url:              embed.set_author(name=title, icon_url=icon_url) @@ -126,10 +122,10 @@ class ModLog(Cog, name="ModLog"):          if channel.guild.id != GuildConstant.id:              return -        if isinstance(channel, CategoryChannel): +        if isinstance(channel, discord.CategoryChannel):              title = "Category created"              message = f"{channel.name} (`{channel.id}`)" -        elif isinstance(channel, VoiceChannel): +        elif isinstance(channel, discord.VoiceChannel):              title = "Voice channel created"              if channel.category: @@ -144,7 +140,7 @@ class ModLog(Cog, name="ModLog"):              else:                  message = f"{channel.name} (`{channel.id}`)" -        await self.send_log_message(Icons.hash_green, Colour(Colours.soft_green), title, message) +        await self.send_log_message(Icons.hash_green, Colours.soft_green, title, message)      @Cog.listener()      async def on_guild_channel_delete(self, channel: GUILD_CHANNEL) -> None: @@ -152,20 +148,20 @@ class ModLog(Cog, name="ModLog"):          if channel.guild.id != GuildConstant.id:              return -        if isinstance(channel, CategoryChannel): +        if isinstance(channel, discord.CategoryChannel):              title = "Category deleted" -        elif isinstance(channel, VoiceChannel): +        elif isinstance(channel, discord.VoiceChannel):              title = "Voice channel deleted"          else:              title = "Text channel deleted" -        if channel.category and not isinstance(channel, CategoryChannel): +        if channel.category and not isinstance(channel, discord.CategoryChannel):              message = f"{channel.category}/{channel.name} (`{channel.id}`)"          else:              message = f"{channel.name} (`{channel.id}`)"          await self.send_log_message( -            Icons.hash_red, Colour(Colours.soft_red), +            Icons.hash_red, Colours.soft_red,              title, message          ) @@ -230,29 +226,29 @@ class ModLog(Cog, name="ModLog"):          )      @Cog.listener() -    async def on_guild_role_create(self, role: Role) -> None: +    async def on_guild_role_create(self, role: discord.Role) -> None:          """Log role create event to mod log."""          if role.guild.id != GuildConstant.id:              return          await self.send_log_message( -            Icons.crown_green, Colour(Colours.soft_green), +            Icons.crown_green, Colours.soft_green,              "Role created", f"`{role.id}`"          )      @Cog.listener() -    async def on_guild_role_delete(self, role: Role) -> None: +    async def on_guild_role_delete(self, role: discord.Role) -> None:          """Log role delete event to mod log."""          if role.guild.id != GuildConstant.id:              return          await self.send_log_message( -            Icons.crown_red, Colour(Colours.soft_red), +            Icons.crown_red, Colours.soft_red,              "Role removed", f"{role.name} (`{role.id}`)"          )      @Cog.listener() -    async def on_guild_role_update(self, before: Role, after: Role) -> None: +    async def on_guild_role_update(self, before: discord.Role, after: discord.Role) -> None:          """Log role update event to mod log."""          if before.guild.id != GuildConstant.id:              return @@ -305,7 +301,7 @@ class ModLog(Cog, name="ModLog"):          )      @Cog.listener() -    async def on_guild_update(self, before: Guild, after: Guild) -> None: +    async def on_guild_update(self, before: discord.Guild, after: discord.Guild) -> None:          """Log guild update event to mod log."""          if before.id != GuildConstant.id:              return @@ -356,7 +352,7 @@ class ModLog(Cog, name="ModLog"):          )      @Cog.listener() -    async def on_member_ban(self, guild: Guild, member: Union[Member, User]) -> None: +    async def on_member_ban(self, guild: discord.Guild, member: UserTypes) -> None:          """Log ban event to mod log."""          if guild.id != GuildConstant.id:              return @@ -366,14 +362,14 @@ class ModLog(Cog, name="ModLog"):              return          await self.send_log_message( -            Icons.user_ban, Colour(Colours.soft_red), +            Icons.user_ban, Colours.soft_red,              "User banned", f"{member.name}#{member.discriminator} (`{member.id}`)",              thumbnail=member.avatar_url_as(static_format="png"),              channel_id=Channels.modlog          )      @Cog.listener() -    async def on_member_join(self, member: Member) -> None: +    async def on_member_join(self, member: discord.Member) -> None:          """Log member join event to user log."""          if member.guild.id != GuildConstant.id:              return @@ -388,14 +384,14 @@ class ModLog(Cog, name="ModLog"):              message = f"{Emojis.new} {message}"          await self.send_log_message( -            Icons.sign_in, Colour(Colours.soft_green), +            Icons.sign_in, Colours.soft_green,              "User joined", message,              thumbnail=member.avatar_url_as(static_format="png"),              channel_id=Channels.userlog          )      @Cog.listener() -    async def on_member_remove(self, member: Member) -> None: +    async def on_member_remove(self, member: discord.Member) -> None:          """Log member leave event to user log."""          if member.guild.id != GuildConstant.id:              return @@ -405,14 +401,14 @@ class ModLog(Cog, name="ModLog"):              return          await self.send_log_message( -            Icons.sign_out, Colour(Colours.soft_red), +            Icons.sign_out, Colours.soft_red,              "User left", f"{member.name}#{member.discriminator} (`{member.id}`)",              thumbnail=member.avatar_url_as(static_format="png"),              channel_id=Channels.userlog          )      @Cog.listener() -    async def on_member_unban(self, guild: Guild, member: User) -> None: +    async def on_member_unban(self, guild: discord.Guild, member: discord.User) -> None:          """Log member unban event to mod log."""          if guild.id != GuildConstant.id:              return @@ -429,7 +425,7 @@ class ModLog(Cog, name="ModLog"):          )      @Cog.listener() -    async def on_member_update(self, before: Member, after: Member) -> None: +    async def on_member_update(self, before: discord.Member, after: discord.Member) -> None:          """Log member update event to user log."""          if before.guild.id != GuildConstant.id:              return @@ -520,7 +516,7 @@ class ModLog(Cog, name="ModLog"):          )      @Cog.listener() -    async def on_message_delete(self, message: Message) -> None: +    async def on_message_delete(self, message: discord.Message) -> None:          """Log message delete event to message change log."""          channel = message.channel          author = message.author @@ -576,7 +572,7 @@ class ModLog(Cog, name="ModLog"):          )      @Cog.listener() -    async def on_raw_message_delete(self, event: RawMessageDeleteEvent) -> None: +    async def on_raw_message_delete(self, event: discord.RawMessageDeleteEvent) -> None:          """Log raw message delete event to message change log."""          if event.guild_id != GuildConstant.id or event.channel_id in GuildConstant.ignored:              return @@ -610,14 +606,14 @@ class ModLog(Cog, name="ModLog"):              )          await self.send_log_message( -            Icons.message_delete, Colour(Colours.soft_red), +            Icons.message_delete, Colours.soft_red,              "Message deleted",              response,              channel_id=Channels.message_log          )      @Cog.listener() -    async def on_message_edit(self, before: Message, after: Message) -> None: +    async def on_message_edit(self, before: discord.Message, after: discord.Message) -> None:          """Log message edit event to message change log."""          if (              not before.guild @@ -692,12 +688,12 @@ class ModLog(Cog, name="ModLog"):          )      @Cog.listener() -    async def on_raw_message_edit(self, event: RawMessageUpdateEvent) -> None: +    async def on_raw_message_edit(self, event: discord.RawMessageUpdateEvent) -> None:          """Log raw message edit event to message change log."""          try:              channel = self.bot.get_channel(int(event.data["channel_id"]))              message = await channel.fetch_message(event.message_id) -        except NotFound:  # Was deleted before we got the event +        except discord.NotFound:  # Was deleted before we got the event              return          if ( diff --git a/bot/cogs/moderation/superstarify.py b/bot/cogs/moderation/superstarify.py index 7e0307181..e5c89e5b5 100644 --- a/bot/cogs/moderation/superstarify.py +++ b/bot/cogs/moderation/superstarify.py @@ -7,9 +7,9 @@ from discord import Colour, Embed, Member  from discord.errors import Forbidden  from discord.ext.commands import Bot, Cog, Context, command -from bot.constants import Icons, MODERATION_ROLES, POSITIVE_REPLIES +from bot import constants  from bot.converters import Duration -from bot.decorators import with_role +from bot.utils.checks import with_role_check  from bot.utils.time import format_infraction  from . import utils  from .modlog import ModLog @@ -136,7 +136,7 @@ class Superstarify(Cog):                  f"Superstardom ends: **{end_timestamp_human}**"              )              await self.modlog.send_log_message( -                icon_url=Icons.user_update, +                icon_url=constants.Icons.user_update,                  colour=Colour.gold(),                  title="Superstar member rejoined server",                  text=mod_log_message, @@ -144,7 +144,6 @@ class Superstarify(Cog):              )      @command(name='superstarify', aliases=('force_nick', 'star')) -    @with_role(*MODERATION_ROLES)      async def superstarify(          self, ctx: Context, member: Member, expiration: Duration, reason: str = None      ) -> None: @@ -198,7 +197,7 @@ class Superstarify(Cog):              f"Superstardom ends: **{expiry_str}**"          )          await self.modlog.send_log_message( -            icon_url=Icons.user_update, +            icon_url=constants.Icons.user_update,              colour=Colour.gold(),              title="Member Achieved Superstardom",              text=mod_log_message, @@ -218,7 +217,6 @@ class Superstarify(Cog):          await ctx.send(embed=embed)      @command(name='unsuperstarify', aliases=('release_nick', 'unstar')) -    @with_role(*MODERATION_ROLES)      async def unsuperstarify(self, ctx: Context, member: Member) -> None:          """Remove the superstarify entry from our database, allowing the user to change their nickname."""          log.debug(f"Attempting to unsuperstarify the following user: {member.display_name}") @@ -246,7 +244,7 @@ class Superstarify(Cog):          embed = Embed()          embed.description = "User has been released from superstar-prison." -        embed.title = random.choice(POSITIVE_REPLIES) +        embed.title = random.choice(constants.POSITIVE_REPLIES)          await utils.notify_pardon(              user=member, @@ -261,3 +259,8 @@ class Superstarify(Cog):          """Randomly select a nickname from the Superstarify nickname list."""          rng = random.Random(str(infraction_id) + str(member_id))          return rng.choice(STAR_NAMES) + +    # This cannot be static (must have a __func__ attribute). +    def cog_check(self, ctx: Context) -> bool: +        """Only allow moderators to invoke the commands in this cog.""" +        return with_role_check(ctx, *constants.MODERATION_ROLES) diff --git a/bot/cogs/moderation/utils.py b/bot/cogs/moderation/utils.py index 0879eb927..f951d39ba 100644 --- a/bot/cogs/moderation/utils.py +++ b/bot/cogs/moderation/utils.py @@ -114,7 +114,7 @@ async def notify_infraction(              **Expires:** {expires_at or "N/A"}              **Reason:** {reason or "No reason provided."}              """), -        colour=discord.Colour(Colours.soft_red) +        colour=Colours.soft_red      )      icon_url = INFRACTION_ICONS[infr_type][0] @@ -137,7 +137,7 @@ async def notify_pardon(      """DM a user about their pardoned infraction and return True if the DM is successful."""      embed = discord.Embed(          description=content, -        colour=discord.Colour(Colours.soft_green) +        colour=Colours.soft_green      )      embed.set_author(name=title, icon_url=icon_url) | 
