diff options
author | 2018-07-31 19:39:57 +0000 | |
---|---|---|
committer | 2018-07-31 19:39:57 +0000 | |
commit | 7f5477c7e8790fd0b55ddf0b0fe568e969600ac9 (patch) | |
tree | 8b26673c575291e18b550c591ea5b21294009513 | |
parent | Merge branch 'feature/userinfo' into 'master' (diff) | |
parent | Better mod-log for infractions. (diff) |
Merge branch 'better-modlogs' into 'master'
Better mod-log for infractions.
See merge request python-discord/projects/bot!45
-rw-r--r-- | bot/cogs/clean.py | 8 | ||||
-rw-r--r-- | bot/cogs/moderation.py | 192 | ||||
-rw-r--r-- | bot/cogs/modlog.py | 40 | ||||
-rw-r--r-- | bot/cogs/verification.py | 4 | ||||
-rw-r--r-- | bot/constants.py | 31 | ||||
-rw-r--r-- | config-default.yml | 4 |
6 files changed, 259 insertions, 20 deletions
diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index da0a5a9f2..ffa247b4a 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -9,8 +9,8 @@ from discord.ext.commands import Bot, Context, group from bot.cogs.modlog import ModLog from bot.constants import ( - Channels, CleanMessages, Colours, Icons, - Keys, NEGATIVE_REPLIES, Roles, URLs + Channels, CleanMessages, Colours, Event, + Icons, Keys, NEGATIVE_REPLIES, Roles, URLs ) from bot.decorators import with_role @@ -169,7 +169,7 @@ class Clean: # Always start by deleting the invocation if not invocation_deleted: - self.mod_log.ignore_message_deletion(message.id) + self.mod_log.ignore(Event.message_delete, message.id) await message.delete() invocation_deleted = True continue @@ -202,7 +202,7 @@ class Clean: self.cleaning = False # We should ignore the ID's we stored, so we don't get mod-log spam. - self.mod_log.ignore_message_deletion(*message_ids) + self.mod_log.ignore(Event.message_delete, *message_ids) # Use bulk delete to actually do the cleaning. It's far faster. await ctx.channel.purge( diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 0a0bbae53..b04887b3f 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -1,6 +1,7 @@ import asyncio import datetime import logging +import textwrap from typing import Dict from aiohttp import ClientError @@ -8,7 +9,8 @@ from discord import Colour, Embed, Guild, Member, Object, User from discord.ext.commands import Bot, Context, command, group from bot import constants -from bot.constants import Keys, Roles, URLs +from bot.cogs.modlog import ModLog +from bot.constants import Colours, Event, Icons, Keys, Roles, URLs from bot.converters import InfractionSearchQuery from bot.decorators import with_role from bot.pagination import LinePaginator @@ -29,6 +31,10 @@ class Moderation: self.expiration_tasks: Dict[str, asyncio.Task] = {} self._muted_role = Object(constants.Roles.muted) + @property + def mod_log(self) -> ModLog: + return self.bot.get_cog("ModLog") + async def on_ready(self): # Schedule expiration for previous infractions response = await self.bot.http_session.get( @@ -111,6 +117,7 @@ class Moderation: await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}") return + self.mod_log.ignore(Event.member_remove, user.id) await user.kick(reason=reason) if reason is None: @@ -120,6 +127,19 @@ class Moderation: await ctx.send(result_message) + # 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", + thumbnail=user.avatar_url_as(static_format="png"), + text=textwrap.dedent(f""" + Member: {user.mention} (`{user.id}`) + Actor: {ctx.message.author} + Reason: {reason} + """) + ) + @with_role(*MODERATION_ROLES) @command(name="ban") async def ban(self, ctx: Context, user: User, *, reason: str = None): @@ -150,6 +170,8 @@ class Moderation: await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}") 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) if reason is None: @@ -159,6 +181,19 @@ class Moderation: await ctx.send(result_message) + # 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", + thumbnail=user.avatar_url_as(static_format="png"), + text=textwrap.dedent(f""" + Member: {user.mention} (`{user.id}`) + Actor: {ctx.message.author} + Reason: {reason} + """) + ) + @with_role(*MODERATION_ROLES) @command(name="mute") async def mute(self, ctx: Context, user: Member, *, reason: str = None): @@ -190,6 +225,7 @@ class Moderation: 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: @@ -199,6 +235,19 @@ class Moderation: 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), + title="Member permanently muted", + thumbnail=user.avatar_url_as(static_format="png"), + text=textwrap.dedent(f""" + Member: {user.mention} (`{user.id}`) + Actor: {ctx.message.author} + Reason: {reason} + """) + ) + # endregion # region: Temporary infractions @@ -234,6 +283,7 @@ class Moderation: await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}") return + self.mod_log.ignore(Event.member_update, user.id) await user.add_roles(self._muted_role, reason=reason) infraction_object = response_object["infraction"] @@ -249,6 +299,21 @@ class Moderation: 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), + title="Member temporarily muted", + thumbnail=user.avatar_url_as(static_format="png"), + text=textwrap.dedent(f""" + Member: {user.mention} (`{user.id}`) + Actor: {ctx.message.author} + Reason: {reason} + Duration: {duration} + Expires: {infraction_expiration} + """) + ) + @with_role(*MODERATION_ROLES) @command(name="tempban") async def tempban(self, ctx, user: User, duration: str, *, reason: str = None): @@ -281,6 +346,8 @@ class Moderation: await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}") 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) @@ -297,6 +364,21 @@ class Moderation: await ctx.send(result_message) + # 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", + text=textwrap.dedent(f""" + Member: {user.mention} (`{user.id}`) + Actor: {ctx.message.author} + Reason: {reason} + Duration: {duration} + Expires: {infraction_expiration} + """) + ) + # endregion # region: Remove infractions (un- commands) @@ -333,6 +415,19 @@ class Moderation: self.cancel_expiration(infraction_object["id"]) await ctx.send(f":ok_hand: Un-muted {user.mention}.") + + # Send a log message to the mod log + await self.mod_log.send_log_message( + icon_url=Icons.user_unmute, + colour=Colour(Colours.soft_green), + title="Member unmuted", + thumbnail=user.avatar_url_as(static_format="png"), + text=textwrap.dedent(f""" + Member: {user.mention} (`{user.id}`) + Actor: {ctx.message.author} + Intended expiry: {infraction_object['expires_at']} + """) + ) except Exception: log.exception("There was an error removing an infraction.") await ctx.send(":x: There was an error removing the infraction.") @@ -371,6 +466,19 @@ class Moderation: self.cancel_expiration(infraction_object["id"]) await ctx.send(f":ok_hand: Un-banned {user.mention}.") + + # Send a log message to the mod log + await self.mod_log.send_log_message( + icon_url=Icons.user_unban, + colour=Colour(Colours.soft_green), + title="Member unbanned", + thumbnail=user.avatar_url_as(static_format="png"), + text=textwrap.dedent(f""" + Member: {user.mention} (`{user.id}`) + Actor: {ctx.message.author} + Intended expiry: {infraction_object['expires_at']} + """) + ) except Exception: log.exception("There was an error removing an infraction.") await ctx.send(":x: There was an error removing the infraction.") @@ -400,6 +508,15 @@ class Moderation: """ try: + previous = await self.bot.http_session.get( + URLs.site_infractions_by_id.format( + infraction_id=infraction_id + ), + headers=self.headers + ) + + previous_object = await previous.json() + if duration == "permanent": duration = None # check the current active infraction @@ -432,6 +549,37 @@ class Moderation: await ctx.send(":x: There was an error updating the infraction.") return + prev_infraction = previous_object["infraction"] + + # Get information about the infraction's user + user_id = int(infraction_object["user"]["user_id"]) + user = ctx.guild.get_member(user_id) + + if user: + member_text = f"{user.mention} (`{user.id}`)" + thumbnail = user.avatar_url_as(static_format="png") + else: + member_text = f"`{user_id}`" + thumbnail = None + + # The infraction's actor + actor_id = int(infraction_object["actor"]["user_id"]) + actor = ctx.guild.get_member(actor_id) or f"`{actor_id}`" + + await self.mod_log.send_log_message( + icon_url=Icons.pencil, + colour=Colour.blurple(), + title="Infraction edited", + thumbnail=thumbnail, + text=textwrap.dedent(f""" + Member: {member_text} + Actor: {actor} + Edited by: {ctx.message.author} + Previous expiry: {prev_infraction['expires_at']} + New expiry: {infraction_object['expires_at']} + """) + ) + @with_role(*MODERATION_ROLES) @infraction_edit_group.command(name="reason") async def edit_reason(self, ctx, infraction_id: str, *, reason: str): @@ -442,6 +590,15 @@ class Moderation: """ try: + previous = await self.bot.http_session.get( + URLs.site_infractions_by_id.format( + infraction_id=infraction_id + ), + headers=self.headers + ) + + previous_object = await previous.json() + response = await self.bot.http_session.patch( URLs.site_infractions, json={ @@ -461,6 +618,38 @@ class Moderation: await ctx.send(":x: There was an error updating the infraction.") return + new_infraction = response_object["infraction"] + prev_infraction = previous_object["infraction"] + + # Get information about the infraction's user + user_id = int(new_infraction["user"]["user_id"]) + user = ctx.guild.get_member(user_id) + + if user: + user_text = f"{user.mention} (`{user.id}`)" + thumbnail = user.avatar_url_as(static_format="png") + else: + user_text = f"`{user_id}`" + thumbnail = None + + # The infraction's actor + actor_id = int(new_infraction["actor"]["user_id"]) + actor = ctx.guild.get_member(actor_id) or f"`{actor_id}`" + + await self.mod_log.send_log_message( + icon_url=Icons.pencil, + colour=Colour.blurple(), + title="Infraction edited", + thumbnail=thumbnail, + text=textwrap.dedent(f""" + Member: {user_text} + Actor: {actor} + Edited by: {ctx.message.author} + Previous reason: {prev_infraction['reason']} + New reason: {new_infraction['reason']} + """) + ) + # endregion # region: Search infractions @@ -609,6 +798,7 @@ class Moderation: member: Member = guild.get_member(user_id) if member: # remove the mute role + self.mod_log.ignore(Event.member_update, member.id) await member.remove_roles(self._muted_role) else: log.warning(f"Failed to un-mute user: {user_id} (not found)") diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py index b5a73d6e0..9dd3dce5d 100644 --- a/bot/cogs/modlog.py +++ b/bot/cogs/modlog.py @@ -13,7 +13,7 @@ from discord import ( from discord.abc import GuildChannel from discord.ext.commands import Bot -from bot.constants import Channels, Colours, Emojis, Icons +from bot.constants import Channels, Colours, Emojis, Event, Icons from bot.constants import Guild as GuildConstant from bot.utils.time import humanize @@ -35,15 +35,15 @@ class ModLog: def __init__(self, bot: Bot): self.bot = bot - self._ignored_deletions = [] + self._ignored = {event: [] for event in Event} self._cached_deletes = [] self._cached_edits = [] - def ignore_message_deletion(self, *message_ids: int): - for message_id in message_ids: - if message_id not in self._ignored_deletions: - self._ignored_deletions.append(message_id) + def ignore(self, event: Event, *items: int): + for item in items: + if item not in self._ignored[event]: + self._ignored[event].append(item) async def send_log_message( self, icon_url: Optional[str], colour: Colour, title: Optional[str], text: str, thumbnail: str = None, @@ -288,6 +288,10 @@ class ModLog: if guild.id != GuildConstant.id: return + if member.id in self._ignored[Event.member_ban]: + self._ignored[Event.member_ban].remove(member.id) + return + await self.send_log_message( Icons.user_ban, Colour(Colours.soft_red), "User banned", f"{member.name}#{member.discriminator} (`{member.id}`)", @@ -318,6 +322,10 @@ class ModLog: if member.guild.id != GuildConstant.id: return + if member.id in self._ignored[Event.member_remove]: + self._ignored[Event.member_remove].remove(member.id) + return + await self.send_log_message( Icons.sign_out, Colour(Colours.soft_red), "User left", f"{member.name}#{member.discriminator} (`{member.id}`)", @@ -328,6 +336,10 @@ class ModLog: if guild.id != GuildConstant.id: return + if member.id in self._ignored[Event.member_unban]: + self._ignored[Event.member_unban].remove(member.id) + return + await self.send_log_message( Icons.user_unban, Colour.blurple(), "User unbanned", f"{member.name}#{member.discriminator} (`{member.id}`)", @@ -338,6 +350,10 @@ class ModLog: if before.guild.id != GuildConstant.id: return + if before.id in self._ignored[Event.member_update]: + self._ignored[Event.member_update].remove(before.id) + return + diff = DeepDiff(before, after) changes = [] done = [] @@ -427,8 +443,8 @@ class ModLog: ignored_messages = 0 for message_id in event.message_ids: - if message_id in self._ignored_deletions: - self._ignored_deletions.remove(message_id) + if message_id in self._ignored[Event.message_delete]: + self._ignored[Event.message_delete].remove(message_id) ignored_messages += 1 if ignored_messages >= len(event.message_ids): @@ -457,8 +473,8 @@ class ModLog: self._cached_deletes.append(message.id) - if message.id in self._ignored_deletions: - self._ignored_deletions.remove(message.id) + if message.id in self._ignored[Event.message_delete]: + self._ignored[Event.message_delete].remove(message.id) return if author.bot: @@ -503,8 +519,8 @@ class ModLog: self._cached_deletes.remove(event.message_id) return - if event.message_id in self._ignored_deletions: - self._ignored_deletions.remove(event.message_id) + if event.message_id in self._ignored[Event.message_delete]: + self._ignored[Event.message_delete].remove(event.message_id) return channel = self.bot.get_channel(event.channel_id) diff --git a/bot/cogs/verification.py b/bot/cogs/verification.py index 84912e947..8d29a4bee 100644 --- a/bot/cogs/verification.py +++ b/bot/cogs/verification.py @@ -4,7 +4,7 @@ from discord import Message, NotFound, Object from discord.ext.commands import Bot, Context, command from bot.cogs.modlog import ModLog -from bot.constants import Channels, Roles +from bot.constants import Channels, Event, Roles from bot.decorators import in_channel, without_role log = logging.getLogger(__name__) @@ -90,7 +90,7 @@ class Verification: log.trace(f"Deleting the message posted by {ctx.author}.") try: - self.mod_log.ignore_message_deletion(ctx.message.id) + self.mod_log.ignore(Event.message_delete, ctx.message.id) await ctx.message.delete() except NotFound: log.trace("No message found, it must have been deleted by another bot.") diff --git a/bot/constants.py b/bot/constants.py index ab426a7ab..756c03027 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -13,6 +13,7 @@ their default values from `config-default.yml`. import logging import os from collections.abc import Mapping +from enum import Enum from pathlib import Path from typing import Dict, List @@ -281,6 +282,11 @@ class Icons(metaclass=YAMLGetter): user_unban: str user_update: str + user_mute: str + user_unmute: str + + pencil: str + class CleanMessages(metaclass=YAMLGetter): section = "bot" @@ -394,6 +400,7 @@ class URLs(metaclass=YAMLGetter): site_infractions: str site_infractions_user: str site_infractions_type: str + site_infractions_by_id: str site_infractions_user_type_current: str site_infractions_user_type: str status: str @@ -464,3 +471,27 @@ ERROR_REPLIES = [ "Are you trying to kill me?", "Noooooo!!" ] + + +class Event(Enum): + """ + Event names. This does not include every event (for example, raw + events aren't here), but only events used in ModLog for now. + """ + + guild_channel_create = "guild_channel_create" + guild_channel_delete = "guild_channel_delete" + guild_channel_update = "guild_channel_update" + guild_role_create = "guild_role_create" + guild_role_delete = "guild_role_delete" + guild_role_update = "guild_role_update" + guild_update = "guild_update" + + member_join = "member_join" + member_remove = "member_remove" + member_ban = "member_ban" + member_unban = "member_unban" + member_update = "member_update" + + message_delete = "message_delete" + message_edit = "message_edit" diff --git a/config-default.yml b/config-default.yml index 0519244b0..415c1fcdf 100644 --- a/config-default.yml +++ b/config-default.yml @@ -65,7 +65,9 @@ style: user_update: "https://cdn.discordapp.com/emojis/469952898684551168.png" user_mute: "https://cdn.discordapp.com/emojis/472472640100106250.png" - user_unmute: "https://cdn.discordapp.com/emojis/472472640100106250.png" + user_unmute: "https://cdn.discordapp.com/emojis/472472639206719508.png" + + pencil: "https://cdn.discordapp.com/emojis/470326272401211415.png" guild: |