aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Senjan21 <[email protected]>2021-02-25 10:35:03 +0100
committerGravatar GitHub <[email protected]>2021-02-25 10:35:03 +0100
commit57cd4cb0285f48c27ae00cf22ff11952e4a8476b (patch)
tree1e0f29a5c1a919668f3847d9b8b231d2cf177e57
parentMakes Off Topic Name Search Case Insensitive (diff)
parentMerge pull request #1433 from python-discord/etiquette-off-topic-tag (diff)
Merge branch 'master' into insensitive-otn-search
-rw-r--r--.github/CODEOWNERS6
-rw-r--r--Dockerfile12
-rw-r--r--bot/api.py2
-rw-r--r--bot/constants.py11
-rw-r--r--bot/errors.py19
-rw-r--r--bot/exts/backend/error_handler.py4
-rw-r--r--bot/exts/filters/token_remover.py4
-rw-r--r--bot/exts/help_channels/_cog.py4
-rw-r--r--bot/exts/help_channels/_message.py33
-rw-r--r--bot/exts/info/pypi.py70
-rw-r--r--bot/exts/moderation/infraction/_utils.py5
-rw-r--r--bot/resources/tags/dict-get.md16
-rw-r--r--bot/resources/tags/free.md5
-rw-r--r--bot/resources/tags/off-topic.md2
-rw-r--r--bot/rules/duplicates.py1
-rw-r--r--config-default.yml17
-rw-r--r--tests/bot/exts/filters/test_token_remover.py4
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,