diff options
Diffstat (limited to 'bot/exts/moderation/modlog.py')
-rw-r--r-- | bot/exts/moderation/modlog.py | 226 |
1 files changed, 165 insertions, 61 deletions
diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index be65ade6e..fc9204998 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -1,25 +1,25 @@ import asyncio import difflib import itertools -import logging import typing as t -from datetime import datetime +from datetime import datetime, timezone from itertools import zip_longest import discord from dateutil.relativedelta import relativedelta from deepdiff import DeepDiff -from discord import Colour +from discord import Colour, Message, Thread from discord.abc import GuildChannel from discord.ext.commands import Cog, Context from discord.utils import escape_markdown from bot.bot import Bot from bot.constants import Categories, Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, Roles, URLs +from bot.log import get_logger from bot.utils.messages import format_user from bot.utils.time import humanize_delta -log = logging.getLogger(__name__) +log = get_logger(__name__) GUILD_CHANNEL = t.Union[discord.CategoryChannel, discord.TextChannel, discord.VoiceChannel] @@ -41,7 +41,6 @@ class ModLog(Cog, name="ModLog"): self.bot = bot self._ignored = {event: [] for event in Event} - self._cached_deletes = [] self._cached_edits = [] async def upload_log( @@ -58,7 +57,7 @@ class ModLog(Cog, name="ModLog"): 'bot/deleted-messages', json={ 'actor': actor_id, - 'creation': datetime.utcnow().isoformat(), + 'creation': datetime.now(timezone.utc).isoformat(), 'deletedmessage_set': [ { 'id': message.id, @@ -97,9 +96,10 @@ class ModLog(Cog, name="ModLog"): footer: t.Optional[str] = None, ) -> Context: """Generate log embed and send to logging channel.""" + await self.bot.wait_until_guild_available() # Truncate string directly here to avoid removing newlines embed = discord.Embed( - description=text[:2045] + "..." if len(text) > 2048 else text + description=text[:4093] + "..." if len(text) > 4096 else text ) if title and icon_url: @@ -251,7 +251,7 @@ class ModLog(Cog, name="ModLog"): message = f"**#{after.name}** (`{after.id}`)\n{message}" await self.send_log_message( - Icons.hash_blurple, Colour.blurple(), + Icons.hash_blurple, Colour.og_blurple(), "Channel updated", message ) @@ -326,7 +326,7 @@ class ModLog(Cog, name="ModLog"): message = f"**{after.name}** (`{after.id}`)\n{message}" await self.send_log_message( - Icons.crown_blurple, Colour.blurple(), + Icons.crown_blurple, Colour.og_blurple(), "Role updated", message ) @@ -376,9 +376,9 @@ class ModLog(Cog, name="ModLog"): message = f"**{after.name}** (`{after.id}`)\n{message}" await self.send_log_message( - Icons.guild_update, Colour.blurple(), + Icons.guild_update, Colour.og_blurple(), "Guild updated", message, - thumbnail=after.icon_url_as(format="png") + thumbnail=after.icon.with_static_format("png") ) @Cog.listener() @@ -394,7 +394,7 @@ class ModLog(Cog, name="ModLog"): await self.send_log_message( Icons.user_ban, Colours.soft_red, "User banned", format_user(member), - thumbnail=member.avatar_url_as(static_format="png"), + thumbnail=member.display_avatar.url, channel_id=Channels.user_log ) @@ -404,7 +404,7 @@ class ModLog(Cog, name="ModLog"): if member.guild.id != GuildConstant.id: return - now = datetime.utcnow() + now = datetime.now(timezone.utc) difference = abs(relativedelta(now, member.created_at)) message = format_user(member) + "\n\n**Account age:** " + humanize_delta(difference) @@ -415,7 +415,7 @@ class ModLog(Cog, name="ModLog"): await self.send_log_message( Icons.sign_in, Colours.soft_green, "User joined", message, - thumbnail=member.avatar_url_as(static_format="png"), + thumbnail=member.display_avatar.url, channel_id=Channels.user_log ) @@ -432,7 +432,7 @@ class ModLog(Cog, name="ModLog"): await self.send_log_message( Icons.sign_out, Colours.soft_red, "User left", format_user(member), - thumbnail=member.avatar_url_as(static_format="png"), + thumbnail=member.display_avatar.url, channel_id=Channels.user_log ) @@ -447,9 +447,9 @@ class ModLog(Cog, name="ModLog"): return await self.send_log_message( - Icons.user_unban, Colour.blurple(), + Icons.user_unban, Colour.og_blurple(), "User unbanned", format_user(member), - thumbnail=member.avatar_url_as(static_format="png"), + thumbnail=member.display_avatar.url, channel_id=Channels.mod_log ) @@ -512,35 +512,62 @@ class ModLog(Cog, name="ModLog"): await self.send_log_message( icon_url=Icons.user_update, - colour=Colour.blurple(), + colour=Colour.og_blurple(), title="Member updated", text=message, - thumbnail=after.avatar_url_as(static_format="png"), + thumbnail=after.display_avatar.url, channel_id=Channels.user_log ) - @Cog.listener() - async def on_message_delete(self, message: discord.Message) -> None: - """Log message delete event to message change log.""" + def is_message_blacklisted(self, message: Message) -> bool: + """Return true if the message is in a blacklisted thread or channel.""" + # Ignore bots or DMs + if message.author.bot or not message.guild: + return True + + return self.is_channel_ignored(message.channel.id) + + def is_channel_ignored(self, channel_id: int) -> bool: + """ + Return true if the channel, or parent channel in the case of threads, passed should be ignored by modlog. + + Currently ignored channels are: + 1. Channels not in the guild we care about (constants.Guild.id). + 2. Channels that mods do not have view permissions to + 3. Channels in constants.Guild.modlog_blacklist + """ + channel = self.bot.get_channel(channel_id) + + # Ignore not found channels, DMs, and messages outside of the main guild. + if not channel or not hasattr(channel, "guild") or channel.guild.id != GuildConstant.id: + return True + + # Look at the parent channel of a thread. + if isinstance(channel, Thread): + channel = channel.parent + + # Mod team doesn't have view permission to the channel. + if not channel.permissions_for(channel.guild.get_role(Roles.mod_team)).view_channel: + return True + + return channel.id in GuildConstant.modlog_blacklist + + async def log_cached_deleted_message(self, message: discord.Message) -> None: + """ + Log the message's details to message change log. + + This is called when a cached message is deleted. + """ channel = message.channel author = message.author - # Ignore DMs. - if not message.guild: + if self.is_message_blacklisted(message): return - if message.guild.id != GuildConstant.id or channel.id in GuildConstant.modlog_blacklist: - return - - self._cached_deletes.append(message.id) - if message.id in self._ignored[Event.message_delete]: self._ignored[Event.message_delete].remove(message.id) return - if author.bot: - return - if channel.category: response = ( f"**Author:** {format_user(author)}\n" @@ -564,7 +591,7 @@ class ModLog(Cog, name="ModLog"): # Shorten the message content if necessary content = message.clean_content - remaining_chars = 2040 - len(response) + remaining_chars = 4090 - len(response) if len(content) > remaining_chars: botlog_url = await self.upload_log(messages=[message], actor_id=message.author.id) @@ -581,17 +608,15 @@ class ModLog(Cog, name="ModLog"): channel_id=Channels.message_log ) - @Cog.listener() - 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.modlog_blacklist: - return + async def log_uncached_deleted_message(self, event: discord.RawMessageDeleteEvent) -> None: + """ + Log the message's details to message change log. - await asyncio.sleep(1) # Wait here in case the normal event was fired - - if event.message_id in self._cached_deletes: - # It was in the cache and the normal event was fired, so we can just ignore it - self._cached_deletes.remove(event.message_id) + This is called when a message absent from the cache is deleted. + Hence, the message contents aren't logged. + """ + await self.bot.wait_until_guild_available() + if self.is_channel_ignored(event.channel_id): return if event.message_id in self._ignored[Event.message_delete]: @@ -623,14 +648,17 @@ class ModLog(Cog, name="ModLog"): ) @Cog.listener() + async def on_raw_message_delete(self, event: discord.RawMessageDeleteEvent) -> None: + """Log message deletions to message change log.""" + if event.cached_message is not None: + await self.log_cached_deleted_message(event.cached_message) + else: + await self.log_uncached_deleted_message(event) + + @Cog.listener() async def on_message_edit(self, msg_before: discord.Message, msg_after: discord.Message) -> None: """Log message edit event to message change log.""" - if ( - not msg_before.guild - or msg_before.guild.id != GuildConstant.id - or msg_before.channel.id in GuildConstant.modlog_blacklist - or msg_before.author.bot - ): + if self.is_message_blacklisted(msg_before): return self._cached_edits.append(msg_before.id) @@ -694,25 +722,21 @@ class ModLog(Cog, name="ModLog"): footer = None await self.send_log_message( - Icons.message_edit, Colour.blurple(), "Message edited", response, + Icons.message_edit, Colour.og_blurple(), "Message edited", response, channel_id=Channels.message_log, timestamp_override=timestamp, footer=footer ) @Cog.listener() async def on_raw_message_edit(self, event: discord.RawMessageUpdateEvent) -> None: """Log raw message edit event to message change log.""" + await self.bot.wait_until_guild_available() try: channel = self.bot.get_channel(int(event.data["channel_id"])) message = await channel.fetch_message(event.message_id) except discord.NotFound: # Was deleted before we got the event return - if ( - not message.guild - or message.guild.id != GuildConstant.id - or message.channel.id in GuildConstant.modlog_blacklist - or message.author.bot - ): + if self.is_message_blacklisted(message): return await asyncio.sleep(1) # Wait here in case the normal event was fired @@ -742,16 +766,95 @@ class ModLog(Cog, name="ModLog"): ) await self.send_log_message( - Icons.message_edit, Colour.blurple(), "Message edited (Before)", + Icons.message_edit, Colour.og_blurple(), "Message edited (Before)", before_response, channel_id=Channels.message_log ) await self.send_log_message( - Icons.message_edit, Colour.blurple(), "Message edited (After)", + Icons.message_edit, Colour.og_blurple(), "Message edited (After)", after_response, channel_id=Channels.message_log ) @Cog.listener() + async def on_thread_update(self, before: Thread, after: Thread) -> None: + """Log thread archiving, un-archiving and name edits.""" + if self.is_channel_ignored(after.id): + log.trace("Ignoring update of thread %s (%d)", after.mention, after.id) + return + + if before.name != after.name: + await self.send_log_message( + Icons.hash_blurple, + Colour.og_blurple(), + "Thread name edited", + ( + f"Thread {after.mention} (`{after.id}`) from {after.parent.mention} (`{after.parent.id}`): " + f"`{before.name}` -> `{after.name}`" + ) + ) + return + + if not before.archived and after.archived: + colour = Colours.soft_red + action = "archived" + icon = Icons.hash_red + elif before.archived and not after.archived: + colour = Colours.soft_green + action = "un-archived" + icon = Icons.hash_green + else: + return + + await self.send_log_message( + icon, + colour, + f"Thread {action}", + ( + f"Thread {after.mention} ({after.name}, `{after.id}`) from {after.parent.mention} " + f"(`{after.parent.id}`) was {action}" + ) + ) + + @Cog.listener() + async def on_thread_delete(self, thread: Thread) -> None: + """Log thread deletion.""" + if self.is_channel_ignored(thread.id): + log.trace("Ignoring deletion of thread %s (%d)", thread.mention, thread.id) + return + + await self.send_log_message( + Icons.hash_red, + Colours.soft_red, + "Thread deleted", + ( + f"Thread {thread.mention} ({thread.name}, `{thread.id}`) from {thread.parent.mention} " + f"(`{thread.parent.id}`) deleted" + ) + ) + + @Cog.listener() + async def on_thread_join(self, thread: Thread) -> None: + """Log thread creation.""" + # If we are in the thread already we can most probably assume we already logged it? + # We don't really have a better way of doing this since the API doesn't make any difference between the two + if thread.me: + return + + if self.is_channel_ignored(thread.id): + log.trace("Ignoring creation of thread %s (%d)", thread.mention, thread.id) + return + + await self.send_log_message( + Icons.hash_green, + Colours.soft_green, + "Thread created", + ( + f"Thread {thread.mention} ({thread.name}, `{thread.id}`) from {thread.parent.mention} " + f"(`{thread.parent.id}`) created" + ) + ) + + @Cog.listener() async def on_voice_state_update( self, member: discord.Member, @@ -761,7 +864,8 @@ class ModLog(Cog, name="ModLog"): """Log member voice state changes to the voice log channel.""" if ( member.guild.id != GuildConstant.id - or (before.channel and before.channel.id in GuildConstant.modlog_blacklist) + or (before.channel and self.is_channel_ignored(before.channel.id)) + or (after.channel and self.is_channel_ignored(after.channel.id)) ): return @@ -783,7 +887,7 @@ class ModLog(Cog, name="ModLog"): diff_values = {**diff.get("values_changed", {}), **diff.get("type_changes", {})} icon = Icons.voice_state_blue - colour = Colour.blurple() + colour = Colour.og_blurple() changes = [] for attr, values in diff_values.items(): @@ -820,7 +924,7 @@ class ModLog(Cog, name="ModLog"): colour=colour, title="Voice state updated", text=message, - thumbnail=member.avatar_url_as(static_format="png"), + thumbnail=member.display_avatar.url, channel_id=Channels.voice_log ) |