diff options
| author | 2021-02-25 10:35:03 +0100 | |
|---|---|---|
| committer | 2021-02-25 10:35:03 +0100 | |
| commit | 57cd4cb0285f48c27ae00cf22ff11952e4a8476b (patch) | |
| tree | 1e0f29a5c1a919668f3847d9b8b231d2cf177e57 | |
| parent | Makes Off Topic Name Search Case Insensitive (diff) | |
| parent | Merge pull request #1433 from python-discord/etiquette-off-topic-tag (diff) | |
Merge branch 'master' into insensitive-otn-search
| -rw-r--r-- | .github/CODEOWNERS | 6 | ||||
| -rw-r--r-- | Dockerfile | 12 | ||||
| -rw-r--r-- | bot/api.py | 2 | ||||
| -rw-r--r-- | bot/constants.py | 11 | ||||
| -rw-r--r-- | bot/errors.py | 19 | ||||
| -rw-r--r-- | bot/exts/backend/error_handler.py | 4 | ||||
| -rw-r--r-- | bot/exts/filters/token_remover.py | 4 | ||||
| -rw-r--r-- | bot/exts/help_channels/_cog.py | 4 | ||||
| -rw-r--r-- | bot/exts/help_channels/_message.py | 33 | ||||
| -rw-r--r-- | bot/exts/info/pypi.py | 70 | ||||
| -rw-r--r-- | bot/exts/moderation/infraction/_utils.py | 5 | ||||
| -rw-r--r-- | bot/resources/tags/dict-get.md | 16 | ||||
| -rw-r--r-- | bot/resources/tags/free.md | 5 | ||||
| -rw-r--r-- | bot/resources/tags/off-topic.md | 2 | ||||
| -rw-r--r-- | bot/rules/duplicates.py | 1 | ||||
| -rw-r--r-- | config-default.yml | 17 | ||||
| -rw-r--r-- | tests/bot/exts/filters/test_token_remover.py | 4 |
17 files changed, 188 insertions, 27 deletions
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ad813d893..7217cb443 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,11 +7,15 @@ bot/exts/utils/extensions.py @MarkKoz bot/exts/utils/snekbox.py @MarkKoz @Akarys42 bot/exts/help_channels/** @MarkKoz @Akarys42 bot/exts/moderation/** @Akarys42 @mbaruh @Den4200 @ks129 -bot/exts/info/** @Akarys42 @mbaruh @Den4200 +bot/exts/info/** @Akarys42 @Den4200 +bot/exts/info/information.py @mbaruh bot/exts/filters/** @mbaruh bot/exts/fun/** @ks129 bot/exts/utils/** @ks129 +# Rules +bot/rules/** @mbaruh + # Utils bot/utils/extensions.py @MarkKoz bot/utils/function.py @MarkKoz diff --git a/Dockerfile b/Dockerfile index 5d0380b44..1a75e5669 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,10 @@ FROM python:3.8-slim -# Define Git SHA build argument -ARG git_sha="development" - # Set pip to have cleaner logs and no saved cache ENV PIP_NO_CACHE_DIR=false \ PIPENV_HIDE_EMOJIS=1 \ PIPENV_IGNORE_VIRTUALENVS=1 \ - PIPENV_NOSPIN=1 \ - GIT_SHA=$git_sha + PIPENV_NOSPIN=1 RUN apt-get -y update \ && apt-get install -y \ @@ -25,6 +21,12 @@ WORKDIR /bot COPY Pipfile* ./ RUN pipenv install --system --deploy +# Define Git SHA build argument +ARG git_sha="development" + +# Set Git SHA environment variable for Sentry +ENV GIT_SHA=$git_sha + # Copy the source code in last to optimize rebuilding the image COPY . . diff --git a/bot/api.py b/bot/api.py index d93f9f2ba..6ce9481f4 100644 --- a/bot/api.py +++ b/bot/api.py @@ -53,7 +53,7 @@ class APIClient: @staticmethod def _url_for(endpoint: str) -> str: - return f"{URLs.site_schema}{URLs.site_api}/{quote_url(endpoint)}" + return f"{URLs.site_api_schema}{URLs.site_api}/{quote_url(endpoint)}" async def close(self) -> None: """Close the aiohttp session.""" diff --git a/bot/constants.py b/bot/constants.py index 95e22513f..69bc82b89 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -246,13 +246,16 @@ class Colours(metaclass=YAMLGetter): section = "style" subsection = "colours" + blue: int bright_green: int - soft_green: int - soft_orange: int - soft_red: int orange: int pink: int purple: int + soft_green: int + soft_orange: int + soft_red: int + white: int + yellow: int class DuckPond(metaclass=YAMLGetter): @@ -323,6 +326,7 @@ class Icons(metaclass=YAMLGetter): filtering: str green_checkmark: str + green_questionmark: str guild_update: str hash_blurple: str @@ -530,6 +534,7 @@ class URLs(metaclass=YAMLGetter): site: str site_api: str site_schema: str + site_api_schema: str # Site endpoints site_logs_view: str diff --git a/bot/errors.py b/bot/errors.py index 65d715203..ab0adcd42 100644 --- a/bot/errors.py +++ b/bot/errors.py @@ -1,4 +1,6 @@ -from typing import Hashable +from typing import Hashable, Union + +from discord import Member, User class LockedResourceError(RuntimeError): @@ -18,3 +20,18 @@ class LockedResourceError(RuntimeError): f"Cannot operate on {self.type.lower()} `{self.id}`; " "it is currently locked and in use by another operation." ) + + +class InvalidInfractedUser(Exception): + """ + Exception raised upon attempt of infracting an invalid user. + + Attributes: + `user` -- User or Member which is invalid + """ + + def __init__(self, user: Union[Member, User], reason: str = "User infracted is a bot."): + self.user = user + self.reason = reason + + super().__init__(reason) diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py index ed7962b06..d2cce5558 100644 --- a/bot/exts/backend/error_handler.py +++ b/bot/exts/backend/error_handler.py @@ -12,7 +12,7 @@ from bot.api import ResponseCodeError from bot.bot import Bot from bot.constants import Colours, ERROR_REPLIES, Icons, MODERATION_ROLES from bot.converters import TagNameConverter -from bot.errors import LockedResourceError +from bot.errors import InvalidInfractedUser, LockedResourceError from bot.exts.backend.branding._errors import BrandingError from bot.utils.checks import InWhitelistCheckFailure @@ -82,6 +82,8 @@ class ErrorHandler(Cog): elif isinstance(e.original, BrandingError): await ctx.send(embed=self._get_error_embed(random.choice(ERROR_REPLIES), str(e.original))) return + elif isinstance(e.original, InvalidInfractedUser): + await ctx.send(f"Cannot infract that user. {e.original.reason}") else: await self.handle_unexpected_error(ctx, e.original) return # Exit early to avoid logging. diff --git a/bot/exts/filters/token_remover.py b/bot/exts/filters/token_remover.py index bd6a1f97a..93f1f3c33 100644 --- a/bot/exts/filters/token_remover.py +++ b/bot/exts/filters/token_remover.py @@ -135,7 +135,7 @@ class TokenRemover(Cog): user_id=user_id, user_name=str(user), kind="BOT" if user.bot else "USER", - ), not user.bot + ), True else: return UNKNOWN_USER_LOG_MESSAGE.format(user_id=user_id), False @@ -147,7 +147,7 @@ class TokenRemover(Cog): channel=msg.channel.mention, user_id=token.user_id, timestamp=token.timestamp, - hmac='x' * len(token.hmac), + hmac='x' * (len(token.hmac) - 3) + token.hmac[-3:], ) @classmethod diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 0995c8a79..6abf99810 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -102,6 +102,10 @@ class HelpChannels(commands.Cog): await _cooldown.revoke_send_permissions(message.author, self.scheduler) await _message.pin(message) + try: + await _message.dm_on_open(message) + except Exception as e: + log.warning("Error occurred while sending DM:", exc_info=e) # Add user with channel for dormant check. await _caches.claimants.set(message.channel.id, message.author.id) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 2bbd4bdd6..36388f9bd 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -1,4 +1,5 @@ import logging +import textwrap import typing as t from datetime import datetime @@ -92,6 +93,38 @@ async def is_empty(channel: discord.TextChannel) -> bool: return False +async def dm_on_open(message: discord.Message) -> None: + """ + DM claimant with a link to the claimed channel's first message, with a 100 letter preview of the message. + + Does nothing if the user has DMs disabled. + """ + embed = discord.Embed( + title="Help channel opened", + description=f"You claimed {message.channel.mention}.", + colour=bot.constants.Colours.bright_green, + timestamp=message.created_at, + ) + + embed.set_thumbnail(url=constants.Icons.green_questionmark) + formatted_message = textwrap.shorten(message.content, width=100, placeholder="...") + if formatted_message: + embed.add_field(name="Your message", value=formatted_message, inline=False) + embed.add_field( + name="Conversation", + value=f"[Jump to message!]({message.jump_url})", + inline=False, + ) + + try: + await message.author.send(embed=embed) + log.trace(f"Sent DM to {message.author.id} after claiming help channel.") + except discord.errors.Forbidden: + log.trace( + f"Ignoring to send DM to {message.author.id} after claiming help channel: DMs disabled." + ) + + async def notify(channel: discord.TextChannel, last_notification: t.Optional[datetime]) -> t.Optional[datetime]: """ Send a message in `channel` notifying about a lack of available help channels. diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py new file mode 100644 index 000000000..3e326e8bb --- /dev/null +++ b/bot/exts/info/pypi.py @@ -0,0 +1,70 @@ +import itertools +import logging +import random + +from discord import Embed +from discord.ext.commands import Cog, Context, command +from discord.utils import escape_markdown + +from bot.bot import Bot +from bot.constants import Colours, NEGATIVE_REPLIES + +URL = "https://pypi.org/pypi/{package}/json" +FIELDS = ("author", "requires_python", "summary", "license") +PYPI_ICON = "https://cdn.discordapp.com/emojis/766274397257334814.png" +PYPI_COLOURS = itertools.cycle((Colours.yellow, Colours.blue, Colours.white)) + +log = logging.getLogger(__name__) + + +class PyPi(Cog): + """Cog for getting information about PyPi packages.""" + + def __init__(self, bot: Bot): + self.bot = bot + + @command(name="pypi", aliases=("package", "pack")) + async def get_package_info(self, ctx: Context, package: str) -> None: + """Provide information about a specific package from PyPI.""" + embed = Embed( + title=random.choice(NEGATIVE_REPLIES), + colour=Colours.soft_red + ) + embed.set_thumbnail(url=PYPI_ICON) + + async with self.bot.http_session.get(URL.format(package=package)) as response: + if response.status == 404: + embed.description = "Package could not be found." + + elif response.status == 200 and response.content_type == "application/json": + response_json = await response.json() + info = response_json["info"] + + embed.title = f"{info['name']} v{info['version']}" + embed.url = info['package_url'] + embed.colour = next(PYPI_COLOURS) + + for field in FIELDS: + field_data = info[field] + + # Field could be completely empty, in some cases can be a string with whitespaces, or None. + if field_data and not field_data.isspace(): + if '\n' in field_data and field == "license": + field_data = field_data.split('\n')[0] + + embed.add_field( + name=field.replace("_", " ").title(), + value=escape_markdown(field_data), + inline=False, + ) + + else: + embed.description = "There was an error when fetching your PyPi package." + log.trace(f"Error when fetching PyPi package: {response.status}.") + + await ctx.send(embed=embed) + + +def setup(bot: Bot) -> None: + """Load the PyPi cog.""" + bot.add_cog(PyPi(bot)) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index d0dc3f0a1..e766c1e5c 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -7,6 +7,7 @@ from discord.ext.commands import Context from bot.api import ResponseCodeError from bot.constants import Colours, Icons +from bot.errors import InvalidInfractedUser log = logging.getLogger(__name__) @@ -79,6 +80,10 @@ async def post_infraction( active: bool = True ) -> t.Optional[dict]: """Posts an infraction to the API.""" + if isinstance(user, (discord.Member, discord.User)) and user.bot: + log.trace(f"Posting of {infr_type} infraction for {user} to the API aborted. User is a bot.") + raise InvalidInfractedUser(user) + log.trace(f"Posting {infr_type} infraction for {user} to the API.") payload = { diff --git a/bot/resources/tags/dict-get.md b/bot/resources/tags/dict-get.md new file mode 100644 index 000000000..e02df03ab --- /dev/null +++ b/bot/resources/tags/dict-get.md @@ -0,0 +1,16 @@ +Often while using dictionaries in Python, you may run into `KeyErrors`. This error is raised when you try to access a key that isn't present in your dictionary. Python gives you some neat ways to handle them. + +**The `dict.get` method** + +The [`dict.get`](https://docs.python.org/3/library/stdtypes.html#dict.get) method will return the value for the key if it exists, and None (or a default value that you specify) if the key doesn't exist. Hence it will _never raise_ a KeyError. +```py +>>> my_dict = {"foo": 1, "bar": 2} +>>> print(my_dict.get("foobar")) +None +``` +Below, 3 is the default value to be returned, because the key doesn't exist- +```py +>>> print(my_dict.get("foobar", 3)) +3 +``` +Some other methods for handling `KeyError`s gracefully are the [`dict.setdefault`](https://docs.python.org/3/library/stdtypes.html#dict.setdefault) method and [`collections.defaultdict`](https://docs.python.org/3/library/collections.html#collections.defaultdict) (check out the `!defaultdict` tag). diff --git a/bot/resources/tags/free.md b/bot/resources/tags/free.md deleted file mode 100644 index 1493076c7..000000000 --- a/bot/resources/tags/free.md +++ /dev/null @@ -1,5 +0,0 @@ -**We have a new help channel system!** - -Please see <#704250143020417084> for further information. - -A more detailed guide can be found on [our website](https://pythondiscord.com/pages/resources/guides/help-channels/). diff --git a/bot/resources/tags/off-topic.md b/bot/resources/tags/off-topic.md index c7f98a813..6a864a1d5 100644 --- a/bot/resources/tags/off-topic.md +++ b/bot/resources/tags/off-topic.md @@ -6,3 +6,5 @@ There are three off-topic channels: • <#463035268514185226> Their names change randomly every 24 hours, but you can always find them under the `OFF-TOPIC/GENERAL` category in the channel list. + +Please read our [off-topic etiquette](https://pythondiscord.com/pages/resources/guides/off-topic-etiquette/) before participating in conversations. diff --git a/bot/rules/duplicates.py b/bot/rules/duplicates.py index 455764b53..8e4fbc12d 100644 --- a/bot/rules/duplicates.py +++ b/bot/rules/duplicates.py @@ -13,6 +13,7 @@ async def apply( if ( msg.author == last_message.author and msg.content == last_message.content + and msg.content ) ) diff --git a/config-default.yml b/config-default.yml index d3b267159..7d9afaa0e 100644 --- a/config-default.yml +++ b/config-default.yml @@ -24,13 +24,16 @@ bot: style: colours: + blue: 0x3775a8 bright_green: 0x01d277 - soft_green: 0x68c290 - soft_orange: 0xf9cb54 - soft_red: 0xcd6d6d orange: 0xe67e22 pink: 0xcf84e0 purple: 0xb734eb + soft_green: 0x68c290 + soft_orange: 0xf9cb54 + soft_red: 0xcd6d6d + white: 0xfffffe + yellow: 0xffd241 emojis: badge_bug_hunter: "<:bug_hunter_lvl1:743882896372269137>" @@ -87,6 +90,7 @@ style: filtering: "https://cdn.discordapp.com/emojis/472472638594482195.png" green_checkmark: "https://raw.githubusercontent.com/python-discord/branding/master/icons/checkmark/green-checkmark-dist.png" + green_questionmark: "https://raw.githubusercontent.com/python-discord/branding/master/icons/checkmark/green-question-mark-dist.png" guild_update: "https://cdn.discordapp.com/emojis/469954765141442561.png" hash_blurple: "https://cdn.discordapp.com/emojis/469950142942806017.png" @@ -335,7 +339,8 @@ keys: urls: # PyDis site vars site: &DOMAIN "pythondiscord.com" - site_api: &API !JOIN ["api.", *DOMAIN] + site_api: &API "pydis-api.default.svc.cluster.local" + site_api_schema: "http://" site_paste: &PASTE !JOIN ["paste.", *DOMAIN] site_schema: &SCHEMA "https://" site_staff: &STAFF !JOIN ["staff.", *DOMAIN] @@ -367,7 +372,7 @@ anti_spam: rules: attachments: interval: 10 - max: 9 + max: 6 burst: interval: 10 @@ -466,7 +471,7 @@ help_channels: deleted_idle_minutes: 5 # Maximum number of channels to put in the available category - max_available: 2 + max_available: 3 # Maximum number of channels across all 3 categories # Note Discord has a hard limit of 50 channels per category, so this shouldn't be > 50 diff --git a/tests/bot/exts/filters/test_token_remover.py b/tests/bot/exts/filters/test_token_remover.py index f99cc3370..51feae9cb 100644 --- a/tests/bot/exts/filters/test_token_remover.py +++ b/tests/bot/exts/filters/test_token_remover.py @@ -291,7 +291,7 @@ class TokenRemoverTests(unittest.IsolatedAsyncioTestCase): channel=self.msg.channel.mention, user_id=token.user_id, timestamp=token.timestamp, - hmac="x" * len(token.hmac), + hmac="xxxxxxxxxxxxxxxxxxxxxxxxjf4", ) @autospec("bot.exts.filters.token_remover", "UNKNOWN_USER_LOG_MESSAGE") @@ -318,7 +318,7 @@ class TokenRemoverTests(unittest.IsolatedAsyncioTestCase): return_value = TokenRemover.format_userid_log_message(msg, token) - self.assertEqual(return_value, (known_user_log_message.format.return_value, False)) + self.assertEqual(return_value, (known_user_log_message.format.return_value, True)) known_user_log_message.format.assert_called_once_with( user_id=472265943062413332, |