diff options
| author | 2019-01-16 22:15:26 -0500 | |
|---|---|---|
| committer | 2019-01-16 22:15:26 -0500 | |
| commit | 26d0242ad72927779436de96996757d212a6b0e0 (patch) | |
| tree | 7d03ef3f2ad36166e5f868bbee2c4657a0a288c1 | |
| parent | Simplify role check logic (diff) | |
| parent | Merge pull request #277 from python-discord/reorder-mod-actions (diff) | |
Merge branch 'master' into moderation-hierarchy-check
| -rw-r--r-- | bot/cogs/events.py | 11 | ||||
| -rw-r--r-- | bot/cogs/filtering.py | 42 | ||||
| -rw-r--r-- | bot/cogs/moderation.py | 535 | ||||
| -rw-r--r-- | bot/cogs/modlog.py | 23 | ||||
| -rw-r--r-- | bot/constants.py | 4 | ||||
| -rw-r--r-- | config-default.yml | 4 |
6 files changed, 380 insertions, 239 deletions
diff --git a/bot/cogs/events.py b/bot/cogs/events.py index f0baecd4b..8dac83d9b 100644 --- a/bot/cogs/events.py +++ b/bot/cogs/events.py @@ -1,4 +1,5 @@ import logging +from functools import partial from discord import Colour, Embed, Member, Object from discord.ext.commands import ( @@ -7,7 +8,6 @@ from discord.ext.commands import ( Context, NoPrivateMessage, UserInputError ) -from bot.cogs.modlog import ModLog from bot.constants import ( Channels, Colours, DEBUG_MODE, Guild, Icons, Keys, @@ -28,8 +28,9 @@ class Events: self.headers = {"X-API-KEY": Keys.site_api} @property - def mod_log(self) -> ModLog: - return self.bot.get_cog("ModLog") + def send_log(self) -> partial: + cog = self.bot.get_cog("ModLog") + return partial(cog.send_log_message, channel_id=Channels.userlog) async def send_updated_users(self, *users, replace_all=False): users = list(filter(lambda user: str(Roles.verified) in user["roles"], users)) @@ -249,7 +250,7 @@ class Events: except Exception as e: log.exception("Failed to persist roles") - await self.mod_log.send_log_message( + await self.send_log( Icons.crown_red, Colour(Colours.soft_red), "Failed to persist roles", f"```py\n{e}\n```", member.avatar_url_as(static_format="png") @@ -290,7 +291,7 @@ class Events: reason="Roles restored" ) - await self.mod_log.send_log_message( + await self.send_log( Icons.crown_blurple, Colour.blurple(), "Roles restored", f"Restored {len(new_roles)} roles", member.avatar_url_as(static_format="png") diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py index 570d6549f..6b4469ceb 100644 --- a/bot/cogs/filtering.py +++ b/bot/cogs/filtering.py @@ -1,7 +1,9 @@ import logging import re +from typing import Optional import discord.errors +from dateutil.relativedelta import relativedelta from discord import Colour, DMChannel, Member, Message, TextChannel from discord.ext.commands import Bot @@ -73,18 +75,11 @@ class Filtering: f"Your URL has been removed because it matched a blacklisted domain. {_staff_mistake_str}" ) }, - "filter_rich_embeds": { - "enabled": Filter.filter_rich_embeds, + "watch_rich_embeds": { + "enabled": Filter.watch_rich_embeds, "function": self._has_rich_embed, - "type": "filter", + "type": "watchlist", "content_only": False, - "user_notification": Filter.notify_user_rich_embeds, - "notification_msg": ( - "Your post has been removed because it contained a rich embed. " - "This indicates that you're either using an unofficial discord client or are using a self-bot, " - f"both of which violate Discord's Terms of Service. {_staff_mistake_str}\n\n" - "Please don't use a self-bot or an unofficial Discord client on our server." - ) }, "watch_words": { "enabled": Filter.watch_words, @@ -107,10 +102,14 @@ class Filtering: async def on_message(self, msg: Message): await self._filter_message(msg) - async def on_message_edit(self, _: Message, after: Message): - await self._filter_message(after) + async def on_message_edit(self, before: Message, after: Message): + if not before.edited_at: + delta = relativedelta(after.edited_at, before.created_at).microseconds + else: + delta = None + await self._filter_message(after, delta) - async def _filter_message(self, msg: Message): + async def _filter_message(self, msg: Message, delta: Optional[int] = None): """ Whenever a message is sent or edited, run it through our filters to see if it @@ -141,6 +140,13 @@ class Filtering: for filter_name, _filter in self.filters.items(): # Is this specific filter enabled in the config? if _filter["enabled"]: + # Double trigger check for the embeds filter + if filter_name == "watch_rich_embeds": + # If the edit delta is less than 0.001 seconds, then we're probably dealing + # with a double filter trigger. + if delta is not None and delta < 100: + return + # Does the filter only need the message content or the full message? if _filter["content_only"]: triggered = await _filter["function"](msg.content) @@ -183,7 +189,7 @@ class Filtering: log.debug(message) - additional_embeds = msg.embeds if filter_name == "filter_rich_embeds" else None + additional_embeds = msg.embeds if filter_name == "watch_rich_embeds" else None # Send pretty mod log embed to mod-alerts await self.mod_log.send_log_message( @@ -311,11 +317,13 @@ class Filtering: @staticmethod async def _has_rich_embed(msg: Message): """ - Returns True if any of the embeds in the message - are of type 'rich', returns False otherwise + Returns True if any of the embeds in the message are of type 'rich', but are not twitter + embeds. Returns False otherwise. """ if msg.embeds: - return any(embed.type == "rich" for embed in msg.embeds) + for embed in msg.embeds: + if embed.type == "rich" and (not embed.url or "twitter.com" not in embed.url): + return True return False async def notify_member(self, filtered_member: Member, reason: str, channel: TextChannel): diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 64c76ae8a..6b90d43ab 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -45,7 +45,7 @@ def proxy_user(user_id: str) -> Object: class Moderation(Scheduler): """ - Rowboat replacement moderation tools. + Server moderation tools. """ def __init__(self, bot: Bot): @@ -66,32 +66,32 @@ class Moderation(Scheduler): headers=self.headers ) infraction_list = await response.json() - loop = asyncio.get_event_loop() for infraction_object in infraction_list: if infraction_object["expires_at"] is not None: - self.schedule_task(loop, infraction_object["id"], infraction_object) + self.schedule_task(self.bot.loop, infraction_object["id"], infraction_object) # region: Permanent infractions @with_role(*MODERATION_ROLES) - @command(name="warn") + @command() async def warn(self, ctx: Context, user: Union[User, proxy_user], *, reason: str = None): """ Create a warning infraction in the database for a user. - :param user: accepts user mention, ID, etc. - :param reason: The reason for the warning. + + **`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: + return + notified = await self.notify_infraction( user=user, infr_type="Warning", reason=reason ) - response_object = await post_infraction(ctx, user, type="warning", reason=reason) - if response_object is None: - return - dm_result = ":incoming_envelope: " if notified else "" action = f"{dm_result}:ok_hand: warned {user.mention}" @@ -100,10 +100,13 @@ class Moderation(Scheduler): else: await ctx.send(f"{action} ({reason}).") - if not notified: - await self.log_notify_failure(user, ctx.author, "warning") + if notified: + dm_status = "Sent" + log_content = None + else: + dm_status = "**Failed**" + log_content = ctx.author.mention - # Send a message to the mod log await self.mod_log.send_log_message( icon_url=Icons.user_warn, colour=Colour(Colours.soft_red), @@ -111,18 +114,22 @@ class Moderation(Scheduler): thumbnail=user.avatar_url_as(static_format="png"), text=textwrap.dedent(f""" Member: {user.mention} (`{user.id}`) - Actor: {ctx.message.author} + Actor: {ctx.author} + DM: {dm_status} Reason: {reason} - """) + """), + content=log_content, + footer=f"ID {response_object['infraction']['id']}" ) @with_role(*MODERATION_ROLES) - @command(name="kick") + @command() async def kick(self, ctx: Context, user: Member, *, reason: str = None): """ Kicks a user. - :param user: accepts user mention, ID, etc. - :param reason: The reason for the kick. + + **`user`:** Accepts user mention, ID, etc. + **`reason`:** The reason for the kick. """ if not await self.respect_role_hierarchy(ctx, user, 'kick'): @@ -130,18 +137,23 @@ 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: + return + notified = await self.notify_infraction( user=user, infr_type="Kick", reason=reason ) - response_object = await post_infraction(ctx, user, type="kick", reason=reason) - if response_object is None: - return - self.mod_log.ignore(Event.member_remove, user.id) - await user.kick(reason=reason) + + try: + await user.kick(reason=reason) + action_result = True + except Forbidden: + action_result = False dm_result = ":incoming_envelope: " if notified else "" action = f"{dm_result}:ok_hand: kicked {user.mention}" @@ -151,29 +163,33 @@ class Moderation(Scheduler): else: await ctx.send(f"{action} ({reason}).") - if not notified: - await self.log_notify_failure(user, ctx.author, "kick") + dm_status = "Sent" if notified else "**Failed**" + title = "Member kicked" if action_result else "Member kicked (Failed)" + log_content = None if all((notified, action_result)) else ctx.author.mention - # Send a log message to the mod log await self.mod_log.send_log_message( icon_url=Icons.sign_out, colour=Colour(Colours.soft_red), - title="Member kicked", + title=title, thumbnail=user.avatar_url_as(static_format="png"), text=textwrap.dedent(f""" Member: {user.mention} (`{user.id}`) Actor: {ctx.message.author} + DM: {dm_status} Reason: {reason} - """) + """), + content=log_content, + footer=f"ID {response_object['infraction']['id']}" ) @with_role(*MODERATION_ROLES) - @command(name="ban") + @command() async def ban(self, ctx: Context, user: Union[User, proxy_user], *, reason: str = None): """ Create a permanent ban infraction in the database for a user. - :param user: Accepts user mention, ID, etc. - :param reason: The reason for the ban. + + **`user`:** Accepts user mention, ID, etc. + **`reason`:** The reason for the ban. """ member = ctx.guild.get_member(user.id) @@ -182,6 +198,10 @@ class Moderation(Scheduler): # Warning is sent to ctx by the helper method return + response_object = await post_infraction(ctx, user, type="ban", reason=reason) + if response_object is None: + return + notified = await self.notify_infraction( user=user, infr_type="Ban", @@ -189,13 +209,14 @@ class Moderation(Scheduler): reason=reason ) - response_object = await post_infraction(ctx, user, type="ban", reason=reason) - if response_object is None: - return - self.mod_log.ignore(Event.member_ban, user.id) self.mod_log.ignore(Event.member_remove, user.id) - await ctx.guild.ban(user, reason=reason, delete_message_days=0) + + try: + await ctx.guild.ban(user, reason=reason, delete_message_days=0) + action_result = True + except Forbidden: + action_result = False dm_result = ":incoming_envelope: " if notified else "" action = f"{dm_result}:ok_hand: permanently banned {user.mention}" @@ -205,46 +226,51 @@ class Moderation(Scheduler): else: await ctx.send(f"{action} ({reason}).") - if not notified: - await self.log_notify_failure(user, ctx.author, "ban") + dm_status = "Sent" if notified else "**Failed**" + log_content = None if all((notified, action_result)) else ctx.author.mention + title = "Member permanently banned" + if not action_result: + title += " (Failed)" - # Send a log message to the mod log await self.mod_log.send_log_message( icon_url=Icons.user_ban, colour=Colour(Colours.soft_red), - title="Member permanently banned", + title=title, thumbnail=user.avatar_url_as(static_format="png"), text=textwrap.dedent(f""" Member: {user.mention} (`{user.id}`) Actor: {ctx.message.author} + DM: {dm_status} Reason: {reason} - """) + """), + content=log_content, + footer=f"ID {response_object['infraction']['id']}" ) @with_role(*MODERATION_ROLES) - @command(name="mute") + @command() async def mute(self, ctx: Context, user: Member, *, reason: str = None): """ Create a permanent mute infraction in the database for a user. - :param user: Accepts user mention, ID, etc. - :param reason: The reason for the mute. - """ - notified = await self.notify_infraction( - user=user, - infr_type="Mute", - duration="Permanent", - reason=reason - ) + **`user`:** Accepts user mention, ID, etc. + **`reason`:** The reason for the mute. + """ response_object = await post_infraction(ctx, user, type="mute", reason=reason) if response_object is None: return - # add the mute role self.mod_log.ignore(Event.member_update, user.id) await user.add_roles(self._muted_role, reason=reason) + notified = await self.notify_infraction( + user=user, + infr_type="Mute", + duration="Permanent", + reason=reason + ) + dm_result = ":incoming_envelope: " if notified else "" action = f"{dm_result}:ok_hand: permanently muted {user.mention}" @@ -253,10 +279,13 @@ class Moderation(Scheduler): else: await ctx.send(f"{action} ({reason}).") - if not notified: - await self.log_notify_failure(user, ctx.author, "mute") + if notified: + dm_status = "Sent" + log_content = None + else: + dm_status = "**Failed**" + log_content = ctx.author.mention - # Send a log message to the mod log await self.mod_log.send_log_message( icon_url=Icons.user_mute, colour=Colour(Colours.soft_red), @@ -265,42 +294,47 @@ class Moderation(Scheduler): text=textwrap.dedent(f""" Member: {user.mention} (`{user.id}`) Actor: {ctx.message.author} + DM: {dm_status} Reason: {reason} - """) + """), + content=log_content, + footer=f"ID {response_object['infraction']['id']}" ) # endregion # region: Temporary infractions @with_role(*MODERATION_ROLES) - @command(name="tempmute") + @command() async def tempmute(self, ctx: Context, user: Member, duration: str, *, reason: str = None): """ Create a temporary mute infraction in the database for a user. - :param user: Accepts user mention, ID, etc. - :param duration: The duration for the temporary mute infraction - :param reason: The reason for the temporary mute. + + **`user`:** Accepts user mention, ID, etc. + **`duration`:** The duration for the temporary mute infraction + **`reason`:** The reason for the temporary mute. """ - notified = await self.notify_infraction( - user=user, - infr_type="Mute", - duration=duration, - reason=reason + response_object = await post_infraction( + ctx, user, type="mute", reason=reason, duration=duration ) - - response_object = await post_infraction(ctx, user, type="mute", reason=reason, duration=duration) if response_object is None: return self.mod_log.ignore(Event.member_update, user.id) await user.add_roles(self._muted_role, reason=reason) + notified = await self.notify_infraction( + user=user, + infr_type="Mute", + duration=duration, + reason=reason + ) + infraction_object = response_object["infraction"] infraction_expiration = infraction_object["expires_at"] - loop = asyncio.get_event_loop() - self.schedule_task(loop, infraction_object["id"], infraction_object) + self.schedule_task(ctx.bot.loop, infraction_object["id"], infraction_object) dm_result = ":incoming_envelope: " if notified else "" action = f"{dm_result}:ok_hand: muted {user.mention} until {infraction_expiration}" @@ -310,10 +344,13 @@ class Moderation(Scheduler): else: await ctx.send(f"{action} ({reason}).") - if not notified: - await self.log_notify_failure(user, ctx.author, "mute") + if notified: + dm_status = "Sent" + log_content = None + else: + dm_status = "**Failed**" + log_content = ctx.author.mention - # Send a log message to the mod log await self.mod_log.send_log_message( icon_url=Icons.user_mute, colour=Colour(Colours.soft_red), @@ -322,20 +359,26 @@ class Moderation(Scheduler): text=textwrap.dedent(f""" Member: {user.mention} (`{user.id}`) Actor: {ctx.message.author} + DM: {dm_status} Reason: {reason} Duration: {duration} Expires: {infraction_expiration} - """) + """), + content=log_content, + footer=f"ID {response_object['infraction']['id']}" ) @with_role(*MODERATION_ROLES) - @command(name="tempban") - async def tempban(self, ctx: Context, user: Union[User, proxy_user], duration: str, *, reason: str = None): + @command() + async def tempban( + self, ctx: Context, user: Union[User, proxy_user], duration: str, *, reason: str = None + ): """ Create a temporary ban infraction in the database for a user. - :param user: Accepts user mention, ID, etc. - :param duration: The duration for the temporary ban infraction - :param reason: The reason for the temporary ban. + + **`user`:** Accepts user mention, ID, etc. + **`duration`:** The duration for the temporary ban infraction + **`reason`:** The reason for the temporary ban. """ member = ctx.guild.get_member(user.id) @@ -344,6 +387,12 @@ class Moderation(Scheduler): # Warning is sent to ctx by the helper method return + response_object = await post_infraction( + ctx, user, type="ban", reason=reason, duration=duration + ) + if response_object is None: + return + notified = await self.notify_infraction( user=user, infr_type="Ban", @@ -351,20 +400,19 @@ class Moderation(Scheduler): reason=reason ) - response_object = await post_infraction(ctx, user, type="ban", reason=reason, duration=duration) - if response_object is None: - return - self.mod_log.ignore(Event.member_ban, user.id) self.mod_log.ignore(Event.member_remove, user.id) - guild: Guild = ctx.guild - await guild.ban(user, reason=reason, delete_message_days=0) + + try: + await ctx.guild.ban(user, reason=reason, delete_message_days=0) + action_result = True + except Forbidden: + action_result = False infraction_object = response_object["infraction"] infraction_expiration = infraction_object["expires_at"] - loop = asyncio.get_event_loop() - self.schedule_task(loop, infraction_object["id"], infraction_object) + self.schedule_task(ctx.bot.loop, infraction_object["id"], infraction_object) dm_result = ":incoming_envelope: " if notified else "" action = f"{dm_result}:ok_hand: banned {user.mention} until {infraction_expiration}" @@ -374,67 +422,74 @@ class Moderation(Scheduler): else: await ctx.send(f"{action} ({reason}).") - if not notified: - await self.log_notify_failure(user, ctx.author, "ban") + dm_status = "Sent" if notified else "**Failed**" + log_content = None if all((notified, action_result)) else ctx.author.mention + title = "Member temporarily banned" + if not action_result: + title += " (Failed)" - # Send a log message to the mod log await self.mod_log.send_log_message( icon_url=Icons.user_ban, colour=Colour(Colours.soft_red), thumbnail=user.avatar_url_as(static_format="png"), - title="Member temporarily banned", + title=title, text=textwrap.dedent(f""" Member: {user.mention} (`{user.id}`) Actor: {ctx.message.author} + DM: {dm_status} Reason: {reason} Duration: {duration} Expires: {infraction_expiration} - """) + """), + content=log_content, + footer=f"ID {response_object['infraction']['id']}" ) # endregion # region: Permanent shadow infractions @with_role(*MODERATION_ROLES) - @command(name="shadow_warn", hidden=True, aliases=['shadowwarn', 'swarn', 'note']) - async def shadow_warn(self, ctx: Context, user: Union[User, proxy_user], *, reason: str = None): + @command(hidden=True, aliases=['shadowwarn', 'swarn', 'shadow_warn']) + async def note(self, ctx: Context, user: Union[User, proxy_user], *, reason: str = None): """ - Create a warning infraction in the database for a user. - :param user: accepts user mention, ID, etc. - :param reason: The reason for the warning. + Create a private infraction note 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, hidden=True) + response_object = await post_infraction( + ctx, user, type="warning", reason=reason, hidden=True + ) if response_object is None: return if reason is None: - result_message = f":ok_hand: note added for {user.mention}." + await ctx.send(f":ok_hand: note added for {user.mention}.") else: - result_message = f":ok_hand: note added for {user.mention} ({reason})." + await ctx.send(f":ok_hand: note added for {user.mention} ({reason}).") - await ctx.send(result_message) - - # Send a message to the mod log await self.mod_log.send_log_message( icon_url=Icons.user_warn, colour=Colour(Colours.soft_red), - title="Member shadow warned", + title="Member note added", thumbnail=user.avatar_url_as(static_format="png"), text=textwrap.dedent(f""" Member: {user.mention} (`{user.id}`) Actor: {ctx.message.author} Reason: {reason} - """) + """), + footer=f"ID {response_object['infraction']['id']}" ) @with_role(*MODERATION_ROLES) - @command(name="shadow_kick", hidden=True, aliases=['shadowkick', 'skick']) + @command(hidden=True, aliases=['shadowkick', 'skick']) async def shadow_kick(self, ctx: Context, user: Member, *, reason: str = None): """ Kicks a user. - :param user: accepts user mention, ID, etc. - :param reason: The reason for the kick. + + **`user`:** accepts user mention, ID, etc. + **`reason`:** The reason for the kick. """ if not await self.respect_role_hierarchy(ctx, user, 'shadowkick'): @@ -447,35 +502,47 @@ class Moderation(Scheduler): return self.mod_log.ignore(Event.member_remove, user.id) - await user.kick(reason=reason) + + try: + await user.kick(reason=reason) + action_result = True + except Forbidden: + action_result = False if reason is None: - result_message = f":ok_hand: kicked {user.mention}." + await ctx.send(f":ok_hand: kicked {user.mention}.") else: - result_message = f":ok_hand: kicked {user.mention} ({reason})." + await ctx.send(f":ok_hand: kicked {user.mention} ({reason}).") - await ctx.send(result_message) + title = "Member shadow kicked" + if action_result: + log_content = None + else: + log_content = ctx.author.mention + title += " (Failed)" - # Send a log message to the mod log await self.mod_log.send_log_message( icon_url=Icons.sign_out, colour=Colour(Colours.soft_red), - title="Member shadow kicked", + title=title, thumbnail=user.avatar_url_as(static_format="png"), text=textwrap.dedent(f""" Member: {user.mention} (`{user.id}`) Actor: {ctx.message.author} Reason: {reason} - """) + """), + content=log_content, + footer=f"ID {response_object['infraction']['id']}" ) @with_role(*MODERATION_ROLES) - @command(name="shadow_ban", hidden=True, aliases=['shadowban', 'sban']) + @command(hidden=True, aliases=['shadowban', 'sban']) async def shadow_ban(self, ctx: Context, user: Union[User, proxy_user], *, reason: str = None): """ Create a permanent ban infraction in the database for a user. - :param user: Accepts user mention, ID, etc. - :param reason: The reason for the ban. + + **`user`:** Accepts user mention, ID, etc. + **`reason`:** The reason for the ban. """ member = ctx.guild.get_member(user.id) @@ -490,53 +557,61 @@ class Moderation(Scheduler): self.mod_log.ignore(Event.member_ban, user.id) self.mod_log.ignore(Event.member_remove, user.id) - await ctx.guild.ban(user, reason=reason, delete_message_days=0) + + try: + await ctx.guild.ban(user, reason=reason, delete_message_days=0) + action_result = True + except Forbidden: + action_result = False if reason is None: - result_message = f":ok_hand: permanently banned {user.mention}." + await ctx.send(f":ok_hand: permanently banned {user.mention}.") else: - result_message = f":ok_hand: permanently banned {user.mention} ({reason})." + await ctx.send(f":ok_hand: permanently banned {user.mention} ({reason}).") - await ctx.send(result_message) + title = "Member permanently banned" + if action_result: + log_content = None + else: + log_content = ctx.author.mention + title += " (Failed)" - # Send a log message to the mod log await self.mod_log.send_log_message( icon_url=Icons.user_ban, colour=Colour(Colours.soft_red), - title="Member permanently banned", + title=title, thumbnail=user.avatar_url_as(static_format="png"), text=textwrap.dedent(f""" Member: {user.mention} (`{user.id}`) Actor: {ctx.message.author} Reason: {reason} - """) + """), + content=log_content, + footer=f"ID {response_object['infraction']['id']}" ) @with_role(*MODERATION_ROLES) - @command(name="shadow_mute", hidden=True, aliases=['shadowmute', 'smute']) + @command(hidden=True, aliases=['shadowmute', 'smute']) async def shadow_mute(self, ctx: Context, user: Member, *, reason: str = None): """ Create a permanent mute infraction in the database for a user. - :param user: Accepts user mention, ID, etc. - :param reason: The reason for the mute. + + **`user`:** Accepts user mention, ID, etc. + **`reason`:** The reason for the mute. """ response_object = await post_infraction(ctx, user, type="mute", reason=reason, hidden=True) if response_object is None: return - # add the mute role self.mod_log.ignore(Event.member_update, user.id) await user.add_roles(self._muted_role, reason=reason) if reason is None: - result_message = f":ok_hand: permanently muted {user.mention}." + await ctx.send(f":ok_hand: permanently muted {user.mention}.") else: - result_message = f":ok_hand: permanently muted {user.mention} ({reason})." + await ctx.send(f":ok_hand: permanently muted {user.mention} ({reason}).") - await ctx.send(result_message) - - # Send a log message to the mod log await self.mod_log.send_log_message( icon_url=Icons.user_mute, colour=Colour(Colours.soft_red), @@ -546,23 +621,29 @@ class Moderation(Scheduler): Member: {user.mention} (`{user.id}`) Actor: {ctx.message.author} Reason: {reason} - """) + """), + footer=f"ID {response_object['infraction']['id']}" ) # endregion # region: Temporary shadow infractions @with_role(*MODERATION_ROLES) - @command(name="shadow_tempmute", hidden=True, aliases=["shadowtempmute, stempmute"]) - async def shadow_tempmute(self, ctx: Context, user: Member, duration: str, *, reason: str = None): + @command(hidden=True, aliases=["shadowtempmute, stempmute"]) + async def shadow_tempmute( + self, ctx: Context, user: Member, duration: str, *, reason: str = None + ): """ Create a temporary mute infraction in the database for a user. - :param user: Accepts user mention, ID, etc. - :param duration: The duration for the temporary mute infraction - :param reason: The reason for the temporary mute. + + **`user`:** Accepts user mention, ID, etc. + **`duration`:** The duration for the temporary mute infraction + **`reason`:** The reason for the temporary mute. """ - response_object = await post_infraction(ctx, user, type="mute", reason=reason, duration=duration, hidden=True) + response_object = await post_infraction( + ctx, user, type="mute", reason=reason, duration=duration, hidden=True + ) if response_object is None: return @@ -572,17 +653,15 @@ class Moderation(Scheduler): infraction_object = response_object["infraction"] infraction_expiration = infraction_object["expires_at"] - loop = asyncio.get_event_loop() - self.schedule_expiration(loop, infraction_object) + self.schedule_expiration(ctx.bot.loop, infraction_object) if reason is None: - result_message = f":ok_hand: muted {user.mention} until {infraction_expiration}." + await ctx.send(f":ok_hand: muted {user.mention} until {infraction_expiration}.") else: - result_message = f":ok_hand: muted {user.mention} until {infraction_expiration} ({reason})." - - await ctx.send(result_message) + await ctx.send( + f":ok_hand: muted {user.mention} until {infraction_expiration} ({reason})." + ) - # Send a log message to the mod log await self.mod_log.send_log_message( icon_url=Icons.user_mute, colour=Colour(Colours.soft_red), @@ -594,19 +673,21 @@ class Moderation(Scheduler): Reason: {reason} Duration: {duration} Expires: {infraction_expiration} - """) + """), + footer=f"ID {response_object['infraction']['id']}" ) @with_role(*MODERATION_ROLES) - @command(name="shadow_tempban", hidden=True, aliases=["shadowtempban, stempban"]) + @command(hidden=True, aliases=["shadowtempban, stempban"]) async def shadow_tempban( self, ctx: Context, user: Union[User, proxy_user], duration: str, *, reason: str = None ): """ Create a temporary ban infraction in the database for a user. - :param user: Accepts user mention, ID, etc. - :param duration: The duration for the temporary ban infraction - :param reason: The reason for the temporary ban. + + **`user`:** Accepts user mention, ID, etc. + **`duration`:** The duration for the temporary ban infraction + **`reason`:** The reason for the temporary ban. """ member = ctx.guild.get_member(user.id) @@ -615,52 +696,67 @@ class Moderation(Scheduler): # 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) + response_object = await post_infraction( + ctx, user, type="ban", reason=reason, duration=duration, hidden=True + ) if response_object is None: return self.mod_log.ignore(Event.member_ban, user.id) self.mod_log.ignore(Event.member_remove, user.id) - guild: Guild = ctx.guild - await guild.ban(user, reason=reason, delete_message_days=0) + + try: + await ctx.guild.ban(user, reason=reason, delete_message_days=0) + action_result = True + except Forbidden: + action_result = False infraction_object = response_object["infraction"] infraction_expiration = infraction_object["expires_at"] - loop = asyncio.get_event_loop() - self.schedule_expiration(loop, infraction_object) + self.schedule_expiration(ctx.bot.loop, infraction_object) if reason is None: - result_message = f":ok_hand: banned {user.mention} until {infraction_expiration}." + await ctx.send(f":ok_hand: banned {user.mention} until {infraction_expiration}.") else: - result_message = f":ok_hand: banned {user.mention} until {infraction_expiration} ({reason})." + await ctx.send( + f":ok_hand: banned {user.mention} until {infraction_expiration} ({reason})." + ) - await ctx.send(result_message) + title = "Member temporarily banned" + if action_result: + log_content = None + else: + log_content = ctx.author.mention + title += " (Failed)" # Send a log message to the mod log await self.mod_log.send_log_message( icon_url=Icons.user_ban, colour=Colour(Colours.soft_red), thumbnail=user.avatar_url_as(static_format="png"), - title="Member temporarily banned", + title=title, text=textwrap.dedent(f""" 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']}" ) # endregion # region: Remove infractions (un- commands) @with_role(*MODERATION_ROLES) - @command(name="unmute") + @command() async def unmute(self, ctx: Context, user: Member): """ Deactivates the active mute infraction for a user. - :param user: Accepts user mention, ID, etc. + + **`user`:** Accepts user mention, ID, etc. """ try: @@ -672,16 +768,20 @@ class Moderation(Scheduler): ), headers=self.headers ) + response_object = await response.json() if "error_code" in response_object: - await ctx.send(f":x: There was an error removing the infraction: {response_object['error_message']}") - return + return await ctx.send( + ":x: There was an error removing the infraction: " + f"{response_object['error_message']}" + ) infraction_object = response_object["infraction"] if infraction_object is None: # no active infraction - await ctx.send(f":x: There is no active mute infraction for user {user.mention}.") - return + return await ctx.send( + f":x: There is no active mute infraction for user {user.mention}." + ) await self._deactivate_infraction(infraction_object) if infraction_object["expires_at"] is not None: @@ -694,11 +794,16 @@ class Moderation(Scheduler): icon_url=Icons.user_unmute ) - dm_result = ":incoming_envelope: " if notified else "" - await ctx.send(f"{dm_result}:ok_hand: Un-muted {user.mention}.") + if notified: + dm_status = "Sent" + dm_emoji = ":incoming_envelope: " + log_content = None + else: + dm_status = "**Failed**" + dm_emoji = "" + log_content = ctx.author.mention - if not notified: - await self.log_notify_failure(user, ctx.author, "unmute") + await ctx.send(f"{dm_emoji}:ok_hand: Un-muted {user.mention}.") # Send a log message to the mod log await self.mod_log.send_log_message( @@ -710,19 +815,23 @@ class Moderation(Scheduler): Member: {user.mention} (`{user.id}`) Actor: {ctx.message.author} Intended expiry: {infraction_object['expires_at']} - """) + DM: {dm_status} + """), + footer=infraction_object["id"], + content=log_content ) - except Exception: - log.exception("There was an error removing an infraction.") + + except Exception as e: + log.exception("There was an error removing an infraction.", exc_info=e) await ctx.send(":x: There was an error removing the infraction.") - return @with_role(*MODERATION_ROLES) - @command(name="unban") + @command() async def unban(self, ctx: Context, user: Union[User, proxy_user]): """ Deactivates the active ban infraction for a user. - :param user: Accepts user mention, ID, etc. + + **`user`:** Accepts user mention, ID, etc. """ try: @@ -736,14 +845,17 @@ class Moderation(Scheduler): ) response_object = await response.json() if "error_code" in response_object: - await ctx.send(f":x: There was an error removing the infraction: {response_object['error_message']}") - return + return await ctx.send( + ":x: There was an error removing the infraction: " + f"{response_object['error_message']}" + ) infraction_object = response_object["infraction"] if infraction_object is None: # no active infraction - await ctx.send(f":x: There is no active ban infraction for user {user.mention}.") - return + return await ctx.send( + f":x: There is no active ban infraction for user {user.mention}." + ) await self._deactivate_infraction(infraction_object) if infraction_object["expires_at"] is not None: @@ -766,7 +878,6 @@ class Moderation(Scheduler): except Exception: log.exception("There was an error removing an infraction.") await ctx.send(":x: There was an error removing the infraction.") - return # endregion # region: Edit infraction commands @@ -789,10 +900,12 @@ class Moderation(Scheduler): @infraction_edit_group.command(name="duration") async def edit_duration(self, ctx: Context, infraction_id: str, duration: str): """ - Sets the duration of the given infraction, relative to the time of updating. - :param infraction_id: the id (UUID) of the infraction - :param duration: the new duration of the infraction, relative to the time of updating. Use "permanent" to mark - the infraction as permanent. + Sets the duration of the given infraction, relative to the time of + updating. + + **`infraction_id`:** The ID (UUID) of the infraction. + **`duration`:** The new duration of the infraction, relative to the + time of updating. Use "permanent" to the infraction as permanent. """ try: @@ -818,8 +931,10 @@ class Moderation(Scheduler): ) response_object = await response.json() if "error_code" in response_object or response_object.get("success") is False: - await ctx.send(f":x: There was an error updating the infraction: {response_object['error_message']}") - return + return await ctx.send( + ":x: There was an error updating the infraction: " + f"{response_object['error_message']}" + ) infraction_object = response_object["infraction"] # Re-schedule @@ -830,7 +945,10 @@ class Moderation(Scheduler): if duration is None: await ctx.send(f":ok_hand: Updated infraction: marked as permanent.") else: - await ctx.send(f":ok_hand: Updated infraction: set to expire on {infraction_object['expires_at']}.") + await ctx.send( + ":ok_hand: Updated infraction: set to expire on " + f"{infraction_object['expires_at']}." + ) except Exception: log.exception("There was an error updating an infraction.") @@ -873,8 +991,8 @@ class Moderation(Scheduler): async def edit_reason(self, ctx: Context, infraction_id: str, *, reason: str): """ Sets the reason of the given infraction. - :param infraction_id: the id (UUID) of the infraction - :param reason: The new reason of the infraction + **`infraction_id`:** The ID (UUID) of the infraction. + **`reason`:** The new reason of the infraction. """ try: @@ -897,14 +1015,15 @@ class Moderation(Scheduler): ) response_object = await response.json() if "error_code" in response_object or response_object.get("success") is False: - await ctx.send(f":x: There was an error updating the infraction: {response_object['error_message']}") - return + return await ctx.send( + ":x: There was an error updating the infraction: " + f"{response_object['error_message']}" + ) await ctx.send(f":ok_hand: Updated infraction: set reason to \"{reason}\".") except Exception: log.exception("There was an error updating an infraction.") - await ctx.send(":x: There was an error updating the infraction.") - return + return await ctx.send(":x: There was an error updating the infraction.") new_infraction = response_object["infraction"] prev_infraction = previous_object["infraction"] @@ -1038,6 +1157,7 @@ class Moderation(Scheduler): def schedule_expiration(self, loop: asyncio.AbstractEventLoop, infraction_object: dict): """ Schedules a task to expire a temporary infraction. + :param loop: the asyncio event loop :param infraction_object: the infraction object to expire at the end of the task """ @@ -1066,9 +1186,10 @@ class Moderation(Scheduler): async def _scheduled_task(self, infraction_object: dict): """ - A co-routine which marks an infraction as expired after the delay from the time of scheduling - to the time of expiration. At the time of expiration, the infraction is marked as inactive on the website, - and the expiration task is cancelled. + A co-routine which marks an infraction as expired after the delay from the time of + scheduling to the time of expiration. At the time of expiration, the infraction is + marked as inactive on the website, and the expiration task is cancelled. + :param infraction_object: the infraction in question """ @@ -1095,8 +1216,9 @@ class Moderation(Scheduler): async def _deactivate_infraction(self, infraction_object): """ - A co-routine which marks an infraction as inactive on the website. This co-routine does not cancel or - un-schedule an expiration task. + A co-routine which marks an infraction as inactive on the website. This co-routine does + not cancel or un-schedule an expiration task. + :param infraction_object: the infraction in question """ @@ -1150,7 +1272,8 @@ class Moderation(Scheduler): return lines.strip() async def notify_infraction( - self, user: Union[User, Member], infr_type: str, duration: str = None, reason: str = None + self, user: Union[User, Member], infr_type: str, duration: str = None, + reason: str = None ): """ Notify a user of their fresh infraction :) @@ -1184,7 +1307,8 @@ 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 ): """ Notify a user that an infraction has been lifted. @@ -1231,7 +1355,10 @@ class Moderation(Scheduler): content=actor.mention, colour=Colour(Colours.soft_red), title="Notification Failed", - text=f"Direct message was unable to be sent.\nUser: {target.mention}\nType: {infraction_type}" + text=( + f"Direct message was unable to be sent.\nUser: {target.mention}\n" + f"Type: {infraction_type}" + ) ) # endregion diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py index 55611c5e4..495795b6d 100644 --- a/bot/cogs/modlog.py +++ b/bot/cogs/modlog.py @@ -116,7 +116,7 @@ class ModLog: content: Optional[str] = None, additional_embeds: Optional[List[Embed]] = None, timestamp_override: Optional[datetime.datetime] = None, - footer_override: Optional[str] = None, + footer: Optional[str] = None, ): embed = Embed(description=text) @@ -127,8 +127,8 @@ class ModLog: embed.timestamp = timestamp_override or datetime.datetime.utcnow() - if footer_override: - embed.set_footer(text=footer_override) + if footer: + embed.set_footer(text=footer) if thumbnail: embed.set_thumbnail(url=thumbnail) @@ -381,7 +381,8 @@ class ModLog: await self.send_log_message( Icons.user_ban, Colour(Colours.soft_red), "User banned", f"{member.name}#{member.discriminator} (`{member.id}`)", - thumbnail=member.avatar_url_as(static_format="png") + thumbnail=member.avatar_url_as(static_format="png"), + channel_id=Channels.modlog ) async def on_member_join(self, member: Member): @@ -400,7 +401,8 @@ class ModLog: await self.send_log_message( Icons.sign_in, Colour(Colours.soft_green), "User joined", message, - thumbnail=member.avatar_url_as(static_format="png") + thumbnail=member.avatar_url_as(static_format="png"), + channel_id=Channels.userlog ) async def on_member_remove(self, member: Member): @@ -414,7 +416,8 @@ class ModLog: await self.send_log_message( Icons.sign_out, Colour(Colours.soft_red), "User left", f"{member.name}#{member.discriminator} (`{member.id}`)", - thumbnail=member.avatar_url_as(static_format="png") + thumbnail=member.avatar_url_as(static_format="png"), + channel_id=Channels.userlog ) async def on_member_unban(self, guild: Guild, member: User): @@ -428,7 +431,8 @@ class ModLog: await self.send_log_message( Icons.user_unban, Colour.blurple(), "User unbanned", f"{member.name}#{member.discriminator} (`{member.id}`)", - thumbnail=member.avatar_url_as(static_format="png") + thumbnail=member.avatar_url_as(static_format="png"), + channel_id=Channels.modlog ) async def on_member_update(self, before: Member, after: Member): @@ -516,7 +520,8 @@ class ModLog: await self.send_log_message( Icons.user_update, Colour.blurple(), "Member updated", message, - thumbnail=after.avatar_url_as(static_format="png") + thumbnail=after.avatar_url_as(static_format="png"), + channel_id=Channels.userlog ) async def on_raw_bulk_message_delete(self, event: RawBulkMessageDeleteEvent): @@ -705,7 +710,7 @@ class ModLog: await self.send_log_message( Icons.message_edit, Colour.blurple(), "Message edited (Before)", before_response, - channel_id=Channels.message_log, timestamp_override=timestamp, footer_override=footer + channel_id=Channels.message_log, timestamp_override=timestamp, footer=footer ) await self.send_log_message( diff --git a/bot/constants.py b/bot/constants.py index be713cef2..61f62b09c 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -201,7 +201,7 @@ class Filter(metaclass=YAMLGetter): filter_zalgo: bool filter_invites: bool filter_domains: bool - filter_rich_embeds: bool + watch_rich_embeds: bool watch_words: bool watch_tokens: bool @@ -209,7 +209,6 @@ class Filter(metaclass=YAMLGetter): notify_user_zalgo: bool notify_user_invites: bool notify_user_domains: bool - notify_user_rich_embeds: bool ping_everyone: bool guild_invite_whitelist: List[int] @@ -352,6 +351,7 @@ class Channels(metaclass=YAMLGetter): off_topic_3: int python: int reddit: int + userlog: int verification: int diff --git a/config-default.yml b/config-default.yml index b6427b489..5938ae533 100644 --- a/config-default.yml +++ b/config-default.yml @@ -114,6 +114,7 @@ guild: python: 267624335836053506 reddit: 458224812528238616 staff_lounge: &STAFF_LOUNGE 464905259261755392 + userlog: 528976905546760203 verification: 352442727016693763 ignored: [*ADMINS, *MESSAGE_LOG, *MODLOG] @@ -140,7 +141,7 @@ filter: filter_zalgo: false filter_invites: true filter_domains: true - filter_rich_embeds: false + watch_rich_embeds: true watch_words: true watch_tokens: true @@ -149,7 +150,6 @@ filter: notify_user_zalgo: false notify_user_invites: true notify_user_domains: false - notify_user_rich_embeds: true # Filter configuration ping_everyone: true # Ping @everyone when we send a mod-alert? |