diff options
| -rw-r--r-- | bot/__init__.py | 14 | ||||
| -rw-r--r-- | bot/__main__.py | 3 | ||||
| -rw-r--r-- | bot/cogs/antispam.py | 3 | ||||
| -rw-r--r-- | bot/cogs/filtering.py | 40 | ||||
| -rw-r--r-- | bot/cogs/modlog.py | 11 | ||||
| -rw-r--r-- | bot/cogs/snekbox.py | 43 | ||||
| -rw-r--r-- | bot/cogs/tags.py | 23 | ||||
| -rw-r--r-- | bot/constants.py | 12 | ||||
| -rw-r--r-- | config-default.yml | 18 |
9 files changed, 110 insertions, 57 deletions
diff --git a/bot/__init__.py b/bot/__init__.py index 5a446d71c..54550842e 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -88,12 +88,8 @@ for key, value in logging.Logger.manager.loggerDict.items(): value.addHandler(handler) -# Silence aio_pika.pika.{callback,channel}, discord, PIL, and and websockets -logging.getLogger("aio_pika.pika.callback").setLevel(logging.ERROR) -logging.getLogger("aio_pika.pika.channel").setLevel(logging.ERROR) -logging.getLogger("discord.client").setLevel(logging.ERROR) -logging.getLogger("discord.gateway").setLevel(logging.ERROR) -logging.getLogger("discord.state").setLevel(logging.ERROR) -logging.getLogger("discord.http").setLevel(logging.ERROR) -logging.getLogger("PIL.PngImagePlugin").setLevel(logging.ERROR) -logging.getLogger("websockets.protocol").setLevel(logging.ERROR) +# Silence irrelevant loggers +logging.getLogger("aio_pika").setLevel(logging.ERROR) +logging.getLogger("discord").setLevel(logging.ERROR) +logging.getLogger("PIL").setLevel(logging.ERROR) +logging.getLogger("websockets").setLevel(logging.ERROR) diff --git a/bot/__main__.py b/bot/__main__.py index 0055e19ba..0018cba9c 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -53,15 +53,14 @@ bot.load_extension("bot.cogs.bot") bot.load_extension("bot.cogs.clean") bot.load_extension("bot.cogs.cogs") - # Only load this in production if not DEBUG_MODE: + bot.load_extension("bot.cogs.doc") bot.load_extension("bot.cogs.verification") # Feature cogs bot.load_extension("bot.cogs.deployment") bot.load_extension("bot.cogs.defcon") -bot.load_extension("bot.cogs.doc") bot.load_extension("bot.cogs.eval") bot.load_extension("bot.cogs.fun") bot.load_extension("bot.cogs.hiphopify") diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index 7a33ba9e8..d5b72718c 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -56,7 +56,8 @@ class AntiSpam: async def on_message(self, message: Message): if ( - message.guild.id != GuildConfig.id + not message.guild + or message.guild.id != GuildConfig.id or message.author.bot or (message.channel.id in WHITELISTED_CHANNELS and not DEBUG_MODE) or (message.author.top_role.id in WHITELISTED_ROLES and not DEBUG_MODE) diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py index 586c99174..36be78a7e 100644 --- a/bot/cogs/filtering.py +++ b/bot/cogs/filtering.py @@ -7,7 +7,7 @@ from discord.ext.commands import Bot from bot.cogs.modlog import ModLog from bot.constants import ( Channels, Colours, DEBUG_MODE, - Filter, Icons + Filter, Icons, URLs ) log = logging.getLogger(__name__) @@ -24,6 +24,9 @@ INVITE_RE = ( URL_RE = "(https?://[^\s]+)" ZALGO_RE = r"[\u0300-\u036F\u0489]" +RETARDED_RE = r"(re+)tar+(d+|t+)(ed)?" +SELF_DEPRECATION_RE = fr"((i'?m)|(i am)|(it'?s)|(it is)) (.+? )?{RETARDED_RE}" +RETARDED_QUESTIONS_RE = fr"{RETARDED_RE} questions?" class Filtering: @@ -111,8 +114,8 @@ class Filtering: message = ( f"The {filter_name} {_filter['type']} was triggered " f"by **{msg.author.name}#{msg.author.discriminator}** " - f"(`{msg.author.id}`) in <#{msg.channel.id}> with the " - f"following message:\n\n" + f"(`{msg.author.id}`) in <#{msg.channel.id}> with [the " + f"following message]({msg.jump_url}):\n\n" f"{msg.content}" ) @@ -148,6 +151,18 @@ class Filtering: for expression in Filter.word_watchlist: if re.search(fr"\b{expression}\b", text, re.IGNORECASE): + + # Special handling for `retarded` + if expression == RETARDED_RE: + + # stuff like "I'm just retarded" + if re.search(SELF_DEPRECATION_RE, text, re.IGNORECASE): + return False + + # stuff like "sorry for all the retarded questions" + elif re.search(RETARDED_QUESTIONS_RE, text, re.IGNORECASE): + return False + return True return False @@ -165,7 +180,10 @@ class Filtering: for expression in Filter.token_watchlist: if re.search(fr"{expression}", text, re.IGNORECASE): - return True + + # Make sure it's not a URL + if not re.search(URL_RE, text, re.IGNORECASE): + return True return False @@ -197,8 +215,7 @@ class Filtering: return bool(re.search(ZALGO_RE, text)) - @staticmethod - async def _has_invites(text: str) -> bool: + async def _has_invites(self, text: str) -> bool: """ Returns True if the text contains an invite which is not on the guild_invite_whitelist in config.yml. @@ -207,7 +224,7 @@ class Filtering: """ # Remove spaces to prevent cases like - # d i s c o r d . c o m / i n v i t e / p y t h o n + # d i s c o r d . c o m / i n v i t e / s e x y t e e n s text = text.replace(" ", "") # Remove backslashes to prevent escape character aroundfuckery like @@ -217,12 +234,13 @@ class Filtering: invites = re.findall(INVITE_RE, text, re.IGNORECASE) for invite in invites: - filter_invite = ( - invite not in Filter.guild_invite_whitelist - and invite.lower() not in Filter.vanity_url_whitelist + response = await self.bot.http_session.get( + f"{URLs.discord_invite_api}/{invite}" ) + response = await response.json() + guild_id = int(response.get("guild", {}).get("id")) - if filter_invite: + if guild_id not in Filter.guild_invite_whitelist: return True return False diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py index 2f72d92fc..9c81661ba 100644 --- a/bot/cogs/modlog.py +++ b/bot/cogs/modlog.py @@ -360,7 +360,7 @@ class ModLog: now = datetime.datetime.utcnow() difference = abs(relativedelta(now, member.created_at)) - message += "\n\n**Account age:** " + humanize_delta(member.created_at) + message += "\n\n**Account age:** " + humanize_delta(difference) if difference.days < 1 and difference.months < 1 and difference.years < 1: # New user account! message = f"{Emojis.new} {message}" @@ -440,7 +440,7 @@ class ModLog: if key in done or key in MEMBER_CHANGES_SUPPRESSED: continue - if key == "roles": + if key == "_roles": new_roles = after.roles old_roles = before.roles @@ -453,10 +453,11 @@ class ModLog: changes.append(f"**Role added:** {role.name} (`{role.id}`)") else: - new = value["new_value"] - old = value["old_value"] + new = value.get("new_value") + old = value.get("old_value") - changes.append(f"**{key.title()}:** `{old}` **->** `{new}`") + if new and old: + changes.append(f"**{key.title()}:** `{old}` **->** `{new}`") done.append(key) diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index 6f618a2c7..fb9164194 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -32,6 +32,23 @@ except Exception as e: """ ESCAPE_REGEX = re.compile("[`\u202E\u200B]{3,}") +FORMATTED_CODE_REGEX = re.compile( + r"^\s*" # any leading whitespace from the beginning of the string + r"(?P<delim>(?P<block>```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block + r"(?(block)(?:(?P<lang>[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline) + r"(?:[ \t]*\n)*" # any blank (empty or tabs/spaces only) lines before the code + r"(?P<code>.*?)" # extract all code inside the markup + r"\s*" # any more whitespace before the end of the code markup + r"(?P=delim)" # match the exact same delimiter from the start again + r"\s*$", # any trailing whitespace until the end of the string + re.DOTALL | re.IGNORECASE # "." also matches newlines, case insensitive +) +RAW_CODE_REGEX = re.compile( + r"^(?:[ \t]*\n)*" # any blank (empty or tabs/spaces only) lines before the code + r"(?P<code>.*?)" # extract all the rest as code + r"\s*$", # any trailing whitespace until the end of the string + re.DOTALL # "." also matches newlines +) BYPASS_ROLES = (Roles.owner, Roles.admin, Roles.moderator, Roles.helpers) WHITELISTED_CHANNELS = (Channels.bot,) WHITELISTED_CHANNELS_STRING = ', '.join(f"<#{channel_id}>" for channel_id in WHITELISTED_CHANNELS) @@ -54,8 +71,6 @@ class Snekbox: Safe evaluation using Snekbox """ - jobs = None # type: dict - def __init__(self, bot: Bot): self.bot = bot self.jobs = {} @@ -85,18 +100,20 @@ class Snekbox: log.info(f"Received code from {ctx.author.name}#{ctx.author.discriminator} for evaluation:\n{code}") self.jobs[ctx.author.id] = datetime.datetime.now() - while code.startswith("\n"): - code = code[1:] - - if code.startswith("```") and code.endswith("```"): - code = code[3:-3] - - if code.startswith("python"): - code = code[6:] - elif code.startswith("py"): - code = code[2:] + # Strip whitespace and inline or block code markdown and extract the code and some formatting info + match = FORMATTED_CODE_REGEX.fullmatch(code) + if match: + code, block, lang, delim = match.group("code", "block", "lang", "delim") + code = textwrap.dedent(code) + if block: + info = (f"'{lang}' highlighted" if lang else "plain") + " code block" + else: + info = f"{delim}-enclosed inline code" + log.trace(f"Extracted {info} for evaluation:\n{code}") + else: + code = textwrap.dedent(RAW_CODE_REGEX.fullmatch(code).group("code")) + log.trace(f"Eval message contains not or badly formatted code, stripping whitespace only:\n{code}") - code = textwrap.dedent(code.strip()) code = textwrap.indent(code, " ") code = CODE_TEMPLATE.replace("{CODE}", code) diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py index baed44a8c..85244b835 100644 --- a/bot/cogs/tags.py +++ b/bot/cogs/tags.py @@ -1,6 +1,7 @@ import logging import random import time +from typing import Optional from discord import Colour, Embed from discord.ext.commands import ( @@ -11,6 +12,7 @@ from discord.ext.commands import ( from bot.constants import ( Channels, Cooldowns, ERROR_REPLIES, Keys, Roles, URLs ) +from bot.converters import ValidURL from bot.decorators import with_role from bot.pagination import LinePaginator @@ -108,12 +110,13 @@ class Tags: return tag_data - async def post_tag_data(self, tag_name: str, tag_content: str) -> dict: + async def post_tag_data(self, tag_name: str, tag_content: str, image_url: Optional[str]) -> dict: """ Send some tag_data to our API to have it saved in the database. :param tag_name: The name of the tag to create or edit. :param tag_content: The content of the tag. + :param image_url: The image URL of the tag, can be `None`. :return: json response from the API in the following format: { 'success': bool @@ -122,7 +125,8 @@ class Tags: params = { 'tag_name': tag_name, - 'tag_content': tag_content + 'tag_content': tag_content, + 'image_url': image_url } response = await self.bot.http_session.post(URLs.site_tags_api, headers=self.headers, json=params) @@ -226,13 +230,20 @@ class Tags: @tags_group.command(name='set', aliases=('add', 'edit', 's')) @with_role(Roles.admin, Roles.owner, Roles.moderator) - async def set_command(self, ctx: Context, tag_name: TagNameConverter, *, tag_content: TagContentConverter): + async def set_command( + self, + ctx: Context, + tag_name: TagNameConverter, + tag_content: TagContentConverter, + image_url: ValidURL = None + ): """ Create a new tag or edit an existing one. :param ctx: discord message context :param tag_name: The name of the tag to create or edit. :param tag_content: The content of the tag. + :param image_url: An optional image for the tag. """ tag_name = tag_name.lower().strip() @@ -240,12 +251,13 @@ class Tags: embed = Embed() embed.colour = Colour.red() - tag_data = await self.post_tag_data(tag_name, tag_content) + tag_data = await self.post_tag_data(tag_name, tag_content, image_url) if tag_data.get("success"): log.debug(f"{ctx.author} successfully added the following tag to our database: \n" f"tag_name: {tag_name}\n" - f"tag_content: '{tag_content}'") + f"tag_content: '{tag_content}'\n" + f"image_url: '{image_url}'") embed.colour = Colour.blurple() embed.title = "Tag successfully added" embed.description = f"**{tag_name}** added to tag database." @@ -253,6 +265,7 @@ class Tags: log.error("There was an unexpected database error when trying to add the following tag: \n" f"tag_name: {tag_name}\n" f"tag_content: '{tag_content}'\n" + f"image_url: '{image_url}'\n" f"response: {tag_data}") embed.title = "Database error" embed.description = ("There was a problem adding the data to the tags database. " diff --git a/bot/constants.py b/bot/constants.py index 3ade4ac7b..68fbc2bc4 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -202,8 +202,7 @@ class Filter(metaclass=YAMLGetter): watch_tokens: bool ping_everyone: bool - guild_invite_whitelist: List[str] - vanity_url_whitelist: List[str] + guild_invite_whitelist: List[int] domain_blacklist: List[str] word_watchlist: List[str] token_watchlist: List[str] @@ -375,10 +374,18 @@ class RabbitMQ(metaclass=YAMLGetter): class URLs(metaclass=YAMLGetter): section = "urls" + # Discord API endpoints + discord_api: str + discord_invite_api: str + + # Misc endpoints bot_avatar: str deploy: str gitlab_bot_repo: str omdb: str + status: str + + # Site endpoints site: str site_api: str site_facts_api: str @@ -401,7 +408,6 @@ class URLs(metaclass=YAMLGetter): site_infractions_by_id: str site_infractions_user_type_current: str site_infractions_user_type: str - status: str paste_service: str diff --git a/config-default.yml b/config-default.yml index b621c5b90..ce7639186 100644 --- a/config-default.yml +++ b/config-default.yml @@ -134,11 +134,10 @@ filter: ping_everyone: true # Ping @everyone when we send a mod-alert? guild_invite_whitelist: - - kWJYurV # Functional Programming - - XBGetGp # STEM - - vanity_url_whitelist: - - python # Python Discord + - 280033776820813825 # Functional Programming + - 267624335836053506 # Python Discord + - 440186186024222721 # Python Discord: ModLog Emojis + - 273944235143593984 # STEM domain_blacklist: - pornhub.com @@ -147,7 +146,6 @@ filter: word_watchlist: - goo+ks* - ky+s+ - - gh?[ae]+y+s* - ki+ke+s* - beaner+s? - coo+ns* @@ -209,6 +207,7 @@ urls: # PyDis site vars site: &DOMAIN "pythondiscord.com" site_api: &API !JOIN ["api.", *DOMAIN] + site_paste: &PASTE !JOIN ["paste.", *DOMAIN] site_schema: &SCHEMA "https://" site_bigbrother_api: !JOIN [*SCHEMA, *API, "/bot/bigbrother"] @@ -231,17 +230,20 @@ urls: site_tags_api: !JOIN [*SCHEMA, *API, "/bot/tags"] site_user_api: !JOIN [*SCHEMA, *API, "/bot/users"] site_user_complete_api: !JOIN [*SCHEMA, *API, "/bot/users/complete"] + paste_service: !JOIN [*SCHEMA, *PASTE, "/{key}"] # Env vars deploy: !ENV "DEPLOY_URL" status: !ENV "STATUS_URL" + # Discord API URLs + discord_api: &DISCORD_API "https://discordapp.com/api/v7/" + discord_invite_api: !JOIN [*DISCORD_API, "invites"] + # Misc URLs bot_avatar: "https://raw.githubusercontent.com/discord-python/branding/master/logos/logo_circle/logo_circle.png" gitlab_bot_repo: "https://gitlab.com/python-discord/projects/bot" omdb: "http://omdbapi.com" - paste_service: "https://paste.pydis.com/{key}" - anti_spam: # Clean messages that violate a rule. |