aboutsummaryrefslogtreecommitdiffstats
path: root/bot/exts/holidays
diff options
context:
space:
mode:
authorGravatar Janine vN <[email protected]>2021-09-04 23:22:12 -0400
committerGravatar Janine vN <[email protected]>2021-09-04 23:22:12 -0400
commit2780043e6ddd5dbee82b62d85289f0518613ce7b (patch)
treeaae3347de61e9d4d53bd5cce301762bad66fe87a /bot/exts/holidays
parentMove AoC and Hacktoberfest into events folder (diff)
Move Easter to Holidays Folder
This moves the easter seasonal features into a more cohesive holidays/easter folder. Additionally, this splits out earth day into its own holiday folder.
Diffstat (limited to 'bot/exts/holidays')
-rw-r--r--bot/exts/holidays/__init__.py0
-rw-r--r--bot/exts/holidays/earth_day/__init__.py0
-rw-r--r--bot/exts/holidays/earth_day/save_the_planet.py25
-rw-r--r--bot/exts/holidays/easter/__init__.py0
-rw-r--r--bot/exts/holidays/easter/april_fools_vids.py30
-rw-r--r--bot/exts/holidays/easter/bunny_name_generator.py94
-rw-r--r--bot/exts/holidays/easter/earth_photos.py66
-rw-r--r--bot/exts/holidays/easter/easter_riddle.py112
-rw-r--r--bot/exts/holidays/easter/egg_decorating.py119
-rw-r--r--bot/exts/holidays/easter/egg_facts.py55
-rw-r--r--bot/exts/holidays/easter/egghead_quiz.py118
-rw-r--r--bot/exts/holidays/easter/traditions.py28
12 files changed, 647 insertions, 0 deletions
diff --git a/bot/exts/holidays/__init__.py b/bot/exts/holidays/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/bot/exts/holidays/__init__.py
diff --git a/bot/exts/holidays/earth_day/__init__.py b/bot/exts/holidays/earth_day/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/bot/exts/holidays/earth_day/__init__.py
diff --git a/bot/exts/holidays/earth_day/save_the_planet.py b/bot/exts/holidays/earth_day/save_the_planet.py
new file mode 100644
index 00000000..13c84886
--- /dev/null
+++ b/bot/exts/holidays/earth_day/save_the_planet.py
@@ -0,0 +1,25 @@
+import json
+from pathlib import Path
+
+from discord import Embed
+from discord.ext import commands
+
+from bot.bot import Bot
+from bot.utils.randomization import RandomCycle
+
+EMBED_DATA = RandomCycle(json.loads(Path("bot/resources/holidays/earth_day/save_the_planet.json").read_text("utf8")))
+
+
+class SaveThePlanet(commands.Cog):
+ """A cog that teaches users how they can help our planet."""
+
+ @commands.command(aliases=("savetheearth", "saveplanet", "saveearth"))
+ async def savetheplanet(self, ctx: commands.Context) -> None:
+ """Responds with a random tip on how to be eco-friendly and help our planet."""
+ return_embed = Embed.from_dict(next(EMBED_DATA))
+ await ctx.send(embed=return_embed)
+
+
+def setup(bot: Bot) -> None:
+ """Load the Save the Planet Cog."""
+ bot.add_cog(SaveThePlanet())
diff --git a/bot/exts/holidays/easter/__init__.py b/bot/exts/holidays/easter/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/bot/exts/holidays/easter/__init__.py
diff --git a/bot/exts/holidays/easter/april_fools_vids.py b/bot/exts/holidays/easter/april_fools_vids.py
new file mode 100644
index 00000000..ae22f751
--- /dev/null
+++ b/bot/exts/holidays/easter/april_fools_vids.py
@@ -0,0 +1,30 @@
+import logging
+import random
+from json import loads
+from pathlib import Path
+
+from discord.ext import commands
+
+from bot.bot import Bot
+
+log = logging.getLogger(__name__)
+
+ALL_VIDS = loads(Path("bot/resources/holidays/easter/april_fools_vids.json").read_text("utf-8"))
+
+
+class AprilFoolVideos(commands.Cog):
+ """A cog for April Fools' that gets a random April Fools' video from Youtube."""
+
+ @commands.command(name="fool")
+ async def april_fools(self, ctx: commands.Context) -> None:
+ """Get a random April Fools' video from Youtube."""
+ video = random.choice(ALL_VIDS)
+
+ channel, url = video["channel"], video["url"]
+
+ await ctx.send(f"Check out this April Fools' video by {channel}.\n\n{url}")
+
+
+def setup(bot: Bot) -> None:
+ """Load the April Fools' Cog."""
+ bot.add_cog(AprilFoolVideos())
diff --git a/bot/exts/holidays/easter/bunny_name_generator.py b/bot/exts/holidays/easter/bunny_name_generator.py
new file mode 100644
index 00000000..f767f7c5
--- /dev/null
+++ b/bot/exts/holidays/easter/bunny_name_generator.py
@@ -0,0 +1,94 @@
+import json
+import logging
+import random
+import re
+from pathlib import Path
+from typing import Optional
+
+from discord.ext import commands
+
+from bot.bot import Bot
+
+log = logging.getLogger(__name__)
+
+BUNNY_NAMES = json.loads(Path("bot/resources/holidays/easter/bunny_names.json").read_text("utf8"))
+
+
+class BunnyNameGenerator(commands.Cog):
+ """Generate a random bunny name, or bunnify your Discord username!"""
+
+ @staticmethod
+ def find_separators(displayname: str) -> Optional[list[str]]:
+ """Check if Discord name contains spaces so we can bunnify an individual word in the name."""
+ new_name = re.split(r"[_.\s]", displayname)
+ if displayname not in new_name:
+ return new_name
+ return None
+
+ @staticmethod
+ def find_vowels(displayname: str) -> Optional[str]:
+ """
+ Finds vowels in the user's display name.
+
+ If the Discord name contains a vowel and the letter y, it will match one or more of these patterns.
+
+ Only the most recently matched pattern will apply the changes.
+ """
+ expressions = [
+ ("a.+y", "patchy"),
+ ("e.+y", "ears"),
+ ("i.+y", "ditsy"),
+ ("o.+y", "oofy"),
+ ("u.+y", "uffy"),
+ ]
+
+ for exp, vowel_sub in expressions:
+ new_name = re.sub(exp, vowel_sub, displayname)
+ if new_name != displayname:
+ return new_name
+
+ @staticmethod
+ def append_name(displayname: str) -> str:
+ """Adds a suffix to the end of the Discord name."""
+ extensions = ["foot", "ear", "nose", "tail"]
+ suffix = random.choice(extensions)
+ appended_name = displayname + suffix
+
+ return appended_name
+
+ @commands.command()
+ async def bunnyname(self, ctx: commands.Context) -> None:
+ """Picks a random bunny name from a JSON file."""
+ await ctx.send(random.choice(BUNNY_NAMES["names"]))
+
+ @commands.command()
+ async def bunnifyme(self, ctx: commands.Context) -> None:
+ """Gets your Discord username and bunnifies it."""
+ username = ctx.author.display_name
+
+ # If name contains spaces or other separators, get the individual words to randomly bunnify
+ spaces_in_name = self.find_separators(username)
+
+ # If name contains vowels, see if it matches any of the patterns in this function
+ # If there are matches, the bunnified name is returned.
+ vowels_in_name = self.find_vowels(username)
+
+ # Default if the checks above return None
+ unmatched_name = self.append_name(username)
+
+ if spaces_in_name is not None:
+ replacements = ["Cotton", "Fluff", "Floof" "Bounce", "Snuffle", "Nibble", "Cuddle", "Velvetpaw", "Carrot"]
+ word_to_replace = random.choice(spaces_in_name)
+ substitute = random.choice(replacements)
+ bunnified_name = username.replace(word_to_replace, substitute)
+ elif vowels_in_name is not None:
+ bunnified_name = vowels_in_name
+ elif unmatched_name:
+ bunnified_name = unmatched_name
+
+ await ctx.send(bunnified_name)
+
+
+def setup(bot: Bot) -> None:
+ """Load the Bunny Name Generator Cog."""
+ bot.add_cog(BunnyNameGenerator())
diff --git a/bot/exts/holidays/easter/earth_photos.py b/bot/exts/holidays/easter/earth_photos.py
new file mode 100644
index 00000000..f65790af
--- /dev/null
+++ b/bot/exts/holidays/easter/earth_photos.py
@@ -0,0 +1,66 @@
+import logging
+
+import discord
+from discord.ext import commands
+
+from bot.bot import Bot
+from bot.constants import Colours
+from bot.constants import Tokens
+
+log = logging.getLogger(__name__)
+
+API_URL = "https://api.unsplash.com/photos/random"
+
+
+class EarthPhotos(commands.Cog):
+ """This cog contains the command for earth photos."""
+
+ def __init__(self, bot: Bot):
+ self.bot = bot
+
+ @commands.command(aliases=("earth",))
+ async def earth_photos(self, ctx: commands.Context) -> None:
+ """Returns a random photo of earth, sourced from Unsplash."""
+ async with ctx.typing():
+ async with self.bot.http_session.get(
+ API_URL,
+ params={"query": "planet_earth", "client_id": Tokens.unsplash_access_key}
+ ) as r:
+ jsondata = await r.json()
+ linksdata = jsondata.get("urls")
+ embedlink = linksdata.get("regular")
+ downloadlinksdata = jsondata.get("links")
+ userdata = jsondata.get("user")
+ username = userdata.get("name")
+ userlinks = userdata.get("links")
+ profile = userlinks.get("html")
+ # Referral flags
+ rf = "?utm_source=Sir%20Lancebot&utm_medium=referral"
+ async with self.bot.http_session.get(
+ downloadlinksdata.get("download_location"),
+ params={"client_id": Tokens.unsplash_access_key}
+ ) as _:
+ pass
+
+ embed = discord.Embed(
+ title="Earth Photo",
+ description="A photo of Earth 🌎 from Unsplash.",
+ color=Colours.grass_green
+ )
+ embed.set_image(url=embedlink)
+ embed.add_field(
+ name="Author",
+ value=(
+ f"Photo by [{username}]({profile}{rf}) "
+ f"on [Unsplash](https://unsplash.com{rf})."
+ )
+ )
+ await ctx.send(embed=embed)
+
+
+def setup(bot: Bot) -> None:
+ """Load the Earth Photos cog."""
+ if not Tokens.unsplash_access_key:
+ log.warning("No Unsplash access key found. Cog not loading.")
+ return
+ bot.add_cog(EarthPhotos(bot))
diff --git a/bot/exts/holidays/easter/easter_riddle.py b/bot/exts/holidays/easter/easter_riddle.py
new file mode 100644
index 00000000..c9b7fc53
--- /dev/null
+++ b/bot/exts/holidays/easter/easter_riddle.py
@@ -0,0 +1,112 @@
+import asyncio
+import logging
+import random
+from json import loads
+from pathlib import Path
+
+import discord
+from discord.ext import commands
+
+from bot.bot import Bot
+from bot.constants import Colours, NEGATIVE_REPLIES
+
+log = logging.getLogger(__name__)
+
+RIDDLE_QUESTIONS = loads(Path("bot/resources/holidays/easter/easter_riddle.json").read_text("utf8"))
+
+TIMELIMIT = 10
+
+
+class EasterRiddle(commands.Cog):
+ """This cog contains the command for the Easter quiz!"""
+
+ def __init__(self, bot: Bot):
+ self.bot = bot
+ self.winners = set()
+ self.correct = ""
+ self.current_channel = None
+
+ @commands.command(aliases=("riddlemethis", "riddleme"))
+ async def riddle(self, ctx: commands.Context) -> None:
+ """
+ Gives a random riddle, then provides 2 hints at certain intervals before revealing the answer.
+
+ The duration of the hint interval can be configured by changing the TIMELIMIT constant in this file.
+ """
+ if self.current_channel:
+ await ctx.send(f"A riddle is already being solved in {self.current_channel.mention}!")
+ return
+
+ # Don't let users start in a DM
+ if not ctx.guild:
+ await ctx.send(
+ embed=discord.Embed(
+ title=random.choice(NEGATIVE_REPLIES),
+ description="You can't start riddles in DMs",
+ colour=discord.Colour.red()
+ )
+ )
+ return
+
+ self.current_channel = ctx.channel
+
+ random_question = random.choice(RIDDLE_QUESTIONS)
+ question = random_question["question"]
+ hints = random_question["riddles"]
+ self.correct = random_question["correct_answer"]
+
+ description = f"You have {TIMELIMIT} seconds before the first hint."
+
+ riddle_embed = discord.Embed(title=question, description=description, colour=Colours.pink)
+
+ await ctx.send(embed=riddle_embed)
+ await asyncio.sleep(TIMELIMIT)
+
+ hint_embed = discord.Embed(
+ title=f"Here's a hint: {hints[0]}!",
+ colour=Colours.pink
+ )
+
+ await ctx.send(embed=hint_embed)
+ await asyncio.sleep(TIMELIMIT)
+
+ hint_embed = discord.Embed(
+ title=f"Here's a hint: {hints[1]}!",
+ colour=Colours.pink
+ )
+
+ await ctx.send(embed=hint_embed)
+ await asyncio.sleep(TIMELIMIT)
+
+ if self.winners:
+ win_list = " ".join(self.winners)
+ content = f"Well done {win_list} for getting it right!"
+ else:
+ content = "Nobody got it right..."
+
+ answer_embed = discord.Embed(
+ title=f"The answer is: {self.correct}!",
+ colour=Colours.pink
+ )
+
+ await ctx.send(content, embed=answer_embed)
+
+ self.winners.clear()
+ self.current_channel = None
+
+ @commands.Cog.listener()
+ async def on_message(self, message: discord.Message) -> None:
+ """If a non-bot user enters a correct answer, their username gets added to self.winners."""
+ if self.current_channel != message.channel:
+ return
+
+ if self.bot.user == message.author:
+ return
+
+ if message.content.lower() == self.correct.lower():
+ self.winners.add(message.author.mention)
+
+
+def setup(bot: Bot) -> None:
+ """Easter Riddle Cog load."""
+ bot.add_cog(EasterRiddle(bot))
diff --git a/bot/exts/holidays/easter/egg_decorating.py b/bot/exts/holidays/easter/egg_decorating.py
new file mode 100644
index 00000000..1db9b347
--- /dev/null
+++ b/bot/exts/holidays/easter/egg_decorating.py
@@ -0,0 +1,119 @@
+import json
+import logging
+import random
+from contextlib import suppress
+from io import BytesIO
+from pathlib import Path
+from typing import Optional, Union
+
+import discord
+from PIL import Image
+from discord.ext import commands
+
+from bot.bot import Bot
+from bot.utils import helpers
+
+log = logging.getLogger(__name__)
+
+HTML_COLOURS = json.loads(Path("bot/resources/fun/html_colours.json").read_text("utf8"))
+
+XKCD_COLOURS = json.loads(Path("bot/resources/fun/xkcd_colours.json").read_text("utf8"))
+
+COLOURS = [
+ (255, 0, 0, 255), (255, 128, 0, 255), (255, 255, 0, 255), (0, 255, 0, 255),
+ (0, 255, 255, 255), (0, 0, 255, 255), (255, 0, 255, 255), (128, 0, 128, 255)
+] # Colours to be replaced - Red, Orange, Yellow, Green, Light Blue, Dark Blue, Pink, Purple
+
+IRREPLACEABLE = [
+ (0, 0, 0, 0), (0, 0, 0, 255)
+] # Colours that are meant to stay the same - Transparent and Black
+
+
+class EggDecorating(commands.Cog):
+ """Decorate some easter eggs!"""
+
+ @staticmethod
+ def replace_invalid(colour: str) -> Optional[int]:
+ """Attempts to match with HTML or XKCD colour names, returning the int value."""
+ with suppress(KeyError):
+ return int(HTML_COLOURS[colour], 16)
+ with suppress(KeyError):
+ return int(XKCD_COLOURS[colour], 16)
+ return None
+
+ @commands.command(aliases=("decorateegg",))
+ async def eggdecorate(
+ self, ctx: commands.Context, *colours: Union[discord.Colour, str]
+ ) -> Optional[Image.Image]:
+ """
+ Picks a random egg design and decorates it using the given colours.
+
+ Colours are split by spaces, unless you wrap the colour name in double quotes.
+ Discord colour names, HTML colour names, XKCD colour names and hex values are accepted.
+ """
+ if len(colours) < 2:
+ await ctx.send("You must include at least 2 colours!")
+ return
+
+ invalid = []
+ colours = list(colours)
+ for idx, colour in enumerate(colours):
+ if isinstance(colour, discord.Colour):
+ continue
+ value = self.replace_invalid(colour)
+ if value:
+ colours[idx] = discord.Colour(value)
+ else:
+ invalid.append(helpers.suppress_links(colour))
+
+ if len(invalid) > 1:
+ await ctx.send(f"Sorry, I don't know these colours: {' '.join(invalid)}")
+ return
+ elif len(invalid) == 1:
+ await ctx.send(f"Sorry, I don't know the colour {invalid[0]}!")
+ return
+
+ async with ctx.typing():
+ # Expand list to 8 colours
+ colours_n = len(colours)
+ if colours_n < 8:
+ q, r = divmod(8, colours_n)
+ colours = colours * q + colours[:r]
+ num = random.randint(1, 6)
+ im = Image.open(Path(f"bot/resources/holidays/easter/easter_eggs/design{num}.png"))
+ data = list(im.getdata())
+
+ replaceable = {x for x in data if x not in IRREPLACEABLE}
+ replaceable = sorted(replaceable, key=COLOURS.index)
+
+ replacing_colours = {colour: colours[i] for i, colour in enumerate(replaceable)}
+ new_data = []
+ for x in data:
+ if x in replacing_colours:
+ new_data.append((*replacing_colours[x].to_rgb(), 255))
+ # Also ensures that the alpha channel has a value
+ else:
+ new_data.append(x)
+ new_im = Image.new(im.mode, im.size)
+ new_im.putdata(new_data)
+
+ bufferedio = BytesIO()
+ new_im.save(bufferedio, format="PNG")
+
+ bufferedio.seek(0)
+
+ file = discord.File(bufferedio, filename="egg.png") # Creates file to be used in embed
+ embed = discord.Embed(
+ title="Your Colourful Easter Egg",
+ description="Here is your pretty little egg. Hope you like it!"
+ )
+ embed.set_image(url="attachment://egg.png")
+ embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.display_avatar.url)
+
+ await ctx.send(file=file, embed=embed)
+ return new_im
+
+
+def setup(bot: Bot) -> None:
+ """Load the Egg decorating Cog."""
+ bot.add_cog(EggDecorating())
diff --git a/bot/exts/holidays/easter/egg_facts.py b/bot/exts/holidays/easter/egg_facts.py
new file mode 100644
index 00000000..5f216e0d
--- /dev/null
+++ b/bot/exts/holidays/easter/egg_facts.py
@@ -0,0 +1,55 @@
+import logging
+import random
+from json import loads
+from pathlib import Path
+
+import discord
+from discord.ext import commands
+
+from bot.bot import Bot
+from bot.constants import Channels, Colours, Month
+from bot.utils.decorators import seasonal_task
+
+log = logging.getLogger(__name__)
+
+EGG_FACTS = loads(Path("bot/resources/holidays/easter/easter_egg_facts.json").read_text("utf8"))
+
+
+class EasterFacts(commands.Cog):
+ """
+ A cog contains a command that will return an easter egg fact when called.
+
+ It also contains a background task which sends an easter egg fact in the event channel everyday.
+ """
+
+ def __init__(self, bot: Bot):
+ self.bot = bot
+ self.daily_fact_task = self.bot.loop.create_task(self.send_egg_fact_daily())
+
+ @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_guild_available()
+
+ channel = self.bot.get_channel(Channels.community_bot_commands)
+ await channel.send(embed=self.make_embed())
+
+ @commands.command(name="eggfact", aliases=("fact",))
+ async def easter_facts(self, ctx: commands.Context) -> None:
+ """Get easter egg facts."""
+ embed = self.make_embed()
+ await ctx.send(embed=embed)
+
+ @staticmethod
+ def make_embed() -> discord.Embed:
+ """Makes a nice embed for the message to be sent."""
+ return discord.Embed(
+ colour=Colours.soft_red,
+ title="Easter Egg Fact",
+ description=random.choice(EGG_FACTS)
+ )
+
+
+def setup(bot: Bot) -> None:
+ """Load the Easter Egg facts Cog."""
+ bot.add_cog(EasterFacts(bot))
diff --git a/bot/exts/holidays/easter/egghead_quiz.py b/bot/exts/holidays/easter/egghead_quiz.py
new file mode 100644
index 00000000..06229537
--- /dev/null
+++ b/bot/exts/holidays/easter/egghead_quiz.py
@@ -0,0 +1,118 @@
+import asyncio
+import logging
+import random
+from json import loads
+from pathlib import Path
+from typing import Union
+
+import discord
+from discord.ext import commands
+
+from bot.bot import Bot
+from bot.constants import Colours
+
+log = logging.getLogger(__name__)
+
+EGGHEAD_QUESTIONS = loads(Path("bot/resources/holidays/easter/egghead_questions.json").read_text("utf8"))
+
+
+EMOJIS = [
+ "\U0001f1e6", "\U0001f1e7", "\U0001f1e8", "\U0001f1e9", "\U0001f1ea",
+ "\U0001f1eb", "\U0001f1ec", "\U0001f1ed", "\U0001f1ee", "\U0001f1ef",
+ "\U0001f1f0", "\U0001f1f1", "\U0001f1f2", "\U0001f1f3", "\U0001f1f4",
+ "\U0001f1f5", "\U0001f1f6", "\U0001f1f7", "\U0001f1f8", "\U0001f1f9",
+ "\U0001f1fa", "\U0001f1fb", "\U0001f1fc", "\U0001f1fd", "\U0001f1fe",
+ "\U0001f1ff"
+] # Regional Indicators A-Z (used for voting)
+
+TIMELIMIT = 30
+
+
+class EggheadQuiz(commands.Cog):
+ """This cog contains the command for the Easter quiz!"""
+
+ def __init__(self):
+ self.quiz_messages = {}
+
+ @commands.command(aliases=("eggheadquiz", "easterquiz"))
+ async def eggquiz(self, ctx: commands.Context) -> None:
+ """
+ Gives a random quiz question, waits 30 seconds and then outputs the answer.
+
+ Also informs of the percentages and votes of each option
+ """
+ random_question = random.choice(EGGHEAD_QUESTIONS)
+ question, answers = random_question["question"], random_question["answers"]
+ answers = [(EMOJIS[i], a) for i, a in enumerate(answers)]
+ correct = EMOJIS[random_question["correct_answer"]]
+
+ valid_emojis = [emoji for emoji, _ in answers]
+
+ description = f"You have {TIMELIMIT} seconds to vote.\n\n"
+ description += "\n".join([f"{emoji} -> **{answer}**" for emoji, answer in answers])
+
+ q_embed = discord.Embed(title=question, description=description, colour=Colours.pink)
+
+ msg = await ctx.send(embed=q_embed)
+ for emoji in valid_emojis:
+ await msg.add_reaction(emoji)
+
+ self.quiz_messages[msg.id] = valid_emojis
+
+ await asyncio.sleep(TIMELIMIT)
+
+ del self.quiz_messages[msg.id]
+
+ msg = await ctx.fetch_message(msg.id) # Refreshes message
+
+ total_no = sum([len(await r.users().flatten()) for r in msg.reactions]) - len(valid_emojis) # - bot's reactions
+
+ if total_no == 0:
+ return await msg.delete() # To avoid ZeroDivisionError if nobody reacts
+
+ results = ["**VOTES:**"]
+ for emoji, _ in answers:
+ num = [len(await r.users().flatten()) for r in msg.reactions if str(r.emoji) == emoji][0] - 1
+ percent = round(100 * num / total_no)
+ s = "" if num == 1 else "s"
+ string = f"{emoji} - {num} vote{s} ({percent}%)"
+ results.append(string)
+
+ mentions = " ".join([
+ u.mention for u in [
+ await r.users().flatten() for r in msg.reactions if str(r.emoji) == correct
+ ][0] if not u.bot
+ ])
+
+ content = f"Well done {mentions} for getting it correct!" if mentions else "Nobody got it right..."
+
+ a_embed = discord.Embed(
+ title=f"The correct answer was {correct}!",
+ description="\n".join(results),
+ colour=Colours.pink
+ )
+
+ await ctx.send(content, embed=a_embed)
+
+ @staticmethod
+ async def already_reacted(message: discord.Message, user: Union[discord.Member, discord.User]) -> bool:
+ """Returns whether a given user has reacted more than once to a given message."""
+ users = [u.id for reaction in [await r.users().flatten() for r in message.reactions] for u in reaction]
+ return users.count(user.id) > 1 # Old reaction plus new reaction
+
+ @commands.Cog.listener()
+ async def on_reaction_add(self, reaction: discord.Reaction, user: Union[discord.Member, discord.User]) -> None:
+ """Listener to listen specifically for reactions of quiz messages."""
+ if user.bot:
+ return
+ if reaction.message.id not in self.quiz_messages:
+ return
+ if str(reaction.emoji) not in self.quiz_messages[reaction.message.id]:
+ return await reaction.message.remove_reaction(reaction, user)
+ if await self.already_reacted(reaction.message, user):
+ return await reaction.message.remove_reaction(reaction, user)
+
+
+def setup(bot: Bot) -> None:
+ """Load the Egghead Quiz Cog."""
+ bot.add_cog(EggheadQuiz())
diff --git a/bot/exts/holidays/easter/traditions.py b/bot/exts/holidays/easter/traditions.py
new file mode 100644
index 00000000..f54ab5c4
--- /dev/null
+++ b/bot/exts/holidays/easter/traditions.py
@@ -0,0 +1,28 @@
+import json
+import logging
+import random
+from pathlib import Path
+
+from discord.ext import commands
+
+from bot.bot import Bot
+
+log = logging.getLogger(__name__)
+
+traditions = json.loads(Path("bot/resources/holidays/easter/traditions.json").read_text("utf8"))
+
+
+class Traditions(commands.Cog):
+ """A cog which allows users to get a random easter tradition or custom from a random country."""
+
+ @commands.command(aliases=("eastercustoms",))
+ async def easter_tradition(self, ctx: commands.Context) -> None:
+ """Responds with a random tradition or custom."""
+ random_country = random.choice(list(traditions))
+
+ await ctx.send(f"{random_country}:\n{traditions[random_country]}")
+
+
+def setup(bot: Bot) -> None:
+ """Load the Traditions Cog."""
+ bot.add_cog(Traditions())