aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Gareth Coles <[email protected]>2018-07-10 23:45:46 +0100
committerGravatar Gareth Coles <[email protected]>2018-07-10 23:45:46 +0100
commit635baa716fd25f7b7d0361fe9b3b0741d4101857 (patch)
treeb4a014cfc2b84adfd9f852dc8802f03b573aa82f
parentForce bot redeploy (diff)
parentMerge branch 'feature/otname-list-command' into 'master' (diff)
Merge remote-tracking branch 'origin/master'
-rw-r--r--bot/__main__.py1
-rw-r--r--bot/cogs/defcon.py4
-rw-r--r--bot/cogs/off_topic_names.py27
-rw-r--r--bot/cogs/token_remover.py94
-rw-r--r--bot/cogs/utils.py9
5 files changed, 129 insertions, 6 deletions
diff --git a/bot/__main__.py b/bot/__main__.py
index f297c8f1a..f470a42d6 100644
--- a/bot/__main__.py
+++ b/bot/__main__.py
@@ -71,6 +71,7 @@ bot.load_extension("bot.cogs.off_topic_names")
bot.load_extension("bot.cogs.snakes")
bot.load_extension("bot.cogs.snekbox")
bot.load_extension("bot.cogs.tags")
+bot.load_extension("bot.cogs.token_remover")
bot.load_extension("bot.cogs.utils")
bot.load_extension("bot.cogs.verification")
diff --git a/bot/cogs/defcon.py b/bot/cogs/defcon.py
index 168cc7666..ea50bdf63 100644
--- a/bot/cogs/defcon.py
+++ b/bot/cogs/defcon.py
@@ -13,7 +13,7 @@ REJECTION_MESSAGE = """
Hi, {user} - Thanks for your interest in our server!
Due to a current (or detected) cyberattack on our community, we've limited access to the server for new accounts. Since
-your account is relatively now, we're unable to provide access to the server at this time.
+your account is relatively new, we're unable to provide access to the server at this time.
Even so, thanks for joining! We're very excited at the possibility of having you here, and we hope that this situation
will be resolved soon. In the meantime, please feel free to peruse the resources on our site at
@@ -63,7 +63,7 @@ class Defcon:
now = datetime.utcnow()
if now - member.created_at < self.days:
- log.info(f"Rejecting user {member}: Account is too old and DEFCON is enabled")
+ log.info(f"Rejecting user {member}: Account is too new and DEFCON is enabled")
try:
await member.send(REJECTION_MESSAGE.format(user=member.mention))
diff --git a/bot/cogs/off_topic_names.py b/bot/cogs/off_topic_names.py
index 2a3cb2aa7..90510c8c4 100644
--- a/bot/cogs/off_topic_names.py
+++ b/bot/cogs/off_topic_names.py
@@ -2,10 +2,12 @@ import asyncio
import logging
from datetime import datetime, timedelta
+from discord import Colour, Embed
from discord.ext.commands import BadArgument, Bot, Context, Converter, command
from bot.constants import Channels, Keys, Roles, URLs
from bot.decorators import with_role
+from bot.pagination import LinePaginator
CHANNELS = (Channels.off_topic_0, Channels.off_topic_1, Channels.off_topic_2)
@@ -20,10 +22,10 @@ class OffTopicName(Converter):
if not (2 <= len(argument) <= 96):
raise BadArgument("Channel name must be between 2 and 96 chars long")
- elif not all(c.isalpha() or c == '-' for c in argument):
+ elif not all(c.isalnum() or c == '-' for c in argument):
raise BadArgument(
"Channel name must only consist of"
- " alphabetic characters or minus signs"
+ " alphanumeric characters or minus signs"
)
elif not argument.islower():
@@ -104,6 +106,27 @@ class OffTopicNames:
error_reason = response.get('message', "No reason provided.")
await ctx.send(f":warning: got non-200 from the API: {error_reason}")
+ @command(name='otname.list()', aliases=['otname.list'])
+ @with_role(Roles.owner, Roles.admin, Roles.moderator)
+ async def otname_list(self, ctx):
+ """
+ Lists all currently known off-topic channel names in a paginator.
+ Restricted to Moderator and above to not spoil the surprise.
+ """
+
+ result = await self.bot.http_session.get(
+ URLs.site_off_topic_names_api,
+ headers=self.headers
+ )
+ response = await result.json()
+ lines = sorted(f"• {name}" for name in response)
+
+ embed = Embed(
+ title=f"Known off-topic names (`{len(response)}` total)",
+ colour=Colour.blue()
+ )
+ await LinePaginator.paginate(lines, ctx, embed, max_size=400, empty=False)
+
def setup(bot: Bot):
bot.add_cog(OffTopicNames(bot))
diff --git a/bot/cogs/token_remover.py b/bot/cogs/token_remover.py
new file mode 100644
index 000000000..c8621118b
--- /dev/null
+++ b/bot/cogs/token_remover.py
@@ -0,0 +1,94 @@
+import base64
+import binascii
+import logging
+import re
+import struct
+from datetime import datetime
+
+from discord import Message
+from discord.ext.commands import Bot
+from discord.utils import snowflake_time
+
+from bot.constants import Channels
+
+
+log = logging.getLogger(__name__)
+
+DELETION_MESSAGE_TEMPLATE = (
+ "Hey {mention}! I noticed you posted a seemingly valid Discord API "
+ "token in your message and have removed your message to prevent abuse. "
+ "We recommend regenerating your token regardless, which you can do here: "
+ "<https://discordapp.com/developers/applications/me>\n"
+ "Feel free to re-post it with the token removed. "
+ "If you believe this was a mistake, please let us know!"
+)
+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
+)
+
+
+class TokenRemover:
+ """Scans messages for potential discord.py bot tokens and removes them."""
+
+ def __init__(self, bot: Bot):
+ self.bot = bot
+ self.modlog = None
+
+ async def on_ready(self):
+ self.modlog = self.bot.get_channel(Channels.modlog)
+
+ async def on_message(self, msg: Message):
+ if msg.author.bot:
+ return
+
+ maybe_match = TOKEN_RE.search(msg.content)
+ if maybe_match is None:
+ return
+
+ try:
+ user_id, creation_timestamp, hmac = maybe_match.group(0).split('.')
+ except ValueError:
+ return
+
+ if self.is_valid_user_id(user_id) and self.is_valid_timestamp(creation_timestamp):
+ await msg.delete()
+ await msg.channel.send(DELETION_MESSAGE_TEMPLATE.format(mention=msg.author.mention))
+ await self.modlog.send(
+ ":key2::mute: 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)}`"
+ )
+
+ @staticmethod
+ def is_valid_user_id(b64_content: str) -> bool:
+ b64_content += '=' * (-len(b64_content) % 4)
+
+ try:
+ content: bytes = base64.b64decode(b64_content)
+ return content.decode('utf-8').isnumeric()
+ except (binascii.Error, UnicodeDecodeError):
+ return False
+
+ @staticmethod
+ def is_valid_timestamp(b64_content: str) -> bool:
+ b64_content += '=' * (-len(b64_content) % 4)
+
+ try:
+ content = base64.urlsafe_b64decode(b64_content)
+ snowflake = struct.unpack('i', content)[0]
+ except (binascii.Error, struct.error):
+ return False
+ return snowflake_time(snowflake + TOKEN_EPOCH) < DISCORD_EPOCH_TIMESTAMP
+
+
+def setup(bot: Bot):
+ bot.add_cog(TokenRemover(bot))
+ log.info("Cog loaded: TokenRemover")
diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py
index ee1f0a199..7b11f521c 100644
--- a/bot/cogs/utils.py
+++ b/bot/cogs/utils.py
@@ -30,8 +30,13 @@ class Utils:
Fetches information about a PEP and sends it to the channel.
"""
- # Attempt to fetch the PEP from Github.
- pep_url = f"{self.base_github_pep_url}{pep_number.zfill(4)}.txt"
+ # Newer PEPs are written in RST instead of txt
+ if int(pep_number) > 542:
+ pep_url = f"{self.base_github_pep_url}{pep_number.zfill(4)}.rst"
+ else:
+ pep_url = f"{self.base_github_pep_url}{pep_number.zfill(4)}.txt"
+
+ # Attempt to fetch the PEP
log.trace(f"Requesting PEP {pep_number} with {pep_url}")
response = await self.bot.http_session.get(pep_url)