aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar MarkKoz <[email protected]>2019-10-01 18:00:02 -0700
committerGravatar MarkKoz <[email protected]>2019-10-01 18:25:35 -0700
commit57af820e62b2bc2934a1dbefc06b3d21a4f00141 (patch)
treead32efede385f6805d1081f938436df7338a4465
parentUse 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.py48
-rw-r--r--bot/cogs/moderation/management.py28
-rw-r--r--bot/cogs/moderation/modlog.py94
-rw-r--r--bot/cogs/moderation/superstarify.py17
-rw-r--r--bot/cogs/moderation/utils.py4
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)