diff options
author | 2019-10-01 18:00:02 -0700 | |
---|---|---|
committer | 2019-10-01 18:25:35 -0700 | |
commit | 57af820e62b2bc2934a1dbefc06b3d21a4f00141 (patch) | |
tree | ad32efede385f6805d1081f938436df7338a4465 | |
parent | Use consistent expiration format in superstarify (diff) |
Tidy up imports
* Remove redundant discord.Colour() usage
* Fix type annotation of colour parameter for modlog.send_log_message()
* Use a cog check in superstarify to require moderation roles
-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) |