diff options
| author | 2018-08-11 16:47:20 +0200 | |
|---|---|---|
| committer | 2018-08-11 16:47:20 +0200 | |
| commit | 9cf103901a8981ced3a16bb47aaa0f070f45b4aa (patch) | |
| tree | 57fddf716a9a6362a66dc5a5a190afd5eeff4e3f | |
| parent | Fixing bug where antispam would not ignore message ids, making antispam mod-a... (diff) | |
Makes TokenRemover, AntiSpam and Clean more consistent, with embeds, message delete ignores, and using the Logs API.
| -rw-r--r-- | bot/cogs/antispam.py | 25 | ||||
| -rw-r--r-- | bot/cogs/clean.py | 63 | ||||
| -rw-r--r-- | bot/cogs/modlog.py | 68 | ||||
| -rw-r--r-- | bot/cogs/token_remover.py | 41 | ||||
| -rw-r--r-- | bot/constants.py | 8 | ||||
| -rw-r--r-- | config-default.yml | 7 |
6 files changed, 131 insertions, 81 deletions
diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index 9f8821d35..65c3f0b4b 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -2,7 +2,7 @@ import asyncio import logging import textwrap from datetime import datetime, timedelta -from typing import Dict, List +from typing import List from dateutil.relativedelta import relativedelta from discord import Colour, Member, Message, Object, TextChannel @@ -99,13 +99,13 @@ class AntiSpam: # Fire it off as a background task to ensure # that the sleep doesn't block further tasks self.bot.loop.create_task( - self.punish(message, member, rule_config, full_reason) + self.punish(message, member, full_reason, relevant_messages) ) await self.maybe_delete_messages(message.channel, relevant_messages) break - async def punish(self, msg: Message, member: Member, rule_config: Dict[str, int], reason: str): + async def punish(self, msg: Message, member: Member, reason: str, messages: List[Message]): # Sanity check to ensure we're not lagging behind if self.muted_role not in member.roles: remove_role_after = AntiSpamConfig.punishment['remove_after'] @@ -116,8 +116,22 @@ class AntiSpam: f"**Triggered by:** {member.display_name}#{member.discriminator} (`{member.id}`)\n" f"**Channel:** {msg.channel.mention}\n" f"**Reason:** {reason}\n" - "See the message and mod log for further details." ) + + # For multiple messages, use the logs API + if len(messages) > 1: + url = await self.mod_log.upload_log(messages) + mod_alert_message += f"A complete log of the offending messages can be found [here]({url})" + else: + mod_alert_message += "Message:\n" + content = messages[0].clean_content + remaining_chars = 2040 - len(mod_alert_message) + + if len(content) > remaining_chars: + content = content[:remaining_chars] + "..." + + mod_alert_message += f"{content}" + await self.mod_log.send_log_message( icon_url=Icons.filtering, colour=Colour(Colours.soft_red), @@ -125,7 +139,7 @@ class AntiSpam: text=mod_alert_message, thumbnail=msg.author.avatar_url_as(static_format="png"), channel_id=Channels.mod_alerts, - ping_everyone=True + ping_everyone=AntiSpamConfig.ping_everyone ) await member.add_roles(self.muted_role, reason=reason) @@ -163,6 +177,7 @@ class AntiSpam: # Otherwise, the bulk delete endpoint will throw up. # Delete the message directly instead. else: + self.mod_log.ignore(Event.message_delete, messages[0].id) await messages[0].delete() diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index ffa247b4a..8a9b01d07 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -3,14 +3,13 @@ import random import re from typing import Optional -from aiohttp.client_exceptions import ClientResponseError from discord import Colour, Embed, Message, User from discord.ext.commands import Bot, Context, group from bot.cogs.modlog import ModLog from bot.constants import ( Channels, CleanMessages, Colours, Event, - Icons, Keys, NEGATIVE_REPLIES, Roles, URLs + Icons, NEGATIVE_REPLIES, Roles ) from bot.decorators import with_role @@ -34,39 +33,12 @@ class Clean: def __init__(self, bot: Bot): self.bot = bot - self.headers = {"X-API-KEY": Keys.site_api} self.cleaning = False @property def mod_log(self) -> ModLog: return self.bot.get_cog("ModLog") - async def _upload_log(self, log_data: list) -> str: - """ - Uploads the log data to the database via - an API endpoint for uploading logs. - - Returns a URL that can be used to view the log. - """ - - response = await self.bot.http_session.post( - URLs.site_clean_api, - headers=self.headers, - json={"log_data": log_data} - ) - - try: - data = await response.json() - log_id = data["log_id"] - except (KeyError, ClientResponseError): - log.debug( - "API returned an unexpected result:\n" - f"{response.text}" - ) - return - - return f"{URLs.site_clean_logs}/{log_id}" - async def _clean_messages( self, amount: int, ctx: Context, bots_only: bool = False, user: User = None, @@ -156,7 +128,7 @@ class Clean: predicate = None # Delete all messages # Look through the history and retrieve message data - message_log = [] + messages = [] message_ids = [] self.cleaning = True invocation_deleted = False @@ -176,28 +148,8 @@ class Clean: # If the message passes predicate, let's save it. if predicate is None or predicate(message): - author = f"{message.author.name}#{message.author.discriminator}" - - # message.author may return either a User or a Member. Users don't have roles. - if type(message.author) is User: - role_id = Roles.developer - else: - role_id = message.author.top_role.id - - content = message.content - embeds = [embed.to_dict() for embed in message.embeds] - attachments = ["<Attachment>" for _ in message.attachments] - message_ids.append(message.id) - message_log.append({ - "content": content, - "author": author, - "user_id": str(message.author.id), - "role_id": str(role_id), - "timestamp": message.created_at.strftime("%D %H:%M"), - "attachments": attachments, - "embeds": embeds, - }) + messages.append(message) self.cleaning = False @@ -211,9 +163,9 @@ class Clean: ) # Reverse the list to restore chronological order - if message_log: - message_log = list(reversed(message_log)) - upload_log = await self._upload_log(message_log) + if messages: + messages = list(reversed(messages)) + log_url = await self.mod_log.upload_log(messages) else: # Can't build an embed, nothing to clean! embed = Embed( @@ -224,10 +176,9 @@ class Clean: return # Build the embed and send it - print(upload_log) message = ( f"**{len(message_ids)}** messages deleted in <#{ctx.channel.id}> by **{ctx.author.name}**\n\n" - f"A log of the deleted messages can be found [here]({upload_log})." + f"A log of the deleted messages can be found [here]({log_url})." ) await self.mod_log.send_log_message( diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py index 9dd3dce5d..226c62952 100644 --- a/bot/cogs/modlog.py +++ b/bot/cogs/modlog.py @@ -3,6 +3,7 @@ import datetime import logging from typing import List, Optional, Union +from aiohttp import ClientResponseError from dateutil.relativedelta import relativedelta from deepdiff import DeepDiff from discord import ( @@ -13,7 +14,7 @@ from discord import ( from discord.abc import GuildChannel from discord.ext.commands import Bot -from bot.constants import Channels, Colours, Emojis, Event, Icons +from bot.constants import Channels, Colours, Emojis, Event, Icons, Keys, Roles, URLs from bot.constants import Guild as GuildConstant from bot.utils.time import humanize @@ -35,11 +36,65 @@ class ModLog: def __init__(self, bot: Bot): self.bot = bot + self.headers = {"X-API-KEY": Keys.site_api} self._ignored = {event: [] for event in Event} self._cached_deletes = [] self._cached_edits = [] + async def upload_log(self, messages: List[Message]) -> Optional[str]: + """ + Uploads the log data to the database via + an API endpoint for uploading logs. + + Used in several mod log embeds. + + Returns a URL that can be used to view the log. + """ + + log_data = [] + + for message in messages: + author = f"{message.author.name}#{message.author.discriminator}" + + # message.author may return either a User or a Member. Users don't have roles. + if type(message.author) is User: + role_id = Roles.developer + else: + role_id = message.author.top_role.id + + content = message.content + embeds = [embed.to_dict() for embed in message.embeds] + attachments = ["<Attachment>" for _ in message.attachments] + + log_data.append({ + "content": content, + "author": author, + "user_id": str(message.author.id), + "role_id": str(role_id), + "timestamp": message.created_at.strftime("%D %H:%M"), + "attachments": attachments, + "embeds": embeds, + }) + + response = await self.bot.http_session.post( + URLs.site_logs_api, + headers=self.headers, + json={"log_data": log_data} + ) + + try: + data = await response.json() + log_id = data["log_id"] + except (KeyError, ClientResponseError): + log.debug( + "API returned an unexpected result:\n" + f"{response.text}" + ) + return + + return f"{URLs.site_logs_view}/{log_id}" + def ignore(self, event: Event, *items: int): for item in items: if item not in self._ignored[event]: @@ -486,7 +541,6 @@ class ModLog: f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{message.id}`\n" "\n" - f"{message.clean_content}" ) else: response = ( @@ -494,9 +548,17 @@ class ModLog: f"**Channel:** #{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{message.id}`\n" "\n" - f"{message.clean_content}" ) + # Shorten the message content if necessary + content = message.clean_content + remaining_chars = 2040 - len(response) + + if len(content) > remaining_chars: + content = content[:remaining_chars] + "..." + + response += f"{content}" + if message.attachments: # Prepend the message metadata with the number of attachments response = f"**Attachments:** {len(message.attachments)}\n" + response diff --git a/bot/cogs/token_remover.py b/bot/cogs/token_remover.py index 74bc0d9b2..846a46f9d 100644 --- a/bot/cogs/token_remover.py +++ b/bot/cogs/token_remover.py @@ -5,12 +5,12 @@ import re import struct from datetime import datetime -from discord import Message +from discord import Colour, Message from discord.ext.commands import Bot from discord.utils import snowflake_time -from bot.constants import Channels - +from bot.cogs.modlog import ModLog +from bot.constants import Channels, Colours, Event, Icons log = logging.getLogger(__name__) @@ -26,12 +26,12 @@ DISCORD_EPOCH_TIMESTAMP = datetime(2017, 1, 1) TOKEN_EPOCH = 1_293_840_000 TOKEN_RE = re.compile( r"(?<=(\"|'))" # Lookbehind: Only match if there's a double or single quote in front - r"[^\W\.]+" # Matches token part 1: The user ID string, encoded as base64 - r"\." # Matches a literal dot between the token parts - r"[^\W\.]+" # Matches token part 2: The creation timestamp, as an integer - r"\." # Matches a literal dot between the token parts - r"[^\W\.]+" # Matches token part 3: The HMAC, unused by us, but check that it isn't empty - r"(?=(\"|'))" # Lookahead: Only match if there's a double or single quote after + r"[^\W\.]+" # Matches token part 1: The user ID string, encoded as base64 + r"\." # Matches a literal dot between the token parts + r"[^\W\.]+" # Matches token part 2: The creation timestamp, as an integer + r"\." # Matches a literal dot between the token parts + r"[^\W\.]+" # Matches token part 3: The HMAC, unused by us, but check that it isn't empty + r"(?=(\"|'))" # Lookahead: Only match if there's a double or single quote after ) @@ -40,10 +40,10 @@ class TokenRemover: def __init__(self, bot: Bot): self.bot = bot - self.mod_log = None - async def on_ready(self): - self.mod_log = self.bot.get_channel(Channels.modlog) + @property + def mod_log(self) -> ModLog: + return self.bot.get_cog("ModLog") async def on_message(self, msg: Message): if msg.author.bot: @@ -59,13 +59,26 @@ class TokenRemover: return if self.is_valid_user_id(user_id) and self.is_valid_timestamp(creation_timestamp): + self.mod_log.ignore(Event.message_delete, msg.id) await msg.delete() await msg.channel.send(DELETION_MESSAGE_TEMPLATE.format(mention=msg.author.mention)) - await self.mod_log.send( - ":key2::mute: censored a seemingly valid token sent by " + + message = ( + "Censored a seemingly valid token sent by " f"{msg.author} (`{msg.author.id}`) in {msg.channel.mention}, token was " f"`{user_id}.{creation_timestamp}.{'x' * len(hmac)}`" ) + log.debug(message) + + # Send pretty mod log embed to mod-alerts + await self.mod_log.send_log_message( + icon_url=Icons.token_removed, + colour=Colour(Colours.soft_red), + title="Token removed!", + text=message, + thumbnail=msg.author.avatar_url_as(static_format="png"), + channel_id=Channels.mod_alerts, + ) @staticmethod def is_valid_user_id(b64_content: str) -> bool: diff --git a/bot/constants.py b/bot/constants.py index 3ded974a4..981aa19ed 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -278,6 +278,8 @@ class Icons(metaclass=YAMLGetter): sign_in: str sign_out: str + token_removed: str + user_ban: str user_unban: str user_update: str @@ -388,9 +390,10 @@ class URLs(metaclass=YAMLGetter): site_api: str site_facts_api: str site_clean_api: str - site_clean_logs: str site_hiphopify_api: str site_idioms_api: str + site_logs_api: str + site_logs_view: str site_names_api: str site_quiz_api: str site_schema: str @@ -419,6 +422,9 @@ class Reddit(metaclass=YAMLGetter): class AntiSpam(metaclass=YAMLGetter): section = 'anti_spam' + clean_offending: bool + ping_everyone: bool + punishment: Dict[str, Dict[str, int]] rules: Dict[str, Dict[str, int]] diff --git a/config-default.yml b/config-default.yml index 651e43693..bd62d1ae5 100644 --- a/config-default.yml +++ b/config-default.yml @@ -60,6 +60,8 @@ style: sign_in: "https://cdn.discordapp.com/emojis/469952898181234698.png" sign_out: "https://cdn.discordapp.com/emojis/469952898089091082.png" + token_removed: "https://cdn.discordapp.com/emojis/470326273298792469.png" + user_ban: "https://cdn.discordapp.com/emojis/469952898026045441.png" user_unban: "https://cdn.discordapp.com/emojis/469952898692808704.png" user_update: "https://cdn.discordapp.com/emojis/469952898684551168.png" @@ -215,8 +217,6 @@ urls: site_schema: &SCHEMA "https://" site_bigbrother_api: !JOIN [*SCHEMA, *API, "/bot/bigbrother"] - site_clean_api: !JOIN [*SCHEMA, *API, "/bot/clean"] - site_clean_logs: !JOIN [*SCHEMA, *API, "/bot/clean_logs"] site_docs_api: !JOIN [*SCHEMA, *API, "/bot/docs"] site_facts_api: !JOIN [*SCHEMA, *API, "/bot/snake_facts"] site_hiphopify_api: !JOIN [*SCHEMA, *API, "/bot/hiphopify"] @@ -226,6 +226,8 @@ urls: site_infractions_type: !JOIN [*SCHEMA, *API, "/bot/infractions/type/{infraction_type}"] site_infractions_by_id: !JOIN [*SCHEMA, *API, "/bot/infractions/id/{infraction_id}"] site_infractions_user_type_current: !JOIN [*SCHEMA, *API, "/bot/infractions/user/{user_id}/{infraction_type}/current"] + site_logs_api: !JOIN [*SCHEMA, *API, "/bot/logs"] + site_logs_view: !JOIN [*SCHEMA, *DOMAIN, "/bot/logs"] site_names_api: !JOIN [*SCHEMA, *API, "/bot/snake_names"] site_off_topic_names_api: !JOIN [*SCHEMA, *API, "/bot/off-topic-names"] site_quiz_api: !JOIN [*SCHEMA, *API, "/bot/snake_quiz"] @@ -249,6 +251,7 @@ urls: anti_spam: # Clean messages that violate a rule. clean_offending: true + ping_everyone: true punishment: role_id: *MUTED_ROLE |