aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/cogs/antispam.py25
-rw-r--r--bot/cogs/clean.py63
-rw-r--r--bot/cogs/modlog.py68
-rw-r--r--bot/cogs/token_remover.py41
-rw-r--r--bot/constants.py8
-rw-r--r--config-default.yml7
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