aboutsummaryrefslogtreecommitdiffstats
path: root/bot/exts
diff options
context:
space:
mode:
authorGravatar ks129 <[email protected]>2020-09-24 18:54:10 +0300
committerGravatar GitHub <[email protected]>2020-09-24 18:54:10 +0300
commit69d2292ec4b3fb2dc83f291ef1ed7bc86eabfd09 (patch)
treeb0c3c0991eef3fc278a0977baf129a7205ff469c /bot/exts
parentMerge remote-tracking branch 'origin/tic-tac-toe' into tic-tac-toe (diff)
parentMerge pull request #456 from Anubhav1603/update_dpy (diff)
Merge branch 'master' into tic-tac-toe
Diffstat (limited to 'bot/exts')
-rw-r--r--bot/exts/christmas/adventofcode.py4
-rw-r--r--bot/exts/easter/april_fools_vids.py2
-rw-r--r--bot/exts/easter/conversationstarters.py28
-rw-r--r--bot/exts/easter/egg_decorating.py4
-rw-r--r--bot/exts/easter/egg_facts.py7
-rw-r--r--bot/exts/evergreen/branding.py6
-rw-r--r--bot/exts/evergreen/conversationstarters.py71
-rw-r--r--bot/exts/evergreen/fun.py139
-rw-r--r--bot/exts/evergreen/issues.py113
-rw-r--r--bot/exts/evergreen/magic_8ball.py2
-rw-r--r--bot/exts/evergreen/minesweeper.py17
-rw-r--r--bot/exts/evergreen/recommend_game.py2
-rw-r--r--bot/exts/evergreen/reddit.py6
-rw-r--r--bot/exts/evergreen/snakes/converter.py4
-rw-r--r--bot/exts/evergreen/snakes/snakes_cog.py12
-rw-r--r--bot/exts/evergreen/speedrun.py2
-rw-r--r--bot/exts/evergreen/status_cats.py33
-rw-r--r--bot/exts/evergreen/trivia_quiz.py2
-rw-r--r--bot/exts/evergreen/wikipedia.py114
-rw-r--r--bot/exts/evergreen/wolfram.py278
-rw-r--r--bot/exts/halloween/candy_collection.py10
-rw-r--r--bot/exts/halloween/hacktoberstats.py4
-rw-r--r--bot/exts/halloween/halloween_facts.py2
-rw-r--r--bot/exts/halloween/halloweenify.py21
-rw-r--r--bot/exts/halloween/monstersurvey.py4
-rw-r--r--bot/exts/halloween/spookyrating.py2
-rw-r--r--bot/exts/halloween/spookysound.py7
-rw-r--r--bot/exts/pride/drag_queen_name.py2
-rw-r--r--bot/exts/pride/pride_anthem.py2
-rw-r--r--bot/exts/pride/pride_facts.py9
-rw-r--r--bot/exts/valentines/be_my_valentine.py2
-rw-r--r--bot/exts/valentines/lovecalculator.py2
-rw-r--r--bot/exts/valentines/myvalenstate.py2
-rw-r--r--bot/exts/valentines/valentine_zodiac.py2
-rw-r--r--bot/exts/valentines/whoisvalentine.py2
35 files changed, 770 insertions, 149 deletions
diff --git a/bot/exts/christmas/adventofcode.py b/bot/exts/christmas/adventofcode.py
index cc3923c8..b3fe0623 100644
--- a/bot/exts/christmas/adventofcode.py
+++ b/bot/exts/christmas/adventofcode.py
@@ -58,7 +58,7 @@ async def countdown_status(bot: commands.Bot) -> None:
hours, minutes = aligned_seconds // 3600, aligned_seconds // 60 % 60
if aligned_seconds == 0:
- playing = f"right now!"
+ playing = "right now!"
elif aligned_seconds == COUNTDOWN_STEP:
playing = f"in less than {minutes} minutes"
elif hours == 0:
@@ -429,7 +429,7 @@ class AdventOfCode(commands.Cog):
def _build_about_embed(self) -> discord.Embed:
"""Build and return the informational "About AoC" embed from the resources file."""
- with self.about_aoc_filepath.open("r") as f:
+ with self.about_aoc_filepath.open("r", encoding="utf8") as f:
embed_fields = json.load(f)
about_embed = discord.Embed(title=self._base_url, colour=Colours.soft_green, url=self._base_url)
diff --git a/bot/exts/easter/april_fools_vids.py b/bot/exts/easter/april_fools_vids.py
index 06108f02..efe7e677 100644
--- a/bot/exts/easter/april_fools_vids.py
+++ b/bot/exts/easter/april_fools_vids.py
@@ -20,7 +20,7 @@ class AprilFoolVideos(commands.Cog):
def load_json() -> dict:
"""A function to load JSON data."""
p = Path('bot/resources/easter/april_fools_vids.json')
- with p.open() as json_file:
+ with p.open(encoding="utf-8") as json_file:
all_vids = load(json_file)
return all_vids
diff --git a/bot/exts/easter/conversationstarters.py b/bot/exts/easter/conversationstarters.py
deleted file mode 100644
index a5f40445..00000000
--- a/bot/exts/easter/conversationstarters.py
+++ /dev/null
@@ -1,28 +0,0 @@
-import json
-import logging
-import random
-from pathlib import Path
-
-from discord.ext import commands
-
-log = logging.getLogger(__name__)
-
-with open(Path("bot/resources/easter/starter.json"), "r", encoding="utf8") as f:
- starters = json.load(f)
-
-
-class ConvoStarters(commands.Cog):
- """Easter conversation topics."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command()
- async def topic(self, ctx: commands.Context) -> None:
- """Responds with a random topic to start a conversation."""
- await ctx.send(random.choice(starters['starters']))
-
-
-def setup(bot: commands.Bot) -> None:
- """Conversation starters Cog load."""
- bot.add_cog(ConvoStarters(bot))
diff --git a/bot/exts/easter/egg_decorating.py b/bot/exts/easter/egg_decorating.py
index be228b2c..b18e6636 100644
--- a/bot/exts/easter/egg_decorating.py
+++ b/bot/exts/easter/egg_decorating.py
@@ -12,10 +12,10 @@ from discord.ext import commands
log = logging.getLogger(__name__)
-with open(Path("bot/resources/evergreen/html_colours.json")) as f:
+with open(Path("bot/resources/evergreen/html_colours.json"), encoding="utf8") as f:
HTML_COLOURS = json.load(f)
-with open(Path("bot/resources/evergreen/xkcd_colours.json")) as f:
+with open(Path("bot/resources/evergreen/xkcd_colours.json"), encoding="utf8") as f:
XKCD_COLOURS = json.load(f)
COLOURS = [
diff --git a/bot/exts/easter/egg_facts.py b/bot/exts/easter/egg_facts.py
index 83918fb0..0051aa50 100644
--- a/bot/exts/easter/egg_facts.py
+++ b/bot/exts/easter/egg_facts.py
@@ -6,6 +6,7 @@ from pathlib import Path
import discord
from discord.ext import commands
+from bot.bot import SeasonalBot
from bot.constants import Channels, Colours, Month
from bot.utils.decorators import seasonal_task
@@ -19,7 +20,7 @@ class EasterFacts(commands.Cog):
It also contains a background task which sends an easter egg fact in the event channel everyday.
"""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: SeasonalBot):
self.bot = bot
self.facts = self.load_json()
@@ -35,7 +36,7 @@ class EasterFacts(commands.Cog):
@seasonal_task(Month.APRIL)
async def send_egg_fact_daily(self) -> None:
"""A background task that sends an easter egg fact in the event channel everyday."""
- await self.bot.wait_until_ready()
+ await self.bot.wait_until_guild_available()
channel = self.bot.get_channel(Channels.seasonalbot_commands)
await channel.send(embed=self.make_embed())
@@ -55,6 +56,6 @@ class EasterFacts(commands.Cog):
)
-def setup(bot: commands.Bot) -> None:
+def setup(bot: SeasonalBot) -> None:
"""Easter Egg facts cog load."""
bot.add_cog(EasterFacts(bot))
diff --git a/bot/exts/evergreen/branding.py b/bot/exts/evergreen/branding.py
index 72f31042..7e531011 100644
--- a/bot/exts/evergreen/branding.py
+++ b/bot/exts/evergreen/branding.py
@@ -171,7 +171,7 @@ class BrandingManager(commands.Cog):
def _read_config(self) -> t.Dict[str, bool]:
"""Read and return persistent config file."""
- with self.config_file.open("r") as persistent_file:
+ with self.config_file.open("r", encoding="utf8") as persistent_file:
return json.load(persistent_file)
def _write_config(self, key: str, value: bool) -> None:
@@ -179,7 +179,7 @@ class BrandingManager(commands.Cog):
current_config = self._read_config()
current_config[key] = value
- with self.config_file.open("w") as persistent_file:
+ with self.config_file.open("w", encoding="utf8") as persistent_file:
json.dump(current_config, persistent_file)
async def _daemon_func(self) -> None:
@@ -198,7 +198,7 @@ class BrandingManager(commands.Cog):
All method calls in the internal loop are considered safe, i.e. no errors propagate
to the daemon's loop. The daemon itself does not perform any error handling on its own.
"""
- await self.bot.wait_until_ready()
+ await self.bot.wait_until_guild_available()
while True:
self.current_season = get_current_season()
diff --git a/bot/exts/evergreen/conversationstarters.py b/bot/exts/evergreen/conversationstarters.py
new file mode 100644
index 00000000..576b8d76
--- /dev/null
+++ b/bot/exts/evergreen/conversationstarters.py
@@ -0,0 +1,71 @@
+from pathlib import Path
+
+import yaml
+from discord import Color, Embed
+from discord.ext import commands
+
+from bot.constants import WHITELISTED_CHANNELS
+from bot.utils.decorators import override_in_channel
+from bot.utils.randomization import RandomCycle
+
+SUGGESTION_FORM = 'https://forms.gle/zw6kkJqv8U43Nfjg9'
+
+with Path("bot/resources/evergreen/starter.yaml").open("r", encoding="utf8") as f:
+ STARTERS = yaml.load(f, Loader=yaml.FullLoader)
+
+with Path("bot/resources/evergreen/py_topics.yaml").open("r", encoding="utf8") as f:
+ # First ID is #python-general and the rest are top to bottom categories of Topical Chat/Help.
+ PY_TOPICS = yaml.load(f, Loader=yaml.FullLoader)
+
+ # Removing `None` from lists of topics, if not a list, it is changed to an empty one.
+ PY_TOPICS = {k: [i for i in v if i] if isinstance(v, list) else [] for k, v in PY_TOPICS.items()}
+
+ # All the allowed channels that the ".topic" command is allowed to be executed in.
+ ALL_ALLOWED_CHANNELS = list(PY_TOPICS.keys()) + list(WHITELISTED_CHANNELS)
+
+# Putting all topics into one dictionary and shuffling lists to reduce same-topic repetitions.
+ALL_TOPICS = {'default': STARTERS, **PY_TOPICS}
+TOPICS = {
+ channel: RandomCycle(topics or ['No topics found for this channel.'])
+ for channel, topics in ALL_TOPICS.items()
+}
+
+
+class ConvoStarters(commands.Cog):
+ """Evergreen conversation topics."""
+
+ def __init__(self, bot: commands.Bot):
+ self.bot = bot
+
+ @commands.command()
+ @override_in_channel(ALL_ALLOWED_CHANNELS)
+ async def topic(self, ctx: commands.Context) -> None:
+ """
+ Responds with a random topic to start a conversation.
+
+ If in a Python channel, a python-related topic will be given.
+
+ Otherwise, a random conversation topic will be received by the user.
+ """
+ # No matter what, the form will be shown.
+ embed = Embed(description=f'Suggest more topics [here]({SUGGESTION_FORM})!', color=Color.blurple())
+
+ try:
+ # Fetching topics.
+ channel_topics = TOPICS[ctx.channel.id]
+
+ # If the channel isn't Python-related.
+ except KeyError:
+ embed.title = f'**{next(TOPICS["default"])}**'
+
+ # If the channel ID doesn't have any topics.
+ else:
+ embed.title = f'**{next(channel_topics)}**'
+
+ finally:
+ await ctx.send(embed=embed)
+
+
+def setup(bot: commands.Bot) -> None:
+ """Conversation starters Cog load."""
+ bot.add_cog(ConvoStarters(bot))
diff --git a/bot/exts/evergreen/fun.py b/bot/exts/evergreen/fun.py
index 67a4bae5..de6a92c6 100644
--- a/bot/exts/evergreen/fun.py
+++ b/bot/exts/evergreen/fun.py
@@ -1,14 +1,16 @@
import functools
+import json
import logging
import random
-from typing import Callable, Tuple, Union
+from pathlib import Path
+from typing import Callable, Iterable, Tuple, Union
from discord import Embed, Message
from discord.ext import commands
-from discord.ext.commands import Bot, Cog, Context, MessageConverter
+from discord.ext.commands import Bot, Cog, Context, MessageConverter, clean_content
from bot import utils
-from bot.constants import Emojis
+from bot.constants import Colours, Emojis
log = logging.getLogger(__name__)
@@ -26,12 +28,35 @@ UWU_WORDS = {
}
+def caesar_cipher(text: str, offset: int) -> Iterable[str]:
+ """
+ Implements a lazy Caesar Cipher algorithm.
+
+ Encrypts a `text` given a specific integer `offset`. The sign
+ of the `offset` dictates the direction in which it shifts to,
+ with a negative value shifting to the left, and a positive
+ value shifting to the right.
+ """
+ for char in text:
+ if not char.isascii() or not char.isalpha() or char.isspace():
+ yield char
+ continue
+
+ case_start = 65 if char.isupper() else 97
+ true_offset = (ord(char) - case_start + offset) % 26
+
+ yield chr(case_start + true_offset)
+
+
class Fun(Cog):
"""A collection of general commands for fun."""
def __init__(self, bot: Bot) -> None:
self.bot = bot
+ with Path("bot/resources/evergreen/caesar_info.json").open("r", encoding="UTF-8") as f:
+ self._caesar_cipher_embed = json.load(f)
+
@commands.command()
async def roll(self, ctx: Context, num_rolls: int = 1) -> None:
"""Outputs a number of random dice emotes (up to 6)."""
@@ -41,17 +66,13 @@ class Fun(Cog):
elif num_rolls < 1:
output = ":no_entry: You must roll at least once."
for _ in range(num_rolls):
- terning = f"terning{random.randint(1, 6)}"
- output += getattr(Emojis, terning, '')
+ dice = f"dice_{random.randint(1, 6)}"
+ output += getattr(Emojis, dice, '')
await ctx.send(output)
@commands.command(name="uwu", aliases=("uwuwize", "uwuify",))
- async def uwu_command(self, ctx: Context, *, text: str) -> None:
- """
- Converts a given `text` into it's uwu equivalent.
-
- Also accepts a valid discord Message ID or link.
- """
+ async def uwu_command(self, ctx: Context, *, text: clean_content(fix_channel_mentions=True)) -> None:
+ """Converts a given `text` into it's uwu equivalent."""
conversion_func = functools.partial(
utils.replace_many, replacements=UWU_WORDS, ignore_case=True, match_case=True
)
@@ -66,12 +87,8 @@ class Fun(Cog):
await ctx.send(content=converted_text, embed=embed)
@commands.command(name="randomcase", aliases=("rcase", "randomcaps", "rcaps",))
- async def randomcase_command(self, ctx: Context, *, text: str) -> None:
- """
- Randomly converts the casing of a given `text`.
-
- Also accepts a valid discord Message ID or link.
- """
+ async def randomcase_command(self, ctx: Context, *, text: clean_content(fix_channel_mentions=True)) -> None:
+ """Randomly converts the casing of a given `text`."""
def conversion_func(text: str) -> str:
"""Randomly converts the casing of a given string."""
return "".join(
@@ -87,22 +104,100 @@ class Fun(Cog):
converted_text = f">>> {converted_text.lstrip('> ')}"
await ctx.send(content=converted_text, embed=embed)
+ @commands.group(name="caesarcipher", aliases=("caesar", "cc",))
+ async def caesarcipher_group(self, ctx: Context) -> None:
+ """
+ Translates a message using the Caesar Cipher.
+
+ See `decrypt`, `encrypt`, and `info` subcommands.
+ """
+ if ctx.invoked_subcommand is None:
+ await ctx.invoke(self.bot.get_command("help"), "caesarcipher")
+
+ @caesarcipher_group.command(name="info")
+ async def caesarcipher_info(self, ctx: Context) -> None:
+ """Information about the Caesar Cipher."""
+ embed = Embed.from_dict(self._caesar_cipher_embed)
+ embed.colour = Colours.dark_green
+
+ await ctx.send(embed=embed)
+
+ @staticmethod
+ async def _caesar_cipher(ctx: Context, offset: int, msg: str, left_shift: bool = False) -> None:
+ """
+ Given a positive integer `offset`, translates and sends the given `msg`.
+
+ Performs a right shift by default unless `left_shift` is specified as `True`.
+
+ Also accepts a valid Discord Message ID or link.
+ """
+ if offset < 0:
+ await ctx.send(":no_entry: Cannot use a negative offset.")
+ return
+
+ if left_shift:
+ offset = -offset
+
+ def conversion_func(text: str) -> str:
+ """Encrypts the given string using the Caesar Cipher."""
+ return "".join(caesar_cipher(text, offset))
+
+ text, embed = await Fun._get_text_and_embed(ctx, msg)
+
+ if embed is not None:
+ embed = Fun._convert_embed(conversion_func, embed)
+
+ converted_text = conversion_func(text)
+
+ if converted_text:
+ converted_text = f">>> {converted_text.lstrip('> ')}"
+
+ await ctx.send(content=converted_text, embed=embed)
+
+ @caesarcipher_group.command(name="encrypt", aliases=("rightshift", "rshift", "enc",))
+ async def caesarcipher_encrypt(self, ctx: Context, offset: int, *, msg: str) -> None:
+ """
+ Given a positive integer `offset`, encrypt the given `msg`.
+
+ Performs a right shift of the letters in the message.
+
+ Also accepts a valid Discord Message ID or link.
+ """
+ await self._caesar_cipher(ctx, offset, msg, left_shift=False)
+
+ @caesarcipher_group.command(name="decrypt", aliases=("leftshift", "lshift", "dec",))
+ async def caesarcipher_decrypt(self, ctx: Context, offset: int, *, msg: str) -> None:
+ """
+ Given a positive integer `offset`, decrypt the given `msg`.
+
+ Performs a left shift of the letters in the message.
+
+ Also accepts a valid Discord Message ID or link.
+ """
+ await self._caesar_cipher(ctx, offset, msg, left_shift=True)
+
@staticmethod
async def _get_text_and_embed(ctx: Context, text: str) -> Tuple[str, Union[Embed, None]]:
"""
Attempts to extract the text and embed from a possible link to a discord Message.
+ Does not retrieve the text and embed from the Message if it is in a channel the user does
+ not have read permissions in.
+
Returns a tuple of:
str: If `text` is a valid discord Message, the contents of the message, else `text`.
Union[Embed, None]: The embed if found in the valid Message, else None
"""
embed = None
- message = await Fun._get_discord_message(ctx, text)
- if isinstance(message, Message):
- text = message.content
+
+ msg = await Fun._get_discord_message(ctx, text)
+ # Ensure the user has read permissions for the channel the message is in
+ if isinstance(msg, Message) and ctx.author.permissions_in(msg.channel).read_messages:
+ text = msg.clean_content
# Take first embed because we can't send multiple embeds
- if message.embeds:
- embed = message.embeds[0]
+ if msg.embeds:
+ embed = msg.embeds[0]
+
return (text, embed)
@staticmethod
diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py
index 4129156a..5a5c82e7 100644
--- a/bot/exts/evergreen/issues.py
+++ b/bot/exts/evergreen/issues.py
@@ -1,9 +1,10 @@
import logging
+import random
import discord
from discord.ext import commands
-from bot.constants import Colours, Emojis, WHITELISTED_CHANNELS
+from bot.constants import Channels, Colours, ERROR_REPLIES, Emojis, Tokens, WHITELISTED_CHANNELS
from bot.utils.decorators import override_in_channel
log = logging.getLogger(__name__)
@@ -13,6 +14,12 @@ BAD_RESPONSE = {
403: "Rate limit has been hit! Please try again later!"
}
+MAX_REQUESTS = 10
+
+REQUEST_HEADERS = dict()
+if GITHUB_TOKEN := Tokens.github:
+ REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}"
+
class Issues(commands.Cog):
"""Cog that allows users to retrieve issues from GitHub."""
@@ -21,53 +28,79 @@ class Issues(commands.Cog):
self.bot = bot
@commands.command(aliases=("pr",))
- @override_in_channel(WHITELISTED_CHANNELS)
+ @override_in_channel(WHITELISTED_CHANNELS + (Channels.dev_contrib, Channels.dev_branding))
async def issue(
- self, ctx: commands.Context, number: int, repository: str = "seasonalbot", user: str = "python-discord"
+ self,
+ ctx: commands.Context,
+ numbers: commands.Greedy[int],
+ repository: str = "seasonalbot",
+ user: str = "python-discord"
) -> None:
- """Command to retrieve issues from a GitHub repository."""
- url = f"https://api.github.com/repos/{user}/{repository}/issues/{number}"
- merge_url = f"https://api.github.com/repos/{user}/{repository}/pulls/{number}/merge"
-
- log.trace(f"Querying GH issues API: {url}")
- async with self.bot.http_session.get(url) as r:
- json_data = await r.json()
-
- if r.status in BAD_RESPONSE:
- log.warning(f"Received response {r.status} from: {url}")
- return await ctx.send(f"[{str(r.status)}] {BAD_RESPONSE.get(r.status)}")
-
- # The initial API request is made to the issues API endpoint, which will return information
- # if the issue or PR is present. However, the scope of information returned for PRs differs
- # from issues: if the 'issues' key is present in the response then we can pull the data we
- # need from the initial API call.
- if "issues" in json_data.get("html_url"):
- if json_data.get("state") == "open":
- icon_url = Emojis.issue
- else:
- icon_url = Emojis.issue_closed
-
- # If the 'issues' key is not contained in the API response and there is no error code, then
- # we know that a PR has been requested and a call to the pulls API endpoint is necessary
- # to get the desired information for the PR.
- else:
- log.trace(f"PR provided, querying GH pulls API for additional information: {merge_url}")
- async with self.bot.http_session.get(merge_url) as m:
+ """Command to retrieve issue(s) from a GitHub repository."""
+ links = []
+ numbers = set(numbers)
+
+ if not numbers:
+ await ctx.invoke(self.bot.get_command('help'), 'issue')
+ return
+
+ if len(numbers) > MAX_REQUESTS:
+ embed = discord.Embed(
+ title=random.choice(ERROR_REPLIES),
+ color=Colours.soft_red,
+ description=f"Too many issues/PRs! (maximum of {MAX_REQUESTS})"
+ )
+ await ctx.send(embed=embed)
+ return
+
+ for number in set(numbers):
+ # Convert from list to set to remove duplicates, if any.
+ url = f"https://api.github.com/repos/{user}/{repository}/issues/{number}"
+ merge_url = f"https://api.github.com/repos/{user}/{repository}/pulls/{number}/merge"
+
+ log.trace(f"Querying GH issues API: {url}")
+ async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as r:
+ json_data = await r.json()
+
+ if r.status in BAD_RESPONSE:
+ log.warning(f"Received response {r.status} from: {url}")
+ return await ctx.send(f"[{str(r.status)}] #{number} {BAD_RESPONSE.get(r.status)}")
+
+ # The initial API request is made to the issues API endpoint, which will return information
+ # if the issue or PR is present. However, the scope of information returned for PRs differs
+ # from issues: if the 'issues' key is present in the response then we can pull the data we
+ # need from the initial API call.
+ if "issues" in json_data.get("html_url"):
if json_data.get("state") == "open":
- icon_url = Emojis.pull_request
- # When the status is 204 this means that the state of the PR is merged
- elif m.status == 204:
- icon_url = Emojis.merge
+ icon_url = Emojis.issue
else:
- icon_url = Emojis.pull_request_closed
+ icon_url = Emojis.issue_closed
+
+ # If the 'issues' key is not contained in the API response and there is no error code, then
+ # we know that a PR has been requested and a call to the pulls API endpoint is necessary
+ # to get the desired information for the PR.
+ else:
+ log.trace(f"PR provided, querying GH pulls API for additional information: {merge_url}")
+ async with self.bot.http_session.get(merge_url) as m:
+ if json_data.get("state") == "open":
+ icon_url = Emojis.pull_request
+ # When the status is 204 this means that the state of the PR is merged
+ elif m.status == 204:
+ icon_url = Emojis.merge
+ else:
+ icon_url = Emojis.pull_request_closed
+
+ issue_url = json_data.get("html_url")
+ links.append([icon_url, f"[{repository}] #{number} {json_data.get('title')}", issue_url])
- issue_url = json_data.get("html_url")
- description_text = f"[{repository}] #{number} {json_data.get('title')}"
+ # Issue/PR format: emoji to show if open/closed/merged, number and the title as a singular link.
+ description_list = ["{0} [{1}]({2})".format(*link) for link in links]
resp = discord.Embed(
colour=Colours.bright_green,
- description=f"{icon_url} [{description_text}]({issue_url})"
+ description='\n'.join(description_list)
)
- resp.set_author(name="GitHub", url=issue_url)
+
+ resp.set_author(name="GitHub", url=f"https://github.com/{user}/{repository}")
await ctx.send(embed=resp)
diff --git a/bot/exts/evergreen/magic_8ball.py b/bot/exts/evergreen/magic_8ball.py
index c10f1f51..f974e487 100644
--- a/bot/exts/evergreen/magic_8ball.py
+++ b/bot/exts/evergreen/magic_8ball.py
@@ -13,7 +13,7 @@ class Magic8ball(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
- with open(Path("bot/resources/evergreen/magic8ball.json"), "r") as file:
+ with open(Path("bot/resources/evergreen/magic8ball.json"), "r", encoding="utf8") as file:
self.answers = json.load(file)
@commands.command(name="8ball")
diff --git a/bot/exts/evergreen/minesweeper.py b/bot/exts/evergreen/minesweeper.py
index ae057b30..3e40f493 100644
--- a/bot/exts/evergreen/minesweeper.py
+++ b/bot/exts/evergreen/minesweeper.py
@@ -141,9 +141,20 @@ class Minesweeper(commands.Cog):
await ctx.message.delete(delay=2)
return
+ try:
+ await ctx.author.send(
+ f"Play by typing: `{Client.prefix}ms reveal xy [xy]` or `{Client.prefix}ms flag xy [xy]` \n"
+ f"Close the game with `{Client.prefix}ms end`\n"
+ )
+ except discord.errors.Forbidden:
+ log.debug(f"{ctx.author.name} ({ctx.author.id}) has disabled DMs from server members")
+ await ctx.send(f":x: {ctx.author.mention}, please enable DMs to play minesweeper.")
+ return
+
# Add game to list
board: GameBoard = self.generate_board(bomb_chance)
revealed_board: GameBoard = [["hidden"] * 10 for _ in range(10)]
+ dm_msg = await ctx.author.send(f"Here's your board!\n{self.format_for_discord(revealed_board)}")
if ctx.guild:
await ctx.send(f"{ctx.author.mention} is playing Minesweeper")
@@ -151,12 +162,6 @@ class Minesweeper(commands.Cog):
else:
chat_msg = None
- await ctx.author.send(
- f"Play by typing: `{Client.prefix}ms reveal xy [xy]` or `{Client.prefix}ms flag xy [xy]` \n"
- f"Close the game with `{Client.prefix}ms end`\n"
- )
- dm_msg = await ctx.author.send(f"Here's your board!\n{self.format_for_discord(revealed_board)}")
-
self.games[ctx.author.id] = Game(
board=board,
revealed=revealed_board,
diff --git a/bot/exts/evergreen/recommend_game.py b/bot/exts/evergreen/recommend_game.py
index 7cd52c2c..5e262a5b 100644
--- a/bot/exts/evergreen/recommend_game.py
+++ b/bot/exts/evergreen/recommend_game.py
@@ -11,7 +11,7 @@ game_recs = []
# Populate the list `game_recs` with resource files
for rec_path in Path("bot/resources/evergreen/game_recs").glob("*.json"):
- with rec_path.open(encoding='utf-8') as file:
+ with rec_path.open(encoding='utf8') as file:
data = json.load(file)
game_recs.append(data)
shuffle(game_recs)
diff --git a/bot/exts/evergreen/reddit.py b/bot/exts/evergreen/reddit.py
index fe204419..49127bea 100644
--- a/bot/exts/evergreen/reddit.py
+++ b/bot/exts/evergreen/reddit.py
@@ -68,9 +68,9 @@ class Reddit(commands.Cog):
# -----------------------------------------------------------
# This code below is bound of change when the emojis are added.
- upvote_emoji = self.bot.get_emoji(638729835245731840)
- comment_emoji = self.bot.get_emoji(638729835073765387)
- user_emoji = self.bot.get_emoji(638729835442602003)
+ upvote_emoji = self.bot.get_emoji(755845219890757644)
+ comment_emoji = self.bot.get_emoji(755845255001014384)
+ user_emoji = self.bot.get_emoji(755845303822974997)
text_emoji = self.bot.get_emoji(676030265910493204)
video_emoji = self.bot.get_emoji(676030265839190047)
image_emoji = self.bot.get_emoji(676030265734201344)
diff --git a/bot/exts/evergreen/snakes/converter.py b/bot/exts/evergreen/snakes/converter.py
index d4e93b56..55609b8e 100644
--- a/bot/exts/evergreen/snakes/converter.py
+++ b/bot/exts/evergreen/snakes/converter.py
@@ -63,12 +63,12 @@ class Snake(Converter):
"""Build list of snakes from the static snake resources."""
# Get all the snakes
if cls.snakes is None:
- with (SNAKE_RESOURCES / "snake_names.json").open() as snakefile:
+ with (SNAKE_RESOURCES / "snake_names.json").open(encoding="utf8") as snakefile:
cls.snakes = json.load(snakefile)
# Get the special cases
if cls.special_cases is None:
- with (SNAKE_RESOURCES / "special_snakes.json").open() as snakefile:
+ with (SNAKE_RESOURCES / "special_snakes.json").open(encoding="utf8") as snakefile:
special_cases = json.load(snakefile)
cls.special_cases = {snake['name'].lower(): snake for snake in special_cases}
diff --git a/bot/exts/evergreen/snakes/snakes_cog.py b/bot/exts/evergreen/snakes/snakes_cog.py
index 36c176ce..9bbad9fe 100644
--- a/bot/exts/evergreen/snakes/snakes_cog.py
+++ b/bot/exts/evergreen/snakes/snakes_cog.py
@@ -567,7 +567,7 @@ class Snakes(Cog):
antidote_embed = Embed(color=SNAKE_COLOR, title="Antidote")
antidote_embed.set_author(name=ctx.author.name, icon_url=ctx.author.avatar_url)
antidote_embed.set_image(url="https://i.makeagif.com/media/7-12-2015/Cj1pts.gif")
- antidote_embed.add_field(name=f"You have created the snake antidote!",
+ antidote_embed.add_field(name="You have created the snake antidote!",
value=f"The solution was: {' '.join(antidote_answer)}\n"
f"You had {10 - antidote_tries} tries remaining.")
await board_id.edit(embed=antidote_embed)
@@ -945,13 +945,15 @@ class Snakes(Cog):
title="About the snake cog",
description=(
"The features in this cog were created by members of the community "
- "during our first ever [code jam event](https://gitlab.com/discord-python/code-jams/code-jam-1). \n\n"
+ "during our first ever "
+ "[code jam event](https://pythondiscord.com/pages/code-jams/code-jam-1-snakes-bot/). \n\n"
"The event saw over 50 participants, who competed to write a discord bot cog with a snake theme over "
"48 hours. The staff then selected the best features from all the best teams, and made modifications "
"to ensure they would all work together before integrating them into the community bot.\n\n"
"It was a tight race, but in the end, <@!104749643715387392> and <@!303940835005825024> "
- "walked away as grand champions. Make sure you check out `!snakes sal`, `!snakes draw` "
- "and `!snakes hatch` to see what they came up with."
+ f"walked away as grand champions. Make sure you check out `{ctx.prefix}snakes sal`,"
+ f"`{ctx.prefix}snakes draw` and `{ctx.prefix}snakes hatch` "
+ "to see what they came up with."
)
)
@@ -1076,7 +1078,7 @@ class Snakes(Cog):
query = snake['name']
# Build the URL and make the request
- url = f'https://www.googleapis.com/youtube/v3/search'
+ url = 'https://www.googleapis.com/youtube/v3/search'
response = await self.bot.http_session.get(
url,
params={
diff --git a/bot/exts/evergreen/speedrun.py b/bot/exts/evergreen/speedrun.py
index 4e8d7aee..21aad5aa 100644
--- a/bot/exts/evergreen/speedrun.py
+++ b/bot/exts/evergreen/speedrun.py
@@ -6,7 +6,7 @@ from random import choice
from discord.ext import commands
log = logging.getLogger(__name__)
-with Path('bot/resources/evergreen/speedrun_links.json').open(encoding="utf-8") as file:
+with Path('bot/resources/evergreen/speedrun_links.json').open(encoding="utf8") as file:
LINKS = json.load(file)
diff --git a/bot/exts/evergreen/status_cats.py b/bot/exts/evergreen/status_cats.py
new file mode 100644
index 00000000..586b8378
--- /dev/null
+++ b/bot/exts/evergreen/status_cats.py
@@ -0,0 +1,33 @@
+from http import HTTPStatus
+
+import discord
+from discord.ext import commands
+
+
+class StatusCats(commands.Cog):
+ """Commands that give HTTP statuses described and visualized by cats."""
+
+ def __init__(self, bot: commands.Bot):
+ self.bot = bot
+
+ @commands.command(aliases=['statuscat'])
+ async def http_cat(self, ctx: commands.Context, code: int) -> None:
+ """Sends an embed with an image of a cat, potraying the status code."""
+ embed = discord.Embed(title=f'**Status: {code}**')
+
+ try:
+ HTTPStatus(code)
+
+ except ValueError:
+ embed.set_footer(text='Inputted status code does not exist.')
+
+ else:
+ embed.set_image(url=f'https://http.cat/{code}.jpg')
+
+ finally:
+ await ctx.send(embed=embed)
+
+
+def setup(bot: commands.Bot) -> None:
+ """Load the StatusCats cog."""
+ bot.add_cog(StatusCats(bot))
diff --git a/bot/exts/evergreen/trivia_quiz.py b/bot/exts/evergreen/trivia_quiz.py
index c1a271e8..8dceceac 100644
--- a/bot/exts/evergreen/trivia_quiz.py
+++ b/bot/exts/evergreen/trivia_quiz.py
@@ -40,7 +40,7 @@ class TriviaQuiz(commands.Cog):
def load_questions() -> dict:
"""Load the questions from the JSON file."""
p = Path("bot", "resources", "evergreen", "trivia_quiz.json")
- with p.open() as json_data:
+ with p.open(encoding="utf8") as json_data:
questions = json.load(json_data)
return questions
diff --git a/bot/exts/evergreen/wikipedia.py b/bot/exts/evergreen/wikipedia.py
new file mode 100644
index 00000000..be36e2c4
--- /dev/null
+++ b/bot/exts/evergreen/wikipedia.py
@@ -0,0 +1,114 @@
+import asyncio
+import datetime
+import logging
+from typing import List, Optional
+
+from aiohttp import client_exceptions
+from discord import Color, Embed, Message
+from discord.ext import commands
+
+from bot.constants import Wikipedia
+
+log = logging.getLogger(__name__)
+
+SEARCH_API = "https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch={search_term}&format=json"
+WIKIPEDIA_URL = "https://en.wikipedia.org/wiki/{title}"
+
+
+class WikipediaSearch(commands.Cog):
+ """Get info from wikipedia."""
+
+ def __init__(self, bot: commands.Bot):
+ self.bot = bot
+ self.http_session = bot.http_session
+
+ @staticmethod
+ def formatted_wiki_url(index: int, title: str) -> str:
+ """Formating wikipedia link with index and title."""
+ return f'`{index}` [{title}]({WIKIPEDIA_URL.format(title=title.replace(" ", "_"))})'
+
+ async def search_wikipedia(self, search_term: str) -> Optional[List[str]]:
+ """Search wikipedia and return the first 10 pages found."""
+ pages = []
+ async with self.http_session.get(SEARCH_API.format(search_term=search_term)) as response:
+ try:
+ data = await response.json()
+
+ search_results = data["query"]["search"]
+
+ # Ignore pages with "may refer to"
+ for search_result in search_results:
+ log.info("trying to append titles")
+ if "may refer to" not in search_result["snippet"]:
+ pages.append(search_result["title"])
+ except client_exceptions.ContentTypeError:
+ pages = None
+
+ log.info("Finished appending titles")
+ return pages
+
+ @commands.cooldown(1, 10, commands.BucketType.user)
+ @commands.command(name="wikipedia", aliases=["wiki"])
+ async def wikipedia_search_command(self, ctx: commands.Context, *, search: str) -> None:
+ """Return list of results containing your search query from wikipedia."""
+ titles = await self.search_wikipedia(search)
+
+ def check(message: Message) -> bool:
+ return message.author.id == ctx.author.id and message.channel == ctx.channel
+
+ if not titles:
+ await ctx.send("Sorry, we could not find a wikipedia article using that search term")
+ return
+
+ async with ctx.typing():
+ log.info("Finished appending titles to titles_no_underscore list")
+
+ s_desc = "\n".join(self.formatted_wiki_url(index, title) for index, title in enumerate(titles, start=1))
+ embed = Embed(colour=Color.blue(), title=f"Wikipedia results for `{search}`", description=s_desc)
+ embed.timestamp = datetime.datetime.utcnow()
+ await ctx.send(embed=embed)
+ embed = Embed(colour=Color.green(), description="Enter number to choose")
+ msg = await ctx.send(embed=embed)
+ titles_len = len(titles) # getting length of list
+
+ for retry_count in range(1, Wikipedia.total_chance + 1):
+ retries_left = Wikipedia.total_chance - retry_count
+ if retry_count < Wikipedia.total_chance:
+ error_msg = f"You have `{retries_left}/{Wikipedia.total_chance}` chances left"
+ else:
+ error_msg = 'Please try again by using `.wiki` command'
+ try:
+ message = await ctx.bot.wait_for('message', timeout=60.0, check=check)
+ response_from_user = await self.bot.get_context(message)
+
+ if response_from_user.command:
+ return
+
+ response = int(message.content)
+ if response < 0:
+ await ctx.send(f"Sorry, but you can't give negative index, {error_msg}")
+ elif response == 0:
+ await ctx.send(f"Sorry, please give an integer between `1` to `{titles_len}`, {error_msg}")
+ else:
+ await ctx.send(WIKIPEDIA_URL.format(title=titles[response - 1].replace(" ", "_")))
+ break
+
+ except asyncio.TimeoutError:
+ embed = Embed(colour=Color.red(), description=f"Time's up {ctx.author.mention}")
+ await msg.edit(embed=embed)
+ break
+
+ except ValueError:
+ await ctx.send(f"Sorry, but you cannot do that, I will only accept an positive integer, {error_msg}")
+
+ except IndexError:
+ await ctx.send(f"Sorry, please give an integer between `1` to `{titles_len}`, {error_msg}")
+
+ except Exception as e:
+ log.info(f"Caught exception {e}, breaking out of retry loop")
+ break
+
+
+def setup(bot: commands.Bot) -> None:
+ """Wikipedia Cog load."""
+ bot.add_cog(WikipediaSearch(bot))
diff --git a/bot/exts/evergreen/wolfram.py b/bot/exts/evergreen/wolfram.py
new file mode 100644
index 00000000..898e8d2a
--- /dev/null
+++ b/bot/exts/evergreen/wolfram.py
@@ -0,0 +1,278 @@
+import logging
+from io import BytesIO
+from typing import Callable, List, Optional, Tuple
+from urllib import parse
+
+import arrow
+import discord
+from discord import Embed
+from discord.ext import commands
+from discord.ext.commands import BucketType, Cog, Context, check, group
+
+from bot.constants import Colours, STAFF_ROLES, Wolfram
+from bot.utils.pagination import ImagePaginator
+
+log = logging.getLogger(__name__)
+
+APPID = Wolfram.key
+DEFAULT_OUTPUT_FORMAT = "JSON"
+QUERY = "http://api.wolframalpha.com/v2/{request}?{data}"
+WOLF_IMAGE = "https://www.symbols.com/gi.php?type=1&id=2886&i=1"
+
+MAX_PODS = 20
+
+# Allows for 10 wolfram calls pr user pr day
+usercd = commands.CooldownMapping.from_cooldown(Wolfram.user_limit_day, 60 * 60 * 24, BucketType.user)
+
+# Allows for max api requests / days in month per day for the entire guild (Temporary)
+guildcd = commands.CooldownMapping.from_cooldown(Wolfram.guild_limit_day, 60 * 60 * 24, BucketType.guild)
+
+
+async def send_embed(
+ ctx: Context,
+ message_txt: str,
+ colour: int = Colours.soft_red,
+ footer: str = None,
+ img_url: str = None,
+ f: discord.File = None
+) -> None:
+ """Generate & send a response embed with Wolfram as the author."""
+ embed = Embed(colour=colour)
+ embed.description = message_txt
+ embed.set_author(name="Wolfram Alpha",
+ icon_url=WOLF_IMAGE,
+ url="https://www.wolframalpha.com/")
+ if footer:
+ embed.set_footer(text=footer)
+
+ if img_url:
+ embed.set_image(url=img_url)
+
+ await ctx.send(embed=embed, file=f)
+
+
+def custom_cooldown(*ignore: List[int]) -> Callable:
+ """
+ Implement per-user and per-guild cooldowns for requests to the Wolfram API.
+
+ A list of roles may be provided to ignore the per-user cooldown
+ """
+ async def predicate(ctx: Context) -> bool:
+ if ctx.invoked_with == 'help':
+ # if the invoked command is help we don't want to increase the ratelimits since it's not actually
+ # invoking the command/making a request, so instead just check if the user/guild are on cooldown.
+ guild_cooldown = not guildcd.get_bucket(ctx.message).get_tokens() == 0 # if guild is on cooldown
+ if not any(r.id in ignore for r in ctx.author.roles): # check user bucket if user is not ignored
+ return guild_cooldown and not usercd.get_bucket(ctx.message).get_tokens() == 0
+ return guild_cooldown
+
+ user_bucket = usercd.get_bucket(ctx.message)
+
+ if all(role.id not in ignore for role in ctx.author.roles):
+ user_rate = user_bucket.update_rate_limit()
+
+ if user_rate:
+ # Can't use api; cause: member limit
+ cooldown = arrow.utcnow().shift(seconds=int(user_rate)).humanize(only_distance=True)
+ message = (
+ "You've used up your limit for Wolfram|Alpha requests.\n"
+ f"Cooldown: {cooldown}"
+ )
+ await send_embed(ctx, message)
+ return False
+
+ guild_bucket = guildcd.get_bucket(ctx.message)
+ guild_rate = guild_bucket.update_rate_limit()
+
+ # Repr has a token attribute to read requests left
+ log.debug(guild_bucket)
+
+ if guild_rate:
+ # Can't use api; cause: guild limit
+ message = (
+ "The max limit of requests for the server has been reached for today.\n"
+ f"Cooldown: {int(guild_rate)}"
+ )
+ await send_embed(ctx, message)
+ return False
+
+ return True
+
+ return check(predicate)
+
+
+async def get_pod_pages(ctx: Context, bot: commands.Bot, query: str) -> Optional[List[Tuple]]:
+ """Get the Wolfram API pod pages for the provided query."""
+ async with ctx.channel.typing():
+ url_str = parse.urlencode({
+ "input": query,
+ "appid": APPID,
+ "output": DEFAULT_OUTPUT_FORMAT,
+ "format": "image,plaintext"
+ })
+ request_url = QUERY.format(request="query", data=url_str)
+
+ async with bot.http_session.get(request_url) as response:
+ json = await response.json(content_type='text/plain')
+
+ result = json["queryresult"]
+
+ if result["error"]:
+ # API key not set up correctly
+ if result["error"]["msg"] == "Invalid appid":
+ message = "Wolfram API key is invalid or missing."
+ log.warning(
+ "API key seems to be missing, or invalid when "
+ f"processing a wolfram request: {url_str}, Response: {json}"
+ )
+ await send_embed(ctx, message)
+ return
+
+ message = "Something went wrong internally with your request, please notify staff!"
+ log.warning(f"Something went wrong getting a response from wolfram: {url_str}, Response: {json}")
+ await send_embed(ctx, message)
+ return
+
+ if not result["success"]:
+ message = f"I couldn't find anything for {query}."
+ await send_embed(ctx, message)
+ return
+
+ if not result["numpods"]:
+ message = "Could not find any results."
+ await send_embed(ctx, message)
+ return
+
+ pods = result["pods"]
+ pages = []
+ for pod in pods[:MAX_PODS]:
+ subs = pod.get("subpods")
+
+ for sub in subs:
+ title = sub.get("title") or sub.get("plaintext") or sub.get("id", "")
+ img = sub["img"]["src"]
+ pages.append((title, img))
+ return pages
+
+
+class Wolfram(Cog):
+ """Commands for interacting with the Wolfram|Alpha API."""
+
+ def __init__(self, bot: commands.Bot):
+ self.bot = bot
+
+ @group(name="wolfram", aliases=("wolf", "wa"), invoke_without_command=True)
+ @custom_cooldown(*STAFF_ROLES)
+ async def wolfram_command(self, ctx: Context, *, query: str) -> None:
+ """Requests all answers on a single image, sends an image of all related pods."""
+ url_str = parse.urlencode({
+ "i": query,
+ "appid": APPID,
+ })
+ query = QUERY.format(request="simple", data=url_str)
+
+ # Give feedback that the bot is working.
+ async with ctx.channel.typing():
+ async with self.bot.http_session.get(query) as response:
+ status = response.status
+ image_bytes = await response.read()
+
+ f = discord.File(BytesIO(image_bytes), filename="image.png")
+ image_url = "attachment://image.png"
+
+ if status == 501:
+ message = "Failed to get response"
+ footer = ""
+ color = Colours.soft_red
+ elif status == 400:
+ message = "No input found"
+ footer = ""
+ color = Colours.soft_red
+ elif status == 403:
+ message = "Wolfram API key is invalid or missing."
+ footer = ""
+ color = Colours.soft_red
+ else:
+ message = ""
+ footer = "View original for a bigger picture."
+ color = Colours.soft_orange
+
+ # Sends a "blank" embed if no request is received, unsure how to fix
+ await send_embed(ctx, message, color, footer=footer, img_url=image_url, f=f)
+
+ @wolfram_command.command(name="page", aliases=("pa", "p"))
+ @custom_cooldown(*STAFF_ROLES)
+ async def wolfram_page_command(self, ctx: Context, *, query: str) -> None:
+ """
+ Requests a drawn image of given query.
+
+ Keywords worth noting are, "like curve", "curve", "graph", "pokemon", etc.
+ """
+ pages = await get_pod_pages(ctx, self.bot, query)
+
+ if not pages:
+ return
+
+ embed = Embed()
+ embed.set_author(name="Wolfram Alpha",
+ icon_url=WOLF_IMAGE,
+ url="https://www.wolframalpha.com/")
+ embed.colour = Colours.soft_orange
+
+ await ImagePaginator.paginate(pages, ctx, embed)
+
+ @wolfram_command.command(name="cut", aliases=("c",))
+ @custom_cooldown(*STAFF_ROLES)
+ async def wolfram_cut_command(self, ctx: Context, *, query: str) -> None:
+ """
+ Requests a drawn image of given query.
+
+ Keywords worth noting are, "like curve", "curve", "graph", "pokemon", etc.
+ """
+ pages = await get_pod_pages(ctx, self.bot, query)
+
+ if not pages:
+ return
+
+ if len(pages) >= 2:
+ page = pages[1]
+ else:
+ page = pages[0]
+
+ await send_embed(ctx, page[0], colour=Colours.soft_orange, img_url=page[1])
+
+ @wolfram_command.command(name="short", aliases=("sh", "s"))
+ @custom_cooldown(*STAFF_ROLES)
+ async def wolfram_short_command(self, ctx: Context, *, query: str) -> None:
+ """Requests an answer to a simple question."""
+ url_str = parse.urlencode({
+ "i": query,
+ "appid": APPID,
+ })
+ query = QUERY.format(request="result", data=url_str)
+
+ # Give feedback that the bot is working.
+ async with ctx.channel.typing():
+ async with self.bot.http_session.get(query) as response:
+ status = response.status
+ response_text = await response.text()
+
+ if status == 501:
+ message = "Failed to get response"
+ color = Colours.soft_red
+ elif status == 400:
+ message = "No input found"
+ color = Colours.soft_red
+ elif response_text == "Error 1: Invalid appid":
+ message = "Wolfram API key is invalid or missing."
+ color = Colours.soft_red
+ else:
+ message = response_text
+ color = Colours.soft_orange
+
+ await send_embed(ctx, message, color)
+
+
+def setup(bot: commands.Bot) -> None:
+ """Load the Wolfram cog."""
+ bot.add_cog(Wolfram(bot))
diff --git a/bot/exts/halloween/candy_collection.py b/bot/exts/halloween/candy_collection.py
index 90c29eb2..caf0df11 100644
--- a/bot/exts/halloween/candy_collection.py
+++ b/bot/exts/halloween/candy_collection.py
@@ -27,7 +27,7 @@ class CandyCollection(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
- with open(json_location) as candy:
+ with open(json_location, encoding="utf8") as candy:
self.candy_json = json.load(candy)
self.msg_reacted = self.candy_json['msg_reacted']
self.get_candyinfo = dict()
@@ -178,7 +178,7 @@ class CandyCollection(commands.Cog):
def save_to_json(self) -> None:
"""Save JSON to a local file."""
- with open(json_location, 'w') as outfile:
+ with open(json_location, 'w', encoding="utf8") as outfile:
json.dump(self.candy_json, outfile)
@in_month(Month.OCTOBER)
@@ -212,9 +212,9 @@ class CandyCollection(commands.Cog):
e = discord.Embed(colour=discord.Colour.blurple())
e.add_field(name="Top Candy Records", value=value, inline=False)
e.add_field(name='\u200b',
- value=f"Candies will randomly appear on messages sent. "
- f"\nHit the candy when it appears as fast as possible to get the candy! "
- f"\nBut beware the ghosts...",
+ value="Candies will randomly appear on messages sent. "
+ "\nHit the candy when it appears as fast as possible to get the candy! "
+ "\nBut beware the ghosts...",
inline=False)
await ctx.send(embed=e)
diff --git a/bot/exts/halloween/hacktoberstats.py b/bot/exts/halloween/hacktoberstats.py
index e01ee50c..db5e37f2 100644
--- a/bot/exts/halloween/hacktoberstats.py
+++ b/bot/exts/halloween/hacktoberstats.py
@@ -123,7 +123,7 @@ class HacktoberStats(commands.Cog):
"""
if self.link_json.exists():
logging.info(f"Loading linked GitHub accounts from '{self.link_json}'")
- with open(self.link_json, 'r') as file:
+ with open(self.link_json, 'r', encoding="utf8") as file:
linked_accounts = json.load(file)
logging.info(f"Loaded {len(linked_accounts)} linked GitHub accounts from '{self.link_json}'")
@@ -145,7 +145,7 @@ class HacktoberStats(commands.Cog):
}
"""
logging.info(f"Saving linked_accounts to '{self.link_json}'")
- with open(self.link_json, 'w') as file:
+ with open(self.link_json, 'w', encoding="utf8") as file:
json.dump(self.linked_accounts, file, default=str)
logging.info(f"linked_accounts saved to '{self.link_json}'")
diff --git a/bot/exts/halloween/halloween_facts.py b/bot/exts/halloween/halloween_facts.py
index 44a66ab2..7eb6d56f 100644
--- a/bot/exts/halloween/halloween_facts.py
+++ b/bot/exts/halloween/halloween_facts.py
@@ -29,7 +29,7 @@ class HalloweenFacts(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
- with open(Path("bot/resources/halloween/halloween_facts.json"), "r") as file:
+ with open(Path("bot/resources/halloween/halloween_facts.json"), "r", encoding="utf8") as file:
self.halloween_facts = json.load(file)
self.facts = list(enumerate(self.halloween_facts))
random.shuffle(self.facts)
diff --git a/bot/exts/halloween/halloweenify.py b/bot/exts/halloween/halloweenify.py
index 5c433a81..596c6682 100644
--- a/bot/exts/halloween/halloweenify.py
+++ b/bot/exts/halloween/halloweenify.py
@@ -4,6 +4,7 @@ from pathlib import Path
from random import choice
import discord
+from discord.errors import Forbidden
from discord.ext import commands
from discord.ext.commands.cooldowns import BucketType
@@ -21,7 +22,7 @@ class Halloweenify(commands.Cog):
async def halloweenify(self, ctx: commands.Context) -> None:
"""Change your nickname into a much spookier one!"""
async with ctx.typing():
- with open(Path("bot/resources/halloween/halloweenify.json"), "r") as f:
+ with open(Path("bot/resources/halloween/halloweenify.json"), "r", encoding="utf8") as f:
data = load(f)
# Choose a random character from our list we loaded above and set apart the nickname and image url.
@@ -37,11 +38,25 @@ class Halloweenify(commands.Cog):
f"**{ctx.author.display_name}** wasn\'t spooky enough for you? That\'s understandable, "
f"{ctx.author.display_name} isn\'t scary at all! "
"Let me think of something better. Hmm... I got it!\n\n "
- f"Your new nickname will be: \n :ghost: **{nickname}** :jack_o_lantern:"
)
embed.set_image(url=image)
- await ctx.author.edit(nick=nickname)
+ if isinstance(ctx.author, discord.Member):
+ try:
+ await ctx.author.edit(nick=nickname)
+ embed.description += f"Your new nickname will be: \n:ghost: **{nickname}** :jack_o_lantern:"
+
+ except Forbidden: # The bot doesn't have enough permission
+ embed.description += (
+ f"Your new nickname should be: \n :ghost: **{nickname}** :jack_o_lantern: \n\n"
+ f"It looks like I cannot change your name, but feel free to change it yourself."
+ )
+
+ else: # The command has been invoked in DM
+ embed.description += (
+ f"Your new nickname should be: \n :ghost: **{nickname}** :jack_o_lantern: \n\n"
+ f"Feel free to change it yourself, or invoke the command again inside the server."
+ )
await ctx.send(embed=embed)
diff --git a/bot/exts/halloween/monstersurvey.py b/bot/exts/halloween/monstersurvey.py
index 27da79b6..7b1a1e84 100644
--- a/bot/exts/halloween/monstersurvey.py
+++ b/bot/exts/halloween/monstersurvey.py
@@ -27,13 +27,13 @@ class MonsterSurvey(Cog):
"""Initializes values for the bot to use within the voting commands."""
self.bot = bot
self.registry_location = os.path.join(os.getcwd(), 'bot', 'resources', 'halloween', 'monstersurvey.json')
- with open(self.registry_location, 'r') as jason:
+ with open(self.registry_location, 'r', encoding="utf8") as jason:
self.voter_registry = json.load(jason)
def json_write(self) -> None:
"""Write voting results to a local JSON file."""
log.info("Saved Monster Survey Results")
- with open(self.registry_location, 'w') as jason:
+ with open(self.registry_location, 'w', encoding="utf8") as jason:
json.dump(self.voter_registry, jason, indent=2)
def cast_vote(self, id: int, monster: str) -> None:
diff --git a/bot/exts/halloween/spookyrating.py b/bot/exts/halloween/spookyrating.py
index 1a48194e..6f069f8c 100644
--- a/bot/exts/halloween/spookyrating.py
+++ b/bot/exts/halloween/spookyrating.py
@@ -11,7 +11,7 @@ from bot.constants import Colours
log = logging.getLogger(__name__)
-with Path("bot/resources/halloween/spooky_rating.json").open() as file:
+with Path("bot/resources/halloween/spooky_rating.json").open(encoding="utf8") as file:
SPOOKY_DATA = json.load(file)
SPOOKY_DATA = sorted((int(key), value) for key, value in SPOOKY_DATA.items())
diff --git a/bot/exts/halloween/spookysound.py b/bot/exts/halloween/spookysound.py
index 325447e5..569a9153 100644
--- a/bot/exts/halloween/spookysound.py
+++ b/bot/exts/halloween/spookysound.py
@@ -5,6 +5,7 @@ from pathlib import Path
import discord
from discord.ext import commands
+from bot.bot import SeasonalBot
from bot.constants import Hacktoberfest
log = logging.getLogger(__name__)
@@ -13,7 +14,7 @@ log = logging.getLogger(__name__)
class SpookySound(commands.Cog):
"""A cog that plays a spooky sound in a voice channel on command."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: SeasonalBot):
self.bot = bot
self.sound_files = list(Path("bot/resources/halloween/spookysounds").glob("*.mp3"))
self.channel = None
@@ -27,7 +28,7 @@ class SpookySound(commands.Cog):
Cannot be used more than once in 2 minutes.
"""
if not self.channel:
- await self.bot.wait_until_ready()
+ await self.bot.wait_until_guild_available()
self.channel = self.bot.get_channel(Hacktoberfest.voice_id)
await ctx.send("Initiating spooky sound...")
@@ -42,6 +43,6 @@ class SpookySound(commands.Cog):
await voice.disconnect()
-def setup(bot: commands.Bot) -> None:
+def setup(bot: SeasonalBot) -> None:
"""Spooky sound Cog load."""
bot.add_cog(SpookySound(bot))
diff --git a/bot/exts/pride/drag_queen_name.py b/bot/exts/pride/drag_queen_name.py
index 95297745..fca9750f 100644
--- a/bot/exts/pride/drag_queen_name.py
+++ b/bot/exts/pride/drag_queen_name.py
@@ -18,7 +18,7 @@ class DragNames(commands.Cog):
@staticmethod
def load_names() -> list:
"""Loads a list of drag queen names."""
- with open(Path("bot/resources/pride/drag_queen_names.json"), "r", encoding="utf-8") as f:
+ with open(Path("bot/resources/pride/drag_queen_names.json"), "r", encoding="utf8") as f:
return json.load(f)
@commands.command(name="dragname", aliases=["dragqueenname", "queenme"])
diff --git a/bot/exts/pride/pride_anthem.py b/bot/exts/pride/pride_anthem.py
index 186c5fff..33cb2a9d 100644
--- a/bot/exts/pride/pride_anthem.py
+++ b/bot/exts/pride/pride_anthem.py
@@ -34,7 +34,7 @@ class PrideAnthem(commands.Cog):
@staticmethod
def load_vids() -> list:
"""Loads a list of videos from the resources folder as dictionaries."""
- with open(Path("bot/resources/pride/anthems.json"), "r", encoding="utf-8") as f:
+ with open(Path("bot/resources/pride/anthems.json"), "r", encoding="utf8") as f:
anthems = json.load(f)
return anthems
diff --git a/bot/exts/pride/pride_facts.py b/bot/exts/pride/pride_facts.py
index f759dcb1..9ff4c9e0 100644
--- a/bot/exts/pride/pride_facts.py
+++ b/bot/exts/pride/pride_facts.py
@@ -9,6 +9,7 @@ import dateutil.parser
import discord
from discord.ext import commands
+from bot.bot import SeasonalBot
from bot.constants import Channels, Colours, Month
from bot.utils.decorators import seasonal_task
@@ -20,7 +21,7 @@ Sendable = Union[commands.Context, discord.TextChannel]
class PrideFacts(commands.Cog):
"""Provides a new fact every day during the Pride season!"""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: SeasonalBot):
self.bot = bot
self.facts = self.load_facts()
@@ -29,13 +30,13 @@ class PrideFacts(commands.Cog):
@staticmethod
def load_facts() -> dict:
"""Loads a dictionary of years mapping to lists of facts."""
- with open(Path("bot/resources/pride/facts.json"), "r", encoding="utf-8") as f:
+ with open(Path("bot/resources/pride/facts.json"), "r", encoding="utf8") as f:
return json.load(f)
@seasonal_task(Month.JUNE)
async def send_pride_fact_daily(self) -> None:
"""Background task to post the daily pride fact every day."""
- await self.bot.wait_until_ready()
+ await self.bot.wait_until_guild_available()
channel = self.bot.get_channel(Channels.seasonalbot_commands)
await self.send_select_fact(channel, datetime.utcnow())
@@ -101,6 +102,6 @@ class PrideFacts(commands.Cog):
)
-def setup(bot: commands.Bot) -> None:
+def setup(bot: SeasonalBot) -> None:
"""Cog loader for pride facts."""
bot.add_cog(PrideFacts(bot))
diff --git a/bot/exts/valentines/be_my_valentine.py b/bot/exts/valentines/be_my_valentine.py
index e5e71d25..b1258307 100644
--- a/bot/exts/valentines/be_my_valentine.py
+++ b/bot/exts/valentines/be_my_valentine.py
@@ -27,7 +27,7 @@ class BeMyValentine(commands.Cog):
def load_json() -> dict:
"""Load Valentines messages from the static resources."""
p = Path("bot/resources/valentines/bemyvalentine_valentines.json")
- with p.open() as json_data:
+ with p.open(encoding="utf8") as json_data:
valentines = load(json_data)
return valentines
diff --git a/bot/exts/valentines/lovecalculator.py b/bot/exts/valentines/lovecalculator.py
index e11e062b..c75ea6cf 100644
--- a/bot/exts/valentines/lovecalculator.py
+++ b/bot/exts/valentines/lovecalculator.py
@@ -15,7 +15,7 @@ from bot.constants import Roles
log = logging.getLogger(__name__)
-with Path("bot/resources/valentines/love_matches.json").open() as file:
+with Path("bot/resources/valentines/love_matches.json").open(encoding="utf8") as file:
LOVE_DATA = json.load(file)
LOVE_DATA = sorted((int(key), value) for key, value in LOVE_DATA.items())
diff --git a/bot/exts/valentines/myvalenstate.py b/bot/exts/valentines/myvalenstate.py
index 7d8737c4..01801847 100644
--- a/bot/exts/valentines/myvalenstate.py
+++ b/bot/exts/valentines/myvalenstate.py
@@ -11,7 +11,7 @@ from bot.constants import Colours
log = logging.getLogger(__name__)
-with open(Path("bot/resources/valentines/valenstates.json"), "r") as file:
+with open(Path("bot/resources/valentines/valenstates.json"), "r", encoding="utf8") as file:
STATES = json.load(file)
diff --git a/bot/exts/valentines/valentine_zodiac.py b/bot/exts/valentines/valentine_zodiac.py
index 1a1273aa..ef9ddc78 100644
--- a/bot/exts/valentines/valentine_zodiac.py
+++ b/bot/exts/valentines/valentine_zodiac.py
@@ -25,7 +25,7 @@ class ValentineZodiac(commands.Cog):
def load_json() -> dict:
"""Load zodiac compatibility from static JSON resource."""
p = Path("bot/resources/valentines/zodiac_compatibility.json")
- with p.open() as json_data:
+ with p.open(encoding="utf8") as json_data:
zodiacs = load(json_data)
return zodiacs
diff --git a/bot/exts/valentines/whoisvalentine.py b/bot/exts/valentines/whoisvalentine.py
index 4ca0289c..0ff9186c 100644
--- a/bot/exts/valentines/whoisvalentine.py
+++ b/bot/exts/valentines/whoisvalentine.py
@@ -10,7 +10,7 @@ from bot.constants import Colours
log = logging.getLogger(__name__)
-with open(Path("bot/resources/valentines/valentine_facts.json"), "r") as file:
+with open(Path("bot/resources/valentines/valentine_facts.json"), "r", encoding="utf8") as file:
FACTS = json.load(file)