aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/pull_request_template.md30
-rw-r--r--Dockerfile6
-rw-r--r--Pipfile2
-rw-r--r--Pipfile.lock50
-rw-r--r--bot/constants.py5
-rw-r--r--bot/exts/easter/april_fools_vids.py26
-rw-r--r--bot/exts/easter/egg_decorating.py4
-rw-r--r--bot/exts/evergreen/battleship.py2
-rw-r--r--bot/exts/evergreen/catify.py87
-rw-r--r--bot/exts/evergreen/error_handler.py10
-rw-r--r--bot/exts/evergreen/fun.py3
-rw-r--r--bot/exts/evergreen/issues.py5
-rw-r--r--bot/exts/evergreen/ping.py27
-rw-r--r--bot/exts/evergreen/reddit.py6
-rw-r--r--bot/exts/evergreen/timed.py4
-rw-r--r--bot/exts/internal_eval/__init__.py10
-rw-r--r--bot/exts/internal_eval/_helpers.py249
-rw-r--r--bot/exts/internal_eval/_internal_eval.py176
-rw-r--r--bot/resources/easter/april_fools_vids.json263
-rw-r--r--bot/resources/halloween/spookysounds/109710__tomlija__horror-gate.mp3bin118125 -> 0 bytes
-rw-r--r--bot/resources/halloween/spookysounds/126113__klankbeeld__laugh.mp3bin112365 -> 0 bytes
-rw-r--r--bot/resources/halloween/spookysounds/133674__klankbeeld__horror-laugh-original-132802-nanakisan-evil-laugh-08.mp3bin137385 -> 0 bytes
-rw-r--r--bot/resources/halloween/spookysounds/14570__oscillator__ghost-fx.mp3bin135405 -> 0 bytes
-rw-r--r--bot/resources/halloween/spookysounds/168650__0xmusex0__doorcreak.mp3bin162421 -> 0 bytes
-rw-r--r--bot/resources/halloween/spookysounds/171078__klankbeeld__horror-scream-woman-long.mp3bin131625 -> 0 bytes
-rw-r--r--bot/resources/halloween/spookysounds/193812__geoneo0__four-voices-whispering-6.mp3bin163257 -> 0 bytes
-rw-r--r--bot/resources/halloween/spookysounds/237282__devilfish101__frantic-violin-screech.mp3bin131566 -> 0 bytes
-rw-r--r--bot/resources/halloween/spookysounds/249686__cylon8472__cthulhu-growl.mp3bin153226 -> 0 bytes
-rw-r--r--bot/resources/halloween/spookysounds/35716__analogchill__scream.mp3bin114773 -> 0 bytes
-rw-r--r--bot/resources/halloween/spookysounds/413315__inspectorj__something-evil-approaches-a.mp3bin298717 -> 0 bytes
-rw-r--r--bot/resources/halloween/spookysounds/60571__gabemiller74__breathofdeath.mp3bin177049 -> 0 bytes
-rw-r--r--bot/resources/halloween/spookysounds/Female_Monster_Growls_.mp3bin148276 -> 0 bytes
-rw-r--r--bot/resources/halloween/spookysounds/Male_Zombie_Roar_.mp3bin62171 -> 0 bytes
-rw-r--r--bot/resources/halloween/spookysounds/Monster_Alien_Growl_Calm_.mp3bin133651 -> 0 bytes
-rw-r--r--bot/resources/halloween/spookysounds/Monster_Alien_Grunt_Hiss_.mp3bin74718 -> 0 bytes
-rw-r--r--bot/resources/halloween/spookysounds/sources.txt41
-rw-r--r--bot/utils/exceptions.py2
-rw-r--r--bot/utils/helpers.py8
38 files changed, 748 insertions, 268 deletions
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index e2739287..fade00e1 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,30 +1,20 @@
## Relevant Issues
-<!-- List relevant issue tickets here. -->
-<!-- Say "Closes #0" for issues that the PR resolves, replacing 0 with the issue number. -->
+<!--
+It is mandatory to link to an issue that has been approved by a Core Developer, indicated by an "approved" label.
+Issues can be skipped with explicit core dev approval, but you have to link the discussion.
+-->
-
-## Description
-<!-- Describe how you've implemented your changes. -->
-
-
-## Reasoning
-<!-- Outline the reasoning for how it's been implemented. -->
+<!-- Link the issue by typing: "Closes #<number>" (Closes #0 to close issue 0 for example). -->
-## Screenshots
-<!-- Remove this section if the changes don't impact anything user-facing. -->
-<!-- You can add images by just copy pasting them directly in the editor. -->
-
-
-## Additional Details
-<!-- Delete this section if not applicable. -->
-
+## Description
+<!-- Describe what changes you made, and how you've implemented them. -->
## Did you:
<!-- These are required when contributing. -->
<!-- Replace [ ] with [x] to mark items as done. -->
- [ ] Join the [**Python Discord Community**](https://discord.gg/python)?
-- [ ] If dependencies have been added or updated, run `pipenv lock`?
-- [ ] **Lint your code** (`pipenv run lint`)?
-- [ ] Set the PR to **allow edits from contributors**?
+- [ ] Read all the comments in this tempelate?
+- [ ] Ensure there is an issue open, or link relevant discord discussions?
+- [ ] Read the [contributing guidelines](https://pythondiscord.com/pages/contributing/contributing-guidelines/)?
diff --git a/Dockerfile b/Dockerfile
index 0db0b0ef..8c4920a9 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,12 +6,6 @@ ENV PIP_NO_CACHE_DIR=false \
PIPENV_IGNORE_VIRTUALENVS=1 \
PIPENV_NOSPIN=1
-# Install git to be able to dowload git dependencies in the Pipfile
-RUN apt-get -y update \
- && apt-get install -y \
- ffmpeg \
- && rm -rf /var/lib/apt/lists/*
-
# Install pipenv
RUN pip install -U pipenv
diff --git a/Pipfile b/Pipfile
index f20f6845..f6118f1a 100644
--- a/Pipfile
+++ b/Pipfile
@@ -12,7 +12,7 @@ pillow = "~=8.1"
pytz = "~=2019.2"
sentry-sdk = "~=0.19"
PyYAML = "~=5.4"
-"discord.py" = {extras = ["voice"], version = "~=1.5.1"}
+"discord.py" = "~=1.5.1"
async-rediscache = {extras = ["fakeredis"], version = "~=0.1.4"}
emojis = "~=0.6.0"
matplotlib = "~=3.4.1"
diff --git a/Pipfile.lock b/Pipfile.lock
index d7fc6b27..915c3784 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "03b52d5b9fdfa6d037780d5aa2896c82fd5454a40bd69acf7e9b0e129557dbd5"
+ "sha256": "96cd9674aea76763df9582acd392eece6546876698fffaf9024e5a2daccb8f6f"
},
"pipfile-spec": 6,
"requires": {
@@ -158,9 +158,7 @@
"version": "==0.10.0"
},
"discord.py": {
- "extras": [
- "voice"
- ],
+ "extras": [],
"hashes": [
"sha256:2367359e31f6527f8a936751fc20b09d7495dd6a76b28c8fb13d4ca6c55b7563",
"sha256:def00dc50cf36d21346d71bc89f0cad8f18f9a3522978dc18c7796287d47de8b"
@@ -443,32 +441,6 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.20"
},
- "pynacl": {
- "hashes": [
- "sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255",
- "sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c",
- "sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e",
- "sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae",
- "sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621",
- "sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56",
- "sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39",
- "sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310",
- "sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1",
- "sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5",
- "sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a",
- "sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786",
- "sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b",
- "sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b",
- "sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f",
- "sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20",
- "sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415",
- "sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715",
- "sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92",
- "sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1",
- "sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0"
- ],
- "version": "==1.3.0"
- },
"pyparsing": {
"hashes": [
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
@@ -639,11 +611,11 @@
},
"flake8": {
"hashes": [
- "sha256:12d05ab02614b6aee8df7c36b97d1a3b2372761222b19b58621355e82acddcff",
- "sha256:78873e372b12b093da7b5e5ed302e8ad9e988b38b063b61ad937f26ca58fc5f0"
+ "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378",
+ "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a"
],
"index": "pypi",
- "version": "==3.9.0"
+ "version": "==3.9.1"
},
"flake8-annotations": {
"hashes": [
@@ -709,11 +681,11 @@
},
"identify": {
"hashes": [
- "sha256:43cb1965e84cdd247e875dec6d13332ef5be355ddc16776396d98089b9053d87",
- "sha256:c7c0f590526008911ccc5ceee6ed7b085cbc92f7b6591d0ee5913a130ad64034"
+ "sha256:398cb92a7599da0b433c65301a1b62b9b1f4bb8248719b84736af6c0b22289d6",
+ "sha256:4537474817e0bbb8cea3e5b7504b7de6d44e3f169a90846cbc6adb0fc8294502"
],
"markers": "python_full_version >= '3.6.1'",
- "version": "==2.2.2"
+ "version": "==2.2.3"
},
"mccabe": {
"hashes": [
@@ -724,10 +696,10 @@
},
"nodeenv": {
"hashes": [
- "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9",
- "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c"
+ "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b",
+ "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"
],
- "version": "==1.5.0"
+ "version": "==1.6.0"
},
"pep8-naming": {
"hashes": [
diff --git a/bot/constants.py b/bot/constants.py
index f390d8ce..6cbfa8db 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -8,6 +8,7 @@ from typing import Dict, NamedTuple
__all__ = (
"AdventOfCode",
"Branding",
+ "Cats",
"Channels",
"Categories",
"Client",
@@ -93,6 +94,10 @@ class Branding:
cycle_frequency = int(environ.get("CYCLE_FREQUENCY", 3)) # 0: never, 1: every day, 2: every other day, ...
+class Cats:
+ cats = ["ᓚᘏᗢ", "ᘡᘏᗢ", "🐈", "ᓕᘏᗢ", "ᓇᘏᗢ", "ᓂᘏᗢ", "ᘣᘏᗢ", "ᕦᘏᗢ", "ᕂᘏᗢ"]
+
+
class Channels(NamedTuple):
advent_of_code = int(environ.get("AOC_CHANNEL_ID", 782715290437943306))
advent_of_code_commands = int(environ.get("AOC_COMMANDS_CHANNEL_ID", 607247579608121354))
diff --git a/bot/exts/easter/april_fools_vids.py b/bot/exts/easter/april_fools_vids.py
index efe7e677..c7a3c014 100644
--- a/bot/exts/easter/april_fools_vids.py
+++ b/bot/exts/easter/april_fools_vids.py
@@ -1,36 +1,26 @@
import logging
import random
from json import load
-from pathlib import Path
from discord.ext import commands
log = logging.getLogger(__name__)
+with open("bot/resources/easter/april_fools_vids.json", encoding="utf-8") as f:
+ ALL_VIDS = load(f)
+
class AprilFoolVideos(commands.Cog):
"""A cog for April Fools' that gets a random April Fools' video from Youtube."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.yt_vids = self.load_json()
- self.youtubers = ['google'] # will add more in future
-
- @staticmethod
- def load_json() -> dict:
- """A function to load JSON data."""
- p = Path('bot/resources/easter/april_fools_vids.json')
- with p.open(encoding="utf-8") as json_file:
- all_vids = load(json_file)
- return all_vids
-
@commands.command(name='fool')
async def april_fools(self, ctx: commands.Context) -> None:
"""Get a random April Fools' video from Youtube."""
- random_youtuber = random.choice(self.youtubers)
- category = self.yt_vids[random_youtuber]
- random_vid = random.choice(category)
- await ctx.send(f"Check out this April Fools' video by {random_youtuber}.\n\n{random_vid['link']}")
+ video = random.choice(ALL_VIDS)
+
+ channel, url = video["channel"], video["url"]
+
+ await ctx.send(f"Check out this April Fools' video by {channel}.\n\n{url}")
def setup(bot: commands.Bot) -> None:
diff --git a/bot/exts/easter/egg_decorating.py b/bot/exts/easter/egg_decorating.py
index b18e6636..a847388d 100644
--- a/bot/exts/easter/egg_decorating.py
+++ b/bot/exts/easter/egg_decorating.py
@@ -10,6 +10,8 @@ import discord
from PIL import Image
from discord.ext import commands
+from bot.utils import helpers
+
log = logging.getLogger(__name__)
with open(Path("bot/resources/evergreen/html_colours.json"), encoding="utf8") as f:
@@ -65,7 +67,7 @@ class EggDecorating(commands.Cog):
if value:
colours[idx] = discord.Colour(value)
else:
- invalid.append(colour)
+ invalid.append(helpers.suppress_links(colour))
if len(invalid) > 1:
return await ctx.send(f"Sorry, I don't know these colours: {' '.join(invalid)}")
diff --git a/bot/exts/evergreen/battleship.py b/bot/exts/evergreen/battleship.py
index fa3fb35c..1681434f 100644
--- a/bot/exts/evergreen/battleship.py
+++ b/bot/exts/evergreen/battleship.py
@@ -227,7 +227,7 @@ class Game:
if message.content.lower() == "surrender":
self.surrender = True
return True
- self.match = re.match("([A-J]|[a-j]) ?((10)|[1-9])", message.content.strip())
+ self.match = re.fullmatch("([A-J]|[a-j]) ?((10)|[1-9])", message.content.strip())
if not self.match:
self.bot.loop.create_task(message.add_reaction(CROSS_EMOJI))
return bool(self.match)
diff --git a/bot/exts/evergreen/catify.py b/bot/exts/evergreen/catify.py
new file mode 100644
index 00000000..d8a7442d
--- /dev/null
+++ b/bot/exts/evergreen/catify.py
@@ -0,0 +1,87 @@
+import random
+from contextlib import suppress
+from typing import Optional
+
+from discord import AllowedMentions, Embed, Forbidden
+from discord.ext import commands
+
+from bot.constants import Cats, Colours, NEGATIVE_REPLIES
+from bot.utils import helpers
+
+
+class Catify(commands.Cog):
+ """Cog for the catify command."""
+
+ def __init__(self, bot: commands.Bot):
+ self.bot = bot
+
+ @commands.command(aliases=["ᓚᘏᗢify", "ᓚᘏᗢ"])
+ async def catify(self, ctx: commands.Context, *, text: Optional[str]) -> None:
+ """
+ Convert the provided text into a cat themed sentence by interspercing cats throughout text.
+
+ If no text is given then the users nickname is edited.
+ """
+ if not text:
+ display_name = ctx.author.display_name
+
+ if len(display_name) > 26:
+ embed = Embed(
+ title=random.choice(NEGATIVE_REPLIES),
+ description=(
+ "Your display name is too long to be catified! "
+ "Please change it to be under 26 characters."
+ ),
+ color=Colours.soft_red
+ )
+ await ctx.send(embed=embed)
+ return
+
+ else:
+ display_name += f" | {random.choice(Cats.cats)}"
+
+ await ctx.send(f"Your catified nickname is: `{display_name}`", allowed_mentions=AllowedMentions.none())
+
+ with suppress(Forbidden):
+ await ctx.author.edit(nick=display_name)
+ else:
+ if len(text) >= 1500:
+ embed = Embed(
+ title=random.choice(NEGATIVE_REPLIES),
+ description="Submitted text was too large! Please submit something under 1500 characters.",
+ color=Colours.soft_red
+ )
+ await ctx.send(embed=embed)
+ return
+
+ string_list = text.split()
+ for index, name in enumerate(string_list):
+ name = name.lower()
+ if "cat" in name:
+ if random.randint(0, 5) == 5:
+ string_list[index] = name.replace("cat", f"**{random.choice(Cats.cats)}**")
+ else:
+ string_list[index] = name.replace("cat", random.choice(Cats.cats))
+ for element in Cats.cats:
+ if element in name:
+ string_list[index] = name.replace(element, "cat")
+
+ string_len = len(string_list) // 3 or len(string_list)
+
+ for _ in range(random.randint(1, string_len)):
+ # insert cat at random index
+ if random.randint(0, 5) == 5:
+ string_list.insert(random.randint(0, len(string_list)), f"**{random.choice(Cats.cats)}**")
+ else:
+ string_list.insert(random.randint(0, len(string_list)), random.choice(Cats.cats))
+
+ text = helpers.suppress_links(" ".join(string_list))
+ await ctx.send(
+ f">>> {text}",
+ allowed_mentions=AllowedMentions.none()
+ )
+
+
+def setup(bot: commands.Bot) -> None:
+ """Loads the catify cog."""
+ bot.add_cog(Catify(bot))
diff --git a/bot/exts/evergreen/error_handler.py b/bot/exts/evergreen/error_handler.py
index 28902503..8db49748 100644
--- a/bot/exts/evergreen/error_handler.py
+++ b/bot/exts/evergreen/error_handler.py
@@ -46,6 +46,11 @@ class CommandErrorHandler(commands.Cog):
logging.debug(f"Command {ctx.command} had its error already handled locally; ignoring.")
return
+ parent_command = ""
+ if subctx := getattr(ctx, "subcontext", None):
+ parent_command = f"{ctx.command} "
+ ctx = subctx
+
error = getattr(error, 'original', error)
logging.debug(
f"Error Encountered: {type(error).__name__} - {str(error)}, "
@@ -63,8 +68,9 @@ class CommandErrorHandler(commands.Cog):
if isinstance(error, commands.UserInputError):
self.revert_cooldown_counter(ctx.command, ctx.message)
+ usage = f"```{ctx.prefix}{parent_command}{ctx.command} {ctx.command.signature}```"
embed = self.error_embed(
- f"Your input was invalid: {error}\n\nUsage:\n```{ctx.prefix}{ctx.command} {ctx.command.signature}```"
+ f"Your input was invalid: {error}\n\nUsage:{usage}"
)
await ctx.send(embed=embed)
return
@@ -95,7 +101,7 @@ class CommandErrorHandler(commands.Cog):
self.revert_cooldown_counter(ctx.command, ctx.message)
embed = self.error_embed(
"The argument you provided was invalid: "
- f"{error}\n\nUsage:\n```{ctx.prefix}{ctx.command} {ctx.command.signature}```"
+ f"{error}\n\nUsage:\n```{ctx.prefix}{parent_command}{ctx.command} {ctx.command.signature}```"
)
await ctx.send(embed=embed)
return
diff --git a/bot/exts/evergreen/fun.py b/bot/exts/evergreen/fun.py
index 101725da..7152d0cb 100644
--- a/bot/exts/evergreen/fun.py
+++ b/bot/exts/evergreen/fun.py
@@ -11,6 +11,7 @@ from discord.ext.commands import BadArgument, Bot, Cog, Context, MessageConverte
from bot import utils
from bot.constants import Client, Colours, Emojis
+from bot.utils import helpers
log = logging.getLogger(__name__)
@@ -83,6 +84,7 @@ class Fun(Cog):
if embed is not None:
embed = Fun._convert_embed(conversion_func, embed)
converted_text = conversion_func(text)
+ converted_text = helpers.suppress_links(converted_text)
# Don't put >>> if only embed present
if converted_text:
converted_text = f">>> {converted_text.lstrip('> ')}"
@@ -101,6 +103,7 @@ class Fun(Cog):
if embed is not None:
embed = Fun._convert_embed(conversion_func, embed)
converted_text = conversion_func(text)
+ converted_text = helpers.suppress_links(converted_text)
# Don't put >>> if only embed present
if converted_text:
converted_text = f">>> {converted_text.lstrip('> ')}"
diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py
index bb6273bb..a0316080 100644
--- a/bot/exts/evergreen/issues.py
+++ b/bot/exts/evergreen/issues.py
@@ -51,7 +51,10 @@ CODE_BLOCK_RE = re.compile(
MAXIMUM_ISSUES = 5
# Regex used when looking for automatic linking in messages
-AUTOMATIC_REGEX = re.compile(r"((?P<org>.+?)\/)?(?P<repo>.+?)#(?P<number>.+?)")
+# regex101 of current regex https://regex101.com/r/V2ji8M/6
+AUTOMATIC_REGEX = re.compile(
+ r"((?P<org>[a-zA-Z0-9][a-zA-Z0-9\-]{1,39})\/)?(?P<repo>[\w\-\.]{1,100})#(?P<number>[0-9]+)"
+)
@dataclass
diff --git a/bot/exts/evergreen/ping.py b/bot/exts/evergreen/ping.py
new file mode 100644
index 00000000..97f8b34d
--- /dev/null
+++ b/bot/exts/evergreen/ping.py
@@ -0,0 +1,27 @@
+from discord import Embed
+from discord.ext import commands
+
+from bot.constants import Colours
+
+
+class Ping(commands.Cog):
+ """Ping the bot to see its latency and state."""
+
+ def __init__(self, bot: commands.Bot):
+ self.bot = bot
+
+ @commands.command(name="ping")
+ async def ping(self, ctx: commands.Context) -> None:
+ """Ping the bot to see its latency and state."""
+ embed = Embed(
+ title=":ping_pong: Pong!",
+ colour=Colours.bright_green,
+ description=f"Gateway Latency: {round(self.bot.latency * 1000)}ms",
+ )
+
+ await ctx.send(embed=embed)
+
+
+def setup(bot: commands.Bot) -> None:
+ """Cog load."""
+ bot.add_cog(Ping(bot))
diff --git a/bot/exts/evergreen/reddit.py b/bot/exts/evergreen/reddit.py
index 49127bea..2be511c8 100644
--- a/bot/exts/evergreen/reddit.py
+++ b/bot/exts/evergreen/reddit.py
@@ -54,16 +54,16 @@ class Reddit(commands.Cog):
if not posts:
return await ctx.send('No posts available!')
- if posts[1]["data"]["over_18"] is True:
+ if posts[0]["data"]["over_18"] is True:
return await ctx.send(
- "You cannot access this Subreddit as it is ment for those who "
+ "You cannot access this Subreddit as it is meant for those who "
"are 18 years or older."
)
embed_titles = ""
# Chooses k unique random elements from a population sequence or set.
- random_posts = random.sample(posts, k=5)
+ random_posts = random.sample(posts, k=min(len(posts), 5))
# -----------------------------------------------------------
# This code below is bound of change when the emojis are added.
diff --git a/bot/exts/evergreen/timed.py b/bot/exts/evergreen/timed.py
index 635ccb32..5f177fd6 100644
--- a/bot/exts/evergreen/timed.py
+++ b/bot/exts/evergreen/timed.py
@@ -21,7 +21,9 @@ class TimedCommands(commands.Cog):
"""Time the command execution of a command."""
new_ctx = await self.create_execution_context(ctx, command)
- if not new_ctx.command:
+ ctx.subcontext = new_ctx
+
+ if not ctx.subcontext.command:
help_command = f"{ctx.prefix}help"
error = f"The command you are trying to time doesn't exist. Use `{help_command}` for a list of commands."
diff --git a/bot/exts/internal_eval/__init__.py b/bot/exts/internal_eval/__init__.py
new file mode 100644
index 00000000..695fa74d
--- /dev/null
+++ b/bot/exts/internal_eval/__init__.py
@@ -0,0 +1,10 @@
+from bot.bot import Bot
+
+
+def setup(bot: Bot) -> None:
+ """Set up the Internal Eval extension."""
+ # Import the Cog at runtime to prevent side effects like defining
+ # RedisCache instances too early.
+ from ._internal_eval import InternalEval
+
+ bot.add_cog(InternalEval(bot))
diff --git a/bot/exts/internal_eval/_helpers.py b/bot/exts/internal_eval/_helpers.py
new file mode 100644
index 00000000..3a50b9f3
--- /dev/null
+++ b/bot/exts/internal_eval/_helpers.py
@@ -0,0 +1,249 @@
+import ast
+import collections
+import contextlib
+import functools
+import inspect
+import io
+import logging
+import sys
+import traceback
+import types
+import typing
+
+
+log = logging.getLogger(__name__)
+
+# A type alias to annotate the tuples returned from `sys.exc_info()`
+ExcInfo = typing.Tuple[typing.Type[Exception], Exception, types.TracebackType]
+Namespace = typing.Dict[str, typing.Any]
+
+# This will be used as an coroutine function wrapper for the code
+# to be evaluated. The wrapper contains one `pass` statement which
+# will be replaced with `ast` with the code that we want to have
+# evaluated.
+# The function redirects output and captures exceptions that were
+# raised in the code we evaluate. The latter is used to provide a
+# meaningful traceback to the end user.
+EVAL_WRAPPER = """
+async def _eval_wrapper_function():
+ try:
+ with contextlib.redirect_stdout(_eval_context.stdout):
+ pass
+ if '_value_last_expression' in locals():
+ if inspect.isawaitable(_value_last_expression):
+ _value_last_expression = await _value_last_expression
+ _eval_context._value_last_expression = _value_last_expression
+ else:
+ _eval_context._value_last_expression = None
+ except Exception:
+ _eval_context.exc_info = sys.exc_info()
+ finally:
+ _eval_context.locals = locals()
+_eval_context.function = _eval_wrapper_function
+"""
+INTERNAL_EVAL_FRAMENAME = "<internal eval>"
+EVAL_WRAPPER_FUNCTION_FRAMENAME = "_eval_wrapper_function"
+
+
+def format_internal_eval_exception(exc_info: ExcInfo, code: str) -> str:
+ """Format an exception caught while evaluation code by inserting lines."""
+ exc_type, exc_value, tb = exc_info
+ stack_summary = traceback.StackSummary.extract(traceback.walk_tb(tb))
+ code = code.split("\n")
+
+ output = ["Traceback (most recent call last):"]
+ for frame in stack_summary:
+ if frame.filename == INTERNAL_EVAL_FRAMENAME:
+ line = code[frame.lineno - 1].lstrip()
+
+ if frame.name == EVAL_WRAPPER_FUNCTION_FRAMENAME:
+ name = INTERNAL_EVAL_FRAMENAME
+ else:
+ name = frame.name
+ else:
+ line = frame.line
+ name = frame.name
+
+ output.append(
+ f' File "{frame.filename}", line {frame.lineno}, in {name}\n'
+ f" {line}"
+ )
+
+ output.extend(traceback.format_exception_only(exc_type, exc_value))
+ return "\n".join(output)
+
+
+class EvalContext:
+ """
+ Represents the current `internal eval` context.
+
+ The context remembers names set during earlier runs of `internal eval`. To
+ clear the context, use the `.internal clear` command.
+ """
+
+ def __init__(self, context_vars: Namespace, local_vars: Namespace) -> None:
+ self._locals = dict(local_vars)
+ self.context_vars = dict(context_vars)
+
+ self.stdout = io.StringIO()
+ self._value_last_expression = None
+ self.exc_info = None
+ self.code = ""
+ self.function = None
+ self.eval_tree = None
+
+ @property
+ def dependencies(self) -> typing.Dict[str, typing.Any]:
+ """
+ Return a mapping of the dependencies for the wrapper function.
+
+ By using a property descriptor, the mapping can't be accidentally
+ mutated during evaluation. This ensures the dependencies are always
+ available.
+ """
+ return {
+ "print": functools.partial(print, file=self.stdout),
+ "contextlib": contextlib,
+ "inspect": inspect,
+ "sys": sys,
+ "_eval_context": self,
+ "_": self._value_last_expression,
+ }
+
+ @property
+ def locals(self) -> typing.Dict[str, typing.Any]:
+ """Return a mapping of names->values needed for evaluation."""
+ return {**collections.ChainMap(self.dependencies, self.context_vars, self._locals)}
+
+ @locals.setter
+ def locals(self, locals_: typing.Dict[str, typing.Any]) -> None:
+ """Update the contextual mapping of names to values."""
+ log.trace(f"Updating {self._locals} with {locals_}")
+ self._locals.update(locals_)
+
+ def prepare_eval(self, code: str) -> typing.Optional[str]:
+ """Prepare an evaluation by processing the code and setting up the context."""
+ self.code = code
+
+ if not self.code:
+ log.debug("No code was attached to the evaluation command")
+ return "[No code detected]"
+
+ try:
+ code_tree = ast.parse(code, filename=INTERNAL_EVAL_FRAMENAME)
+ except SyntaxError:
+ log.debug("Got a SyntaxError while parsing the eval code")
+ return "".join(traceback.format_exception(*sys.exc_info(), limit=0))
+
+ log.trace("Parsing the AST to see if there's a trailing expression we need to capture")
+ code_tree = CaptureLastExpression(code_tree).capture()
+
+ log.trace("Wrapping the AST in the AST of the wrapper coroutine")
+ eval_tree = WrapEvalCodeTree(code_tree).wrap()
+
+ self.eval_tree = eval_tree
+ return None
+
+ async def run_eval(self) -> Namespace:
+ """Run the evaluation and return the updated locals."""
+ log.trace("Compiling the AST to bytecode using `exec` mode")
+ compiled_code = compile(self.eval_tree, filename=INTERNAL_EVAL_FRAMENAME, mode="exec")
+
+ log.trace("Executing the compiled code with the desired namespace environment")
+ exec(compiled_code, self.locals) # noqa: B102,S102
+
+ log.trace("Awaiting the created evaluation wrapper coroutine.")
+ await self.function()
+
+ log.trace("Returning the updated captured locals.")
+ return self._locals
+
+ def format_output(self) -> str:
+ """Format the output of the most recent evaluation."""
+ output = []
+
+ log.trace(f"Getting output from stdout `{id(self.stdout)}`")
+ stdout_text = self.stdout.getvalue()
+ if stdout_text:
+ log.trace("Appending output captured from stdout/print")
+ output.append(stdout_text)
+
+ if self._value_last_expression is not None:
+ log.trace("Appending the output of a captured trialing expression")
+ output.append(f"[Captured] {self._value_last_expression!r}")
+
+ if self.exc_info:
+ log.trace("Appending exception information")
+ output.append(format_internal_eval_exception(self.exc_info, self.code))
+
+ log.trace(f"Generated output: {output!r}")
+ return "\n".join(output) or "[No output]"
+
+
+class WrapEvalCodeTree(ast.NodeTransformer):
+ """Wraps the AST of eval code with the wrapper function."""
+
+ def __init__(self, eval_code_tree: ast.AST, *args, **kwargs) -> None:
+ super().__init__(*args, **kwargs)
+ self.eval_code_tree = eval_code_tree
+
+ # To avoid mutable aliasing, parse the WRAPPER_FUNC for each wrapping
+ self.wrapper = ast.parse(EVAL_WRAPPER, filename=INTERNAL_EVAL_FRAMENAME)
+
+ def wrap(self) -> ast.AST:
+ """Wrap the tree of the code by the tree of the wrapper function."""
+ new_tree = self.visit(self.wrapper)
+ return ast.fix_missing_locations(new_tree)
+
+ def visit_Pass(self, node: ast.Pass) -> typing.List[ast.AST]: # noqa: N802
+ """
+ Replace the `_ast.Pass` node in the wrapper function by the eval AST.
+
+ This method works on the assumption that there's a single `pass`
+ statement in the wrapper function.
+ """
+ return list(ast.iter_child_nodes(self.eval_code_tree))
+
+
+class CaptureLastExpression(ast.NodeTransformer):
+ """Captures the return value from a loose expression."""
+
+ def __init__(self, tree: ast.AST, *args, **kwargs) -> None:
+ super().__init__(*args, **kwargs)
+ self.tree = tree
+ self.last_node = list(ast.iter_child_nodes(tree))[-1]
+
+ def visit_Expr(self, node: ast.Expr) -> typing.Union[ast.Expr, ast.Assign]: # noqa: N802
+ """
+ Replace the Expr node that is last child node of Module with an assignment.
+
+ We use an assignment to capture the value of the last node, if it's a loose
+ Expr node. Normally, the value of an Expr node is lost, meaning we don't get
+ the output of such a last "loose" expression. By assigning it a name, we can
+ retrieve it for our output.
+ """
+ if node is not self.last_node:
+ return node
+
+ log.trace("Found a trailing last expression in the evaluation code")
+
+ log.trace("Creating assignment statement with trailing expression as the right-hand side")
+ right_hand_side = list(ast.iter_child_nodes(node))[0]
+
+ assignment = ast.Assign(
+ targets=[ast.Name(id='_value_last_expression', ctx=ast.Store())],
+ value=right_hand_side,
+ lineno=node.lineno,
+ col_offset=0,
+ )
+ ast.fix_missing_locations(assignment)
+ return assignment
+
+ def capture(self) -> ast.AST:
+ """Capture the value of the last expression with an assignment."""
+ if not isinstance(self.last_node, ast.Expr):
+ # We only have to replace a node if the very last node is an Expr node
+ return self.tree
+
+ new_tree = self.visit(self.tree)
+ return ast.fix_missing_locations(new_tree)
diff --git a/bot/exts/internal_eval/_internal_eval.py b/bot/exts/internal_eval/_internal_eval.py
new file mode 100644
index 00000000..757a2a1e
--- /dev/null
+++ b/bot/exts/internal_eval/_internal_eval.py
@@ -0,0 +1,176 @@
+import logging
+import re
+import textwrap
+import typing
+
+import discord
+from discord.ext import commands
+
+from bot.bot import Bot
+from bot.constants import Roles
+from bot.utils.decorators import with_role
+from bot.utils.extensions import invoke_help_command
+from ._helpers import EvalContext
+
+__all__ = ["InternalEval"]
+
+log = logging.getLogger(__name__)
+
+FORMATTED_CODE_REGEX = re.compile(
+ 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
+ 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
+)
+
+
+class InternalEval(commands.Cog):
+ """Top secret code evaluation for admins and owners."""
+
+ def __init__(self, bot: Bot):
+ self.bot = bot
+ self.locals = {}
+
+ @staticmethod
+ def shorten_output(
+ output: str,
+ max_length: int = 1900,
+ placeholder: str = "\n[output truncated]"
+ ) -> str:
+ """
+ Shorten the `output` so it's shorter than `max_length`.
+
+ There are three tactics for this, tried in the following order:
+ - Shorten the output on a line-by-line basis
+ - Shorten the output on any whitespace character
+ - Shorten the output solely on character count
+ """
+ max_length = max_length - len(placeholder)
+
+ shortened_output = []
+ char_count = 0
+ for line in output.split("\n"):
+ if char_count + len(line) > max_length:
+ break
+ shortened_output.append(line)
+ char_count += len(line) + 1 # account for (possible) line ending
+
+ if shortened_output:
+ shortened_output.append(placeholder)
+ return "\n".join(shortened_output)
+
+ shortened_output = textwrap.shorten(output, width=max_length, placeholder=placeholder)
+
+ if shortened_output.strip() == placeholder.strip():
+ # `textwrap` was unable to find whitespace to shorten on, so it has
+ # reduced the output to just the placeholder. Let's shorten based on
+ # characters instead.
+ shortened_output = output[:max_length] + placeholder
+
+ return shortened_output
+
+ async def _upload_output(self, output: str) -> typing.Optional[str]:
+ """Upload `internal eval` output to our pastebin and return the url."""
+ try:
+ async with self.bot.http_session.post(
+ "https://paste.pythondiscord.com/documents", data=output, raise_for_status=True
+ ) as resp:
+ data = await resp.json()
+
+ if "key" in data:
+ return f"https://paste.pythondiscord.com/{data['key']}"
+ except Exception:
+ # 400 (Bad Request) means there are too many characters
+ log.exception("Failed to upload `internal eval` output to paste service!")
+
+ async def _send_output(self, ctx: commands.Context, output: str) -> None:
+ """Send the `internal eval` output to the command invocation context."""
+ upload_message = ""
+ if len(output) >= 1980:
+ # The output is too long, let's truncate it for in-channel output and
+ # upload the complete output to the paste service.
+ url = await self._upload_output(output)
+
+ if url:
+ upload_message = f"\nFull output here: {url}"
+ else:
+ upload_message = "\n:warning: Failed to upload full output!"
+
+ output = self.shorten_output(output)
+
+ await ctx.send(f"```py\n{output}\n```{upload_message}")
+
+ async def _eval(self, ctx: commands.Context, code: str) -> None:
+ """Evaluate the `code` in the current evaluation context."""
+ context_vars = {
+ "message": ctx.message,
+ "author": ctx.message.author,
+ "channel": ctx.channel,
+ "guild": ctx.guild,
+ "ctx": ctx,
+ "self": self,
+ "bot": self.bot,
+ "discord": discord,
+ }
+
+ eval_context = EvalContext(context_vars, self.locals)
+
+ log.trace("Preparing the evaluation by parsing the AST of the code")
+ error = eval_context.prepare_eval(code)
+
+ if error:
+ log.trace("The code can't be evaluated due to an error")
+ await ctx.send(f"```py\n{error}\n```")
+ return
+
+ log.trace("Evaluate the AST we've generated for the evaluation")
+ new_locals = await eval_context.run_eval()
+
+ log.trace("Updating locals with those set during evaluation")
+ self.locals.update(new_locals)
+
+ log.trace("Sending the formatted output back to the context")
+ await self._send_output(ctx, eval_context.format_output())
+
+ @commands.group(name='internal', aliases=('int',))
+ @with_role(Roles.admin)
+ async def internal_group(self, ctx: commands.Context) -> None:
+ """Internal commands. Top secret!"""
+ if not ctx.invoked_subcommand:
+ await invoke_help_command(ctx)
+
+ @internal_group.command(name='eval', aliases=('e',))
+ @with_role(Roles.admin)
+ async def eval(self, ctx: commands.Context, *, code: str) -> None:
+ """Run eval in a REPL-like format."""
+ if match := list(FORMATTED_CODE_REGEX.finditer(code)):
+ blocks = [block for block in match if block.group("block")]
+
+ if len(blocks) > 1:
+ code = '\n'.join(block.group("code") for block in blocks)
+ else:
+ match = match[0] if len(blocks) == 0 else blocks[0]
+ code, block, lang, delim = match.group("code", "block", "lang", "delim")
+
+ else:
+ code = RAW_CODE_REGEX.fullmatch(code).group("code")
+
+ code = textwrap.dedent(code)
+ await self._eval(ctx, code)
+
+ @internal_group.command(name='reset', aliases=("clear", "exit", "r", "c"))
+ @with_role(Roles.admin)
+ async def reset(self, ctx: commands.Context) -> None:
+ """Reset the context and locals of the eval session."""
+ self.locals = {}
+ await ctx.send("The evaluation context was reset.")
diff --git a/bot/resources/easter/april_fools_vids.json b/bot/resources/easter/april_fools_vids.json
index b2cbd07b..e1e8c70a 100644
--- a/bot/resources/easter/april_fools_vids.json
+++ b/bot/resources/easter/april_fools_vids.json
@@ -1,133 +1,130 @@
-{
- "google": [
- {
- "title": "Introducing Bad Joke Detector",
- "link": "https://youtu.be/OYcv406J_J4"
- },
- {
- "title": "Introducing Google Cloud Hummus API - Find your Hummus!",
- "link": "https://youtu.be/0_5X6N6DHyk"
- },
- {
- "title": "Introducing Google Play for Pets",
- "link": "https://youtu.be/UmJ2NBHXTqo"
- },
- {
- "title": "Haptic Helpers: bringing you to your senses",
- "link": "https://youtu.be/3MA6_21nka8"
- },
- {
- "title": "Introducing Google Wind",
- "link": "https://youtu.be/QAwL0O5nXe0"
- },
- {
- "title": "Experience YouTube in #SnoopaVision",
- "link": "https://youtu.be/DPEJB-FCItk"
- },
- {
- "title": "Introducing the self-driving bicycle in the Netherlands",
- "link": "https://youtu.be/LSZPNwZex9s"
- },
- {
- "title": "Android Developer Story: The Guardian goes galactic with Android and Google Play",
- "link": "https://youtu.be/dFrgNiweQDk"
- },
- {
- "title": "Introducing new delivery technology from Google Express",
- "link": "https://youtu.be/F0F6SnbqUcE"
- },
- {
- "title": "Google Cardboard Plastic",
- "link": "https://youtu.be/VkOuShXpoKc"
- },
- {
- "title": "Google Photos: Search your photos by emoji",
- "link": "https://youtu.be/HQtGFBbwKEk"
- },
- {
- "title": "Introducing Google Actual Cloud Platform",
- "link": "https://youtu.be/Cp10_PygJ4o"
- },
- {
- "title": "Introducing Dial-Up mode",
- "link": "https://youtu.be/XTTtkisylQw"
- },
- {
- "title": "Smartbox by Inbox: the mailbox of tomorrow, today",
- "link": "https://youtu.be/hydLZJXG3Tk"
- },
- {
- "title": "Introducing Coffee to the Home",
- "link": "https://youtu.be/U2JBFlW--UU"
- },
- {
- "title": "Chrome for Android and iOS: Emojify the Web",
- "link": "https://youtu.be/G3NXNnoGr3Y"
- },
- {
- "title": "Google Maps: Pokémon Challenge",
- "link": "https://youtu.be/4YMD6xELI_k"
- },
- {
- "title": "Introducing Google Fiber to the Pole",
- "link": "https://youtu.be/qcgWRpQP6ds"
- },
- {
- "title": "Introducing Gmail Blue",
- "link": "https://youtu.be/Zr4JwPb99qU"
- },
- {
- "title": "Introducing Google Nose",
- "link": "https://youtu.be/VFbYadm_mrw"
- },
- {
- "title": "Explore Treasure Mode with Google Maps",
- "link": "https://youtu.be/_qFFHC0eIUc"
- },
- {
- "title": "YouTube's ready to select a winner",
- "link": "https://youtu.be/H542nLTTbu0"
- },
- {
- "title": "A word about Gmail Tap",
- "link": "https://youtu.be/Je7Xq9tdCJc"
- },
- {
- "title": "Introducing the Google Fiber Bar",
- "link": "https://youtu.be/re0VRK6ouwI"
- },
- {
- "title": "Introducing Gmail Tap",
- "link": "https://youtu.be/1KhZKNZO8mQ"
- },
- {
- "title": "Chrome Multitask Mode",
- "link": "https://youtu.be/UiLSiqyDf4Y"
- },
- {
- "title": "Google Maps 8-bit for NES",
- "link": "https://youtu.be/rznYifPHxDg"
- },
- {
- "title": "Being a Google Autocompleter",
- "link": "https://youtu.be/blB_X38YSxQ"
- },
- {
- "title": "Introducing Gmail Motion",
- "link": "https://youtu.be/Bu927_ul_X0"
- },
- {
- "title": "Introducing GeForce GTX G-Assist",
- "link": "https://youtu.be/smM-Wdk2RLQ"
- },
- {
- "title": "The Hovering Mouse - Project McFly | Razer",
- "link": "https://youtu.be/IlCx5gjAmqI"
- },
- {
- "title": "Be the Machine | Project Venom v2",
- "link": "https://youtu.be/j8UJE7DoyJ8"
- }
- ]
-
-}
+[
+ {
+ "url": "https://youtu.be/OYcv406J_J4",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/0_5X6N6DHyk",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/UmJ2NBHXTqo",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/3MA6_21nka8",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/QAwL0O5nXe0",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/DPEJB-FCItk",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/LSZPNwZex9s",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/dFrgNiweQDk",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/F0F6SnbqUcE",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/VkOuShXpoKc",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/HQtGFBbwKEk",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/Cp10_PygJ4o",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/XTTtkisylQw",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/hydLZJXG3Tk",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/U2JBFlW--UU",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/G3NXNnoGr3Y",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/4YMD6xELI_k",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/qcgWRpQP6ds",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/Zr4JwPb99qU",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/VFbYadm_mrw",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/_qFFHC0eIUc",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/H542nLTTbu0",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/Je7Xq9tdCJc",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/re0VRK6ouwI",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/1KhZKNZO8mQ",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/UiLSiqyDf4Y",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/rznYifPHxDg",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/blB_X38YSxQ",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/Bu927_ul_X0",
+ "channel": "google"
+ },
+ {
+ "url": "https://youtu.be/smM-Wdk2RLQ",
+ "channel": "nvidia"
+ },
+ {
+ "url": "https://youtu.be/IlCx5gjAmqI",
+ "channel": "razer"
+ },
+ {
+ "url": "https://youtu.be/j8UJE7DoyJ8",
+ "channel": "razer"
+ }
+]
diff --git a/bot/resources/halloween/spookysounds/109710__tomlija__horror-gate.mp3 b/bot/resources/halloween/spookysounds/109710__tomlija__horror-gate.mp3
deleted file mode 100644
index 495f2bd1..00000000
--- a/bot/resources/halloween/spookysounds/109710__tomlija__horror-gate.mp3
+++ /dev/null
Binary files differ
diff --git a/bot/resources/halloween/spookysounds/126113__klankbeeld__laugh.mp3 b/bot/resources/halloween/spookysounds/126113__klankbeeld__laugh.mp3
deleted file mode 100644
index 538feabc..00000000
--- a/bot/resources/halloween/spookysounds/126113__klankbeeld__laugh.mp3
+++ /dev/null
Binary files differ
diff --git a/bot/resources/halloween/spookysounds/133674__klankbeeld__horror-laugh-original-132802-nanakisan-evil-laugh-08.mp3 b/bot/resources/halloween/spookysounds/133674__klankbeeld__horror-laugh-original-132802-nanakisan-evil-laugh-08.mp3
deleted file mode 100644
index 17f66698..00000000
--- a/bot/resources/halloween/spookysounds/133674__klankbeeld__horror-laugh-original-132802-nanakisan-evil-laugh-08.mp3
+++ /dev/null
Binary files differ
diff --git a/bot/resources/halloween/spookysounds/14570__oscillator__ghost-fx.mp3 b/bot/resources/halloween/spookysounds/14570__oscillator__ghost-fx.mp3
deleted file mode 100644
index 5670657c..00000000
--- a/bot/resources/halloween/spookysounds/14570__oscillator__ghost-fx.mp3
+++ /dev/null
Binary files differ
diff --git a/bot/resources/halloween/spookysounds/168650__0xmusex0__doorcreak.mp3 b/bot/resources/halloween/spookysounds/168650__0xmusex0__doorcreak.mp3
deleted file mode 100644
index 42f9e9fd..00000000
--- a/bot/resources/halloween/spookysounds/168650__0xmusex0__doorcreak.mp3
+++ /dev/null
Binary files differ
diff --git a/bot/resources/halloween/spookysounds/171078__klankbeeld__horror-scream-woman-long.mp3 b/bot/resources/halloween/spookysounds/171078__klankbeeld__horror-scream-woman-long.mp3
deleted file mode 100644
index 1cdb0f4d..00000000
--- a/bot/resources/halloween/spookysounds/171078__klankbeeld__horror-scream-woman-long.mp3
+++ /dev/null
Binary files differ
diff --git a/bot/resources/halloween/spookysounds/193812__geoneo0__four-voices-whispering-6.mp3 b/bot/resources/halloween/spookysounds/193812__geoneo0__four-voices-whispering-6.mp3
deleted file mode 100644
index 89150d57..00000000
--- a/bot/resources/halloween/spookysounds/193812__geoneo0__four-voices-whispering-6.mp3
+++ /dev/null
Binary files differ
diff --git a/bot/resources/halloween/spookysounds/237282__devilfish101__frantic-violin-screech.mp3 b/bot/resources/halloween/spookysounds/237282__devilfish101__frantic-violin-screech.mp3
deleted file mode 100644
index b5f85f8d..00000000
--- a/bot/resources/halloween/spookysounds/237282__devilfish101__frantic-violin-screech.mp3
+++ /dev/null
Binary files differ
diff --git a/bot/resources/halloween/spookysounds/249686__cylon8472__cthulhu-growl.mp3 b/bot/resources/halloween/spookysounds/249686__cylon8472__cthulhu-growl.mp3
deleted file mode 100644
index d141f68e..00000000
--- a/bot/resources/halloween/spookysounds/249686__cylon8472__cthulhu-growl.mp3
+++ /dev/null
Binary files differ
diff --git a/bot/resources/halloween/spookysounds/35716__analogchill__scream.mp3 b/bot/resources/halloween/spookysounds/35716__analogchill__scream.mp3
deleted file mode 100644
index a0614b53..00000000
--- a/bot/resources/halloween/spookysounds/35716__analogchill__scream.mp3
+++ /dev/null
Binary files differ
diff --git a/bot/resources/halloween/spookysounds/413315__inspectorj__something-evil-approaches-a.mp3 b/bot/resources/halloween/spookysounds/413315__inspectorj__something-evil-approaches-a.mp3
deleted file mode 100644
index 38374316..00000000
--- a/bot/resources/halloween/spookysounds/413315__inspectorj__something-evil-approaches-a.mp3
+++ /dev/null
Binary files differ
diff --git a/bot/resources/halloween/spookysounds/60571__gabemiller74__breathofdeath.mp3 b/bot/resources/halloween/spookysounds/60571__gabemiller74__breathofdeath.mp3
deleted file mode 100644
index f769d9d8..00000000
--- a/bot/resources/halloween/spookysounds/60571__gabemiller74__breathofdeath.mp3
+++ /dev/null
Binary files differ
diff --git a/bot/resources/halloween/spookysounds/Female_Monster_Growls_.mp3 b/bot/resources/halloween/spookysounds/Female_Monster_Growls_.mp3
deleted file mode 100644
index 8b04f0f5..00000000
--- a/bot/resources/halloween/spookysounds/Female_Monster_Growls_.mp3
+++ /dev/null
Binary files differ
diff --git a/bot/resources/halloween/spookysounds/Male_Zombie_Roar_.mp3 b/bot/resources/halloween/spookysounds/Male_Zombie_Roar_.mp3
deleted file mode 100644
index 964d685e..00000000
--- a/bot/resources/halloween/spookysounds/Male_Zombie_Roar_.mp3
+++ /dev/null
Binary files differ
diff --git a/bot/resources/halloween/spookysounds/Monster_Alien_Growl_Calm_.mp3 b/bot/resources/halloween/spookysounds/Monster_Alien_Growl_Calm_.mp3
deleted file mode 100644
index 9e643773..00000000
--- a/bot/resources/halloween/spookysounds/Monster_Alien_Growl_Calm_.mp3
+++ /dev/null
Binary files differ
diff --git a/bot/resources/halloween/spookysounds/Monster_Alien_Grunt_Hiss_.mp3 b/bot/resources/halloween/spookysounds/Monster_Alien_Grunt_Hiss_.mp3
deleted file mode 100644
index ad99cf76..00000000
--- a/bot/resources/halloween/spookysounds/Monster_Alien_Grunt_Hiss_.mp3
+++ /dev/null
Binary files differ
diff --git a/bot/resources/halloween/spookysounds/sources.txt b/bot/resources/halloween/spookysounds/sources.txt
deleted file mode 100644
index 7df03c2e..00000000
--- a/bot/resources/halloween/spookysounds/sources.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-Female_Monster_Growls_
-Male_Zombie_Roar_
-Monster_Alien_Growl_Calm_
-Monster_Alien_Grunt_Hiss_
-https://www.youtube.com/audiolibrary/soundeffects
-
-413315__inspectorj__something-evil-approaches-a
-https://freesound.org/people/InspectorJ/sounds/413315/
-
-133674__klankbeeld__horror-laugh-original-132802-nanakisan-evil-laugh-08
-https://freesound.org/people/klankbeeld/sounds/133674/
-
-35716__analogchill__scream
-https://freesound.org/people/analogchill/sounds/35716/
-
-249686__cylon8472__cthulhu-growl
-https://freesound.org/people/cylon8472/sounds/249686/
-
-126113__klankbeeld__laugh
-https://freesound.org/people/klankbeeld/sounds/126113/
-
-14570__oscillator__ghost-fx
-https://freesound.org/people/oscillator/sounds/14570/
-
-60571__gabemiller74__breathofdeath
-https://freesound.org/people/gabemiller74/sounds/60571/
-
-168650__0xmusex0__doorcreak
-https://freesound.org/people/0XMUSEX0/sounds/168650/
-
-193812__geoneo0__four-voices-whispering-6
-https://freesound.org/people/geoneo0/sounds/193812/
-
-109710__tomlija__horror-gate
-https://freesound.org/people/Tomlija/sounds/109710/
-
-171078__klankbeeld__horror-scream-woman-long
-https://freesound.org/people/klankbeeld/sounds/171078/
-
-237282__devilfish101__frantic-violin-screech
-https://freesound.org/people/devilfish101/sounds/237282/
diff --git a/bot/utils/exceptions.py b/bot/utils/exceptions.py
index 2b1c1b31..9e080759 100644
--- a/bot/utils/exceptions.py
+++ b/bot/utils/exceptions.py
@@ -1,4 +1,4 @@
class UserNotPlayingError(Exception):
- """Will raised when user try to use game commands when not playing."""
+ """Raised when users try to use game commands when they are not playing."""
pass
diff --git a/bot/utils/helpers.py b/bot/utils/helpers.py
new file mode 100644
index 00000000..74c2ccd0
--- /dev/null
+++ b/bot/utils/helpers.py
@@ -0,0 +1,8 @@
+import re
+
+
+def suppress_links(message: str) -> str:
+ """Accepts a message that may contain links, suppresses them, and returns them."""
+ for link in set(re.findall(r"https?://[^\s]+", message, re.IGNORECASE)):
+ message = message.replace(link, f"<{link}>")
+ return message