aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Sebastian Kuipers <[email protected]>2021-02-26 00:02:24 +0100
committerGravatar GitHub <[email protected]>2021-02-26 00:02:24 +0100
commite6e5226475cd0c6ba9d7ea37ac70ca74816d92e8 (patch)
tree658289fd8d93f96ebe29bdf9f9874208d758bf65
parentApply suggestions from code review (diff)
parentMerge pull request #1428 from python-discord/wookie184-tag-updates (diff)
Merge branch 'master' into json-tag
-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/converters.py25
-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/fun/off_topic_names.py18
-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/defaultdict.md21
-rw-r--r--bot/resources/tags/dict-get.md16
-rw-r--r--bot/resources/tags/dictcomps.md24
-rw-r--r--bot/resources/tags/f-strings.md20
-rw-r--r--bot/resources/tags/free.md5
-rw-r--r--bot/resources/tags/inline.md15
-rw-r--r--bot/resources/tags/listcomps.md23
-rw-r--r--bot/resources/tags/local-file.md23
-rw-r--r--bot/resources/tags/off-topic.md2
-rw-r--r--bot/resources/tags/pep8.md6
-rw-r--r--bot/rules/duplicates.py1
-rw-r--r--config-default.yml17
-rw-r--r--tests/bot/exts/filters/test_token_remover.py4
26 files changed, 300 insertions, 90 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/converters.py b/bot/converters.py
index 0d9a519df..80ce99459 100644
--- a/bot/converters.py
+++ b/bot/converters.py
@@ -357,27 +357,38 @@ class Duration(DurationDelta):
class OffTopicName(Converter):
"""A converter that ensures an added off-topic name is valid."""
+ ALLOWED_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ!?'`-"
+
+ @classmethod
+ def translate_name(cls, name: str, *, from_unicode: bool = True) -> str:
+ """
+ Translates `name` into a format that is allowed in discord channel names.
+
+ If `from_unicode` is True, the name is translated from a discord-safe format, back to normalized text.
+ """
+ if from_unicode:
+ table = str.maketrans(cls.ALLOWED_CHARACTERS, '๐– ๐–ก๐–ข๐–ฃ๐–ค๐–ฅ๐–ฆ๐–ง๐–จ๐–ฉ๐–ช๐–ซ๐–ฌ๐–ญ๐–ฎ๐–ฏ๐–ฐ๐–ฑ๐–ฒ๐–ณ๐–ด๐–ต๐–ถ๐–ท๐–ธ๐–นวƒ๏ผŸโ€™โ€™-')
+ else:
+ table = str.maketrans('๐– ๐–ก๐–ข๐–ฃ๐–ค๐–ฅ๐–ฆ๐–ง๐–จ๐–ฉ๐–ช๐–ซ๐–ฌ๐–ญ๐–ฎ๐–ฏ๐–ฐ๐–ฑ๐–ฒ๐–ณ๐–ด๐–ต๐–ถ๐–ท๐–ธ๐–นวƒ๏ผŸโ€™โ€™-', cls.ALLOWED_CHARACTERS)
+
+ return name.translate(table)
+
async def convert(self, ctx: Context, argument: str) -> str:
"""Attempt to replace any invalid characters with their approximate Unicode equivalent."""
- allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ!?'`-"
-
# Chain multiple words to a single one
argument = "-".join(argument.split())
if not (2 <= len(argument) <= 96):
raise BadArgument("Channel name must be between 2 and 96 chars long")
- elif not all(c.isalnum() or c in allowed_characters for c in argument):
+ elif not all(c.isalnum() or c in self.ALLOWED_CHARACTERS for c in argument):
raise BadArgument(
"Channel name must only consist of "
"alphanumeric characters, minus signs or apostrophes."
)
# Replace invalid characters with unicode alternatives.
- table = str.maketrans(
- allowed_characters, '๐– ๐–ก๐–ข๐–ฃ๐–ค๐–ฅ๐–ฆ๐–ง๐–จ๐–ฉ๐–ช๐–ซ๐–ฌ๐–ญ๐–ฎ๐–ฏ๐–ฐ๐–ฑ๐–ฒ๐–ณ๐–ด๐–ต๐–ถ๐–ท๐–ธ๐–นวƒ๏ผŸโ€™โ€™-'
- )
- return argument.translate(table)
+ return self.translate_name(argument)
class ISODateTime(Converter):
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/fun/off_topic_names.py b/bot/exts/fun/off_topic_names.py
index 7fc93b88c..845b8175c 100644
--- a/bot/exts/fun/off_topic_names.py
+++ b/bot/exts/fun/off_topic_names.py
@@ -139,10 +139,20 @@ class OffTopicNames(Cog):
@has_any_role(*MODERATION_ROLES)
async def search_command(self, ctx: Context, *, query: OffTopicName) -> None:
"""Search for an off-topic name."""
- result = await self.bot.api_client.get('bot/off-topic-channel-names')
- in_matches = {name for name in result if query in name}
- close_matches = difflib.get_close_matches(query, result, n=10, cutoff=0.70)
- lines = sorted(f"โ€ข {name}" for name in in_matches.union(close_matches))
+ query = OffTopicName.translate_name(query, from_unicode=False).lower()
+
+ # Map normalized names to returned names for search purposes
+ result = {
+ OffTopicName.translate_name(name, from_unicode=False).lower(): name
+ for name in await self.bot.api_client.get('bot/off-topic-channel-names')
+ }
+
+ # Search normalized keys
+ in_matches = {name for name in result.keys() if query in name}
+ close_matches = difflib.get_close_matches(query, result.keys(), n=10, cutoff=0.70)
+
+ # Send Results
+ lines = sorted(f"โ€ข {result[name]}" for name in in_matches.union(close_matches))
embed = Embed(
title="Query results",
colour=Colour.blue()
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/defaultdict.md b/bot/resources/tags/defaultdict.md
new file mode 100644
index 000000000..b6c3175fc
--- /dev/null
+++ b/bot/resources/tags/defaultdict.md
@@ -0,0 +1,21 @@
+**[`collections.defaultdict`](https://docs.python.org/3/library/collections.html#collections.defaultdict)**
+
+The Python `defaultdict` type behaves almost exactly like a regular Python dictionary, but if you try to access or modify a missing key, the `defaultdict` will automatically insert the key and generate a default value for it.
+While instantiating a `defaultdict`, we pass in a function that tells it how to create a default value for missing keys.
+
+```py
+>>> from collections import defaultdict
+>>> my_dict = defaultdict(int)
+>>> my_dict
+defaultdict(<class 'int'>, {})
+```
+
+In this example, we've used the `int` class which returns 0 when called like a function, so any missing key will get a default value of 0. You can also get an empty list by default with `list` or an empty string with `str`.
+
+```py
+>>> my_dict["foo"]
+0
+>>> my_dict["bar"] += 5
+>>> my_dict
+defaultdict(<class 'int'>, {'foo': 0, 'bar': 5})
+```
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/dictcomps.md b/bot/resources/tags/dictcomps.md
index 11867d77b..6c8018761 100644
--- a/bot/resources/tags/dictcomps.md
+++ b/bot/resources/tags/dictcomps.md
@@ -1,20 +1,14 @@
-**Dictionary Comprehensions**
-
-Like lists, there is a convenient way of creating dictionaries:
+Dictionary comprehensions (*dict comps*) provide a convenient way to make dictionaries, just like list comps:
```py
->>> ftoc = {f: round((5/9)*(f-32)) for f in range(-40,101,20)}
->>> print(ftoc)
-{-40: -40, -20: -29, 0: -18, 20: -7, 40: 4, 60: 16, 80: 27, 100: 38}
+>>> {word.lower(): len(word) for word in ('I', 'love', 'Python')}
+{'i': 1, 'love': 4, 'python': 6}
```
-In the example above, I created a dictionary of temperatures in Fahrenheit, that are mapped to (*roughly*) their Celsius counterpart within a small range. These comprehensions are useful for succinctly creating dictionaries from some other sequence.
+The syntax is very similar to list comps except that you surround it with curly braces and have two expressions: one for the key and one for the value.
-They are also very useful for inverting the key value pairs of a dictionary that already exists, such that the value in the old dictionary is now the key, and the corresponding key is now its value:
+One can use a dict comp to change an existing dictionary using its `items` method
```py
->>> ctof = {v:k for k, v in ftoc.items()}
->>> print(ctof)
-{-40: -40, -29: -20, -18: 0, -7: 20, 4: 40, 16: 60, 27: 80, 38: 100}
+>>> first_dict = {'i': 1, 'love': 4, 'python': 6}
+>>> {key.upper(): value * 2 for key, value in first_dict.items()}
+{'I': 2, 'LOVE': 8, 'PYTHON': 12}
```
-
-Also like list comprehensions, you can add a conditional to it in order to filter out items you don't want.
-
-For more information and examples, check [PEP 274](https://www.python.org/dev/peps/pep-0274/)
+For more information and examples, check out [PEP 274](https://www.python.org/dev/peps/pep-0274/)
diff --git a/bot/resources/tags/f-strings.md b/bot/resources/tags/f-strings.md
index 69bc82487..5ccafe723 100644
--- a/bot/resources/tags/f-strings.md
+++ b/bot/resources/tags/f-strings.md
@@ -1,17 +1,9 @@
-In Python, there are several ways to do string interpolation, including using `%s`\'s and by using the `+` operator to concatenate strings together. However, because some of these methods offer poor readability and require typecasting to prevent errors, you should for the most part be using a feature called format strings.
+Creating a Python string with your variables using the `+` operator can be difficult to write and read. F-strings (*format-strings*) make it easy to insert values into a string. If you put an `f` in front of the first quote, you can then put Python expressions between curly braces in the string.
-**In Python 3.6 or later, we can use f-strings like this:**
```py
-snake = "Pythons"
-print(f"{snake} are some of the largest snakes in the world")
-```
-**In earlier versions of Python or in projects where backwards compatibility is very important, use str.format() like this:**
-```py
-snake = "Pythons"
-
-# With str.format() you can either use indexes
-print("{0} are some of the largest snakes in the world".format(snake))
-
-# Or keyword arguments
-print("{family} are some of the largest snakes in the world".format(family=snake))
+>>> snake = "pythons"
+>>> number = 21
+>>> f"There are {number * 2} {snake} on the plane."
+"There are 42 pythons on the plane."
```
+Note that even when you include an expression that isn't a string, like `number * 2`, Python will convert it to a string for you.
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/inline.md b/bot/resources/tags/inline.md
index a6a7c35d6..4ece74ef7 100644
--- a/bot/resources/tags/inline.md
+++ b/bot/resources/tags/inline.md
@@ -1,16 +1,7 @@
**Inline codeblocks**
-In addition to multi-line codeblocks, discord has support for inline codeblocks as well. These are small codeblocks that are usually a single line, that can fit between non-codeblocks on the same line.
+Inline codeblocks look `like this`. To create them you surround text with single backticks, so \`hello\` would become `hello`.
-The following is an example of how it's done:
+Note that backticks are not quotes, see [this](https://superuser.com/questions/254076/how-do-i-type-the-tick-and-backtick-characters-on-windows/254077#254077) if you are struggling to find the backtick key.
-The \`\_\_init\_\_\` method customizes the newly created instance.
-
-And results in the following:
-
-The `__init__` method customizes the newly created instance.
-
-**Note:**
-โ€ข These are **backticks** not quotes
-โ€ข Avoid using them for multiple lines
-โ€ข Useful for negating formatting you don't want
+For how to make multiline codeblocks see the `!codeblock` tag.
diff --git a/bot/resources/tags/listcomps.md b/bot/resources/tags/listcomps.md
index 0003b9bb8..ba00a4bf7 100644
--- a/bot/resources/tags/listcomps.md
+++ b/bot/resources/tags/listcomps.md
@@ -1,14 +1,19 @@
-Do you ever find yourself writing something like:
+Do you ever find yourself writing something like this?
```py
-even_numbers = []
-for n in range(20):
- if n % 2 == 0:
- even_numbers.append(n)
+>>> squares = []
+>>> for n in range(5):
+... squares.append(n ** 2)
+[0, 1, 4, 9, 16]
```
-Using list comprehensions can simplify this significantly, and greatly improve code readability. If we rewrite the example above to use list comprehensions, it would look like this:
+Using list comprehensions can make this both shorter and more readable. As a list comprehension, the same code would look like this:
```py
-even_numbers = [n for n in range(20) if n % 2 == 0]
+>>> [n ** 2 for n in range(5)]
+[0, 1, 4, 9, 16]
+```
+List comprehensions also get an `if` statement:
+```python
+>>> [n ** 2 for n in range(5) if n % 2 == 0]
+[0, 4, 16]
```
-This also works for generators, dicts and sets by using `()` or `{}` instead of `[]`.
-For more info, see [this pythonforbeginners.com post](http://www.pythonforbeginners.com/basics/list-comprehensions-in-python) or [PEP 202](https://www.python.org/dev/peps/pep-0202/).
+For more info, see [this pythonforbeginners.com post](http://www.pythonforbeginners.com/basics/list-comprehensions-in-python).
diff --git a/bot/resources/tags/local-file.md b/bot/resources/tags/local-file.md
new file mode 100644
index 000000000..ae41d589c
--- /dev/null
+++ b/bot/resources/tags/local-file.md
@@ -0,0 +1,23 @@
+Thanks to discord.py, sending local files as embed images is simple. You have to create an instance of [`discord.File`](https://discordpy.readthedocs.io/en/latest/api.html#discord.File) class:
+```py
+# When you know the file exact path, you can pass it.
+file = discord.File("/this/is/path/to/my/file.png", filename="file.png")
+
+# When you have the file-like object, then you can pass this instead path.
+with open("/this/is/path/to/my/file.png", "rb") as f:
+ file = discord.File(f)
+```
+When using the file-like object, you have to open it in `rb` mode. Also, in this case, passing `filename` to it is not necessary.
+Please note that `filename` can't contain underscores. This is a Discord limitation.
+
+[`discord.Embed`](https://discordpy.readthedocs.io/en/latest/api.html#discord.Embed) instances have a [`set_image`](https://discordpy.readthedocs.io/en/latest/api.html#discord.Embed.set_image) method which can be used to set an attachment as an image:
+```py
+embed = discord.Embed()
+# Set other fields
+embed.set_image(url="attachment://file.png") # Filename here must be exactly same as attachment filename.
+```
+After this, you can send an embed with an attachment to Discord:
+```py
+await channel.send(file=file, embed=embed)
+```
+This example uses [`discord.TextChannel`](https://discordpy.readthedocs.io/en/latest/api.html#discord.TextChannel) for sending, but any instance of [`discord.abc.Messageable`](https://discordpy.readthedocs.io/en/latest/api.html#discord.abc.Messageable) can be used for sending.
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/resources/tags/pep8.md b/bot/resources/tags/pep8.md
index cab4c4db8..57b176122 100644
--- a/bot/resources/tags/pep8.md
+++ b/bot/resources/tags/pep8.md
@@ -1,3 +1,5 @@
-**PEP 8** is the official style guide for Python. It includes comprehensive guidelines for code formatting, variable naming, and making your code easy to read. Professional Python developers are usually required to follow the guidelines, and will often use code-linters like `flake8` to verify that the code they\'re writing complies with the style guide.
+**PEP 8** is the official style guide for Python. It includes comprehensive guidelines for code formatting, variable naming, and making your code easy to read. Professional Python developers are usually required to follow the guidelines, and will often use code-linters like flake8 to verify that the code they're writing complies with the style guide.
-You can find the PEP 8 document [here](https://www.python.org/dev/peps/pep-0008).
+More information:
+โ€ข [PEP 8 document](https://www.python.org/dev/peps/pep-0008)
+โ€ข [Our PEP 8 song!](https://www.youtube.com/watch?v=hgI0p1zf31k) :notes:
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,