aboutsummaryrefslogtreecommitdiffstats
path: root/bot
diff options
context:
space:
mode:
Diffstat (limited to 'bot')
-rw-r--r--bot/__init__.py8
-rw-r--r--bot/bot.py47
-rw-r--r--bot/constants.py27
-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
-rw-r--r--bot/resources/easter/starter.json24
-rw-r--r--bot/resources/evergreen/caesar_info.json4
-rw-r--r--bot/resources/evergreen/py_topics.yaml89
-rw-r--r--bot/resources/evergreen/starter.yaml22
-rw-r--r--bot/resources/evergreen/trivia_quiz.json30
-rw-r--r--bot/utils/decorators.py2
-rw-r--r--bot/utils/pagination.py4
-rw-r--r--bot/utils/persist.py5
-rw-r--r--bot/utils/randomization.py23
47 files changed, 1015 insertions, 189 deletions
diff --git a/bot/__init__.py b/bot/__init__.py
index 4729e50c..a9a0865e 100644
--- a/bot/__init__.py
+++ b/bot/__init__.py
@@ -1,3 +1,4 @@
+import asyncio
import logging
import logging.handlers
import os
@@ -36,7 +37,7 @@ os.makedirs(log_dir, exist_ok=True)
# File handler rotates logs every 5 MB
file_handler = logging.handlers.RotatingFileHandler(
- log_file, maxBytes=5*(2**20), backupCount=10)
+ log_file, maxBytes=5 * (2**20), backupCount=10)
file_handler.setLevel(logging.TRACE if Client.debug else logging.DEBUG)
# Console handler prints to terminal
@@ -63,3 +64,8 @@ logging.basicConfig(
handlers=[console_handler, file_handler]
)
logging.getLogger().info('Logging initialization complete')
+
+
+# On Windows, the selector event loop is required for aiodns.
+if os.name == "nt":
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
diff --git a/bot/bot.py b/bot/bot.py
index 4f238df8..ffaf4284 100644
--- a/bot/bot.py
+++ b/bot/bot.py
@@ -10,7 +10,7 @@ from aiohttp import AsyncResolver, ClientSession, TCPConnector
from discord import DiscordException, Embed, Guild, User
from discord.ext import commands
-from bot.constants import Channels, Client
+from bot.constants import Channels, Client, MODERATION_ROLES
from bot.utils.decorators import mock_in_debug
log = logging.getLogger(__name__)
@@ -44,6 +44,8 @@ class SeasonalBot(commands.Bot):
self.http_session = ClientSession(
connector=TCPConnector(resolver=AsyncResolver(), family=socket.AF_INET)
)
+ self._guild_available = asyncio.Event()
+
self.loop.create_task(self.send_log("SeasonalBot", "Connected!"))
@property
@@ -101,7 +103,7 @@ class SeasonalBot(commands.Bot):
return False
else:
- log.info(f"Asset successfully applied")
+ log.info("Asset successfully applied")
return True
@mock_in_debug(return_value=True)
@@ -149,7 +151,7 @@ class SeasonalBot(commands.Bot):
async def send_log(self, title: str, details: str = None, *, icon: str = None) -> None:
"""Send an embed message to the devlog channel."""
- await self.wait_until_ready()
+ await self.wait_until_guild_available()
devlog = self.get_channel(Channels.devlog)
if not devlog:
@@ -168,5 +170,42 @@ class SeasonalBot(commands.Bot):
await devlog.send(embed=embed)
+ async def on_guild_available(self, guild: discord.Guild) -> None:
+ """
+ Set the internal `_guild_available` event when PyDis guild becomes available.
+
+ If the cache appears to still be empty (no members, no channels, or no roles), the event
+ will not be set.
+ """
+ if guild.id != Client.guild:
+ return
+
+ if not guild.roles or not guild.members or not guild.channels:
+ log.warning("Guild available event was dispatched but the cache appears to still be empty!")
+ return
+
+ self._guild_available.set()
+
+ async def on_guild_unavailable(self, guild: discord.Guild) -> None:
+ """Clear the internal `_guild_available` event when PyDis guild becomes unavailable."""
+ if guild.id != Client.guild:
+ return
+
+ self._guild_available.clear()
+
+ async def wait_until_guild_available(self) -> None:
+ """
+ Wait until the PyDis guild becomes available (and the cache is ready).
+
+ The on_ready event is inadequate because it only waits 2 seconds for a GUILD_CREATE
+ gateway event before giving up and thus not populating the cache for unavailable guilds.
+ """
+ await self._guild_available.wait()
+
-bot = SeasonalBot(command_prefix=Client.prefix)
+_allowed_roles = [discord.Object(id_) for id_ in MODERATION_ROLES]
+bot = SeasonalBot(
+ command_prefix=Client.prefix,
+ activity=discord.Game(name=f"Commands: {Client.prefix}help"),
+ allowed_mentions=discord.AllowedMentions(everyone=False, roles=_allowed_roles),
+)
diff --git a/bot/constants.py b/bot/constants.py
index a8dd03e6..5d4d303f 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -17,6 +17,7 @@ __all__ = (
"Month",
"Roles",
"Tokens",
+ "Wolfram",
"MODERATION_ROLES",
"STAFF_ROLES",
"WHITELISTED_CHANNELS",
@@ -50,6 +51,8 @@ class Channels(NamedTuple):
checkpoint_test = 422077681434099723
devalerts = 460181980097675264
devlog = int(environ.get("CHANNEL_DEVLOG", 622895325144940554))
+ dev_contrib = 635950537262759947
+ dev_branding = 753252897059373066
help_0 = 303906576991780866
help_1 = 303906556754395136
help_2 = 303906514266226689
@@ -91,10 +94,11 @@ class Colours:
dark_green = 0x1f8b4c
orange = 0xe67e22
pink = 0xcf84e0
+ purple = 0xb734eb
soft_green = 0x68c290
+ soft_orange = 0xf9cb54
soft_red = 0xcd6d6d
yellow = 0xf9f586
- purple = 0xb734eb
class Emojis:
@@ -105,12 +109,12 @@ class Emojis:
trashcan = "<:trashcan:637136429717389331>"
ok_hand = ":ok_hand:"
- terning1 = "<:terning1:431249668983488527>"
- terning2 = "<:terning2:462339216987127808>"
- terning3 = "<:terning3:431249694467948544>"
- terning4 = "<:terning4:579980271475228682>"
- terning5 = "<:terning5:431249716328792064>"
- terning6 = "<:terning6:431249726705369098>"
+ dice_1 = "<:dice_1:755891608859443290>"
+ dice_2 = "<:dice_2:755891608741740635>"
+ dice_3 = "<:dice_3:755891608251138158>"
+ dice_4 = "<:dice_4:755891607882039327>"
+ dice_5 = "<:dice_5:755891608091885627>"
+ dice_6 = "<:dice_6:755891607680843838>"
issue = "<:IssueOpen:629695470327037963>"
issue_closed = "<:IssueClosed:629695470570307614>"
@@ -203,6 +207,12 @@ class Tokens(NamedTuple):
github = environ.get("GITHUB_TOKEN")
+class Wolfram(NamedTuple):
+ user_limit_day = int(environ.get("WOLFRAM_USER_LIMIT_DAY", 10))
+ guild_limit_day = int(environ.get("WOLFRAM_GUILD_LIMIT_DAY", 67))
+ key = environ.get("WOLFRAM_API_KEY")
+
+
# Default role combinations
MODERATION_ROLES = Roles.moderator, Roles.admin, Roles.owner
STAFF_ROLES = Roles.helpers, Roles.moderator, Roles.admin, Roles.owner
@@ -269,3 +279,6 @@ POSITIVE_REPLIES = [
"Aye aye, cap'n!",
"I'll allow it.",
]
+
+class Wikipedia:
+ total_chance = 3
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)
diff --git a/bot/resources/easter/starter.json b/bot/resources/easter/starter.json
deleted file mode 100644
index 31e2cbc9..00000000
--- a/bot/resources/easter/starter.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "starters": [
- "What is your favourite Easter candy or treat?",
- "What is your earliest memory of Easter?",
- "What is the title of the last book you read?",
- "What is better: Milk, Dark or White chocolate?",
- "What is your favourite holiday?",
- "If you could have any superpower, what would it be?",
- "Name one thing you like about a person to your right.",
- "If you could be anyone else for one day, who would it be?",
- "What Easter tradition do you enjoy most?",
- "What is the best gift you've been given?",
- "Name one famous person you would like to have at your easter dinner.",
- "What was the last movie you saw in a cinema?",
- "What is your favourite food?",
- "If you could travel anywhere in the world, where would you go?",
- "Tell us 5 things you do well.",
- "What is your favourite place that you have visited?",
- "What is your favourite color?",
- "If you had $100 bill in your Easter Basket, what would you do with it?",
- "What would you do if you know you could succeed at anything you chose to do?",
- "If you could take only three things from your house, what would they be?"
- ]
-}
diff --git a/bot/resources/evergreen/caesar_info.json b/bot/resources/evergreen/caesar_info.json
new file mode 100644
index 00000000..8229c4f3
--- /dev/null
+++ b/bot/resources/evergreen/caesar_info.json
@@ -0,0 +1,4 @@
+{
+ "title": "Caesar Cipher",
+ "description": "**Information**\nThe Caesar Cipher, named after the Roman General Julius Caesar, is one of the simplest and most widely known encryption techniques. It is a type of substitution cipher in which each letter in the plaintext is replaced by a letter given a specific position offset in the alphabet, with the letters wrapping around both sides.\n\n**Examples**\n1) `Hello World` <=> `Khoor Zruog` where letters are shifted forwards by `3`.\n2) `Julius Caesar` <=> `Yjaxjh Rpthpg` where letters are shifted backwards by `11`."
+}
diff --git a/bot/resources/evergreen/py_topics.yaml b/bot/resources/evergreen/py_topics.yaml
new file mode 100644
index 00000000..1e53429a
--- /dev/null
+++ b/bot/resources/evergreen/py_topics.yaml
@@ -0,0 +1,89 @@
+# Conversation starters for Python-related channels.
+
+# python-general
+267624335836053506:
+ - What's your favorite PEP?
+ - What's your current text editor/IDE, and what functionality do you like about it the most when programming in Python?
+ - What functionality is your text editor/IDE missing for programming Python?
+ - What parts of your life has Python automated, if any?
+ - Which Python project are you the most proud of making?
+ - What made you want to learn Python?
+ - When did you start learning Python?
+ - What reasons are you learning Python for?
+ - Where's the strangest place you've seen Python?
+ - How has learning Python changed your life?
+ - Is there a package you wish existed but doesn't? What is it?
+ - What feature do you think should be added to Python?
+ - Has Python helped you in school? If so, how?
+ - What was the first thing you created with Python?
+
+# async
+630504881542791169:
+ - Are there any frameworks you wish were async?
+ - How have coroutines changed the way you write Python?
+
+# c-extensions
+728390945384431688:
+ -
+
+# computer-science
+650401909852864553:
+ -
+
+# databases
+342318764227821568:
+ - Where do you get your best data?
+
+# data-science
+366673247892275221:
+ -
+
+# discord.py
+343944376055103488:
+ - What unique features does your bot contain, if any?
+ - What commands/features are you proud of making?
+ - What feature would you be the most interested in making?
+ - What feature would you like to see added to the library? what feature in the library do you think is redundant?
+ - Do you think there's a way in which Discord could handle bots better?
+
+# esoteric-python
+470884583684964352:
+ - What's a common part of programming we can make harder?
+ - What are the pros and cons of messing with __magic__()?
+
+# game-development
+660625198390837248:
+ -
+
+# microcontrollers
+545603026732318730:
+ -
+
+# networking
+716325106619777044:
+ - If you could wish for a library involving networking, what would it be?
+
+# security
+366674035876167691:
+ - If you could wish for a library involving net-sec, what would it be?
+
+# software-testing
+463035728335732738:
+ -
+
+# tools-and-devops
+463035462760792066:
+ - What editor would you recommend to a beginner? Why?
+ - What editor would you recommend to be the most efficient? Why?
+
+# unix
+491523972836360192:
+ -
+
+# user-interfaces
+338993628049571840:
+ - What's the most impressive Desktop Application you've made with Python so far?
+
+# web-development
+366673702533988363:
+ - How has Python helped you in web development?
diff --git a/bot/resources/evergreen/starter.yaml b/bot/resources/evergreen/starter.yaml
new file mode 100644
index 00000000..53c89364
--- /dev/null
+++ b/bot/resources/evergreen/starter.yaml
@@ -0,0 +1,22 @@
+# Conversation starters for channels that are not Python-related.
+
+- What is your favourite Easter candy or treat?
+- What is your earliest memory of Easter?
+- What is the title of the last book you read?
+- "What is better: Milk, Dark or White chocolate?"
+- What is your favourite holiday?
+- If you could have any superpower, what would it be?
+- Name one thing you like about a person to your right.
+- If you could be anyone else for one day, who would it be?
+- What Easter tradition do you enjoy most?
+- What is the best gift you've been given?
+- Name one famous person you would like to have at your easter dinner.
+- What was the last movie you saw in a cinema?
+- What is your favourite food?
+- If you could travel anywhere in the world, where would you go?
+- Tell us 5 things you do well.
+- What is your favourite place that you have visited?
+- What is your favourite color?
+- If you had $100 bill in your Easter Basket, what would you do with it?
+- What would you do if you know you could succeed at anything you chose to do?
+- If you could take only three things from your house, what would they be?
diff --git a/bot/resources/evergreen/trivia_quiz.json b/bot/resources/evergreen/trivia_quiz.json
index 6100ca62..8f0a4114 100644
--- a/bot/resources/evergreen/trivia_quiz.json
+++ b/bot/resources/evergreen/trivia_quiz.json
@@ -217,6 +217,36 @@
"question": "What does the acronym GPRS stand for?",
"answer": "General Packet Radio Service",
"info": "General Packet Radio Service (GPRS) is a packet-based mobile data service on the global system for mobile communications (GSM) of 3G and 2G cellular communication systems. It is a non-voice, high-speed and useful packet-switching technology intended for GSM networks."
+ },
+ {
+ "id": 131,
+ "question": "In what country is the Ebro river located?",
+ "answer": "Spain",
+ "info": "The Ebro river is located in Spain. It is 930 kilometers long and it's the second longest river that ends on the Mediterranean Sea."
+ },
+ {
+ "id": 132,
+ "question": "What year was the IBM PC model 5150 introduced into the market?",
+ "answer": "1981",
+ "info": "The IBM PC was introduced into the market in 1981. It used the Intel 8088, with a clock speed of 4.77 MHz, along with the MDA and CGA as a video card."
+ },
+ {
+ "id": 133,
+ "question": "What's the world's largest urban area?",
+ "answer": "Tokyo",
+ "info": "Tokyo is the most populated city in the world, with a population of 37 million people. It is located in Japan."
+ },
+ {
+ "id": 134,
+ "question": "How many planets are there in the Solar system?",
+ "answer": "8",
+ "info": "In the Solar system, there are 8 planets: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus and Neptune. Pluto isn't considered a planet in the Solar System anymore."
+ },
+ {
+ "id": 135,
+ "question": "What is the capital of Iraq?",
+ "answer": "Baghdad",
+ "info": "Baghdad is the capital of Iraq. It has a population of 7 million people."
}
]
}
diff --git a/bot/utils/decorators.py b/bot/utils/decorators.py
index 519e61a9..9e6ef73d 100644
--- a/bot/utils/decorators.py
+++ b/bot/utils/decorators.py
@@ -285,7 +285,7 @@ def locked() -> t.Union[t.Callable, None]:
embed = Embed()
embed.colour = Colour.red()
- log.debug(f"User tried to invoke a locked command.")
+ log.debug("User tried to invoke a locked command.")
embed.description = (
"You're already using this command. Please wait until "
"it is done before you use it again."
diff --git a/bot/utils/pagination.py b/bot/utils/pagination.py
index 9a7a0382..a4d0cc56 100644
--- a/bot/utils/pagination.py
+++ b/bot/utils/pagination.py
@@ -128,7 +128,7 @@ class LinePaginator(Paginator):
if not lines:
if exception_on_empty_embed:
- log.exception(f"Pagination asked for empty lines iterable")
+ log.exception("Pagination asked for empty lines iterable")
raise EmptyPaginatorEmbed("No lines to paginate")
log.debug("No lines to add to paginator, adding '(nothing to display)' message")
@@ -335,7 +335,7 @@ class ImagePaginator(Paginator):
if not pages:
if exception_on_empty_embed:
- log.exception(f"Pagination asked for empty image list")
+ log.exception("Pagination asked for empty image list")
raise EmptyPaginatorEmbed("No images to paginate")
log.debug("No images to add to paginator, adding '(no images to display)' message")
diff --git a/bot/utils/persist.py b/bot/utils/persist.py
index d78e5420..1e178569 100644
--- a/bot/utils/persist.py
+++ b/bot/utils/persist.py
@@ -25,13 +25,16 @@ def make_persistent(file_path: Path) -> Path:
as otherwise only one datafile can be persistent and will be returned for
both cases.
+ Ensure that all open files are using explicit appropriate encoding to avoid
+ encoding errors from diffent OS systems.
+
Example Usage:
>>> import json
>>> template_datafile = Path("bot", "resources", "evergreen", "myfile.json")
>>> path_to_persistent_file = make_persistent(template_datafile)
>>> print(path_to_persistent_file)
data/evergreen/myfile.json
- >>> with path_to_persistent_file.open("w+") as f:
+ >>> with path_to_persistent_file.open("w+", encoding="utf8") as f:
>>> data = json.load(f)
"""
# ensure the persistent data directory exists
diff --git a/bot/utils/randomization.py b/bot/utils/randomization.py
new file mode 100644
index 00000000..8f47679a
--- /dev/null
+++ b/bot/utils/randomization.py
@@ -0,0 +1,23 @@
+import itertools
+import random
+import typing as t
+
+
+class RandomCycle:
+ """
+ Cycles through elements from a randomly shuffled iterable, repeating indefinitely.
+
+ The iterable is reshuffled after each full cycle.
+ """
+
+ def __init__(self, iterable: t.Iterable) -> None:
+ self.iterable = list(iterable)
+ self.index = itertools.cycle(range(len(iterable)))
+
+ def __next__(self) -> t.Any:
+ idx = next(self.index)
+
+ if idx == 0:
+ random.shuffle(self.iterable)
+
+ return self.iterable[idx]