aboutsummaryrefslogtreecommitdiffstats
path: root/bot
diff options
context:
space:
mode:
Diffstat (limited to 'bot')
-rw-r--r--bot/__init__.py38
-rw-r--r--bot/bot.py6
-rw-r--r--bot/constants.py28
-rw-r--r--bot/decorators.py41
-rw-r--r--bot/pagination.py480
-rw-r--r--bot/resources/advent_of_code/about.json2
-rw-r--r--bot/resources/evergreen/magic8ball.json22
-rw-r--r--bot/resources/snakes/snake_cards/backs/card_back1.jpgbin0 -> 165788 bytes
-rw-r--r--bot/resources/snakes/snake_cards/backs/card_back2.jpgbin0 -> 140868 bytes
-rw-r--r--bot/resources/snakes/snake_cards/card_bottom.pngbin0 -> 18165 bytes
-rw-r--r--bot/resources/snakes/snake_cards/card_frame.pngbin0 -> 1460 bytes
-rw-r--r--bot/resources/snakes/snake_cards/card_top.pngbin0 -> 12581 bytes
-rw-r--r--bot/resources/snakes/snake_cards/expressway.ttfbin0 -> 156244 bytes
-rw-r--r--bot/resources/snakes/snake_facts.json233
-rw-r--r--bot/resources/snakes/snake_idioms.json275
-rw-r--r--bot/resources/snakes/snake_names.json2170
-rw-r--r--bot/resources/snakes/snake_quiz.json200
-rw-r--r--bot/resources/snakes/snakes_and_ladders/banner.jpgbin0 -> 17928 bytes
-rw-r--r--bot/resources/snakes/snakes_and_ladders/board.jpgbin0 -> 80264 bytes
-rw-r--r--bot/resources/snakes/special_snakes.json16
-rw-r--r--bot/resources/valentines/bemyvalentine_valentines.json45
-rw-r--r--bot/resources/valentines/date_ideas.json127
-rw-r--r--bot/resources/valentines/love_matches.json58
-rw-r--r--bot/resources/valentines/pickup_lines.json97
-rw-r--r--bot/resources/valentines/valenstates.json122
-rw-r--r--bot/resources/valentines/valentine_facts.json24
-rw-r--r--bot/resources/valentines/zodiac_compatibility.json262
-rw-r--r--bot/seasons/__init__.py2
-rw-r--r--bot/seasons/christmas/adventofcode.py15
-rw-r--r--bot/seasons/easter/__init__.py17
-rw-r--r--bot/seasons/evergreen/__init__.py2
-rw-r--r--bot/seasons/evergreen/error_handler.py101
-rw-r--r--bot/seasons/evergreen/fun.py38
-rw-r--r--bot/seasons/evergreen/magic_8ball.py36
-rw-r--r--bot/seasons/evergreen/snakes/__init__.py10
-rw-r--r--bot/seasons/evergreen/snakes/converter.py80
-rw-r--r--bot/seasons/evergreen/snakes/snakes_cog.py1189
-rw-r--r--bot/seasons/evergreen/snakes/utils.py700
-rw-r--r--bot/seasons/evergreen/uptime.py4
-rw-r--r--bot/seasons/halloween/candy_collection.py6
-rw-r--r--bot/seasons/halloween/hacktoberstats.py4
-rw-r--r--bot/seasons/halloween/halloween_facts.py5
-rw-r--r--bot/seasons/halloween/halloweenify.py4
-rw-r--r--bot/seasons/halloween/monstersurvey.py6
-rw-r--r--bot/seasons/halloween/scarymovie.py4
-rw-r--r--bot/seasons/halloween/spookyavatar.py6
-rw-r--r--bot/seasons/halloween/spookygif.py4
-rw-r--r--bot/seasons/halloween/spookyreact.py6
-rw-r--r--bot/seasons/halloween/spookysound.py4
-rw-r--r--bot/seasons/pride/__init__.py17
-rw-r--r--bot/seasons/season.py29
-rw-r--r--bot/seasons/valentines/be_my_valentine.py241
-rw-r--r--bot/seasons/valentines/lovecalculator.py107
-rw-r--r--bot/seasons/valentines/movie_generator.py66
-rw-r--r--bot/seasons/valentines/myvalenstate.py85
-rw-r--r--bot/seasons/valentines/pickuplines.py44
-rw-r--r--bot/seasons/valentines/savethedate.py45
-rw-r--r--bot/seasons/valentines/valentine_zodiac.py59
-rw-r--r--bot/seasons/valentines/whoisvalentine.py54
-rw-r--r--bot/utils/__init__.py79
60 files changed, 7205 insertions, 110 deletions
diff --git a/bot/__init__.py b/bot/__init__.py
index dc97df3d..54b242ee 100644
--- a/bot/__init__.py
+++ b/bot/__init__.py
@@ -1,3 +1,4 @@
+import logging
import logging.handlers
import os
from pathlib import Path
@@ -6,25 +7,44 @@ import arrow
from bot.constants import Client
-# start datetime
+
+# Configure the "TRACE" logging level (e.g. "log.trace(message)")
+logging.TRACE = 5
+logging.addLevelName(logging.TRACE, "TRACE")
+
+
+def monkeypatch_trace(self, msg, *args, **kwargs):
+ """
+ Log 'msg % args' with severity 'TRACE'.
+ To pass exception information, use the keyword argument exc_info with
+ a true value, e.g.
+ logger.trace("Houston, we have an %s", "interesting problem", exc_info=1)
+ """
+ if self.isEnabledFor(logging.TRACE):
+ self._log(logging.TRACE, msg, args, **kwargs)
+
+
+logging.Logger.trace = monkeypatch_trace
+
+# Set timestamp of when execution started (approximately)
start_time = arrow.utcnow()
-# set up logging
+# Set up file logging
log_dir = Path("bot", "log")
log_file = log_dir / "hackbot.log"
os.makedirs(log_dir, exist_ok=True)
-# file handler sets up rotating logs every 5 MB
+# File handler rotates logs every 5 MB
file_handler = logging.handlers.RotatingFileHandler(
log_file, maxBytes=5*(2**20), backupCount=10)
-file_handler.setLevel(logging.DEBUG)
+file_handler.setLevel(logging.TRACE if Client.debug else logging.DEBUG)
-# console handler prints to terminal
+# Console handler prints to terminal
console_handler = logging.StreamHandler()
-level = logging.DEBUG if Client.debug else logging.INFO
+level = logging.TRACE if Client.debug else logging.INFO
console_handler.setLevel(level)
-# remove old loggers if any
+# Remove old loggers, if any
root = logging.getLogger()
if root.handlers:
for handler in root.handlers:
@@ -34,11 +54,11 @@ if root.handlers:
logging.getLogger("discord").setLevel(logging.ERROR)
logging.getLogger("websockets").setLevel(logging.ERROR)
-# setup new logging configuration
+# Setup new logging configuration
logging.basicConfig(
format='%(asctime)s - %(name)s %(levelname)s: %(message)s',
datefmt="%D %H:%M:%S",
- level=logging.DEBUG,
+ level=logging.TRACE if Client.debug else logging.DEBUG,
handlers=[console_handler, file_handler]
)
logging.getLogger().info('Logging initialization complete')
diff --git a/bot/bot.py b/bot/bot.py
index 2df550dc..d60390c3 100644
--- a/bot/bot.py
+++ b/bot/bot.py
@@ -18,10 +18,7 @@ class SeasonalBot(Bot):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.http_session = ClientSession(
- connector=TCPConnector(
- resolver=AsyncResolver(),
- family=socket.AF_INET,
- )
+ connector=TCPConnector(resolver=AsyncResolver(), family=socket.AF_INET)
)
def load_extensions(self, exts: List[str]):
@@ -48,6 +45,7 @@ class SeasonalBot(Bot):
"""
Send an embed message to the devlog channel
"""
+
devlog = self.get_channel(constants.Channels.devlog)
if not devlog:
diff --git a/bot/constants.py b/bot/constants.py
index 63f923d4..b19d494b 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -6,7 +6,7 @@ from bot.bot import SeasonalBot
__all__ = (
"AdventOfCode", "Channels", "Client", "Colours", "Emojis", "Hacktoberfest", "Roles",
- "Tokens", "bot"
+ "Tokens", "ERROR_REPLIES", "bot"
)
log = logging.getLogger(__name__)
@@ -15,7 +15,7 @@ log = logging.getLogger(__name__)
class AdventOfCode:
leaderboard_cache_age_threshold_seconds = 3600
leaderboard_id = 363275
- leaderboard_join_code = "363275-442b6939"
+ leaderboard_join_code = str(environ.get("AOC_JOIN_CODE", None))
leaderboard_max_displayed_members = 10
year = 2018
channel_id = int(environ.get("AOC_CHANNEL_ID", 517745814039166986))
@@ -61,6 +61,7 @@ class Client(NamedTuple):
class Colours:
soft_red = 0xcd6d6d
soft_green = 0x68c290
+ bright_green = 0x01d277
dark_green = 0x1f8b4c
orange = 0xe67e22
pink = 0xcf84e0
@@ -71,6 +72,13 @@ class Emojis:
christmas_tree = "\U0001F384"
check = "\u2611"
+ terning1 = "<:terning1:431249668983488527>"
+ terning2 = "<:terning2:462339216987127808>"
+ terning3 = "<:terning3:431249694467948544>"
+ terning4 = "<:terning4:431249704769290241>"
+ terning5 = "<:terning5:431249716328792064>"
+ terning6 = "<:terning6:431249726705369098>"
+
class Lovefest:
channel_id = int(environ.get("LOVEFEST_CHANNEL_ID", 542272993192050698))
@@ -101,6 +109,22 @@ class Roles(NamedTuple):
class Tokens(NamedTuple):
giphy = environ.get("GIPHY_TOKEN")
aoc_session_cookie = environ.get("AOC_SESSION_COOKIE")
+ omdb = environ.get("OMDB_API_KEY")
+ youtube = environ.get("YOUTUBE_API_KEY")
+
+
+ERROR_REPLIES = [
+ "Please don't do that.",
+ "You have to stop.",
+ "Do you mind?",
+ "In the future, don't do that.",
+ "That was a mistake.",
+ "You blew it.",
+ "You're bad at computers.",
+ "Are you trying to kill me?",
+ "Noooooo!!",
+ "I can't believe you've done this",
+]
bot = SeasonalBot(command_prefix=Client.prefix)
diff --git a/bot/decorators.py b/bot/decorators.py
index b84b2c36..f5ffadf4 100644
--- a/bot/decorators.py
+++ b/bot/decorators.py
@@ -1,8 +1,15 @@
import logging
+import random
+from asyncio import Lock
+from functools import wraps
+from weakref import WeakValueDictionary
+from discord import Colour, Embed
from discord.ext import commands
from discord.ext.commands import Context
+from bot.constants import ERROR_REPLIES
+
log = logging.getLogger(__name__)
@@ -46,3 +53,37 @@ def in_channel(channel_id):
f"The result of the in_channel check was {check}.")
return check
return commands.check(predicate)
+
+
+def locked():
+ """
+ Allows the user to only run one instance of the decorated command at a time.
+ Subsequent calls to the command from the same author are
+ ignored until the command has completed invocation.
+
+ This decorator has to go before (below) the `command` decorator.
+ """
+
+ def wrap(func):
+ func.__locks = WeakValueDictionary()
+
+ @wraps(func)
+ async def inner(self, ctx, *args, **kwargs):
+ lock = func.__locks.setdefault(ctx.author.id, Lock())
+ if lock.locked():
+ embed = Embed()
+ embed.colour = Colour.red()
+
+ log.debug(f"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."
+ )
+ embed.title = random.choice(ERROR_REPLIES)
+ await ctx.send(embed=embed)
+ return
+
+ async with func.__locks.setdefault(ctx.author.id, Lock()):
+ return await func(self, ctx, *args, **kwargs)
+ return inner
+ return wrap
diff --git a/bot/pagination.py b/bot/pagination.py
new file mode 100644
index 00000000..0ad5b81f
--- /dev/null
+++ b/bot/pagination.py
@@ -0,0 +1,480 @@
+import asyncio
+import logging
+from typing import Iterable, List, Optional, Tuple
+
+from discord import Embed, Member, Reaction
+from discord.abc import User
+from discord.ext.commands import Context, Paginator
+
+FIRST_EMOJI = "\u23EE" # [:track_previous:]
+LEFT_EMOJI = "\u2B05" # [:arrow_left:]
+RIGHT_EMOJI = "\u27A1" # [:arrow_right:]
+LAST_EMOJI = "\u23ED" # [:track_next:]
+DELETE_EMOJI = "\u274c" # [:x:]
+
+PAGINATION_EMOJI = [FIRST_EMOJI, LEFT_EMOJI, RIGHT_EMOJI, LAST_EMOJI, DELETE_EMOJI]
+
+log = logging.getLogger(__name__)
+
+
+class EmptyPaginatorEmbed(Exception):
+ pass
+
+
+class LinePaginator(Paginator):
+ """
+ A class that aids in paginating code blocks for Discord messages.
+
+ Attributes
+ -----------
+ prefix: :class:`str`
+ The prefix inserted to every page. e.g. three backticks.
+ suffix: :class:`str`
+ The suffix appended at the end of every page. e.g. three backticks.
+ max_size: :class:`int`
+ The maximum amount of codepoints allowed in a page.
+ max_lines: :class:`int`
+ The maximum amount of lines allowed in a page.
+ """
+
+ def __init__(self, prefix='```', suffix='```',
+ max_size=2000, max_lines=None):
+ """
+ This function overrides the Paginator.__init__
+ from inside discord.ext.commands.
+ It overrides in order to allow us to configure
+ the maximum number of lines per page.
+ """
+ self.prefix = prefix
+ self.suffix = suffix
+ self.max_size = max_size - len(suffix)
+ self.max_lines = max_lines
+ self._current_page = [prefix]
+ self._linecount = 0
+ self._count = len(prefix) + 1 # prefix + newline
+ self._pages = []
+
+ def add_line(self, line='', *, empty=False):
+ """Adds a line to the current page.
+
+ If the line exceeds the :attr:`max_size` then an exception
+ is raised.
+
+ This function overrides the Paginator.add_line
+ from inside discord.ext.commands.
+ It overrides in order to allow us to configure
+ the maximum number of lines per page.
+
+ Parameters
+ -----------
+ line: str
+ The line to add.
+ empty: bool
+ Indicates if another empty line should be added.
+
+ Raises
+ ------
+ RuntimeError
+ The line was too big for the current :attr:`max_size`.
+ """
+ if len(line) > self.max_size - len(self.prefix) - 2:
+ raise RuntimeError('Line exceeds maximum page size %s' % (self.max_size - len(self.prefix) - 2))
+
+ if self.max_lines is not None:
+ if self._linecount >= self.max_lines:
+ self._linecount = 0
+ self.close_page()
+
+ self._linecount += 1
+ if self._count + len(line) + 1 > self.max_size:
+ self.close_page()
+
+ self._count += len(line) + 1
+ self._current_page.append(line)
+
+ if empty:
+ self._current_page.append('')
+ self._count += 1
+
+ @classmethod
+ async def paginate(cls, lines: Iterable[str], ctx: Context, embed: Embed,
+ prefix: str = "", suffix: str = "", max_lines: Optional[int] = None, max_size: int = 500,
+ empty: bool = True, restrict_to_user: User = None, timeout: int = 300,
+ footer_text: str = None, url: str = None, exception_on_empty_embed: bool = False):
+ """
+ Use a paginator and set of reactions to provide pagination over a set of lines. The reactions are used to
+ switch page, or to finish with pagination.
+ When used, this will send a message using `ctx.send()` and apply a set of reactions to it. These reactions may
+ be used to change page, or to remove pagination from the message. Pagination will also be removed automatically
+ if no reaction is added for five minutes (300 seconds).
+ >>> embed = Embed()
+ >>> embed.set_author(name="Some Operation", url=url, icon_url=icon)
+ >>> await LinePaginator.paginate(
+ ... (line for line in lines),
+ ... ctx, embed
+ ... )
+ :param lines: The lines to be paginated
+ :param ctx: Current context object
+ :param embed: A pre-configured embed to be used as a template for each page
+ :param prefix: Text to place before each page
+ :param suffix: Text to place after each page
+ :param max_lines: The maximum number of lines on each page
+ :param max_size: The maximum number of characters on each page
+ :param empty: Whether to place an empty line between each given line
+ :param restrict_to_user: A user to lock pagination operations to for this message, if supplied
+ :param exception_on_empty_embed: Should there be an exception if the embed is empty?
+ :param url: the url to use for the embed headline
+ :param timeout: The amount of time in seconds to disable pagination of no reaction is added
+ :param footer_text: Text to prefix the page number in the footer with
+ """
+
+ def event_check(reaction_: Reaction, user_: Member):
+ """
+ Make sure that this reaction is what we want to operate on
+ """
+
+ no_restrictions = (
+ # Pagination is not restricted
+ not restrict_to_user
+ # The reaction was by a whitelisted user
+ or user_.id == restrict_to_user.id
+ )
+
+ return (
+ # Conditions for a successful pagination:
+ all((
+ # Reaction is on this message
+ reaction_.message.id == message.id,
+ # Reaction is one of the pagination emotes
+ reaction_.emoji in PAGINATION_EMOJI,
+ # Reaction was not made by the Bot
+ user_.id != ctx.bot.user.id,
+ # There were no restrictions
+ no_restrictions
+ ))
+ )
+
+ paginator = cls(prefix=prefix, suffix=suffix, max_size=max_size, max_lines=max_lines)
+ current_page = 0
+
+ if not lines:
+ if exception_on_empty_embed:
+ log.exception(f"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")
+ lines.append("(nothing to display)")
+
+ for line in lines:
+ try:
+ paginator.add_line(line, empty=empty)
+ except Exception:
+ log.exception(f"Failed to add line to paginator: '{line}'")
+ raise # Should propagate
+ else:
+ log.trace(f"Added line to paginator: '{line}'")
+
+ log.debug(f"Paginator created with {len(paginator.pages)} pages")
+
+ embed.description = paginator.pages[current_page]
+
+ if len(paginator.pages) <= 1:
+ if footer_text:
+ embed.set_footer(text=footer_text)
+ log.trace(f"Setting embed footer to '{footer_text}'")
+
+ if url:
+ embed.url = url
+ log.trace(f"Setting embed url to '{url}'")
+
+ log.debug("There's less than two pages, so we won't paginate - sending single page on its own")
+ return await ctx.send(embed=embed)
+ else:
+ if footer_text:
+ embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
+ else:
+ embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
+ log.trace(f"Setting embed footer to '{embed.footer.text}'")
+
+ if url:
+ embed.url = url
+ log.trace(f"Setting embed url to '{url}'")
+
+ log.debug("Sending first page to channel...")
+ message = await ctx.send(embed=embed)
+
+ log.debug("Adding emoji reactions to message...")
+
+ for emoji in PAGINATION_EMOJI:
+ # Add all the applicable emoji to the message
+ log.trace(f"Adding reaction: {repr(emoji)}")
+ await message.add_reaction(emoji)
+
+ while True:
+ try:
+ reaction, user = await ctx.bot.wait_for("reaction_add", timeout=timeout, check=event_check)
+ log.trace(f"Got reaction: {reaction}")
+ except asyncio.TimeoutError:
+ log.debug("Timed out waiting for a reaction")
+ break # We're done, no reactions for the last 5 minutes
+
+ if reaction.emoji == DELETE_EMOJI:
+ log.debug("Got delete reaction")
+ break
+
+ if reaction.emoji == FIRST_EMOJI:
+ await message.remove_reaction(reaction.emoji, user)
+ current_page = 0
+
+ log.debug(f"Got first page reaction - changing to page 1/{len(paginator.pages)}")
+
+ embed.description = ""
+ await message.edit(embed=embed)
+ embed.description = paginator.pages[current_page]
+ if footer_text:
+ embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
+ else:
+ embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
+ await message.edit(embed=embed)
+
+ if reaction.emoji == LAST_EMOJI:
+ await message.remove_reaction(reaction.emoji, user)
+ current_page = len(paginator.pages) - 1
+
+ log.debug(f"Got last page reaction - changing to page {current_page + 1}/{len(paginator.pages)}")
+
+ embed.description = ""
+ await message.edit(embed=embed)
+ embed.description = paginator.pages[current_page]
+ if footer_text:
+ embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
+ else:
+ embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
+ await message.edit(embed=embed)
+
+ if reaction.emoji == LEFT_EMOJI:
+ await message.remove_reaction(reaction.emoji, user)
+
+ if current_page <= 0:
+ log.debug("Got previous page reaction, but we're on the first page - ignoring")
+ continue
+
+ current_page -= 1
+ log.debug(f"Got previous page reaction - changing to page {current_page + 1}/{len(paginator.pages)}")
+
+ embed.description = ""
+ await message.edit(embed=embed)
+ embed.description = paginator.pages[current_page]
+
+ if footer_text:
+ embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
+ else:
+ embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
+
+ await message.edit(embed=embed)
+
+ if reaction.emoji == RIGHT_EMOJI:
+ await message.remove_reaction(reaction.emoji, user)
+
+ if current_page >= len(paginator.pages) - 1:
+ log.debug("Got next page reaction, but we're on the last page - ignoring")
+ continue
+
+ current_page += 1
+ log.debug(f"Got next page reaction - changing to page {current_page + 1}/{len(paginator.pages)}")
+
+ embed.description = ""
+ await message.edit(embed=embed)
+ embed.description = paginator.pages[current_page]
+
+ if footer_text:
+ embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
+ else:
+ embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
+
+ await message.edit(embed=embed)
+
+ log.debug("Ending pagination and removing all reactions...")
+ await message.clear_reactions()
+
+
+class ImagePaginator(Paginator):
+ """
+ Helper class that paginates images for embeds in messages.
+ Close resemblance to LinePaginator, except focuses on images over text.
+
+ Refer to ImagePaginator.paginate for documentation on how to use.
+ """
+
+ def __init__(self, prefix="", suffix=""):
+ super().__init__(prefix, suffix)
+ self._current_page = [prefix]
+ self.images = []
+ self._pages = []
+
+ def add_line(self, line: str = '', *, empty: bool = False) -> None:
+ """
+ Adds a line to each page, usually just 1 line in this context
+ :param line: str to be page content / title
+ :param empty: if there should be new lines between entries
+ """
+
+ if line:
+ self._count = len(line)
+ else:
+ self._count = 0
+ self._current_page.append(line)
+ self.close_page()
+
+ def add_image(self, image: str = None) -> None:
+ """
+ Adds an image to a page
+ :param image: image url to be appended
+ """
+
+ self.images.append(image)
+
+ @classmethod
+ async def paginate(cls, pages: List[Tuple[str, str]], ctx: Context, embed: Embed,
+ prefix: str = "", suffix: str = "", timeout: int = 300,
+ exception_on_empty_embed: bool = False):
+ """
+ Use a paginator and set of reactions to provide
+ pagination over a set of title/image pairs.The reactions are
+ used to switch page, or to finish with pagination.
+
+ When used, this will send a message using `ctx.send()` and
+ apply a set of reactions to it. These reactions may
+ be used to change page, or to remove pagination from the message.
+
+ Note: Pagination will be removed automatically
+ if no reaction is added for five minutes (300 seconds).
+
+ >>> embed = Embed()
+ >>> embed.set_author(name="Some Operation", url=url, icon_url=icon)
+ >>> await ImagePaginator.paginate(pages, ctx, embed)
+
+ Parameters
+ -----------
+ :param pages: An iterable of tuples with title for page, and img url
+ :param ctx: ctx for message
+ :param embed: base embed to modify
+ :param prefix: prefix of message
+ :param suffix: suffix of message
+ :param timeout: timeout for when reactions get auto-removed
+ """
+
+ def check_event(reaction_: Reaction, member: Member) -> bool:
+ """
+ Checks each reaction added, if it matches our conditions pass the wait_for
+ :param reaction_: reaction added
+ :param member: reaction added by member
+ """
+
+ return all((
+ # Reaction is on the same message sent
+ reaction_.message.id == message.id,
+ # The reaction is part of the navigation menu
+ reaction_.emoji in PAGINATION_EMOJI,
+ # The reactor is not a bot
+ not member.bot
+ ))
+
+ paginator = cls(prefix=prefix, suffix=suffix)
+ current_page = 0
+
+ if not pages:
+ if exception_on_empty_embed:
+ log.exception(f"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")
+ pages.append(("(no images to display)", ""))
+
+ for text, image_url in pages:
+ paginator.add_line(text)
+ paginator.add_image(image_url)
+
+ embed.description = paginator.pages[current_page]
+ image = paginator.images[current_page]
+
+ if image:
+ embed.set_image(url=image)
+
+ if len(paginator.pages) <= 1:
+ return await ctx.send(embed=embed)
+
+ embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
+ message = await ctx.send(embed=embed)
+
+ for emoji in PAGINATION_EMOJI:
+ await message.add_reaction(emoji)
+
+ while True:
+ # Start waiting for reactions
+ try:
+ reaction, user = await ctx.bot.wait_for("reaction_add", timeout=timeout, check=check_event)
+ except asyncio.TimeoutError:
+ log.debug("Timed out waiting for a reaction")
+ break # We're done, no reactions for the last 5 minutes
+
+ # Deletes the users reaction
+ await message.remove_reaction(reaction.emoji, user)
+
+ # Delete reaction press - [:x:]
+ if reaction.emoji == DELETE_EMOJI:
+ log.debug("Got delete reaction")
+ break
+
+ # First reaction press - [:track_previous:]
+ if reaction.emoji == FIRST_EMOJI:
+ if current_page == 0:
+ log.debug("Got first page reaction, but we're on the first page - ignoring")
+ continue
+
+ current_page = 0
+ reaction_type = "first"
+
+ # Last reaction press - [:track_next:]
+ if reaction.emoji == LAST_EMOJI:
+ if current_page >= len(paginator.pages) - 1:
+ log.debug("Got last page reaction, but we're on the last page - ignoring")
+ continue
+
+ current_page = len(paginator.pages - 1)
+ reaction_type = "last"
+
+ # Previous reaction press - [:arrow_left: ]
+ if reaction.emoji == LEFT_EMOJI:
+ if current_page <= 0:
+ log.debug("Got previous page reaction, but we're on the first page - ignoring")
+ continue
+
+ current_page -= 1
+ reaction_type = "previous"
+
+ # Next reaction press - [:arrow_right:]
+ if reaction.emoji == RIGHT_EMOJI:
+ if current_page >= len(paginator.pages) - 1:
+ log.debug("Got next page reaction, but we're on the last page - ignoring")
+ continue
+
+ current_page += 1
+ reaction_type = "next"
+
+ # Magic happens here, after page and reaction_type is set
+ embed.description = ""
+ await message.edit(embed=embed)
+ embed.description = paginator.pages[current_page]
+
+ image = paginator.images[current_page]
+ if image:
+ embed.set_image(url=image)
+
+ embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
+ log.debug(f"Got {reaction_type} page reaction - changing to page {current_page + 1}/{len(paginator.pages)}")
+
+ await message.edit(embed=embed)
+
+ log.debug("Ending pagination and removing all reactions...")
+ await message.clear_reactions()
diff --git a/bot/resources/advent_of_code/about.json b/bot/resources/advent_of_code/about.json
index 3fe6fcc9..4abf9145 100644
--- a/bot/resources/advent_of_code/about.json
+++ b/bot/resources/advent_of_code/about.json
@@ -21,7 +21,7 @@
},
{
"name": "Join our private leaderboard!",
- "value": "In addition to the global leaderboard, AoC also offers private leaderboards, where you can compete against a smaller group of friends!\n\nHead over to AoC's [private leaderboard page](https://adventofcode.com/leaderboard/private) and enter code `363275-442b6939` to join the PyDis private leaderboard!",
+ "value": "In addition to the global leaderboard, AoC also offers private leaderboards, where you can compete against a smaller group of friends!\n\nGet the join code using `.aoc join` and head over to AoC's [private leaderboard page](https://adventofcode.com/leaderboard/private) to join the PyDis private leaderboard!",
"inline": false
}
] \ No newline at end of file
diff --git a/bot/resources/evergreen/magic8ball.json b/bot/resources/evergreen/magic8ball.json
new file mode 100644
index 00000000..6fe86950
--- /dev/null
+++ b/bot/resources/evergreen/magic8ball.json
@@ -0,0 +1,22 @@
+[
+ "It is certain",
+ "It is decidedly so",
+ "Without a doubt",
+ "Yes definitely",
+ "You may rely on it",
+ "As I see it, yes",
+ "Most likely",
+ "Outlook good",
+ "Yes",
+ "Signs point to yes",
+ "Reply hazy try again",
+ "Ask again later",
+ "Better not tell you now",
+ "Cannot predict now",
+ "Concentrate and ask again",
+ "Don't count on it",
+ "My reply is no",
+ "My sources say no",
+ "Outlook not so good",
+ "Very doubtful"
+] \ No newline at end of file
diff --git a/bot/resources/snakes/snake_cards/backs/card_back1.jpg b/bot/resources/snakes/snake_cards/backs/card_back1.jpg
new file mode 100644
index 00000000..22959fa7
--- /dev/null
+++ b/bot/resources/snakes/snake_cards/backs/card_back1.jpg
Binary files differ
diff --git a/bot/resources/snakes/snake_cards/backs/card_back2.jpg b/bot/resources/snakes/snake_cards/backs/card_back2.jpg
new file mode 100644
index 00000000..d56edc32
--- /dev/null
+++ b/bot/resources/snakes/snake_cards/backs/card_back2.jpg
Binary files differ
diff --git a/bot/resources/snakes/snake_cards/card_bottom.png b/bot/resources/snakes/snake_cards/card_bottom.png
new file mode 100644
index 00000000..8b2b91c5
--- /dev/null
+++ b/bot/resources/snakes/snake_cards/card_bottom.png
Binary files differ
diff --git a/bot/resources/snakes/snake_cards/card_frame.png b/bot/resources/snakes/snake_cards/card_frame.png
new file mode 100644
index 00000000..149a0a5f
--- /dev/null
+++ b/bot/resources/snakes/snake_cards/card_frame.png
Binary files differ
diff --git a/bot/resources/snakes/snake_cards/card_top.png b/bot/resources/snakes/snake_cards/card_top.png
new file mode 100644
index 00000000..e329c873
--- /dev/null
+++ b/bot/resources/snakes/snake_cards/card_top.png
Binary files differ
diff --git a/bot/resources/snakes/snake_cards/expressway.ttf b/bot/resources/snakes/snake_cards/expressway.ttf
new file mode 100644
index 00000000..39e15794
--- /dev/null
+++ b/bot/resources/snakes/snake_cards/expressway.ttf
Binary files differ
diff --git a/bot/resources/snakes/snake_facts.json b/bot/resources/snakes/snake_facts.json
new file mode 100644
index 00000000..ca9ba769
--- /dev/null
+++ b/bot/resources/snakes/snake_facts.json
@@ -0,0 +1,233 @@
+[
+ {
+ "fact": "The decapitated head of a dead snake can still bite, even hours after death. These types of bites usually contain huge amounts of venom."
+ },
+ {
+ "fact": "What is considered the most 'dangerous' snake depends on both a specific country’s health care and the availability of antivenom following a bite. Based on these criteria, the most dangerous snake in the world is the saw-scaled viper, which bites and kills more people each year than any other snake."
+ },
+ {
+ "fact": "Snakes live everywhere on Earth except Ireland, Iceland, New Zealand, and the North and South Poles."
+ },
+ {
+ "fact": "Of the approximately 725 species of venomous snakes worldwide, 250 can kill a human with one bite."
+ },
+ {
+ "fact": "Snakes evolved from a four-legged reptilian ancestor—most likely a small, burrowing, land-bound lizard—about 100 million years ago. Some snakes, such as pythons and boas, still have traces of back legs."
+ },
+ {
+ "fact": "The fear of snakes (ophiophobia or herpetophobia) is one of the most common phobias worldwide. Approximately 1/3 of all adult humans areophidiophobic , which suggests that humans have an innate, evolutionary fear of snakes."
+ },
+ {
+ "fact": "The top 5 most venomous snakes in the world are the inland taipan, the eastern brown snake, the coastal taipan, the tiger snake, and the black tiger snake."
+ },
+ {
+ "fact": "The warmer a snake’s body, the more quickly it can digest its prey. Typically, it takes 3–5 days for a snake to digest its meal. For very large snakes, such as the anaconda, digestion can take weeks."
+ },
+ {
+ "fact": "Some animals, such as the Mongoose, are immune to snake venom."
+ },
+ {
+ "fact": "To avoid predators, some snakes can poop whenever they want. They make themselves so dirty and smelly that predators will run away."
+ },
+ {
+ "fact": "The heaviest snake in the world is the anaconda. It weighs over 595 pounds (270 kg) and can grow to over 30 feet (9m) long. It has been known to eat caimans, capybaras, and jaguars."
+ },
+ {
+ "fact": "The Brahminy Blind Snake, or flowerpot snake, is the only snake species made up of solely females and, as such, does not need a mate to reproduce. It is also the most widespread terrestrial snake in the world."
+ },
+ {
+ "fact": "If a person suddenly turned into a snake, they would be about 4 times longer than they are now and only a few inches thick. While humans have 24 ribs, some snakes can have more than 400."
+ },
+ {
+ "fact": "The most advanced snake species in the world is believed to be the black mamba. It has the most highly evolved venom delivery system of any snake on Earth. It can strike up to 12 times in a row, though just one bite is enough to kill a grown man.o"
+ },
+ {
+ "fact": "The inland taipan is the world’s most toxic snake, meaning it has both the most toxic venom and it injects the most venom when it bites. Its venom sacs hold enough poison to kill up to 80 people."
+ },
+ {
+ "fact": "The death adder has the fastest strike of any snake in the world. It can attack, inject venom, and go back to striking position in under 0.15 seconds."
+ },
+ {
+ "fact": "While snakes do not have external ears or eardrums, their skin, muscles, and bones carry sound vibrations to their inner ears."
+ },
+ {
+ "fact": "Some snakes have been known to explode after eating a large meal. For example, a 13-foot python blew up after it tried to eat a 6-foot alligator. The python was found with the alligator’s tail protruding from its midsection. Its head was missing."
+ },
+ {
+ "fact": "The word 'snake' is from the Proto-Indo-European root *sneg -, meaning 'to crawl, creeping thing.' The word 'serpent' is from the Proto-Indo-European root *serp -, meaning 'to crawl, creep.'"
+ },
+ {
+ "fact": "Rattlesnake rattles are made of rings of keratin, which is the same material as human hair and fingernails. A rattler will add a new ring each time it sheds its skin."
+ },
+ {
+ "fact": "Some snakes have over 200 teeth. The teeth aren’t used for chewing but they point backward to prevent prey from escaping the snake’s throat."
+ },
+ {
+ "fact": "There are about 500 genera and 3,000 different species of snakes. All of them are predators."
+ },
+ {
+ "fact": "Naturalist Paul Rosolie attempted to be the first person to survive being swallowed by an anaconda in 2014. Though he was wearing a specially designed carbon fiber suit equipped with a breathing system, cameras, and a communication system, he ultimately called off his stunt when he felt like the anaconda was breaking his arm as it tightened its grip around his body."
+ },
+ {
+ "fact": "There are five recognized species of flying snakes. Growing up to 4 feet, some types can glide up to 330 feet through the air."
+ },
+ {
+ "fact": "Scales cover every inch of a snake’s body, even its eyes. Scales are thick, tough pieces of skin made from keratin, which is the same material human nails and hair are made from."
+ },
+ {
+ "fact": "The most common snake in North America is the garter (gardener) snake. This snake is also Massachusetts’s state reptile. While previously thought to be nonvenomous, garter snakes do, in fact, produce a mild neurotoxic venom that is harmless to humans."
+ },
+ {
+ "fact": "Snakes do not lap up water like mammals do. Instead, they dunk their snouts underwater and use their throats to pump water into their stomachs."
+ },
+ {
+ "fact": "A snake’s fangs usually last about 6–10 weeks. When a fang wears out, a new one grows in its place."
+ },
+ {
+ "fact": "Because the end of a snake’s tongue is forked, the two tips taste different amounts of chemicals. Essentially, a snake 'smells in stereo' and can even tell which direction a smell is coming from. It identifies scents on its tongue using pits in the roof of its mouth called the Jacobson’s organ."
+ },
+ {
+ "fact": "The amount of food a snake eats determines how many offspring it will have. The Arafura file snake eats the least and lays just one egg every decade."
+ },
+ {
+ "fact": "While smaller snakes, such a tree- or- ground-dwelling snakes, use their tongues to follow the scent trails of prey (such as spiders, birds, and other snakes). Larger snakes, such as boas, have heat-sensing organs called labial (lip) pits in their snouts."
+ },
+ {
+ "fact": "Snakes typically need to eat only 6–30 meals each year to be healthy."
+ },
+ {
+ "fact": "Snakes like to lie on roads and rocky areas because stones and rocks absorb heat from the sun, which warms them. Basking on these surfaces warms a snake quickly so it can move. If the temperature reaches below 50° Fahrenheit, a snake’s body does not work properly."
+ },
+ {
+ "fact": "The Mozambique spitting cobra can spit venom over 8 feet away. It can spit from any position, including lying on the ground or raised up. It prefers to aim for its victim’s eyes."
+ },
+ {
+ "fact": "Snakes cannot chew, so they must swallow their food whole. They are able to stretch their mouths very wide because they have a very flexible lower jaw. Snakes can eat other animals that are 75%–100% bigger than their own bodies."
+ },
+ {
+ "fact": "To keep from choking on large prey, a snake will push the end of its trachea, or windpipe, out of its mouth, similar to the way a snorkel works."
+ },
+ {
+ "fact": "The Gaboon viper has the longest fangs of any snake, reaching about 2 inches (5 cm) long."
+ },
+ {
+ "fact": "Anacondas can hold their breath for up to 10 minutes under water. Additionally, similar to crocodiles, anacondas have eyes and nostrils that can poke above the water’s surface to increase their stealth and hunting prowess."
+ },
+ {
+ "fact": "The longest snake ever recorded is the reticulated python. It can reach over 33 feet long, which is big enough to swallow a pig, a deer, or even a person."
+ },
+ {
+ "fact": "Sea snakes with their paddle-shaped tails can dive over 300 feet into the ocean."
+ },
+ {
+ "fact": "If a snake is threatened soon after a meal, it will often regurgitate its food so it can quickly escape the perceived threat. A snake’s digestive system can dissolve everything but a prey’s hair, feathers, and claws."
+ },
+ {
+ "fact": "Snakes do not have eyelids; rather, a single transparent scale called a brille protects their eyes. Most snakes see very well, especially if the object is moving."
+ },
+ {
+ "fact": "The world’s longest venomous snake is the king cobra from Asia. It can grow up to 18 feet, rear almost as high as a person, growl loudly, and inject enough venom to kill an elephant."
+ },
+ {
+ "fact": "The king cobra is thought to be one of the most intelligent of all snakes. Additionally, unlike most snakes, who do not care for their young, king cobras are careful parents who defend and protect their eggs from enemies."
+ },
+ {
+ "fact": "Not all snakes have fangs—only those that kill their prey with venom have them. When their fangs are not in use, they fold them back into the roof of the mouth (except for the coral snake, whose fangs do not fold back)."
+ },
+ {
+ "fact": "Some venomous snakes have died after biting and poisoning themselves by mistake."
+ },
+ {
+ "fact": "Elephant trunk snakes are almost completely aquatic. They cannot slither because they lack the broad scales in the belly that help other snakes move on land. Rather, elephant trunk snakes have large knobby scales to hold onto slippery fish and constrict them underwater."
+ },
+ {
+ "fact": "The shortest known snake is the thread snake. It is about 4 inches long and lives on the island of Barbados in the Caribbean. It is said to be as 'thin as spaghetti' and it feeds primarily on termites and larvae."
+ },
+ {
+ "fact": "In 2009, a farm worker in East Africa survived an epic 3-hour battle with a 12-foot python after accidentally stepping on the large snake. It coiled around the man and carried him into a tree. The man wrapped his shirt over the snake’s mouth to prevent it from swallowing him, and he was finally rescued by police after calling for help on his cell phone."
+ },
+ {
+ "fact": "The venom from a Brazilian pit viper is used in a drug to treat high blood pressure."
+ },
+ {
+ "fact": "The word 'cobra' means 'hooded.' Some cobras have large spots on the back of their hood that look like eyes to make them appear intimating even from behind."
+ },
+ {
+ "fact": "Some desert snakes, such as the African rock python, sleep during the hottest parts of the desert summer. This summer sleep is similar to hibernation and is called “aestivation.”"
+ },
+ {
+ "fact": "The black mamba is the world’s fastest snake and the world’s second-longest venomous snake in the world, after the king cobra. Found in East Africa, it can reach speeds of up to 12 mph (19kph). It’s named not from the color of its scales, which is olive green, but from the inside of its mouth, which is inky black. Its venom is highly toxic, and without anti-venom, death in humans usually occurs within 7–15 hours."
+ },
+ {
+ "fact": "Although a snake’s growth rate slows as it gets older, a snake never stops growing."
+ },
+ {
+ "fact": "While a snake cannot hear the music of a snake charmer, the snake responds to the vibrations of the charmer’s tapping foot or to the movement of the flute."
+ },
+ {
+ "fact": "Most snakes are not harmful to humans and they help balance the ecosystem by keeping the population of rats, mice, and birds under control."
+ },
+ {
+ "fact": "The largest snake fossil ever found is the Titanoboa. It lived over 60 million years ago and reached over 50 feet (15 meters) long. It weighed more than 20 people and ate crocodiles and giant tortoises."
+ },
+ {
+ "fact": "Two-headed snakes are similar to conjoined twins: an embryo begins to split to create identical twins, but the process does not finish. Such snakes rarely survive in the wild because the two heads have duplicate senses, they fight over food, and one head may try to eat the other head."
+ },
+ {
+ "fact": "Snakes can be grouped into two sections: primitive snakes and true (typical) snakes. Primitive snakes—such as blind snakes, worm snakes, and thread snakes—represent the earliest forms of snakes. True snakes, such as rat snakes and king snakes, are more evolved and more active."
+ },
+ {
+ "fact": "The oldest written record that describes snakes is in the Brooklyn Papyrus, which is a medical papyrus dating from ancient Egypt (450 B.C.)."
+ },
+ {
+ "fact": "Approximately 70% of snakes lay eggs. Those that lay eggs are called oviparous. The other 30% of snakes live in colder climates and give birth to live young because it is too cold for eggs outside the body to develop and hatch."
+ },
+ {
+ "fact": "Most snakes have an elongated right lung, many have a smaller left lung, and a few even have a third lung. They do not have a sense of taste, and most of their organs are organized linearly."
+ },
+ {
+ "fact": "The most rare and endangered snake is the St. Lucia racer. There are only 18 to 100 of these snakes left."
+ },
+ {
+ "fact": "Snakes kill over 40,000 people a year—though, with unreported incidents, the total may be over 100,000. About half of these deaths are in India."
+ },
+ {
+ "fact": "In some cultures, eating snakes is considered a delicacy. For example, snake soup has been a popular Cantonese delicacy for over 2,000 years."
+ },
+ {
+ "fact": "In some Asian countries, it is believed that drinking the blood of snakes, particularly the cobra, will increase sexual virility. The blood is usually drained from a live snake and then mixed with liquor."
+ },
+ {
+ "fact": "In the United States, fewer than 1 in 37,500 people are bitten by venomous snakes each year (7,000–8,000 bites per year), and only 1 in 50 million people will die from snake bite (5–6 fatalities per year). In the U.S., a person is 9 times more likely to die from being struck by lightening than to die from a venomous snakebite."
+ },
+ {
+ "fact": "Some members of the U.S. Army Special Forces are taught to kill and eat snakes during their survival training, which has earned them the nickname 'Snake Eaters.'"
+ },
+ {
+ "fact": "One of the great feats of the legendary Greek hero Perseus was to kill Medusa, a female monster whose hair consisted of writhing, venomous snakes."
+ },
+ {
+ "fact": "The symbol of the snake is one of the most widespread and oldest cultural symbols in history. Snakes often represent the duality of good and evil and of life and death."
+ },
+ {
+ "fact": "Because snakes shed their skin, they are often symbols of rebirth, transformation, and healing. For example, Asclepius, the god of medicine, carries a staff encircled by a snake."
+ },
+ {
+ "fact": "The snake has held various meanings throughout history. For example, The Egyptians viewed the snake as representing royalty and deity. In the Jewish rabbinical tradition and in Hinduism, it represents sexual passion and desire. And the Romans interpreted the snake as a symbol of eternal love."
+ },
+ {
+ "fact": "Anacondas mate in a huge 'breeding ball.' The ball consists of 1 female and nearly 12 males. They stay in a 'mating ball' for up to a month."
+ },
+ {
+ "fact": "Depending on the species, snakes can live from 4 to over 25 years."
+ },
+ {
+ "fact": "Snakes that are poisonous have pupils that are shaped like a diamond. Nonpoisonous snakes have round pupils."
+ },
+ {
+ "fact": "Endangered snakes include the San Francisco garter snake, eastern indigo snake, the king cobra, and Dumeril’s boa."
+ },
+ {
+ "fact": "A mysterious, new 'mad snake disease' causes captive pythons and boas to tie themselves in knots. Other symptoms include 'stargazing,' which is when snakes stare upwards for long periods of time. Snake experts believe a rodent virus causes the fatal disease."
+ }
+]
diff --git a/bot/resources/snakes/snake_idioms.json b/bot/resources/snakes/snake_idioms.json
new file mode 100644
index 00000000..37148c42
--- /dev/null
+++ b/bot/resources/snakes/snake_idioms.json
@@ -0,0 +1,275 @@
+[
+ {
+ "idiom": "snek it up"
+ },
+ {
+ "idiom": "get ur snek on"
+ },
+ {
+ "idiom": "snek ur heart out"
+ },
+ {
+ "idiom": "snek 4 ever"
+ },
+ {
+ "idiom": "i luve snek"
+ },
+ {
+ "idiom": "snek bff"
+ },
+ {
+ "idiom": "boyfriend snek"
+ },
+ {
+ "idiom": "dont snek ur homies"
+ },
+ {
+ "idiom": "garden snek"
+ },
+ {
+ "idiom": "snektie"
+ },
+ {
+ "idiom": "snek keks"
+ },
+ {
+ "idiom": "birthday snek!"
+ },
+ {
+ "idiom": "snek tonight?"
+ },
+ {
+ "idiom": "snek hott lips"
+ },
+ {
+ "idiom": "snek u latr"
+ },
+ {
+ "idiom": "netflx and snek"
+ },
+ {
+ "idiom": "holy snek prey4u"
+ },
+ {
+ "idiom": "ghowst snek hauntt u"
+ },
+ {
+ "idiom": "ipekek snek syrop"
+ },
+ {
+ "idiom": "2 snek 2 furius"
+ },
+ {
+ "idiom": "the shawsnek redumpton"
+ },
+ {
+ "idiom": "snekler's list"
+ },
+ {
+ "idiom": "snekablanca"
+ },
+ {
+ "idiom": "romeo n snekulet"
+ },
+ {
+ "idiom": "citizn snek"
+ },
+ {
+ "idiom": "gon wit the snek"
+ },
+ {
+ "idiom": "dont step on snek"
+ },
+ {
+ "idiom": "the wizrd uf snek"
+ },
+ {
+ "idiom": "forrest snek"
+ },
+ {
+ "idiom": "snek of musik"
+ },
+ {
+ "idiom": "west snek story"
+ },
+ {
+ "idiom": "snek wars eposide XI"
+ },
+ {
+ "idiom": "2001: a snek odyssuuy"
+ },
+ {
+ "idiom": "E.T. the snekstra terrastriul"
+ },
+ {
+ "idiom": "snekkin' inth rain"
+ },
+ {
+ "idiom": "dr sneklove"
+ },
+ {
+ "idiom": "snekley kubrik"
+ },
+ {
+ "idiom": "willium snekspeare"
+ },
+ {
+ "idiom": "snek on tutanic"
+ },
+ {
+ "idiom": "a snekwork orunge"
+ },
+ {
+ "idiom": "the snek the bad n the ogly"
+ },
+ {
+ "idiom": "the sneksorcist"
+ },
+ {
+ "idiom": "gudd snek huntin"
+ },
+ {
+ "idiom": "leonurdo disnekrio"
+ },
+ {
+ "idiom": "denzal snekington"
+ },
+ {
+ "idiom": "snekuel l jocksons"
+ },
+ {
+ "idiom": "kevn snek"
+ },
+ {
+ "idiom": "snekthony hopkuns"
+ },
+ {
+ "idiom": "hugh snekman"
+ },
+ {
+ "idiom": "snek but it glow in durk"
+ },
+ {
+ "idiom": "snek but u cn ride it"
+ },
+ {
+ "idiom": "snek but slep in ur bed"
+ },
+ {
+ "idiom": "snek but mad frum plastk"
+ },
+ {
+ "idiom": "snek but bulong 2 ur frnd"
+ },
+ {
+ "idiom": "sneks on plene"
+ },
+ {
+ "idiom": "baby snek"
+ },
+ {
+ "idiom": "trouser snek"
+ },
+ {
+ "idiom": "momo snek"
+ },
+ {
+ "idiom": "fast snek"
+ },
+ {
+ "idiom": "super slow snek"
+ },
+ {
+ "idiom": "old snek"
+ },
+ {
+ "idiom": "slimy snek"
+ },
+ {
+ "idiom": "snek attekk"
+ },
+ {
+ "idiom": "snek get wrekk"
+ },
+ {
+ "idiom": "snek you long time"
+ },
+ {
+ "idiom": "carpenter snek"
+ },
+ {
+ "idiom": "drain snek"
+ },
+ {
+ "idiom": "eat ur face snek"
+ },
+ {
+ "idiom": "kawaii snek"
+ },
+ {
+ "idiom": "dis snek is soft"
+ },
+ {
+ "idiom": "snek is 4 yers uld"
+ },
+ {
+ "idiom": "pls feed snek, is hingry"
+ },
+ {
+ "idiom": "snek? snek? sneeeeek!!"
+ },
+ {
+ "idiom": "solid snek"
+ },
+ {
+ "idiom": "big bos snek"
+ },
+ {
+ "idiom": "snek republic"
+ },
+ {
+ "idiom": "snekoslovakia"
+ },
+ {
+ "idiom": "snek please!"
+ },
+ {
+ "idiom": "i brok my snek :("
+ },
+ {
+ "idiom": "star snek the nxt generatin"
+ },
+ {
+ "idiom": "azsnek tempul"
+ },
+ {
+ "idiom": "discosnek"
+ },
+ {
+ "idiom": "bottlsnek"
+ },
+ {
+ "idiom": "turtlsnek"
+ },
+ {
+ "idiom": "cashiers snek"
+ },
+ {
+ "idiom": "mega snek!!"
+ },
+ {
+ "idiom": "one tim i saw snek neked"
+ },
+ {
+ "idiom": "snek cnt clim trees"
+ },
+ {
+ "idiom": "snek in muth is jus tongue"
+ },
+ {
+ "idiom": "juan snek"
+ },
+ {
+ "idiom": "photosnek"
+ }
+] \ No newline at end of file
diff --git a/bot/resources/snakes/snake_names.json b/bot/resources/snakes/snake_names.json
new file mode 100644
index 00000000..8ba9dbd7
--- /dev/null
+++ b/bot/resources/snakes/snake_names.json
@@ -0,0 +1,2170 @@
+[
+ {
+ "name": "Acanthophis",
+ "scientific": "Acanthophis"
+ },
+ {
+ "name": "Aesculapian snake",
+ "scientific": "Aesculapian snake"
+ },
+ {
+ "name": "African beaked snake",
+ "scientific": "Rufous beaked snake"
+ },
+ {
+ "name": "African puff adder",
+ "scientific": "Bitis arietans"
+ },
+ {
+ "name": "African rock python",
+ "scientific": "African rock python"
+ },
+ {
+ "name": "African twig snake",
+ "scientific": "Twig snake"
+ },
+ {
+ "name": "Agkistrodon piscivorus",
+ "scientific": "Agkistrodon piscivorus"
+ },
+ {
+ "name": "Ahaetulla",
+ "scientific": "Ahaetulla"
+ },
+ {
+ "name": "Amazonian palm viper",
+ "scientific": "Bothriopsis bilineata"
+ },
+ {
+ "name": "American copperhead",
+ "scientific": "Agkistrodon contortrix"
+ },
+ {
+ "name": "Amethystine python",
+ "scientific": "Amethystine python"
+ },
+ {
+ "name": "Anaconda",
+ "scientific": "Anaconda"
+ },
+ {
+ "name": "Andaman cat snake",
+ "scientific": "Boiga andamanensis"
+ },
+ {
+ "name": "Andrea's keelback",
+ "scientific": "Amphiesma andreae"
+ },
+ {
+ "name": "Annulated sea snake",
+ "scientific": "Hydrophis cyanocinctus"
+ },
+ {
+ "name": "Arafura file snake",
+ "scientific": "Acrochordus arafurae"
+ },
+ {
+ "name": "Arizona black rattlesnake",
+ "scientific": "Crotalus oreganus cerberus"
+ },
+ {
+ "name": "Arizona coral snake",
+ "scientific": "Coral snake"
+ },
+ {
+ "name": "Aruba rattlesnake",
+ "scientific": "Crotalus durissus unicolor"
+ },
+ {
+ "name": "Asian cobra",
+ "scientific": "Indian cobra"
+ },
+ {
+ "name": "Asian keelback",
+ "scientific": "Amphiesma vibakari"
+ },
+ {
+ "name": "Asp (reptile)",
+ "scientific": "Asp (reptile)"
+ },
+ {
+ "name": "Assam keelback",
+ "scientific": "Amphiesma pealii"
+ },
+ {
+ "name": "Australian copperhead",
+ "scientific": "Austrelaps"
+ },
+ {
+ "name": "Australian scrub python",
+ "scientific": "Amethystine python"
+ },
+ {
+ "name": "Baird's rat snake",
+ "scientific": "Pantherophis bairdi"
+ },
+ {
+ "name": "Banded Flying Snake",
+ "scientific": "Banded flying snake"
+ },
+ {
+ "name": "Banded cat-eyed snake",
+ "scientific": "Banded cat-eyed snake"
+ },
+ {
+ "name": "Banded krait",
+ "scientific": "Banded krait"
+ },
+ {
+ "name": "Barred wolf snake",
+ "scientific": "Lycodon striatus"
+ },
+ {
+ "name": "Beaked sea snake",
+ "scientific": "Enhydrina schistosa"
+ },
+ {
+ "name": "Beauty rat snake",
+ "scientific": "Beauty rat snake"
+ },
+ {
+ "name": "Beddome's cat snake",
+ "scientific": "Boiga beddomei"
+ },
+ {
+ "name": "Beddome's coral snake",
+ "scientific": "Beddome's coral snake"
+ },
+ {
+ "name": "Bird snake",
+ "scientific": "Twig snake"
+ },
+ {
+ "name": "Black-banded trinket snake",
+ "scientific": "Oreocryptophis porphyraceus"
+ },
+ {
+ "name": "Black-headed snake",
+ "scientific": "Western black-headed snake"
+ },
+ {
+ "name": "Black-necked cobra",
+ "scientific": "Black-necked spitting cobra"
+ },
+ {
+ "name": "Black-necked spitting cobra",
+ "scientific": "Black-necked spitting cobra"
+ },
+ {
+ "name": "Black-striped keelback",
+ "scientific": "Buff striped keelback"
+ },
+ {
+ "name": "Black-tailed horned pit viper",
+ "scientific": "Mixcoatlus melanurus"
+ },
+ {
+ "name": "Black headed python",
+ "scientific": "Black-headed python"
+ },
+ {
+ "name": "Black krait",
+ "scientific": "Greater black krait"
+ },
+ {
+ "name": "Black mamba",
+ "scientific": "Black mamba"
+ },
+ {
+ "name": "Black rat snake",
+ "scientific": "Rat snake"
+ },
+ {
+ "name": "Black tree cobra",
+ "scientific": "Cobra"
+ },
+ {
+ "name": "Blind snake",
+ "scientific": "Scolecophidia"
+ },
+ {
+ "name": "Blonde hognose snake",
+ "scientific": "Hognose"
+ },
+ {
+ "name": "Blood python",
+ "scientific": "Python brongersmai"
+ },
+ {
+ "name": "Blue krait",
+ "scientific": "Bungarus candidus"
+ },
+ {
+ "name": "Blunt-headed tree snake",
+ "scientific": "Imantodes cenchoa"
+ },
+ {
+ "name": "Boa constrictor",
+ "scientific": "Boa constrictor"
+ },
+ {
+ "name": "Bocourt's water snake",
+ "scientific": "Subsessor"
+ },
+ {
+ "name": "Boelen python",
+ "scientific": "Morelia boeleni"
+ },
+ {
+ "name": "Boidae",
+ "scientific": "Boidae"
+ },
+ {
+ "name": "Boiga",
+ "scientific": "Boiga"
+ },
+ {
+ "name": "Boomslang",
+ "scientific": "Boomslang"
+ },
+ {
+ "name": "Brahminy blind snake",
+ "scientific": "Indotyphlops braminus"
+ },
+ {
+ "name": "Brazilian coral snake",
+ "scientific": "Coral snake"
+ },
+ {
+ "name": "Brazilian smooth snake",
+ "scientific": "Hydrodynastes gigas"
+ },
+ {
+ "name": "Brown snake (disambiguation)",
+ "scientific": "Brown snake"
+ },
+ {
+ "name": "Brown tree snake",
+ "scientific": "Brown tree snake"
+ },
+ {
+ "name": "Brown white-lipped python",
+ "scientific": "Leiopython"
+ },
+ {
+ "name": "Buff striped keelback",
+ "scientific": "Buff striped keelback"
+ },
+ {
+ "name": "Bull snake",
+ "scientific": "Bull snake"
+ },
+ {
+ "name": "Burmese keelback",
+ "scientific": "Burmese keelback water snake"
+ },
+ {
+ "name": "Burmese krait",
+ "scientific": "Burmese krait"
+ },
+ {
+ "name": "Burmese python",
+ "scientific": "Burmese python"
+ },
+ {
+ "name": "Burrowing viper",
+ "scientific": "Atractaspidinae"
+ },
+ {
+ "name": "Buttermilk racer",
+ "scientific": "Coluber constrictor anthicus"
+ },
+ {
+ "name": "California kingsnake",
+ "scientific": "California kingsnake"
+ },
+ {
+ "name": "Cantor's pitviper",
+ "scientific": "Trimeresurus cantori"
+ },
+ {
+ "name": "Cape cobra",
+ "scientific": "Cape cobra"
+ },
+ {
+ "name": "Cape coral snake",
+ "scientific": "Aspidelaps lubricus"
+ },
+ {
+ "name": "Cape gopher snake",
+ "scientific": "Cape gopher snake"
+ },
+ {
+ "name": "Carpet viper",
+ "scientific": "Echis"
+ },
+ {
+ "name": "Cat-eyed night snake",
+ "scientific": "Banded cat-eyed snake"
+ },
+ {
+ "name": "Cat-eyed snake",
+ "scientific": "Banded cat-eyed snake"
+ },
+ {
+ "name": "Cat snake",
+ "scientific": "Boiga"
+ },
+ {
+ "name": "Central American lyre snake",
+ "scientific": "Trimorphodon biscutatus"
+ },
+ {
+ "name": "Central ranges taipan",
+ "scientific": "Taipan"
+ },
+ {
+ "name": "Chappell Island tiger snake",
+ "scientific": "Tiger snake"
+ },
+ {
+ "name": "Checkered garter snake",
+ "scientific": "Checkered garter snake"
+ },
+ {
+ "name": "Checkered keelback",
+ "scientific": "Checkered keelback"
+ },
+ {
+ "name": "Children's python",
+ "scientific": "Children's python"
+ },
+ {
+ "name": "Chinese cobra",
+ "scientific": "Chinese cobra"
+ },
+ {
+ "name": "Coachwhip snake",
+ "scientific": "Masticophis flagellum"
+ },
+ {
+ "name": "Coastal taipan",
+ "scientific": "Coastal taipan"
+ },
+ {
+ "name": "Cobra",
+ "scientific": "Cobra"
+ },
+ {
+ "name": "Collett's snake",
+ "scientific": "Collett's snake"
+ },
+ {
+ "name": "Common adder",
+ "scientific": "Vipera berus"
+ },
+ {
+ "name": "Common cobra",
+ "scientific": "Chinese cobra"
+ },
+ {
+ "name": "Common garter snake",
+ "scientific": "Common garter snake"
+ },
+ {
+ "name": "Common ground snake",
+ "scientific": "Western ground snake"
+ },
+ {
+ "name": "Common keelback (disambiguation)",
+ "scientific": "Common keelback"
+ },
+ {
+ "name": "Common tiger snake",
+ "scientific": "Tiger snake"
+ },
+ {
+ "name": "Common worm snake",
+ "scientific": "Indotyphlops braminus"
+ },
+ {
+ "name": "Congo snake",
+ "scientific": "Amphiuma"
+ },
+ {
+ "name": "Congo water cobra",
+ "scientific": "Naja christyi"
+ },
+ {
+ "name": "Coral snake",
+ "scientific": "Coral snake"
+ },
+ {
+ "name": "Corn snake",
+ "scientific": "Corn snake"
+ },
+ {
+ "name": "Coronado Island rattlesnake",
+ "scientific": "Crotalus oreganus caliginis"
+ },
+ {
+ "name": "Crossed viper",
+ "scientific": "Vipera berus"
+ },
+ {
+ "name": "Crotalus cerastes",
+ "scientific": "Crotalus cerastes"
+ },
+ {
+ "name": "Crotalus durissus",
+ "scientific": "Crotalus durissus"
+ },
+ {
+ "name": "Crotalus horridus",
+ "scientific": "Timber rattlesnake"
+ },
+ {
+ "name": "Crowned snake",
+ "scientific": "Tantilla"
+ },
+ {
+ "name": "Cuban boa",
+ "scientific": "Chilabothrus angulifer"
+ },
+ {
+ "name": "Cuban wood snake",
+ "scientific": "Tropidophis melanurus"
+ },
+ {
+ "name": "Dasypeltis",
+ "scientific": "Dasypeltis"
+ },
+ {
+ "name": "Desert death adder",
+ "scientific": "Desert death adder"
+ },
+ {
+ "name": "Desert kingsnake",
+ "scientific": "Desert kingsnake"
+ },
+ {
+ "name": "Desert woma python",
+ "scientific": "Woma python"
+ },
+ {
+ "name": "Diamond python",
+ "scientific": "Morelia spilota spilota"
+ },
+ {
+ "name": "Dog-toothed cat snake",
+ "scientific": "Boiga cynodon"
+ },
+ {
+ "name": "Down's tiger snake",
+ "scientific": "Tiger snake"
+ },
+ {
+ "name": "Dubois's sea snake",
+ "scientific": "Aipysurus duboisii"
+ },
+ {
+ "name": "Durango rock rattlesnake",
+ "scientific": "Crotalus lepidus klauberi"
+ },
+ {
+ "name": "Dusty hognose snake",
+ "scientific": "Hognose"
+ },
+ {
+ "name": "Dwarf beaked snake",
+ "scientific": "Dwarf beaked snake"
+ },
+ {
+ "name": "Dwarf boa",
+ "scientific": "Boa constrictor"
+ },
+ {
+ "name": "Dwarf pipe snake",
+ "scientific": "Anomochilus"
+ },
+ {
+ "name": "Eastern brown snake",
+ "scientific": "Eastern brown snake"
+ },
+ {
+ "name": "Eastern coral snake",
+ "scientific": "Micrurus fulvius"
+ },
+ {
+ "name": "Eastern diamondback rattlesnake",
+ "scientific": "Eastern diamondback rattlesnake"
+ },
+ {
+ "name": "Eastern green mamba",
+ "scientific": "Eastern green mamba"
+ },
+ {
+ "name": "Eastern hognose snake",
+ "scientific": "Eastern hognose snake"
+ },
+ {
+ "name": "Eastern mud snake",
+ "scientific": "Mud snake"
+ },
+ {
+ "name": "Eastern racer",
+ "scientific": "Coluber constrictor"
+ },
+ {
+ "name": "Eastern tiger snake",
+ "scientific": "Tiger snake"
+ },
+ {
+ "name": "Eastern water cobra",
+ "scientific": "Cobra"
+ },
+ {
+ "name": "Elaps harlequin snake",
+ "scientific": "Micrurus fulvius"
+ },
+ {
+ "name": "Eunectes",
+ "scientific": "Eunectes"
+ },
+ {
+ "name": "European Smooth Snake",
+ "scientific": "Smooth snake"
+ },
+ {
+ "name": "False cobra",
+ "scientific": "False cobra"
+ },
+ {
+ "name": "False coral snake",
+ "scientific": "Coral snake"
+ },
+ {
+ "name": "False water cobra",
+ "scientific": "Hydrodynastes gigas"
+ },
+ {
+ "name": "Fierce snake",
+ "scientific": "Inland taipan"
+ },
+ {
+ "name": "Flying snake",
+ "scientific": "Chrysopelea"
+ },
+ {
+ "name": "Forest cobra",
+ "scientific": "Forest cobra"
+ },
+ {
+ "name": "Forsten's cat snake",
+ "scientific": "Boiga forsteni"
+ },
+ {
+ "name": "Fox snake",
+ "scientific": "Fox snake"
+ },
+ {
+ "name": "Gaboon viper",
+ "scientific": "Gaboon viper"
+ },
+ {
+ "name": "Garter snake",
+ "scientific": "Garter snake"
+ },
+ {
+ "name": "Giant Malagasy hognose snake",
+ "scientific": "Hognose"
+ },
+ {
+ "name": "Glossy snake",
+ "scientific": "Glossy snake"
+ },
+ {
+ "name": "Gold-ringed cat snake",
+ "scientific": "Boiga dendrophila"
+ },
+ {
+ "name": "Gold tree cobra",
+ "scientific": "Pseudohaje goldii"
+ },
+ {
+ "name": "Golden tree snake",
+ "scientific": "Chrysopelea ornata"
+ },
+ {
+ "name": "Gopher snake",
+ "scientific": "Pituophis catenifer"
+ },
+ {
+ "name": "Grand Canyon rattlesnake",
+ "scientific": "Crotalus oreganus abyssus"
+ },
+ {
+ "name": "Grass snake",
+ "scientific": "Grass snake"
+ },
+ {
+ "name": "Gray cat snake",
+ "scientific": "Boiga ocellata"
+ },
+ {
+ "name": "Great Plains rat snake",
+ "scientific": "Pantherophis emoryi"
+ },
+ {
+ "name": "Green anaconda",
+ "scientific": "Green anaconda"
+ },
+ {
+ "name": "Green rat snake",
+ "scientific": "Rat snake"
+ },
+ {
+ "name": "Green tree python",
+ "scientific": "Green tree python"
+ },
+ {
+ "name": "Grey-banded kingsnake",
+ "scientific": "Gray-banded kingsnake"
+ },
+ {
+ "name": "Grey Lora",
+ "scientific": "Leptophis stimsoni"
+ },
+ {
+ "name": "Halmahera python",
+ "scientific": "Morelia tracyae"
+ },
+ {
+ "name": "Harlequin coral snake",
+ "scientific": "Micrurus fulvius"
+ },
+ {
+ "name": "Herald snake",
+ "scientific": "Caduceus"
+ },
+ {
+ "name": "High Woods coral snake",
+ "scientific": "Coral snake"
+ },
+ {
+ "name": "Hill keelback",
+ "scientific": "Amphiesma monticola"
+ },
+ {
+ "name": "Himalayan keelback",
+ "scientific": "Amphiesma platyceps"
+ },
+ {
+ "name": "Hognose snake",
+ "scientific": "Hognose"
+ },
+ {
+ "name": "Hognosed viper",
+ "scientific": "Porthidium"
+ },
+ {
+ "name": "Hook Nosed Sea Snake",
+ "scientific": "Enhydrina schistosa"
+ },
+ {
+ "name": "Hoop snake",
+ "scientific": "Hoop snake"
+ },
+ {
+ "name": "Hopi rattlesnake",
+ "scientific": "Crotalus viridis nuntius"
+ },
+ {
+ "name": "Indian cobra",
+ "scientific": "Indian cobra"
+ },
+ {
+ "name": "Indian egg-eater",
+ "scientific": "Indian egg-eating snake"
+ },
+ {
+ "name": "Indian flying snake",
+ "scientific": "Chrysopelea ornata"
+ },
+ {
+ "name": "Indian krait",
+ "scientific": "Bungarus"
+ },
+ {
+ "name": "Indigo snake",
+ "scientific": "Drymarchon"
+ },
+ {
+ "name": "Inland carpet python",
+ "scientific": "Morelia spilota metcalfei"
+ },
+ {
+ "name": "Inland taipan",
+ "scientific": "Inland taipan"
+ },
+ {
+ "name": "Jamaican boa",
+ "scientific": "Jamaican boa"
+ },
+ {
+ "name": "Jan's hognose snake",
+ "scientific": "Hognose"
+ },
+ {
+ "name": "Japanese forest rat snake",
+ "scientific": "Euprepiophis conspicillatus"
+ },
+ {
+ "name": "Japanese rat snake",
+ "scientific": "Japanese rat snake"
+ },
+ {
+ "name": "Japanese striped snake",
+ "scientific": "Japanese striped snake"
+ },
+ {
+ "name": "Kayaudi dwarf reticulated python",
+ "scientific": "Reticulated python"
+ },
+ {
+ "name": "Keelback",
+ "scientific": "Natricinae"
+ },
+ {
+ "name": "Khasi Hills keelback",
+ "scientific": "Amphiesma khasiense"
+ },
+ {
+ "name": "King Island tiger snake",
+ "scientific": "Tiger snake"
+ },
+ {
+ "name": "King brown",
+ "scientific": "Mulga snake"
+ },
+ {
+ "name": "King cobra",
+ "scientific": "King cobra"
+ },
+ {
+ "name": "King rat snake",
+ "scientific": "Rat snake"
+ },
+ {
+ "name": "King snake",
+ "scientific": "Kingsnake"
+ },
+ {
+ "name": "Krait",
+ "scientific": "Bungarus"
+ },
+ {
+ "name": "Krefft's tiger snake",
+ "scientific": "Tiger snake"
+ },
+ {
+ "name": "Lance-headed rattlesnake",
+ "scientific": "Crotalus polystictus"
+ },
+ {
+ "name": "Lancehead",
+ "scientific": "Bothrops"
+ },
+ {
+ "name": "Large shield snake",
+ "scientific": "Pseudotyphlops"
+ },
+ {
+ "name": "Leptophis ahaetulla",
+ "scientific": "Leptophis ahaetulla"
+ },
+ {
+ "name": "Lesser black krait",
+ "scientific": "Lesser black krait"
+ },
+ {
+ "name": "Long-nosed adder",
+ "scientific": "Eastern hognose snake"
+ },
+ {
+ "name": "Long-nosed tree snake",
+ "scientific": "Western hognose snake"
+ },
+ {
+ "name": "Long-nosed whip snake",
+ "scientific": "Ahaetulla nasuta"
+ },
+ {
+ "name": "Long-tailed rattlesnake",
+ "scientific": "Rattlesnake"
+ },
+ {
+ "name": "Longnosed worm snake",
+ "scientific": "Leptotyphlops macrorhynchus"
+ },
+ {
+ "name": "Lyre snake",
+ "scientific": "Trimorphodon"
+ },
+ {
+ "name": "Madagascar ground boa",
+ "scientific": "Acrantophis madagascariensis"
+ },
+ {
+ "name": "Malayan krait",
+ "scientific": "Bungarus candidus"
+ },
+ {
+ "name": "Malayan long-glanded coral snake",
+ "scientific": "Calliophis bivirgata"
+ },
+ {
+ "name": "Malayan pit viper",
+ "scientific": "Pit viper"
+ },
+ {
+ "name": "Mamba",
+ "scientific": "Mamba"
+ },
+ {
+ "name": "Mamushi",
+ "scientific": "Mamushi"
+ },
+ {
+ "name": "Manchurian Black Water Snake",
+ "scientific": "Elaphe schrenckii"
+ },
+ {
+ "name": "Mandarin rat snake",
+ "scientific": "Mandarin rat snake"
+ },
+ {
+ "name": "Mangrove snake (disambiguation)",
+ "scientific": "Mangrove snake"
+ },
+ {
+ "name": "Many-banded krait",
+ "scientific": "Many-banded krait"
+ },
+ {
+ "name": "Many-banded tree snake",
+ "scientific": "Many-banded tree snake"
+ },
+ {
+ "name": "Many-spotted cat snake",
+ "scientific": "Boiga multomaculata"
+ },
+ {
+ "name": "Massasauga rattlesnake",
+ "scientific": "Massasauga"
+ },
+ {
+ "name": "Mexican black kingsnake",
+ "scientific": "Mexican black kingsnake"
+ },
+ {
+ "name": "Mexican green rattlesnake",
+ "scientific": "Crotalus basiliscus"
+ },
+ {
+ "name": "Mexican hognose snake",
+ "scientific": "Hognose"
+ },
+ {
+ "name": "Mexican parrot snake",
+ "scientific": "Leptophis mexicanus"
+ },
+ {
+ "name": "Mexican racer",
+ "scientific": "Coluber constrictor oaxaca"
+ },
+ {
+ "name": "Mexican vine snake",
+ "scientific": "Oxybelis aeneus"
+ },
+ {
+ "name": "Mexican west coast rattlesnake",
+ "scientific": "Crotalus basiliscus"
+ },
+ {
+ "name": "Micropechis ikaheka",
+ "scientific": "Micropechis ikaheka"
+ },
+ {
+ "name": "Midget faded rattlesnake",
+ "scientific": "Crotalus oreganus concolor"
+ },
+ {
+ "name": "Milk snake",
+ "scientific": "Milk snake"
+ },
+ {
+ "name": "Moccasin snake",
+ "scientific": "Agkistrodon piscivorus"
+ },
+ {
+ "name": "Modest keelback",
+ "scientific": "Amphiesma modestum"
+ },
+ {
+ "name": "Mojave desert sidewinder",
+ "scientific": "Crotalus cerastes"
+ },
+ {
+ "name": "Mojave rattlesnake",
+ "scientific": "Crotalus scutulatus"
+ },
+ {
+ "name": "Mole viper",
+ "scientific": "Atractaspidinae"
+ },
+ {
+ "name": "Moluccan flying snake",
+ "scientific": "Chrysopelea"
+ },
+ {
+ "name": "Montpellier snake",
+ "scientific": "Malpolon monspessulanus"
+ },
+ {
+ "name": "Mud adder",
+ "scientific": "Mud adder"
+ },
+ {
+ "name": "Mud snake",
+ "scientific": "Mud snake"
+ },
+ {
+ "name": "Mussurana",
+ "scientific": "Mussurana"
+ },
+ {
+ "name": "Narrowhead Garter Snake",
+ "scientific": "Garter snake"
+ },
+ {
+ "name": "Nicobar Island keelback",
+ "scientific": "Amphiesma nicobariense"
+ },
+ {
+ "name": "Nicobar cat snake",
+ "scientific": "Boiga wallachi"
+ },
+ {
+ "name": "Night snake",
+ "scientific": "Night snake"
+ },
+ {
+ "name": "Nilgiri keelback",
+ "scientific": "Nilgiri keelback"
+ },
+ {
+ "name": "North eastern king snake",
+ "scientific": "Eastern hognose snake"
+ },
+ {
+ "name": "Northeastern hill krait",
+ "scientific": "Northeastern hill krait"
+ },
+ {
+ "name": "Northern black-tailed rattlesnake",
+ "scientific": "Crotalus molossus"
+ },
+ {
+ "name": "Northern tree snake",
+ "scientific": "Dendrelaphis calligastra"
+ },
+ {
+ "name": "Northern water snake",
+ "scientific": "Northern water snake"
+ },
+ {
+ "name": "Northern white-lipped python",
+ "scientific": "Leiopython"
+ },
+ {
+ "name": "Oaxacan small-headed rattlesnake",
+ "scientific": "Crotalus intermedius gloydi"
+ },
+ {
+ "name": "Okinawan habu",
+ "scientific": "Okinawan habu"
+ },
+ {
+ "name": "Olive sea snake",
+ "scientific": "Aipysurus laevis"
+ },
+ {
+ "name": "Opheodrys",
+ "scientific": "Opheodrys"
+ },
+ {
+ "name": "Orange-collared keelback",
+ "scientific": "Rhabdophis himalayanus"
+ },
+ {
+ "name": "Ornate flying snake",
+ "scientific": "Chrysopelea ornata"
+ },
+ {
+ "name": "Oxybelis",
+ "scientific": "Oxybelis"
+ },
+ {
+ "name": "Palestine viper",
+ "scientific": "Vipera palaestinae"
+ },
+ {
+ "name": "Paradise flying snake",
+ "scientific": "Chrysopelea paradisi"
+ },
+ {
+ "name": "Parrot snake",
+ "scientific": "Leptophis ahaetulla"
+ },
+ {
+ "name": "Patchnose snake",
+ "scientific": "Salvadora (snake)"
+ },
+ {
+ "name": "Pelagic sea snake",
+ "scientific": "Yellow-bellied sea snake"
+ },
+ {
+ "name": "Peninsula tiger snake",
+ "scientific": "Tiger snake"
+ },
+ {
+ "name": "Perrotet's shieldtail snake",
+ "scientific": "Plectrurus perrotetii"
+ },
+ {
+ "name": "Persian rat snake",
+ "scientific": "Rat snake"
+ },
+ {
+ "name": "Pine snake",
+ "scientific": "Pine snake"
+ },
+ {
+ "name": "Pit viper",
+ "scientific": "Pit viper"
+ },
+ {
+ "name": "Plains hognose snake",
+ "scientific": "Western hognose snake"
+ },
+ {
+ "name": "Prairie kingsnake",
+ "scientific": "Lampropeltis calligaster"
+ },
+ {
+ "name": "Pygmy python",
+ "scientific": "Pygmy python"
+ },
+ {
+ "name": "Pythonidae",
+ "scientific": "Pythonidae"
+ },
+ {
+ "name": "Queen snake",
+ "scientific": "Queen snake"
+ },
+ {
+ "name": "Rat snake",
+ "scientific": "Rat snake"
+ },
+ {
+ "name": "Rattler",
+ "scientific": "Rattlesnake"
+ },
+ {
+ "name": "Rattlesnake",
+ "scientific": "Rattlesnake"
+ },
+ {
+ "name": "Red-bellied black snake",
+ "scientific": "Red-bellied black snake"
+ },
+ {
+ "name": "Red-headed krait",
+ "scientific": "Red-headed krait"
+ },
+ {
+ "name": "Red-necked keelback",
+ "scientific": "Rhabdophis subminiatus"
+ },
+ {
+ "name": "Red-tailed bamboo pitviper",
+ "scientific": "Trimeresurus erythrurus"
+ },
+ {
+ "name": "Red-tailed boa",
+ "scientific": "Boa constrictor"
+ },
+ {
+ "name": "Red-tailed pipe snake",
+ "scientific": "Cylindrophis ruffus"
+ },
+ {
+ "name": "Red blood python",
+ "scientific": "Python brongersmai"
+ },
+ {
+ "name": "Red diamond rattlesnake",
+ "scientific": "Crotalus ruber"
+ },
+ {
+ "name": "Reticulated python",
+ "scientific": "Reticulated python"
+ },
+ {
+ "name": "Ribbon snake",
+ "scientific": "Ribbon snake"
+ },
+ {
+ "name": "Ringed hognose snake",
+ "scientific": "Hognose"
+ },
+ {
+ "name": "Rosy boa",
+ "scientific": "Rosy boa"
+ },
+ {
+ "name": "Rough green snake",
+ "scientific": "Opheodrys aestivus"
+ },
+ {
+ "name": "Rubber boa",
+ "scientific": "Rubber boa"
+ },
+ {
+ "name": "Rufous beaked snake",
+ "scientific": "Rufous beaked snake"
+ },
+ {
+ "name": "Russell's viper",
+ "scientific": "Russell's viper"
+ },
+ {
+ "name": "San Francisco garter snake",
+ "scientific": "San Francisco garter snake"
+ },
+ {
+ "name": "Sand boa",
+ "scientific": "Erycinae"
+ },
+ {
+ "name": "Sand viper",
+ "scientific": "Sand viper"
+ },
+ {
+ "name": "Saw-scaled viper",
+ "scientific": "Echis"
+ },
+ {
+ "name": "Scarlet kingsnake",
+ "scientific": "Scarlet kingsnake"
+ },
+ {
+ "name": "Sea snake",
+ "scientific": "Hydrophiinae"
+ },
+ {
+ "name": "Selayer reticulated python",
+ "scientific": "Reticulated python"
+ },
+ {
+ "name": "Shield-nosed cobra",
+ "scientific": "Shield-nosed cobra"
+ },
+ {
+ "name": "Shield-tailed snake",
+ "scientific": "Uropeltidae"
+ },
+ {
+ "name": "Sikkim keelback",
+ "scientific": "Sikkim keelback"
+ },
+ {
+ "name": "Sind krait",
+ "scientific": "Sind krait"
+ },
+ {
+ "name": "Smooth green snake",
+ "scientific": "Smooth green snake"
+ },
+ {
+ "name": "South American hognose snake",
+ "scientific": "Hognose"
+ },
+ {
+ "name": "South Andaman krait",
+ "scientific": "South Andaman krait"
+ },
+ {
+ "name": "South eastern corn snake",
+ "scientific": "Corn snake"
+ },
+ {
+ "name": "Southern Pacific rattlesnake",
+ "scientific": "Crotalus oreganus helleri"
+ },
+ {
+ "name": "Southern black racer",
+ "scientific": "Southern black racer"
+ },
+ {
+ "name": "Southern hognose snake",
+ "scientific": "Southern hognose snake"
+ },
+ {
+ "name": "Southern white-lipped python",
+ "scientific": "Leiopython"
+ },
+ {
+ "name": "Southwestern blackhead snake",
+ "scientific": "Tantilla hobartsmithi"
+ },
+ {
+ "name": "Southwestern carpet python",
+ "scientific": "Morelia spilota imbricata"
+ },
+ {
+ "name": "Southwestern speckled rattlesnake",
+ "scientific": "Crotalus mitchellii pyrrhus"
+ },
+ {
+ "name": "Speckled hognose snake",
+ "scientific": "Hognose"
+ },
+ {
+ "name": "Speckled kingsnake",
+ "scientific": "Lampropeltis getula holbrooki"
+ },
+ {
+ "name": "Spectacled cobra",
+ "scientific": "Indian cobra"
+ },
+ {
+ "name": "Sri Lanka cat snake",
+ "scientific": "Boiga ceylonensis"
+ },
+ {
+ "name": "Stiletto snake",
+ "scientific": "Atractaspidinae"
+ },
+ {
+ "name": "Stimson's python",
+ "scientific": "Stimson's python"
+ },
+ {
+ "name": "Striped snake",
+ "scientific": "Japanese striped snake"
+ },
+ {
+ "name": "Sumatran short-tailed python",
+ "scientific": "Python curtus"
+ },
+ {
+ "name": "Sunbeam snake",
+ "scientific": "Xenopeltis"
+ },
+ {
+ "name": "Taipan",
+ "scientific": "Taipan"
+ },
+ {
+ "name": "Tan racer",
+ "scientific": "Coluber constrictor etheridgei"
+ },
+ {
+ "name": "Tancitaran dusky rattlesnake",
+ "scientific": "Crotalus pusillus"
+ },
+ {
+ "name": "Tanimbar python",
+ "scientific": "Reticulated python"
+ },
+ {
+ "name": "Tasmanian tiger snake",
+ "scientific": "Tiger snake"
+ },
+ {
+ "name": "Tawny cat snake",
+ "scientific": "Boiga ochracea"
+ },
+ {
+ "name": "Temple pit viper",
+ "scientific": "Pit viper"
+ },
+ {
+ "name": "Tentacled snake",
+ "scientific": "Erpeton tentaculatum"
+ },
+ {
+ "name": "Texas Coral Snake",
+ "scientific": "Coral snake"
+ },
+ {
+ "name": "Texas blind snake",
+ "scientific": "Leptotyphlops dulcis"
+ },
+ {
+ "name": "Texas garter snake",
+ "scientific": "Texas garter snake"
+ },
+ {
+ "name": "Texas lyre snake",
+ "scientific": "Trimorphodon biscutatus vilkinsonii"
+ },
+ {
+ "name": "Texas night snake",
+ "scientific": "Hypsiglena jani"
+ },
+ {
+ "name": "Thai cobra",
+ "scientific": "King cobra"
+ },
+ {
+ "name": "Three-lined ground snake",
+ "scientific": "Atractus trilineatus"
+ },
+ {
+ "name": "Tic polonga",
+ "scientific": "Russell's viper"
+ },
+ {
+ "name": "Tiger rattlesnake",
+ "scientific": "Crotalus tigris"
+ },
+ {
+ "name": "Tiger snake",
+ "scientific": "Tiger snake"
+ },
+ {
+ "name": "Tigre snake",
+ "scientific": "Spilotes pullatus"
+ },
+ {
+ "name": "Timber rattlesnake",
+ "scientific": "Timber rattlesnake"
+ },
+ {
+ "name": "Tree snake",
+ "scientific": "Brown tree snake"
+ },
+ {
+ "name": "Tri-color hognose snake",
+ "scientific": "Hognose"
+ },
+ {
+ "name": "Trinket snake",
+ "scientific": "Trinket snake"
+ },
+ {
+ "name": "Tropical rattlesnake",
+ "scientific": "Crotalus durissus"
+ },
+ {
+ "name": "Twig snake",
+ "scientific": "Twig snake"
+ },
+ {
+ "name": "Twin-Barred tree snake",
+ "scientific": "Banded flying snake"
+ },
+ {
+ "name": "Twin-spotted rat snake",
+ "scientific": "Rat snake"
+ },
+ {
+ "name": "Twin-spotted rattlesnake",
+ "scientific": "Crotalus pricei"
+ },
+ {
+ "name": "Uracoan rattlesnake",
+ "scientific": "Crotalus durissus vegrandis"
+ },
+ {
+ "name": "Viperidae",
+ "scientific": "Viperidae"
+ },
+ {
+ "name": "Wall's keelback",
+ "scientific": "Amphiesma xenura"
+ },
+ {
+ "name": "Wart snake",
+ "scientific": "Acrochordidae"
+ },
+ {
+ "name": "Water adder",
+ "scientific": "Agkistrodon piscivorus"
+ },
+ {
+ "name": "Water moccasin",
+ "scientific": "Agkistrodon piscivorus"
+ },
+ {
+ "name": "West Indian racer",
+ "scientific": "Antiguan racer"
+ },
+ {
+ "name": "Western blind snake",
+ "scientific": "Leptotyphlops humilis"
+ },
+ {
+ "name": "Western carpet python",
+ "scientific": "Morelia spilota"
+ },
+ {
+ "name": "Western coral snake",
+ "scientific": "Coral snake"
+ },
+ {
+ "name": "Western diamondback rattlesnake",
+ "scientific": "Western diamondback rattlesnake"
+ },
+ {
+ "name": "Western green mamba",
+ "scientific": "Western green mamba"
+ },
+ {
+ "name": "Western ground snake",
+ "scientific": "Western ground snake"
+ },
+ {
+ "name": "Western hognose snake",
+ "scientific": "Western hognose snake"
+ },
+ {
+ "name": "Western mud snake",
+ "scientific": "Mud snake"
+ },
+ {
+ "name": "Western tiger snake",
+ "scientific": "Tiger snake"
+ },
+ {
+ "name": "Western woma python",
+ "scientific": "Woma python"
+ },
+ {
+ "name": "White-lipped keelback",
+ "scientific": "Amphiesma leucomystax"
+ },
+ {
+ "name": "Wolf snake",
+ "scientific": "Lycodon capucinus"
+ },
+ {
+ "name": "Woma python",
+ "scientific": "Woma python"
+ },
+ {
+ "name": "Wutu",
+ "scientific": "Bothrops alternatus"
+ },
+ {
+ "name": "Wynaad keelback",
+ "scientific": "Amphiesma monticola"
+ },
+ {
+ "name": "Yellow-banded sea snake",
+ "scientific": "Yellow-bellied sea snake"
+ },
+ {
+ "name": "Yellow-bellied sea snake",
+ "scientific": "Yellow-bellied sea snake"
+ },
+ {
+ "name": "Yellow-lipped sea snake",
+ "scientific": "Yellow-lipped sea krait"
+ },
+ {
+ "name": "Yellow-striped rat snake",
+ "scientific": "Rat snake"
+ },
+ {
+ "name": "Yellow anaconda",
+ "scientific": "Yellow anaconda"
+ },
+ {
+ "name": "Yellow cobra",
+ "scientific": "Cape cobra"
+ },
+ {
+ "name": "Yunnan keelback",
+ "scientific": "Amphiesma parallelum"
+ },
+ {
+ "name": "Abaco Island boa",
+ "scientific": "Epicrates exsul"
+ },
+ {
+ "name": "Agkistrodon bilineatus",
+ "scientific": "Agkistrodon bilineatus"
+ },
+ {
+ "name": "Amazon tree boa",
+ "scientific": "Corallus hortulanus"
+ },
+ {
+ "name": "Andaman cobra",
+ "scientific": "Andaman cobra"
+ },
+ {
+ "name": "Angolan python",
+ "scientific": "Python anchietae"
+ },
+ {
+ "name": "Arabian cobra",
+ "scientific": "Arabian cobra"
+ },
+ {
+ "name": "Asp viper",
+ "scientific": "Vipera aspis"
+ },
+ {
+ "name": "Ball Python",
+ "scientific": "Ball python"
+ },
+ {
+ "name": "Ball python",
+ "scientific": "Ball python"
+ },
+ {
+ "name": "Bamboo pitviper",
+ "scientific": "Trimeresurus gramineus"
+ },
+ {
+ "name": "Banded pitviper",
+ "scientific": "Trimeresurus fasciatus"
+ },
+ {
+ "name": "Banded water cobra",
+ "scientific": "Naja annulata"
+ },
+ {
+ "name": "Barbour's pit viper",
+ "scientific": "Mixcoatlus barbouri"
+ },
+ {
+ "name": "Bismarck ringed python",
+ "scientific": "Bothrochilus"
+ },
+ {
+ "name": "Black-speckled palm-pitviper",
+ "scientific": "Bothriechis nigroviridis"
+ },
+ {
+ "name": "Bluntnose viper",
+ "scientific": "Macrovipera lebetina"
+ },
+ {
+ "name": "Bornean pitviper",
+ "scientific": "Trimeresurus borneensis"
+ },
+ {
+ "name": "Borneo short-tailed python",
+ "scientific": "Borneo python"
+ },
+ {
+ "name": "Bothrops jararacussu",
+ "scientific": "Bothrops jararacussu"
+ },
+ {
+ "name": "Bredl's python",
+ "scientific": "Morelia bredli"
+ },
+ {
+ "name": "Brongersma's pitviper",
+ "scientific": "Trimeresurus brongersmai"
+ },
+ {
+ "name": "Brown spotted pitviper",
+ "scientific": "Trimeresurus mucrosquamatus"
+ },
+ {
+ "name": "Brown water python",
+ "scientific": "Liasis fuscus"
+ },
+ {
+ "name": "Burrowing cobra",
+ "scientific": "Egyptian cobra"
+ },
+ {
+ "name": "Bush viper",
+ "scientific": "Atheris"
+ },
+ {
+ "name": "Calabar python",
+ "scientific": "Calabar python"
+ },
+ {
+ "name": "Caspian cobra",
+ "scientific": "Caspian cobra"
+ },
+ {
+ "name": "Centralian carpet python",
+ "scientific": "Morelia bredli"
+ },
+ {
+ "name": "Chinese tree viper",
+ "scientific": "Trimeresurus stejnegeri"
+ },
+ {
+ "name": "Coastal carpet python",
+ "scientific": "Morelia spilota mcdowelli"
+ },
+ {
+ "name": "Colorado desert sidewinder",
+ "scientific": "Crotalus cerastes laterorepens"
+ },
+ {
+ "name": "Common lancehead",
+ "scientific": "Bothrops atrox"
+ },
+ {
+ "name": "Cyclades blunt-nosed viper",
+ "scientific": "Macrovipera schweizeri"
+ },
+ {
+ "name": "Dauan Island water python",
+ "scientific": "Liasis fuscus"
+ },
+ {
+ "name": "De Schauensee's anaconda",
+ "scientific": "Eunectes deschauenseei"
+ },
+ {
+ "name": "Dumeril's boa",
+ "scientific": "Acrantophis dumerili"
+ },
+ {
+ "name": "Dusky pigmy rattlesnake",
+ "scientific": "Sistrurus miliarius barbouri"
+ },
+ {
+ "name": "Dwarf sand adder",
+ "scientific": "Bitis peringueyi"
+ },
+ {
+ "name": "Egyptian cobra",
+ "scientific": "Egyptian cobra"
+ },
+ {
+ "name": "Elegant pitviper",
+ "scientific": "Trimeresurus elegans"
+ },
+ {
+ "name": "Emerald tree boa",
+ "scientific": "Emerald tree boa"
+ },
+ {
+ "name": "Equatorial spitting cobra",
+ "scientific": "Equatorial spitting cobra"
+ },
+ {
+ "name": "European asp",
+ "scientific": "Vipera aspis"
+ },
+ {
+ "name": "Eyelash palm-pitviper",
+ "scientific": "Bothriechis schlegelii"
+ },
+ {
+ "name": "Eyelash pit viper",
+ "scientific": "Bothriechis schlegelii"
+ },
+ {
+ "name": "Eyelash viper",
+ "scientific": "Bothriechis schlegelii"
+ },
+ {
+ "name": "False horned viper",
+ "scientific": "Pseudocerastes"
+ },
+ {
+ "name": "Fan-Si-Pan horned pitviper",
+ "scientific": "Trimeresurus cornutus"
+ },
+ {
+ "name": "Fea's viper",
+ "scientific": "Azemiops"
+ },
+ {
+ "name": "Fifty pacer",
+ "scientific": "Deinagkistrodon"
+ },
+ {
+ "name": "Flat-nosed pitviper",
+ "scientific": "Trimeresurus puniceus"
+ },
+ {
+ "name": "Godman's pit viper",
+ "scientific": "Cerrophidion godmani"
+ },
+ {
+ "name": "Great Lakes bush viper",
+ "scientific": "Atheris nitschei"
+ },
+ {
+ "name": "Green palm viper",
+ "scientific": "Bothriechis lateralis"
+ },
+ {
+ "name": "Green tree pit viper",
+ "scientific": "Trimeresurus gramineus"
+ },
+ {
+ "name": "Guatemalan palm viper",
+ "scientific": "Bothriechis aurifer"
+ },
+ {
+ "name": "Guatemalan tree viper",
+ "scientific": "Bothriechis bicolor"
+ },
+ {
+ "name": "Hagen's pitviper",
+ "scientific": "Trimeresurus hageni"
+ },
+ {
+ "name": "Hairy bush viper",
+ "scientific": "Atheris hispida"
+ },
+ {
+ "name": "Himehabu",
+ "scientific": "Ovophis okinavensis"
+ },
+ {
+ "name": "Hogg Island boa",
+ "scientific": "Boa constrictor imperator"
+ },
+ {
+ "name": "Honduran palm viper",
+ "scientific": "Bothriechis marchi"
+ },
+ {
+ "name": "Horned desert viper",
+ "scientific": "Cerastes cerastes"
+ },
+ {
+ "name": "Horseshoe pitviper",
+ "scientific": "Trimeresurus strigatus"
+ },
+ {
+ "name": "Hundred pacer",
+ "scientific": "Deinagkistrodon"
+ },
+ {
+ "name": "Hutton's tree viper",
+ "scientific": "Tropidolaemus huttoni"
+ },
+ {
+ "name": "Indian python",
+ "scientific": "Python molurus"
+ },
+ {
+ "name": "Indian tree viper",
+ "scientific": "Trimeresurus gramineus"
+ },
+ {
+ "name": "Indochinese spitting cobra",
+ "scientific": "Indochinese spitting cobra"
+ },
+ {
+ "name": "Indonesian water python",
+ "scientific": "Liasis mackloti"
+ },
+ {
+ "name": "Javan spitting cobra",
+ "scientific": "Javan spitting cobra"
+ },
+ {
+ "name": "Jerdon's pitviper",
+ "scientific": "Trimeresurus jerdonii"
+ },
+ {
+ "name": "Jumping viper",
+ "scientific": "Atropoides"
+ },
+ {
+ "name": "Jungle carpet python",
+ "scientific": "Morelia spilota cheynei"
+ },
+ {
+ "name": "Kanburian pit viper",
+ "scientific": "Trimeresurus kanburiensis"
+ },
+ {
+ "name": "Kaulback's lance-headed pitviper",
+ "scientific": "Trimeresurus kaulbacki"
+ },
+ {
+ "name": "Kaznakov's viper",
+ "scientific": "Vipera kaznakovi"
+ },
+ {
+ "name": "Kham Plateau pitviper",
+ "scientific": "Protobothrops xiangchengensis"
+ },
+ {
+ "name": "Lachesis (genus)",
+ "scientific": "Lachesis (genus)"
+ },
+ {
+ "name": "Large-eyed pitviper",
+ "scientific": "Trimeresurus macrops"
+ },
+ {
+ "name": "Large-scaled tree viper",
+ "scientific": "Trimeresurus macrolepis"
+ },
+ {
+ "name": "Leaf-nosed viper",
+ "scientific": "Eristicophis"
+ },
+ {
+ "name": "Leaf viper",
+ "scientific": "Atheris squamigera"
+ },
+ {
+ "name": "Levant viper",
+ "scientific": "Macrovipera lebetina"
+ },
+ {
+ "name": "Long-nosed viper",
+ "scientific": "Vipera ammodytes"
+ },
+ {
+ "name": "Macklot's python",
+ "scientific": "Liasis mackloti"
+ },
+ {
+ "name": "Madagascar tree boa",
+ "scientific": "Sanzinia"
+ },
+ {
+ "name": "Malabar rock pitviper",
+ "scientific": "Trimeresurus malabaricus"
+ },
+ {
+ "name": "Malcolm's tree viper",
+ "scientific": "Trimeresurus sumatranus malcolmi"
+ },
+ {
+ "name": "Mandalay cobra",
+ "scientific": "Mandalay spitting cobra"
+ },
+ {
+ "name": "Mangrove pit viper",
+ "scientific": "Trimeresurus purpureomaculatus"
+ },
+ {
+ "name": "Mangshan pitviper",
+ "scientific": "Trimeresurus mangshanensis"
+ },
+ {
+ "name": "McMahon's viper",
+ "scientific": "Eristicophis"
+ },
+ {
+ "name": "Mexican palm-pitviper",
+ "scientific": "Bothriechis rowleyi"
+ },
+ {
+ "name": "Monocled cobra",
+ "scientific": "Monocled cobra"
+ },
+ {
+ "name": "Motuo bamboo pitviper",
+ "scientific": "Trimeresurus medoensis"
+ },
+ {
+ "name": "Mozambique spitting cobra",
+ "scientific": "Mozambique spitting cobra"
+ },
+ {
+ "name": "Namaqua dwarf adder",
+ "scientific": "Bitis schneideri"
+ },
+ {
+ "name": "Namib dwarf sand adder",
+ "scientific": "Bitis peringueyi"
+ },
+ {
+ "name": "New Guinea carpet python",
+ "scientific": "Morelia spilota variegata"
+ },
+ {
+ "name": "Nicobar bamboo pitviper",
+ "scientific": "Trimeresurus labialis"
+ },
+ {
+ "name": "Nitsche's bush viper",
+ "scientific": "Atheris nitschei"
+ },
+ {
+ "name": "Nitsche's tree viper",
+ "scientific": "Atheris nitschei"
+ },
+ {
+ "name": "Northwestern carpet python",
+ "scientific": "Morelia spilota variegata"
+ },
+ {
+ "name": "Nubian spitting cobra",
+ "scientific": "Nubian spitting cobra"
+ },
+ {
+ "name": "Oenpelli python",
+ "scientific": "Oenpelli python"
+ },
+ {
+ "name": "Olive python",
+ "scientific": "Liasis olivaceus"
+ },
+ {
+ "name": "Pallas' viper",
+ "scientific": "Gloydius halys"
+ },
+ {
+ "name": "Palm viper",
+ "scientific": "Bothriechis lateralis"
+ },
+ {
+ "name": "Papuan python",
+ "scientific": "Apodora"
+ },
+ {
+ "name": "Peringuey's adder",
+ "scientific": "Bitis peringueyi"
+ },
+ {
+ "name": "Philippine cobra",
+ "scientific": "Philippine cobra"
+ },
+ {
+ "name": "Philippine pitviper",
+ "scientific": "Trimeresurus flavomaculatus"
+ },
+ {
+ "name": "Pope's tree viper",
+ "scientific": "Trimeresurus popeorum"
+ },
+ {
+ "name": "Portuguese viper",
+ "scientific": "Vipera seoanei"
+ },
+ {
+ "name": "Puerto Rican boa",
+ "scientific": "Puerto Rican boa"
+ },
+ {
+ "name": "Rainbow boa",
+ "scientific": "Rainbow boa"
+ },
+ {
+ "name": "Red spitting cobra",
+ "scientific": "Red spitting cobra"
+ },
+ {
+ "name": "Rhinoceros viper",
+ "scientific": "Bitis nasicornis"
+ },
+ {
+ "name": "Rhombic night adder",
+ "scientific": "Causus maculatus"
+ },
+ {
+ "name": "Rinkhals",
+ "scientific": "Rinkhals"
+ },
+ {
+ "name": "Rinkhals cobra",
+ "scientific": "Rinkhals"
+ },
+ {
+ "name": "River jack",
+ "scientific": "Bitis nasicornis"
+ },
+ {
+ "name": "Rough-scaled bush viper",
+ "scientific": "Atheris hispida"
+ },
+ {
+ "name": "Rough-scaled python",
+ "scientific": "Rough-scaled python"
+ },
+ {
+ "name": "Rough-scaled tree viper",
+ "scientific": "Atheris hispida"
+ },
+ {
+ "name": "Royal python",
+ "scientific": "Ball python"
+ },
+ {
+ "name": "Rungwe tree viper",
+ "scientific": "Atheris nitschei rungweensis"
+ },
+ {
+ "name": "Sakishima habu",
+ "scientific": "Trimeresurus elegans"
+ },
+ {
+ "name": "Savu python",
+ "scientific": "Liasis mackloti savuensis"
+ },
+ {
+ "name": "Schlegel's viper",
+ "scientific": "Bothriechis schlegelii"
+ },
+ {
+ "name": "Schultze's pitviper",
+ "scientific": "Trimeresurus schultzei"
+ },
+ {
+ "name": "Sedge viper",
+ "scientific": "Atheris nitschei"
+ },
+ {
+ "name": "Sharp-nosed viper",
+ "scientific": "Deinagkistrodon"
+ },
+ {
+ "name": "Siamese palm viper",
+ "scientific": "Trimeresurus puniceus"
+ },
+ {
+ "name": "Side-striped palm-pitviper",
+ "scientific": "Bothriechis lateralis"
+ },
+ {
+ "name": "Snorkel viper",
+ "scientific": "Deinagkistrodon"
+ },
+ {
+ "name": "Snouted cobra",
+ "scientific": "Snouted cobra"
+ },
+ {
+ "name": "Sonoran sidewinder",
+ "scientific": "Crotalus cerastes cercobombus"
+ },
+ {
+ "name": "Southern Indonesian spitting cobra",
+ "scientific": "Javan spitting cobra"
+ },
+ {
+ "name": "Southern Philippine cobra",
+ "scientific": "Samar cobra"
+ },
+ {
+ "name": "Spiny bush viper",
+ "scientific": "Atheris hispida"
+ },
+ {
+ "name": "Spitting cobra",
+ "scientific": "Spitting cobra"
+ },
+ {
+ "name": "Spotted python",
+ "scientific": "Spotted python"
+ },
+ {
+ "name": "Sri Lankan pit viper",
+ "scientific": "Trimeresurus trigonocephalus"
+ },
+ {
+ "name": "Stejneger's bamboo pitviper",
+ "scientific": "Trimeresurus stejnegeri"
+ },
+ {
+ "name": "Storm water cobra",
+ "scientific": "Naja annulata"
+ },
+ {
+ "name": "Sumatran tree viper",
+ "scientific": "Trimeresurus sumatranus"
+ },
+ {
+ "name": "Temple viper",
+ "scientific": "Tropidolaemus wagleri"
+ },
+ {
+ "name": "Tibetan bamboo pitviper",
+ "scientific": "Trimeresurus tibetanus"
+ },
+ {
+ "name": "Tiger pit viper",
+ "scientific": "Trimeresurus kanburiensis"
+ },
+ {
+ "name": "Timor python",
+ "scientific": "Python timoriensis"
+ },
+ {
+ "name": "Tokara habu",
+ "scientific": "Trimeresurus tokarensis"
+ },
+ {
+ "name": "Tree boa",
+ "scientific": "Emerald tree boa"
+ },
+ {
+ "name": "Undulated pit viper",
+ "scientific": "Ophryacus undulatus"
+ },
+ {
+ "name": "Ursini's viper",
+ "scientific": "Vipera ursinii"
+ },
+ {
+ "name": "Wagler's pit viper",
+ "scientific": "Tropidolaemus wagleri"
+ },
+ {
+ "name": "West African brown spitting cobra",
+ "scientific": "Mozambique spitting cobra"
+ },
+ {
+ "name": "White-lipped tree viper",
+ "scientific": "Trimeresurus albolabris"
+ },
+ {
+ "name": "Wirot's pit viper",
+ "scientific": "Trimeresurus puniceus"
+ },
+ {
+ "name": "Yellow-lined palm viper",
+ "scientific": "Bothriechis lateralis"
+ },
+ {
+ "name": "Zebra spitting cobra",
+ "scientific": "Naja nigricincta"
+ },
+ {
+ "name": "Yarara",
+ "scientific": "Bothrops jararaca"
+ },
+ {
+ "name": "Wetar Island python",
+ "scientific": "Liasis macklot"
+ },
+ {
+ "name": "Urutus",
+ "scientific": "Bothrops alternatus"
+ },
+ {
+ "name": "Titanboa",
+ "scientific": "Titanoboa"
+ }
+] \ No newline at end of file
diff --git a/bot/resources/snakes/snake_quiz.json b/bot/resources/snakes/snake_quiz.json
new file mode 100644
index 00000000..8c426b22
--- /dev/null
+++ b/bot/resources/snakes/snake_quiz.json
@@ -0,0 +1,200 @@
+[
+ {
+ "id": 0,
+ "question": "How long have snakes been roaming the Earth for?",
+ "options": {
+ "a": "3 million years",
+ "b": "30 million years",
+ "c": "130 million years",
+ "d": "200 million years"
+ },
+ "answerkey": "c"
+ },
+ {
+ "id": 1,
+ "question": "What characteristics do all snakes share?",
+ "options": {
+ "a": "They are carnivoes",
+ "b": "They are all programming languages",
+ "c": "They're all cold-blooded",
+ "d": "They are both carnivores and cold-blooded"
+ },
+ "answerkey": "c"
+ },
+ {
+ "id": 2,
+ "question": "How do snakes hear?",
+ "options": {
+ "a": "With small ears",
+ "b": "Through their skin",
+ "c": "Through their tail",
+ "d": "They don't use their ears at all"
+ },
+ "answerkey": "b"
+ },
+ {
+ "id": 3,
+ "question": "What can't snakes see?",
+ "options": {
+ "a": "Colour",
+ "b": "Light",
+ "c": "Both of the above",
+ "d": "Other snakes"
+ },
+ "answerkey": "a"
+ },
+ {
+ "id": 4,
+ "question": "What unique vision ability do boas and pythons possess?",
+ "options": {
+ "a": "Night vision",
+ "b": "Infrared vision",
+ "c": "See through walls",
+ "d": "They don't have vision"
+ },
+ "answerkey": "b"
+ },
+ {
+ "id": 5,
+ "question": "How does a snake smell?",
+ "options": {
+ "a": "Quite pleasant",
+ "b": "Through its nose",
+ "c": "Through its tongues",
+ "d": "Both through its nose and its tongues"
+ },
+ "answerkey": "d"
+ },
+ {
+ "id": 6,
+ "question": "Where are Jacobson's organs located in snakes?",
+ "options": {
+ "a": "Mouth",
+ "b": "Tail",
+ "c": "Stomach",
+ "d": "Liver"
+ },
+ "answerkey": "a"
+ },
+ {
+ "id": 7,
+ "question": "Snakes have very similar internal organs compared to humans. Snakes, however; lack the following:",
+ "options": {
+ "a": "A diaphragm",
+ "b": "Intestines",
+ "c": "Lungs",
+ "d": "Kidney"
+ },
+ "answerkey": "a"
+ },
+ {
+ "id": 8,
+ "question": "Snakes have different shaped lungs than humans. What do snakes have?",
+ "options": {
+ "a": "An elongated right lung",
+ "b": "A small left lung",
+ "c": "Both of the above",
+ "d": "None of the above"
+ },
+ "answerkey": "c"
+ },
+ {
+ "id": 9,
+ "question": "What's true about two-headed snakes?",
+ "options": {
+ "a": "They're a myth!",
+ "b": "They rarely survive in the wild",
+ "c": "They're very dangerous",
+ "d": "They can kiss each other"
+ },
+ "answerkey": "b"
+ },
+ {
+ "id": 10,
+ "question": "What substance covers a snake's skin?",
+ "options": {
+ "a": "Calcium",
+ "b": "Keratin",
+ "c": "Copper",
+ "d": "Iron"
+ },
+ "answerkey": "b"
+ },
+ {
+ "id": 11,
+ "question": "What snake doesn't have to have a mate to lay eggs?",
+ "options": {
+ "a": "Copperhead",
+ "b": "Cornsnake",
+ "c": "Kingsnake",
+ "d": "Flower pot snake"
+ },
+ "answerkey": "d"
+ },
+ {
+ "id": 12,
+ "question": "What snake is the longest?",
+ "options": {
+ "a": "Green anaconda",
+ "b": "Reticulated python",
+ "c": "King cobra",
+ "d": "Kingsnake"
+ },
+ "answerkey": "b"
+ },
+ {
+ "id": 13,
+ "question": "Though invasive species can now be found in the Everglades, in which three continents are pythons (members of the family Pythonidae) found in the wild?",
+ "options": {
+ "a": "Africa, Asia and Australia",
+ "b": "Africa, Australia and Europe",
+ "c": "Africa, Australia and South America",
+ "d": "Africa, Asia and South America"
+ },
+ "answerkey": "a"
+ },
+ {
+ "id": 14,
+ "question": "Pythons are held as some of the most dangerous snakes on earth, but are often confused with anacondas. Which of these is *not* a difference between pythons and anacondas?",
+ "options": {
+ "a": "Pythons suffocate their prey, anacondas crush them",
+ "b": "Pythons lay eggs, anacondas give birth to live young",
+ "c": "Pythons grow longer, anacondas grow heavier",
+ "d": "Pythons generally spend less time in water than anacondas do"
+ },
+ "answerkey": "a"
+ },
+ {
+ "id": 15,
+ "question": "Pythons are unable to chew their food, and so swallow prey whole. Which of these methods is most commonly demonstrated to help a python to swallow large prey?",
+ "options": {
+ "a": "The python's stomach pressure is reduced, so prey is sucked in",
+ "b": "An extra set of upper teeth 'walk' along the prey",
+ "c": "The python holds its head up, so prey falls into its stomach",
+ "d": "Prey is pushed against a barrier and is forced down the python's throat"
+ },
+ "answerkey": "b"
+ },
+ {
+ "id": 16,
+ "question": "Pythons, like many large constrictors, possess vestigial hind limbs. Whilst these 'spurs' serve no purpose in locomotion, how are they put to use by some male pythons? ",
+ "options": {
+ "a": "To store sperm",
+ "b": "To release pheromones",
+ "c": "To grip females during mating",
+ "d": "To fight off rival males"
+ },
+ "answerkey": "c"
+ },
+ {
+ "id": 17,
+ "question": "Pythons tend to travel by the rectilinear method (in straight lines) when on land, as opposed to the concertina method (s-shaped movement). Why do large pythons tend not to use the concertina method? ",
+ "options": {
+ "a": "Their spine is too inflexible",
+ "b": "They move too slowly",
+ "c": "The scales on their backs are too rigid",
+ "d": "They are too heavy"
+ },
+ "answerkey": "d"
+ }
+]
diff --git a/bot/resources/snakes/snakes_and_ladders/banner.jpg b/bot/resources/snakes/snakes_and_ladders/banner.jpg
new file mode 100644
index 00000000..69eaaf12
--- /dev/null
+++ b/bot/resources/snakes/snakes_and_ladders/banner.jpg
Binary files differ
diff --git a/bot/resources/snakes/snakes_and_ladders/board.jpg b/bot/resources/snakes/snakes_and_ladders/board.jpg
new file mode 100644
index 00000000..20032e39
--- /dev/null
+++ b/bot/resources/snakes/snakes_and_ladders/board.jpg
Binary files differ
diff --git a/bot/resources/snakes/special_snakes.json b/bot/resources/snakes/special_snakes.json
new file mode 100644
index 00000000..8159f914
--- /dev/null
+++ b/bot/resources/snakes/special_snakes.json
@@ -0,0 +1,16 @@
+[
+ {
+ "name": "Bob Ross",
+ "info": "Robert Norman Ross (October 29, 1942 – July 4, 1995) was an American painter, art instructor, and television host. He was the creator and host of The Joy of Painting, an instructional television program that aired from 1983 to 1994 on PBS in the United States, and also aired in Canada, Latin America, and Europe.",
+ "image_list": [
+ "https://d3atagt0rnqk7k.cloudfront.net/wp-content/uploads/2016/09/23115633/bob-ross-1-1280x800.jpg"
+ ]
+ },
+ {
+ "name": "Mystery Snake",
+ "info": "The Mystery Snake is rumored to be a thin, serpentine creature that hides in spaghetti dinners. It has yellow, pasta-like scales with a completely smooth texture, and is quite glossy. ",
+ "image_list": [
+ "https://img.thrfun.com/img/080/349/spaghetti_dinner_l1.jpg"
+ ]
+ }
+] \ No newline at end of file
diff --git a/bot/resources/valentines/bemyvalentine_valentines.json b/bot/resources/valentines/bemyvalentine_valentines.json
new file mode 100644
index 00000000..7d5d3705
--- /dev/null
+++ b/bot/resources/valentines/bemyvalentine_valentines.json
@@ -0,0 +1,45 @@
+{
+ "valentine_poems": [
+
+ "If you were my rose,\nthen I'd be your sun,\npainting you rainbows when the rains come.\nI'd change my orbit to banish the night,\nas to keep you in my nurturing light.",
+ "If you were my world, then I'd be your moon,\nyour silent protector, a night-light in the gloom.\nOur fates intertwined, two bodies in motion through time and space,\nour dance of devotion.",
+ "If you were my island, then I'd be your sea,\ncaressing your shores, soft and gentle I'd be.\nMy tidal embrace would leave gifts on your sands,\nbut by current and storm, I'd ward your gentle lands.",
+ "If you were love's promise, then I would be time,\nyour constant companion till stars align.\nAnd though we are mere mortals,\ntrue love is divine,and my devotion eternal,\nto my one valentine.",
+ "Have I told you,\nValentine, That I’m all wrapped up in you?\nMy feelings for you bring to me A joy I never knew.\n \n You light up everything for me; In my heart you shine;\nIlluminating my whole life,\nMy darling Valentine.",
+ "My days are filled with yearning;\nMy nights are full of dreams.\nI’m always thinking of you;\nI’m in a trance, it seems.\n\n You’re all I ever wanted;\nI wish you could be mine;\nAnd so I have to ask you: Will you be my Valentine?",
+ "My Valentine, I love just you;\nMy devotion I declare.\nI’ll spend my life looking for ways To show you that I care.\n\nPlease say you feel the same for me;\nSay you’ll be forever mine;\nWe’ll share a life of happiness,\nMy treasured Valentine.",
+ "Every day with you is Valentine's Day, my love.\nEvery day is filled with romance, with love, with sharing and caring.\nEvery day I am reminded how blessed I am to have you as my,\nValentine, my sweetheart, my lover, my friend, my playmate, my companion.\nNo Valentine card, no words at all could express how much I love You,\nhow ecstatic I feel to know that you are mine.\nMy Valentine, every day,\nI'll try to show you that every day I love you more.",
+ "I lucked out when I met you, sweet thing.\nYou've brought richness to each day I exist.\nYou fill my days with the excitement of love,\nAnd you have since the moment we kissed.\nSo I celebrate Valentine's Day with you,\nWith a love that will always stay fresh and new.",
+ "You are my everything, Valentine.\nAs a desert creature longs for water, my thirst for you can never be slaked.\nIn a ho-hum day dragging on, thoughts of you bring excitement, joy and pleasure.\nAs a child opens the birthday gift he most wanted,\nI see everything I want in you.\nYou are my everything, Valentine.",
+ "My love for you is like the raging sea,\nSo powerful and deep it will forever be.\nThrough storm, wind, and heavy rain, It will withstand every pain.\nOur hearts are so pure and love so sweet.\nI love you more with every heartbeat!",
+ "A million stars up in the sky.\nOne shines brighter - I can't deny.\nA love so precious, a love so true,\na love that comes from me to you.\nThe angels sing when you are near.\nWithin your arms I have nothing to fear.\nYou always know just what to say.\nJust talking to you makes my day.\nI love you, honey, with all of my heart.\nTogether forever and never to part.",
+ "What do I do,\nWhen I'm still in love with you?\nYou walked away, Cause you didn't want to stay.\nYou broke my heart, you tore me apart.\nEvery day I wait for you, Telling myself our love was true.\nBut when you don't show, more tears start to flow.\nThat's when I know I have to let go.",
+ "When I say I love you, please believe it's true.\nWhen I say forever, know I'll never leave you.\nWhen I say goodbye, promise me you won't cry,\nBecause the day I'll be saying that will be the day I die.",
+ "Beauty isn't seen by eyes.\nIt's felt by hearts,\nRecognized by souls,\nIn the presence of love.",
+ "L is for \"laughter\" we had along the way.\nO is for \"optimism\" you gave me every day.\nV is for \"value\" of being my best friend.\nE is for \"eternity,\" a love that has no end.",
+ "If roses were red and violets could be blue,\nI'd take us away to a place just for two.\nYou'd see my true colors and all that I felt.\nI'd see that you could love me and nobody else.\nWe'd build ourselves a fortress where we'd run and play.\nYou'd be mine and I'd be yours till our dying day.\nThen I wake and realize you were never here.\nIt's all just my thoughts, my dreams, my hopes...\nBut now it's only tears!"
+
+ ],
+ "valentine_compliments": [
+
+ "To the love of my life. I’m the luckiest person on Earth because I have you! I adore you! You’ve taught me that the best thing to hold onto in life is each other. You are my sweetheart, you are my life, you are my everything.",
+ "It was a million tiny little things that, when you added them all up, they meant we were supposed to be together.",
+ "When you smile, the whole world stops and stares for a while, cause you're amazing, just the way you are.",
+ "Take love, multiply it by infinity and take it to the depths of forever... and you still have only a glimpse of how I feel for you.",
+ "When you kiss me, the world just melts away. When you hold me in your arms, I feel safe. Being in love with you has made me see how wonderful life can be. I love you.",
+ "No matter how dark the days get, you still know how to make me smile. Even after all this time, you still take my breath away.",
+ "I don't know what my future holds, but I know I want you in it. I would travel to the moon and back just to see you smile.",
+ "I may not always say it, but know that with every breath in my body and every beat of my heart I know I will always love you.",
+ "Darling I will be loving you till we're 70. And baby my heart could still fall as hard at 23. And I'm thinking about how people fall in love in mysterious ways. Maybe just the touch of a hand. Oh me, I fall in love with you every single day. And I just wanna tell you I am. So honey now. Take me into your loving arms. Kiss me under the light of a thousand stars. Place your head on my beating heart. I'm thinking out loud. Maybe we found love right where we are.",
+ "I love you. I knew it the minute I met you. I'm sorry it took so long for me to catch up. I just got stuck.",
+ "You are truly beautiful from within. I am serious! It's not just about how pretty you are (which you are, of course), but you have a beautiful heart.",
+ "If you could see yourself through my eyes, you would know how much I love you. You hold a very special place in my heart! I will love you forever!",
+ "I don’t need a thousand reasons to feel special. All I need is you to live in this world. You are the sunshine of my life.",
+ "I wish to be everything that brings a smile on your face and happiness in your heart. I want to love you like no else ever did!",
+ "Every morning of my life gives you a new reason to love you and to appreciate you for what you’ve given me. You are the one that holds the key to my heart!",
+ "Each time I look at you, I just smile to myself and think, ‘I certainly could not have done better’. You are perfect the way you are. I love you honey.",
+ "Look at the computer keyboard, U and I were placed side by side. That’s how the alphabets should be arranged because my love will never cease to exist as long as it’s you and me."
+
+ ]
+
+}
diff --git a/bot/resources/valentines/date_ideas.json b/bot/resources/valentines/date_ideas.json
new file mode 100644
index 00000000..09d31067
--- /dev/null
+++ b/bot/resources/valentines/date_ideas.json
@@ -0,0 +1,127 @@
+{
+ "ideas": [
+ {
+ "name": "Chick flick marathon",
+ "description": "This is a very basic yet highly romantic way of spending the day with your partner. Take a few days to prepare the right playlist and create a romantic atmosphere at home. You can order out some food, open a bottle of wine and cuddle up in front of the TV."
+ },
+
+ {
+ "name": "Recreate your first date",
+ "description": "Rated pretty high on the “romantic gestures scale,” this is guaranteed to impress your significant other. It requires a good memory and a bit of work to make it just right, but it is well worth it. Walk down the same streets where you first kissed and have a couple of drinks in that old coffee shop where you had your first drinks together. Don’t be afraid to spend a bit extra and add a little romantic gift into the mix."
+ },
+ {
+ "name": "Cook for your loved one",
+ "description": "Start researching good recipes for a romantic dinner for two, get the right ingredients and prepare a couple of practice dinners to make sure you’ve got your technique and presentation down pat. Cooking for someone can be a big turn on and you can create some incredible meals without spending too much money. Take it up a notch by dressing classy, decorating your dining room and presenting your partner with a printed menu."
+ },
+ {
+ "name": "Organize your very own ancient Greek party",
+ "description": "Here’s another one of those creative date ideas for the stay-at-home couple. The ancient Greek private party can be a very fun and erotic experience. You can decorate by using big bowls full of grapes, spreading some white sheets all over the place, placing some plastic vines here and there, putting up a few posters depicting Greek parties and having plenty of wine lying around. Wear nothing but light sheets or costumes and channel some of that hot-blooded Greek spirit."
+ },
+ {
+ "name": "A romantic weekend getaway in the mountains",
+ "description": "For those looking for a change of scenery and an escape from the busy city, there is nothing better than a quiet, romantic weekend in the mountains. There are plenty of fun activities like skiing that will keep you active. You can have fun making a snowman or engaging in a snowball fight, and, of course, there is plenty of privacy and great room service waiting for you back at your room."
+ },
+ {
+ "name": "Fun day at the shooting range",
+ "description": "A bit unconventional but an incredibly fun and exciting date that will get your blood pumping and put a huge smile on your faces. Try out a number of guns and have a bit of a competition. Some outdoor ranges have fully automatic rifles, which are a blast to shoot."
+ },
+ {
+ "name": "Rent an expensive sports car for a day",
+ "description": "Don’t be afraid to live large from time to time—even if you can’t afford the glamorous lifestyle of the stars, you can most definitely play pretend for a day. Put on some classy clothes and drive around town in a rented sports car. The quick acceleration and high speed are sure to provide an exhilarating experience. "
+ },
+ {
+ "name": "Go on a shopping spree together",
+ "description": "Very few things can elicit such a huge dopamine rush as a good old shopping spree. Get some new sexy lingerie, pretty shoes, a nice shirt and tie, a couple of new video games or whatever else you need or want. This is a unique chance to bond, have fun and get some stuff that you’ve been waiting to buy for a while now."
+ },
+ {
+ "name": "Hit the clubs",
+ "description": "For all the party animals out there, one of the best date ideas is to go out drinking, dancing, and just generally enjoying the night life. Visit a few good clubs, then go to an after-party and keep that party spirit going for as long as you can."
+ },
+ {
+ "name": "Spend the day driving around the city and visiting new places",
+ "description": "This one is geared towards couples who have been together for a year or two and want to experience a few new things together. Visit a few cool coffee places on the other side of town, check out interesting restaurants you’ve never been to, and consider going to see a play or having fun at a comedy club on open mic night."
+ },
+ {
+ "name": "Wine and chocolates at sunset",
+ "description": "Pick out a romantic location, such as a camping spot on a hill overlooking the city or a balcony in a restaurant with a nice view, open a bottle of wine and a box of chocolates and wait for that perfect moment when the sky turns fiery red to embrace and share a passionate kiss."
+ },
+ {
+ "name": "Ice skating",
+ "description": "There is something incredibly fun about ice skating that brings people closer together and just keeps you laughing (maybe it’s all the falling and clinging to the other person for dear life). You can have some great fun and then move on to a more private location for some alone time."
+ },
+ {
+ "name": "Model clothes for each other",
+ "description": "This one goes well when combined with a shopping spree, but you can just get a bunch of your clothes—old and new—from the closet, set up a catwalk area and then try on different combinations. You can be stylish, funny, handsome and sexy. It’s a great after-dinner show and a good way to transition into a more intimate atmosphere."
+ },
+ {
+ "name": "Dance the night away",
+ "description": "If you and your significant other are relatively good dancers, or if you simply enjoy moving your body to the rhythm of the music, then a night at salsa club or similar venue is the perfect thing for you. Alternatively, you can set up dance floor at home, play your favorite music, have a few drinks and dance like there is no tomorrow."
+ },
+ {
+ "name": "Organize a nature walk",
+ "description": "Being outside has many health benefits, but what you are going for is the beautiful view, seclusion, and the thrill of engaging in some erotic behavior out in the open. You can rent a cottage far from the city, bring some food and drinks, and explore the wilderness. This is nice way to spice things up a bit and get away from the loud and busy city life."
+ },
+ {
+ "name": "Travel abroad",
+ "description": "This takes a bit of planning in advance and may be a bit costly, but if you can afford it, there are very few things that can match a trip to France, Italy, Egypt, Turkey, Greece, or a number of other excellent locations."
+ },
+ {
+ "name": "Go on a hot-air balloon ride",
+ "description": "These are very fun and romantic—you get an incredible view, get to experience the thrill of flying, and you’ve got enough room for a romantic dinner and some champagne. Just be sure to wear warm clothes, it can get pretty cold high up in the air."
+ },
+ {
+ "name": "A relaxing day at the spa",
+ "description": "Treat your body, mind and senses to a relaxing day at the spa. You and your partner will feel fresh, comfortable, relaxed, and sexy as hell—a perfect date for the more serious couples who don’t get to spend as much time with each other as they’d like."
+ },
+ {
+ "name": "Fun times at a karaoke bar",
+ "description": "A great choice for couples celebrating their first Valentine’s Day together—it’s fairly informal and inexpensive, yet incredibly fun and allows for deeper bonding. Once you have a few drinks in your system and come to terms with the fact that you are making a complete fool of yourself, you’ll have the time of your life!"
+ },
+ {
+ "name": "Horseback riding",
+ "description": "Horseback riding is incredibly fun, especially if you’ve never done it before. And what girl doesn’t dream of a prince coming to take her on an adventure on his noble steed? It evokes a sense of nobility and is a very good bonding experience."
+ },
+ {
+ "name": "Plan a fun date night with other couples",
+ "description": "Take a break and rent a cabin in the woods, go to a mountain resort, a couple’s retreat, or just organize a huge date night at someone’s place and hang out with other couples. This is a great option for couples who have spent at least one Valentine’s Day together and allows you to customize your experience to suit your needs. Also, you can always retire early and get some alone time with your partner if you so desire."
+ },
+ {
+ "name": "Go to a concert",
+ "description": "There are a whole bunch of things happening around Valentine’s Day, so go online and check out what’s happening near you. You’ll surely be able to find tickets for a cool concert or some type of festival with live music."
+ },
+ {
+ "name": "Fancy night on the town",
+ "description": "Buy some elegant new clothes, rent a limo for the night and go to a nice restaurant, followed by a jazz club or gallery exhibition. Walk tall, make a few sarcastic quips, and have a few laughs with your partner while letting your inner snob take charge for a few hours."
+ },
+ {
+ "name": "Live out a James Bond film at a casino",
+ "description": "A beautiful lady in a simple yet sensual, form-fitting, black dress, and a strong and handsome, if somewhat stern-looking man in a fine suit walk up to a roulette table with drinks in hand and place bets at random as they smile at each other seductively. This is a scenario most of us wish to play out, but rarely get a chance. It can be a bit costly, but this is one of the most incredibly adventurous and romantic date ideas."
+ },
+ {
+ "name": "Go bungee jumping",
+ "description": "People in long-term relationships often talk about things like keeping a relationship fun and exciting, doing new things together, trusting each other and using aphrodisiacs. Well, bungee jumping is a fun, exhilarating activity you can both enjoy; it requires trust and the adrenaline rush you get from it is better than any aphrodisiac out there. Just saying, give it a shot and you won’t regret it. "
+ },
+ {
+ "name": "Play some sports",
+ "description": "Some one-on-one basketball, a soccer match against another couple, a bit of tennis, or even something as simple as a table tennis tournament (make it fun by stripping off items of clothing when you lose a game). You can combine this with date idea #13 and paint team uniforms on each other and play in the nude."
+ },
+ {
+ "name": "Take skydiving lessons",
+ "description": "An adrenaline-filled date, skydiving is sure to get your heart racing like crazy and leave you with a goofy grin for the rest of the day. You can offset all the excitement by ending the day with a quiet dinner at home."
+ },
+ {
+ "name": "Go for some paintball",
+ "description": "Playing war games is an excellent way to get your body moving, focus on some of that hand-eye-coordination, and engage your brain in coming up with tactical solutions in the heat of the moment. It is also a great bonding experience, adrenaline-fueled fun, and role-playing all wrapped into one. And when you get back home, you can always act out the wounded soldier scenario."
+ },
+ {
+ "name": "Couples’ Yoga",
+ "description": "Getting up close, hot, and sweaty? Sounds like a Valentine’s Day movie to me. By signing up with your partner for a couples’ yoga class, you can sneak in a workout while getting some face-to-face time with your date.This type of yoga focuses on poses that can be done with a partner, such as back-to-back bends, assisted stretches, and face-to-face breathing exercises. By working out together, you strengthen your bond while stretching away the stress of the week. Finish the date off by heading to the juice bar for a smoothie, or indulging in healthy salads for two. Expect to spend around $35 per person, or approximately $50 to $60 per couple."
+ },
+ {
+ "name": "Volunteer Together",
+ "description": "Getting your hands dirty for a good cause might not be the first thing that pops into your mind when you think “romance,” but there’s something to be said for a date that gives you a glimpse of your partner’s charitable side. Consider volunteering at an animal rescue, where you might be able to play with pups or help a few lovebirds pick out their perfect pet. Or, sign up to visit the elderly at a care center, where you can be a completely different kind of Valentine for someone in need."
+ }
+ ]
+}
+
+
diff --git a/bot/resources/valentines/love_matches.json b/bot/resources/valentines/love_matches.json
new file mode 100644
index 00000000..8d50cd79
--- /dev/null
+++ b/bot/resources/valentines/love_matches.json
@@ -0,0 +1,58 @@
+{
+ "0": {
+ "titles": [
+ "\ud83d\udc94 There's no real connection between you two \ud83d\udc94"
+ ],
+ "text": "The chance of this relationship working out is really low. You can get it to work, but with high costs and no guarantee of working out. Do not sit back, spend as much time together as possible, talk a lot with each other to increase the chances of this relationship's survival."
+ },
+ "5": {
+ "titles": [
+ "\ud83d\udc99 A small acquaintance \ud83d\udc99"
+ ],
+ "text": "There might be a chance of this relationship working out somewhat well, but it is not very high. With a lot of time and effort you'll get it to work eventually, however don't count on it. It might fall apart quicker than you'd expect."
+ },
+ "20": {
+ "titles": [
+ "\ud83d\udc9c You two seem like casual friends \ud83d\udc9c"
+ ],
+ "text": "The chance of this relationship working is not very high. You both need to put time and effort into this relationship, if you want it to work out well for both of you. Talk with each other about everything and don't lock yourself up. Spend time together. This will improve the chances of this relationship's survival by a lot."
+ },
+ "30": {
+ "titles": [
+ "\ud83d\udc97 You seem like you are good friends \ud83d\udc97"
+ ],
+ "text": "The chance of this relationship working is not very high, but its not that low either. If you both want this relationship to work, and put time and effort into it, meaning spending time together, talking to each other etc., than nothing shall stand in your way."
+ },
+ "45": {
+ "titles": [
+ "\ud83d\udc98 You two are really close aren't you? \ud83d\udc98"
+ ],
+ "text": "Your relationship has a reasonable amount of working out. But do not overestimate yourself there. Your relationship will suffer good and bad times. Make sure to not let the bad times destroy your relationship, so do not hesitate to talk to each other, figure problems out together etc."
+ },
+ "60": {
+ "titles": [
+ "\u2764 So when will you two go on a date? \u2764"
+ ],
+ "text": "Your relationship will most likely work out. It won't be perfect and you two need to spend a lot of time together, but if you keep on having contact, the good times in your relationship will outweigh the bad ones."
+ },
+ "80": {
+ "titles": [
+ "\ud83d\udc95 Aww look you two fit so well together \ud83d\udc95"
+ ],
+ "text": "Your relationship will most likely work out well. Don't hesitate on making contact with each other though, as your relationship might suffer from a lack of time spent together. Talking with each other and spending time together is key."
+ },
+ "95": {
+ "titles": [
+ "\ud83d\udc96 Love is in the air \ud83d\udc96",
+ "\ud83d\udc96 Planned your future yet? \ud83d\udc96"
+ ],
+ "text": "Your relationship will most likely work out perfect. This doesn't mean thought that you don't need to put effort into it. Talk to each other, spend time together, and you two won't have a hard time."
+ },
+ "100": {
+ "titles": [
+ "\ud83d\udc9b When will you two marry? \ud83d\udc9b",
+ "\ud83d\udc9b Now kiss already \ud83d\udc9b"
+ ],
+ "text": "You two will most likely have the perfect relationship. But don't think that this means you don't have to do anything for it to work. Talking to each other and spending time together is key, even in a seemingly perfect relationship."
+ }
+} \ No newline at end of file
diff --git a/bot/resources/valentines/pickup_lines.json b/bot/resources/valentines/pickup_lines.json
new file mode 100644
index 00000000..a18d0840
--- /dev/null
+++ b/bot/resources/valentines/pickup_lines.json
@@ -0,0 +1,97 @@
+{
+ "placeholder": "https://i.imgur.com/BB52sxj.jpg",
+ "lines": [
+ {
+ "line": "Hey baby are you allergic to dairy cause I **laktose** clothes you're wearing",
+ "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8f/Cheese_%281105942243%29.jpg/800px-Cheese_%281105942243%29.jpg"
+ },
+ {
+ "line": "I’m not a photographer, but I can **picture** me and you together.",
+ "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/57/2016_Minolta_Dynax_404si.JPG/220px-2016_Minolta_Dynax_404si.JPG"
+ },
+ {
+ "line": "I seem to have lost my phone number. Can I have yours?"
+ },
+ {
+ "line": "Are you French? Because **Eiffel** for you.",
+ "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/8/85/Tour_Eiffel_Wikimedia_Commons_%28cropped%29.jpg/240px-Tour_Eiffel_Wikimedia_Commons_%28cropped%29.jpg"
+ },
+ {
+ "line": "Hey babe are you a cat? Because I'm **feline** a connection between us.",
+ "image": "https://upload.wikimedia.org/wikipedia/commons/4/4d/Cat_November_2010-1a.jpg"
+ },
+ {
+ "line": "Baby, life without you is like a broken pencil... **pointless**.",
+ "image": "https://upload.wikimedia.org/wikipedia/commons/0/08/Pencils_hb.jpg"
+ },
+ {
+ "line": "Babe did it hurt when you fell from heaven?"
+ },
+ {
+ "line": "If I could rearrange the alphabet, I would put **U** and **I** together.",
+ "image": "https://images-na.ssl-images-amazon.com/images/I/51wJaFX%2BnGL._SX425_.jpg"
+ },
+ {
+ "line": "Is your name Google? Because you're everything I'm searching for.",
+ "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Google_%22G%22_Logo.svg/1024px-Google_%22G%22_Logo.svg.png"
+ },
+ {
+ "line": "Are you from Starbucks? Because I like you a **latte**.",
+ "image": "https://upload.wikimedia.org/wikipedia/en/thumb/d/d3/Starbucks_Corporation_Logo_2011.svg/1200px-Starbucks_Corporation_Logo_2011.svg.png"
+ },
+ {
+ "line": "Are you a banana? Because I find you **a peeling**.",
+ "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/44/Bananas_white_background_DS.jpg/220px-Bananas_white_background_DS.jpg"
+ },
+ {
+ "line": "Do you like vegetables because I love you from my head **tomatoes**.",
+ "image": "https://vignette.wikia.nocookie.net/veggietales-the-ultimate-veggiepedia/images/e/ec/Bobprofile.jpg/revision/latest?cb=20161227190344"
+ },
+ {
+ "line": "Do you like science because I've got my **ion** you.",
+ "image": "https://www.chromacademy.com/lms/sco101/assets/c1_010_equations.jpg"
+ },
+ {
+ "line": "Are you an angle? Because you are **acute**.",
+ "image": "https://juicebubble.co.za/wp-content/uploads/2018/03/acute-angle-white-400x400.png"
+ },
+ {
+ "line": "If you were a fruit, you'd be a **fineapple**."
+ },
+ {
+ "line": "Did you swallow magnets? Cause you're **attractive**.",
+ "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Magnetic_quadrupole_moment.svg/1200px-Magnetic_quadrupole_moment.svg.png"
+ },
+ {
+ "line": "Hey pretty thang, do you have a name or can I call you mine?"
+ },
+ {
+ "line": "Is your name Wi-Fi? Because I'm feeling a connection.",
+ "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/ae/WiFi_Logo.svg/1200px-WiFi_Logo.svg.png"
+ },
+ {
+ "line": "Are you Australian? Because you meet all of my **koala**fications.",
+ "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/49/Koala_climbing_tree.jpg/240px-Koala_climbing_tree.jpg"
+ },
+ {
+ "line": "If I were a cat I'd spend all 9 lives with you.",
+ "image": "https://upload.wikimedia.org/wikipedia/commons/4/4d/Cat_November_2010-1a.jpg"
+ },
+ {
+ "line": "My love for you is like dividing by 0. It's undefinable.",
+ "image": "https://i.pinimg.com/originals/05/f5/9a/05f59a9fa44689e3435b5e46937544bb.png"
+ },
+ {
+ "line": "Take away gravity, I'll still fall for you.",
+ "image": "https://i.pinimg.com/originals/05/f5/9a/05f59a9fa44689e3435b5e46937544bb.png"
+ },
+ {
+ "line": "Are you a criminal? Because you just stole my heart.",
+ "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/98/Hinged_Handcuffs_Rear_Back_To_Back.JPG/174px-Hinged_Handcuffs_Rear_Back_To_Back.JPG"
+ },
+ {
+ "line": "Hey babe I'm here. What were your other two wishes?",
+ "image": "https://upload.wikimedia.org/wikipedia/en/thumb/0/0c/The_Genie_Aladdin.png/220px-The_Genie_Aladdin.png"
+ }
+ ]
+} \ No newline at end of file
diff --git a/bot/resources/valentines/valenstates.json b/bot/resources/valentines/valenstates.json
new file mode 100644
index 00000000..06cbb2e5
--- /dev/null
+++ b/bot/resources/valentines/valenstates.json
@@ -0,0 +1,122 @@
+{
+ "Australia": {
+ "text": "Australia is the oldest, flattest and driest inhabited continent on earth. It is one of the 18 megadiverse countries, featuring a wide variety of plants and animals, the most iconic ones being the koalas and kangaroos, as well as its deadly wildlife and trees falling under the Eucalyptus genus.",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/8/88/Flag_of_Australia_%28converted%29.svg/1920px-Flag_of_Australia_%28converted%29.svg.png"
+ },
+ "Austria": {
+ "text": "Austria is part of the european continent, lying in the alps. Due to its location, Austria possesses a variety of very tall mountains like the Großglockner (3798 m) or the Wildspitze (3772 m).",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Flag_of_Austria.svg/1920px-Flag_of_Austria.svg.png"
+ },
+ "Brazil": {
+ "text": "Being the largest and most populated country in South and Latin America, Brazil, as one of the 18 megadiverse countries, features a wide variety of plants and animals, especially in the Amazon rainforest, the most biodiverse rainforest in the world.",
+ "flag": "https://upload.wikimedia.org/wikipedia/en/thumb/0/05/Flag_of_Brazil.svg/1280px-Flag_of_Brazil.svg.png"
+ },
+ "Canada": {
+ "text": "Canada is the second-largest country in the world measured by total area, only surpassed by Russia. It's widely known for its astonishing national parks.",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Flag_of_Canada_%28Pantone%29.svg/1920px-Flag_of_Canada_%28Pantone%29.svg.png"
+ },
+ "Croatia": {
+ "text": "Croatia is a country at the crossroads of Central and Southeast Europe, mostly known for its beautiful beaches and waters.",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/1/1b/Flag_of_Croatia.svg/1920px-Flag_of_Croatia.svg.png"
+ },
+ "England": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/en/thumb/b/be/Flag_of_England.svg/1920px-Flag_of_England.svg.png"
+ },
+ "Finland": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/bc/Flag_of_Finland.svg/1920px-Flag_of_Finland.svg.png"
+ },
+ "France": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/en/thumb/c/c3/Flag_of_France.svg/1920px-Flag_of_France.svg.png"
+ },
+ "Germany": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Flag_of_Germany.svg/1920px-Flag_of_Germany.svg.png"
+ },
+ "Greece": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5c/Flag_of_Greece.svg/1920px-Flag_of_Greece.svg.png"
+ },
+ "Iceland": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/ce/Flag_of_Iceland.svg/1280px-Flag_of_Iceland.svg.png"
+ },
+ "India": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/en/thumb/4/41/Flag_of_India.svg/1920px-Flag_of_India.svg.png"
+ },
+ "Indonesia": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9f/Flag_of_Indonesia.svg/1920px-Flag_of_Indonesia.svg.png"
+ },
+ "Ireland": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/45/Flag_of_Ireland.svg/1920px-Flag_of_Ireland.svg.png"
+ },
+ "Italy": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/en/thumb/0/03/Flag_of_Italy.svg/1920px-Flag_of_Italy.svg.png"
+ },
+ "Mexico": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Flag_of_Mexico.svg/1920px-Flag_of_Mexico.svg.png"
+ },
+ "New Zealand": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3e/Flag_of_New_Zealand.svg/1920px-Flag_of_New_Zealand.svg.png"
+ },
+ "Norway": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Flag_of_Norway.svg/1280px-Flag_of_Norway.svg.png"
+ },
+ "Peru": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Flag_of_Peru.svg/1920px-Flag_of_Peru.svg.png"
+ },
+ "Portugal": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5c/Flag_of_Portugal.svg/1920px-Flag_of_Portugal.svg.png"
+ },
+ "Scotland": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/1/10/Flag_of_Scotland.svg/1920px-Flag_of_Scotland.svg.png"
+ },
+ "Slovenia": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f0/Flag_of_Slovenia.svg/1920px-Flag_of_Slovenia.svg.png"
+ },
+ "South Africa": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/af/Flag_of_South_Africa.svg/1920px-Flag_of_South_Africa.svg.png"
+ },
+ "Spain": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/en/thumb/9/9a/Flag_of_Spain.svg/1920px-Flag_of_Spain.svg.png"
+ },
+ "Sweden": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Flag_of_Sweden.svg/1920px-Flag_of_Sweden.svg.png"
+ },
+ "Switzerland": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Flag_of_Switzerland_%28Pantone%29.svg/1024px-Flag_of_Switzerland_%28Pantone%29.svg.png"
+ },
+ "Turkey": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b4/Flag_of_Turkey.svg/1920px-Flag_of_Turkey.svg.png"
+ },
+ "United States": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/en/thumb/a/a4/Flag_of_the_United_States.svg/1920px-Flag_of_the_United_States.svg.png"
+ },
+ "Vietnam": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/Flag_of_Vietnam.svg/1920px-Flag_of_Vietnam.svg.png"
+ },
+ "Wales": {
+ "text": "",
+ "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a9/Flag_of_Wales_%281959%E2%80%93present%29.svg/1920px-Flag_of_Wales_%281959%E2%80%93present%29.svg.png"
+ }
+} \ No newline at end of file
diff --git a/bot/resources/valentines/valentine_facts.json b/bot/resources/valentines/valentine_facts.json
new file mode 100644
index 00000000..d2ffa980
--- /dev/null
+++ b/bot/resources/valentines/valentine_facts.json
@@ -0,0 +1,24 @@
+{
+ "whois": "Saint Valentine, officially Saint Valentine of Rome, was a widely recognized 3rd-century christian saint, commemorated on February 14. He was a priest and bishop, ministering persecuted Christians in the Roman Empire, and is associated with a tradition of courtly love since the High Middle Ages, a period commenced around the year 1000AD and lasting until around 1250AD. He was martyred and buried at a Christian cemetery on the Via Flaminia on February 14.\n\nThere are a bunch of inconsistencies in the identification of the saint, however there are evidences for 3 saints that appear in connection with February 14. One of them, Saint Valentine of Terni, is believed to be the one associated with a vision restoration miracle, which happening during his imprisonment. In that, he restored the eyesight of his jailer's daughter, and, on the evening before his execution, supposedly sent her a letter signed with 'Your Valentine' (tuum valentinum). This makes this saint the one we today associate with Saint Valentine's Day.\n\nThe artist Cicero Moraes attempted a facial reconstruction of Saint Valentine, which can be seen in the thumbnail.",
+ "titles": [
+ "\u2764 Facts \u00e1 la carte \u2764",
+ "\u2764 Would you like some cheese with your wi... facts? \u2764",
+ "\u2764 Facts to raise your pulse \u2764",
+ "\u2764 Love Facts, Episode #42 \u2764",
+ "\u2764 It's a fact not a fact, duh \u2764",
+ "\u2764 Candlelight din... facts! \u2764"
+ ],
+ "text": [
+ "The expression 'From your Valentine' derives from a legend in which Saint Valentine, imprisoned after persecution and not wanting to convert to Roman paganism, performed a miracle on Julia, his jailer Asterius's blind daughter, restoring her eyesight. On the evening before his execution, he is supposed to have written a letter to the jailers daughter, signing as 'Your Valentine' (tuum valentinum).",
+ "Valentine's Day wasn't really associated with anything romantic, until the 14th century England where it's association with romantic love had begun from within the circle of Geoffrey Chaucer, a famous english poet and author, also called 'Father of English literature' for his work. He is best known for 'The Caunterbury Tales', a collection of 24 stories, which are presented as part of a story-telling contest by a group of pilgrims on their travel from London to Canterbury.",
+ "It's only been roughly 300 years, that the Valentine's Day evolved into what we know today.",
+ "The wide usage of hearts on Valentine's day derived from a legend, in which Saint Valentine cut hearts from parchment, giving them to persecuted Christians and soldiers married by him. He did that \"to remind these man of their vows and God's love\"",
+ "In 1797, a British publisher developed \"The Young Man's Valentine Writer\", to assist young men in composing their own sentimental verses to ladies they felt attracted to.",
+ "If you've never gotten any handwritten Valentine cards, this may be due to the fact, that in the 19th century, handwritten notes have given away to mass-produced greeting cards.",
+ "In 1868, a British chocolate company called Cadbury created so called 'Fancy Boxes', which essentially were a decorated box of chocolates in the shape of a heart. This set a trend, such that these boxes were quickly associated with Valentine's Day.",
+ "Roses are red,\nviolet's are blue,\nI can't rhyme,\nbut I still do.\n\u200b\nThis poem in particular,\nit will stay forever,\nderives from The Faerie Queene,\nan epic poem you probably have never seen.\n\n\"She bath'd with roses red, and violets blew,\nAnd all the sweetest flowres, that in the forrest grew.\"\n\nThese verses, with most immense sway,\nlead to the poem we still hear today.",
+ "The earliest Valentine poem known is a rondeau, a form of medieval/renaissance French poetry, composed by Charles, Duke of Orl\u00e9ans to his wife:\n\n\"Je suis desja d'amour tann\u00e9,\nMa tres doulce Valentin\u00e9e\"",
+ "There's a form of cryptological communication called 'Floriography', in which you communicate through flowers. Meaning has been attributed to flowers for thousands of years, and some form of floriography has been practiced in traditional cultures throughout Europe, Asia, and Africa. Here are some meanings for roses you might want to take a look at, if you plan on gifting your loved one a bouquet of roses on Valentine's Day:\n\u200b\nRed: eternal love\nPink: young, developing love\nWhite: innocence, fervor, loyalty\nOrange: happiness, security\nViolet: love at first sight\nBlue: unfulfilled longing, quiet desire\nYellow: friendship, jealousy, envy, infidelity\nBlack: unfulfilled longing, quiet desire, grief, hatred, misfortune, death",
+ "Traditionally, young girls in the U.S. and the U.K. believed they could tell what type of man they would marry depending on the type of bird they saw first on Valentine's Day. If they saw a blackbird, they would marry a clergyman, a robin redbreast indicated a sailor, and a goldfinch indicated a rich man. A sparrow meant they would marry a farmer, a bluebird indicated a happy man, and a crossbill meant an argumentative man. If they saw a dove, they would marry a good man, but seeing a woodpecker meant they would not marry at all."
+ ]
+} \ No newline at end of file
diff --git a/bot/resources/valentines/zodiac_compatibility.json b/bot/resources/valentines/zodiac_compatibility.json
new file mode 100644
index 00000000..4e337714
--- /dev/null
+++ b/bot/resources/valentines/zodiac_compatibility.json
@@ -0,0 +1,262 @@
+{
+ "aries":[
+ {
+ "Zodiac" : "Sagittarius",
+ "description" : "The Archer is one of the most compatible signs Aries should consider when searching out relationships that will bear fruit. Sagittarians share a certain love of freedom with Aries that will help the two of them conquer new territory together.",
+ "compatibility_score" : "87%"
+ },
+ {
+ "Zodiac" : "Leo",
+ "description" : "Leo is the center of attention and typically the life of the party, but can also be courageous, bold, and powerful. These two signs share a gregarious nature that helps them walk life's path in sync. They may vie occasionally to see which one leads the way, but all things considered, they share a great capacity for compatibility.",
+ "compatibility_score" : "83%"
+ },
+ {
+ "Zodiac" : "Aquarius",
+ "description" : "Aquarius' need for personal space dovetails nicely with Aries' love of freedom. This doesn't mean that the Ram and the Water Bearer can't forge a deep bond; in fact, quite the opposite. Their mutual respect for one another's needs and space leaves room for both to grow on their own and together.",
+ "compatibility_score" : "68%"
+ },
+ {
+ "Zodiac" : "Gemini",
+ "description" : "The Twins are known for their adaptability, a trait that easily follows Aries' need to lead their closest companions down new paths. Geminis are also celebrated for their personal charm and intellect, traits the discerning Ram is also capable of fully appreciating.",
+ "compatibility_score" : "74%"
+ }
+ ],
+ "taurus":[
+ {
+ "Zodiac" : "Virgo",
+ "description" : "Although these signs have their set of differences, the Virgo Taurus compatibility is usually pretty strong. This is because both the signs want the same thing ultimately and have generally synchronous ways of reaching those points. This helps them complement each other and create a healthy relationship between them.",
+ "compatibility_score" : "73%"
+ },
+ {
+ "Zodiac" : "Capricorn",
+ "description" : "A great compatibility is seen in this match as far as the philosophical and spiritual aspects of life are concerned. Taurus and Capricorn have a practical approach towards life. The ambitions and calmness of the Goat will attract the Bull, who will attract the former with his strong determination.The compatibility between these two zodiac signs is at the greatest height due to their mutual understanding, faith and consonance.",
+ "compatibility_score" : "89%"
+ },
+ {
+ "Zodiac" : "Pisces",
+ "description" : "This relationship will survive the test of time if both parties involved have unbreakable trust in each other and nurture that connection they have painstakingly built over the years. They must remember to be honest and committed to their partner through all times.If natural communication flows between them like clockwork, this will be a beautiful love story with a prominent tag of 'happily-ever-after' pinned right to it!",
+ "compatibility_score" : "88%"
+ },
+ {
+ "Zodiac" : "Cancer",
+ "description" : "This is a peaceful union of two reliable and kind souls. It is one of the strong zodiac pairings in terms of compatibility, and certainly has a real chance of lasting a lifetime in astrological charts. If Taurus man and Cancer woman and vice-versa are mature enough to handle the occasional friction, which they usually are, their bond will transcend all boundaries and is likely to grow in strength through the years.",
+ "compatibility_score" : "91%"
+ }
+ ],
+ "gemini":[
+ {
+ "Zodiac" : "Aries",
+ "description" : "The theorem of astrology says that Aries and Gemini have a zero tolerance for boredom and will at once get rid of anything dull. An Arian will let a Geminian enjoy his personal freedom and the Gemini will respect his individuality.",
+ "compatibility_score" : "74%"
+ },
+ {
+ "Zodiac" : "Leo",
+ "description" : "Gemini and Leo love match that can go through extravagant heights and crushing lows, but one is sure to leave both of them breathless. This is a pair that forms one of the most exciting relationships of the zodiac chart, but the converse is equally true; they are vulnerable to experiencing terrible lows.",
+ "compatibility_score" : "82%"
+ },
+ {
+ "Zodiac" : "Libra",
+ "description" : "It is relatively easier for them to adjust to each other as while they might have different outlooks to some things in life, they rarely collide or crash against one another. Hence, it is easy for them to go forward in harmony, forming a formidable bond in the process.",
+ "compatibility_score" : "78%"
+ },
+ {
+ "Zodiac" : "Aquarius",
+ "description" : "This is one of the most successful pairs of the zodiac chart.If they manage to smooth this solitary wrinkle, this will be a beautiful union for both the parties involved. It will turn into a gift that will keep giving happiness, contentment and encouragement to the air signs.",
+ "compatibility_score" : "91%"
+ }
+ ],
+ "cancer":[
+ {
+ "Zodiac" : "Taurus",
+ "description" : "The Cancer Taurus zodiac relationship compatibility is strong because of their mutual love for safety, stability, and comfort. Their mutual understanding will always be powerful, which will be the pillar of strength of their relationship.",
+ "compatibility_score" : "91%"
+ },
+ {
+ "Zodiac" : "Scorpio",
+ "description" : "The Cancer man and Scorpio woman and vice-versa connect on an intuitive level and will find refuge in each other from the outset of the relationship.",
+ "compatibility_score" : "79%"
+ },
+ {
+ "Zodiac" : "Virgo",
+ "description" : "This is a match made in heaven! Very few zodiac combinations work as well as Cancer and Virgo. Their blissful union is one which is generally filled with memories of happiness, contentment, loyalty, and harmony. They will build an extremely strong bond with each other that can pass all the tests of time and emerge stronger after every challenge that is thrown in its way!",
+ "compatibility_score" : "77%"
+ }
+ ],
+ "leo":[
+ {
+ "Zodiac" : "Aries",
+ "description" : "A Leo is generous and an Arian is open to life. Sharing the same likes and dislikes, they both crave for fun, romance and excitement. A Leo respects an Arian's need for freedom because an Arian does not interfere much in the life of a Leo. Aries will love the charisma and ideas of the Leo.",
+ "compatibility_score" : "83%"
+ },
+ {
+ "Zodiac" : "Gemini",
+ "description" : "This is one of the most compatible pairings in the zodiac chart and if the minor friction is handled with maturity by both the parties, the Leo and Gemini compatibility relationship can last a lifetime. A lifetime, which will be filled with love, happiness, and success.",
+ "compatibility_score" : "82%"
+ },
+ {
+ "Zodiac" : "Libra",
+ "description" : "he thing that attributes for the greater compatibility of this love match is that both the signs like to complement each other. Leo and Libra love to attend social gatherings. They like to engage themselves in romance and act as lovers. This love match is strong enough to face adverse situations due to their opposite individual characters. This pair likes to enjoy candlelit dinners, long drives, dancing",
+ "compatibility_score" : "75%"
+ },
+ {
+ "Zodiac" : "Sagittarius",
+ "description" : "The key for this relationship to work is that both Leo star sign and Sagittarius star sign natives should know where they want it to go in the long run. If they are ready for a long-time commitment and are mutually aware of the same, they will form a brilliant and buoyant bond together. It can last for a lifetime and will be characterized by the excitement, love, happiness, and unending loyalty that Leo and Sagittarius will bring to each other’s life.",
+ "compatibility_score" : "75%"
+ }
+ ],
+ "virgo":[
+ {
+ "Zodiac" : "Taurus",
+ "description" : "Although these signs have their set of differences, the Virgo Taurus compatibility is usually pretty strong. This is because both the signs want the same thing ultimately and have generally synchronous ways of reaching those points. This helps them complement each other and create a healthy relationship between them.",
+ "compatibility_score" : "73%"
+ },
+ {
+ "Zodiac" : "Cancer",
+ "description" : "This is one of the few zodiac compatibilities that look great not only on paper, but also in reality. The shared outlook that Cancer and Virgo have towards life makes their relationship a positive, reliable, and beautiful entity for both the sides.",
+ "compatibility_score" : "77%"
+ },
+ {
+ "Zodiac" : "Scorpio",
+ "description" : "The Virgo Scorpio love match definitely has something unique to offer to the zodiac world, and to both the parties involved in this pair. If they manage to control their negative traits, this relationship truly has the chance to survive everything life throws at it.",
+ "compatibility_score" : "76%"
+ },
+ {
+ "Zodiac" : "Capricorn",
+ "description" : "The compatibility of this match is quite well as both rely on each other and possess the maturity to understand their individual outlook. Capricorn plans in advance and steadily reaches his goal and a Virgo has faith in his abilities and skills.",
+ "compatibility_score" : "77%"
+ }
+ ],
+ "libra":[
+ {
+ "Zodiac" : "Leo",
+ "description" : "Libra and Leo love match can work well for both the partners and truly help them learn from each other and grow individually, as well as together. Libra and Leo, when in the right frame of mind, form a formidable couple that attracts admiration and respect everywhere it goes.",
+ "compatibility_score" : "75%"
+ },
+ {
+ "Zodiac" : "Aquarius",
+ "description" : "Libra Aquarius Relationship has a good chance of working because of the immense understanding that both the zodiac signs have for each other. They are likely to fight less as Libra believes in avoiding conflict and Aquarius usually is an extremely open-minded and non-judgmental sign.",
+ "compatibility_score" : "68%"
+ },
+ {
+ "Zodiac" : "Gemini",
+ "description" : "It is relatively easier for them to adjust to each other as while they might have different outlooks to some things in life, they rarely collide or crash against one another. Hence, it is easy for them to go forward in harmony, forming a formidable bond in the process.",
+ "compatibility_score" : "78%"
+ },
+ {
+ "Zodiac" : "sagittarius",
+ "description" : "If Sagittarius and Libra want to make it with each other, they must give themselves the time to understand each other fully. Rushing through the relationship will only need to problems in the future and that can be avoided if both the parties are careful not to get too ahead of themselves at any point.",
+ "compatibility_score" : "71%"
+ }
+ ],
+ "scorpio":[
+ {
+ "Zodiac" : "Cancer",
+ "description" : "This union is not unusual, but will take a fair share of work in the start. A strong foundation of clear cut communication is mandatory to make this a loving and stress free relationship!",
+ "compatibility_score" : "79%"
+ },
+ {
+ "Zodiac" : "Virgo",
+ "description" : "Very rarely does it so happen that a pairing can be so perfect and perfectly horrid at the same time. As such, it is imperative that the individuals involved take time before they jump into a serious commitment. Knowing these two zodiacs, commitments last for a lifetime.",
+ "compatibility_score" : "76%"
+ },
+ {
+ "Zodiac" : "Capricorn",
+ "description" : "The relationship of Scorpio and Capricorn can be inspiring for both partners to search for the truth, dig up under their family tree and deal with any unresolved karma and debt. They are both deep and don’t take things lightly, and this will help them build a strong foundation for a relationship that can last for a long time.",
+ "compatibility_score" : "72%"
+ },
+ {
+ "Zodiac" : "Pisces",
+ "description" : "Scorpio-Pisces is one of the most compatible signs of the zodiac calendar. They have a gamut of intense emotions that are consistently in sync with each other. Only if the Scorpion learns to let it go once in a while and the Pisces develops a sense of self-assurance, it will be a successful partnership that will meet its short-term as well as long-term goals.",
+ "compatibility_score" : "81%"
+ }
+ ],
+ "sagittarius":[
+ {
+ "Zodiac" : "Aries",
+ "description" : "Sagittarius and Aries can make a very compatible pair. Their relationship will have a lot of passion, enthusiasm, and energy. These are very good traits to make their relationship deeper and stronger. Both Aries and Sagittarius will enjoy each other's company and their energy level rises as the relationship grows. Both will support and help in fighting hardships and failures.",
+ "compatibility_score" : "87%"
+ },
+ {
+ "Zodiac" : "Leo",
+ "description" : "While there might be a few blips in the Sagittarius Leo relationship, they are certainly one of the most compatible couples when it comes to astrological studies. They can bring the best out of each other and shine in the light of their positive all exciting relationship.",
+ "compatibility_score" : "75%"
+ },
+ {
+ "Zodiac" : "Libra",
+ "description" : "The connectivity between Sagittarians and Librans is amazing. Librans are inclined towards maintaining balance and harmony. Sagittarians are intelligent and fun loving. But, Librans have a strange trait of transiting from one phase of emotion to other.",
+ "compatibility_score" : "71%"
+ },
+ {
+ "Zodiac" : "Aquarius",
+ "description" : "This is a combination where the positives outweigh the negatives by a considerable margin. As long as this pair builds and retains their passion for each other in the relationship, it will be a hit that can pass every test that is thrown at it.The Sagittarius Aquarius compatibility is one of a kind and can go the distance in most cases.",
+ "compatibility_score" : "83%"
+ }
+ ],
+ "capricorn":[
+ {
+ "Zodiac" : "Taurus",
+ "description" : "This is one of the most grounded and reliable bonds of the zodiac chart. If Capricorn and Taurus do find a way to handle their minor issues, they have a good chance of making it together and that too, in a happy, peaceful, and healthy relationship.",
+ "compatibility_score" : "89%"
+ },
+ {
+ "Zodiac" : "Virgo",
+ "description" : "Virgo and Capricorn will connect to each other on a visceral level and will find enduring stability in their relationship. Since this bond is something they can rely on, they tend to ease up individually and thus, are likely to be more relaxed and happier in life.",
+ "compatibility_score" : "77%"
+ },
+ {
+ "Zodiac" : "Scorpio",
+ "description" : "Though they have a couple of blips in the relationship, the combination of Scorpion and Capricorn is a match with powerful compatibility. They will shine with each other, develop each other, and build a beautiful bond with each other. If and when the two decide to commit to each other, their relationship is one that is likely to go the distance nine times out of ten!",
+ "compatibility_score" : "70%"
+ },
+ {
+ "Zodiac" : "Pisces",
+ "description" : "Pisces and Capricorn signs have primary difference of qualities, but their union is aided seamlessly due to that very discrepancy of their personas. It is a highly compatible relationship that can go a long way with mutual respect, amicable understanding and immense trust between the concerned parties.",
+ "compatibility_score" : "76%"
+ }
+ ],
+ "aquarius":[
+ {
+ "Zodiac" : "Aries",
+ "description" : "The relationship of Aries and Aquarius is very exciting, adventurous and interesting. They will enjoy each other's company as both of them love fun and freedom.This is a couple that lacks tenderness. They are not two brutes who let their relationship fade as soon as their passion does.",
+ "compatibility_score" : "72%"
+ },
+ {
+ "Zodiac" : "Gemini",
+ "description" : "If you want to see a relationship that is full of conversation, ideas and \"eureka moments\", you observe the Aquarius Gemini relationship. They will enjoy a camaraderie that you won't find in most zodiac relationships.",
+ "compatibility_score" : "85%"
+ },
+ {
+ "Zodiac" : "Libra",
+ "description" : "The couple will share a lot of similar qualities and will enjoy the relationship. Both of them love to socialize with people and to make new friends.",
+ "compatibility_score" : "68%"
+ },
+ {
+ "Zodiac" : "Sagittarius",
+ "description" : "These two will get together when it is time for both of them to go through a change in their lives or leave a partner they feel restricted with. Their relationship is often a shiny beacon to everyone around them because it gives priority to the future and brings hope of a better time.",
+ "compatibility_score" : "83%"
+ }
+ ],
+ "pisces":[
+ {
+ "Zodiac" : "Taurus",
+ "description" : "This relationship will survive the test of time if both parties involved have unbreakable trust in each other and nurture that connection they have painstakingly built over the years. They must remember to be honest and committed to their partner through all times.If natural communication flows between them like clockwork, this will be a beautiful love story with a prominent tag of ‘happily-ever-after’ pinned right to it!",
+ "compatibility_score" : "88%"
+ },
+ {
+ "Zodiac" : "Cancer",
+ "description" : "The Pisces Cancer union is a beautiful one. Both zodiacs are normally blessed with appealing looks and attractive minds. They are both prone to be drawn to each other. Mostly, for very valid reasons. However, both the zodiac star signs need to make sure that they do not let their love overpower them.",
+ "compatibility_score" : "72%"
+ },
+ {
+ "Zodiac" : "Scorpio",
+ "description" : "Though there are a few problems that might test this relationship, these star sign compatibility of two water signs has many features that can help it survive. Pisces and Scorpio are both passionate in their own ways and thus, end up striking the perfect rhythm with each other.If and when they decide to commit to the relationship, they can script a special story of camaraderie and bliss with each other.",
+ "compatibility_score" : "81%"
+ },
+ {
+ "Zodiac" : "Capricorn",
+ "description" : "A relationship between Capricorn and Pisces tells a story about possibilities of inspiration. If someone like Capricorn can be pulled into a crazy love story, exciting and unpredictable, this must be done by Pisces. In return, Capricorn will offer their Pisces partner stability, peace and some rest from their usual emotional tornadoes. There is a fine way in which Capricorn can help Pisces be more realistic and practical, while feeling more cheerful and optimistic themselves.",
+ "compatibility_score" : "76%"
+ }
+ ]
+
+} \ No newline at end of file
diff --git a/bot/seasons/__init__.py b/bot/seasons/__init__.py
index c43334a4..1512fae2 100644
--- a/bot/seasons/__init__.py
+++ b/bot/seasons/__init__.py
@@ -9,4 +9,4 @@ log = logging.getLogger(__name__)
def setup(bot):
bot.add_cog(SeasonManager(bot))
- log.debug("SeasonManager cog loaded")
+ log.info("SeasonManager cog loaded")
diff --git a/bot/seasons/christmas/adventofcode.py b/bot/seasons/christmas/adventofcode.py
index 3b199a4a..2995c3fd 100644
--- a/bot/seasons/christmas/adventofcode.py
+++ b/bot/seasons/christmas/adventofcode.py
@@ -108,7 +108,7 @@ async def day_countdown(bot: commands.Bot):
await asyncio.sleep(120)
-class AdventOfCode:
+class AdventOfCode(commands.Cog):
"""
Advent of Code festivities! Ho Ho Ho!
"""
@@ -205,14 +205,21 @@ class AdventOfCode:
@adventofcode_group.command(name="join", aliases=("j",), brief="Learn how to join PyDis' private AoC leaderboard")
async def join_leaderboard(self, ctx: commands.Context):
"""
- Retrieve the link to join the PyDis AoC private leaderboard
+ DM the user the information for joining the PyDis AoC private leaderboard
"""
+ author = ctx.message.author
+ log.info(f"{author.name} ({author.id}) has requested the PyDis AoC leaderboard code")
+
info_str = (
"Head over to https://adventofcode.com/leaderboard/private "
f"with code `{AocConfig.leaderboard_join_code}` to join the PyDis private leaderboard!"
)
- await ctx.send(info_str)
+ try:
+ await author.send(info_str)
+ except discord.errors.Forbidden:
+ log.debug(f"{author.name} ({author.id}) has disabled DMs from server members")
+ await ctx.send(f":x: {author.mention}, please (temporarily) enable DMs to receive the join code")
@adventofcode_group.command(
name="leaderboard",
@@ -723,4 +730,4 @@ def _error_embed_helper(title: str, description: str) -> discord.Embed:
def setup(bot: commands.Bot) -> None:
bot.add_cog(AdventOfCode(bot))
- log.info("Cog loaded: adventofcode")
+ log.info("AdventOfCode cog loaded")
diff --git a/bot/seasons/easter/__init__.py b/bot/seasons/easter/__init__.py
new file mode 100644
index 00000000..bfad772d
--- /dev/null
+++ b/bot/seasons/easter/__init__.py
@@ -0,0 +1,17 @@
+from bot.seasons import SeasonBase
+
+
+class Easter(SeasonBase):
+ """
+ Easter is a beautiful time of the year often celebrated after the first Full Moon of the new spring season.
+ This time is quite beautiful due to the colorful flowers coming out to greet us. So. let's greet Spring
+ in an Easter celebration of contributions.
+ """
+
+ name = "easter"
+ bot_name = "BunnyBot"
+ greeting = "Happy Easter to us all!"
+
+ # Duration of season
+ start_date = "01/04"
+ end_date = "30/04"
diff --git a/bot/seasons/evergreen/__init__.py b/bot/seasons/evergreen/__init__.py
index db5b5684..db610e7c 100644
--- a/bot/seasons/evergreen/__init__.py
+++ b/bot/seasons/evergreen/__init__.py
@@ -2,4 +2,4 @@ from bot.seasons import SeasonBase
class Evergreen(SeasonBase):
- pass
+ bot_icon = "/logos/logo_seasonal/evergreen/logo_evergreen.png"
diff --git a/bot/seasons/evergreen/error_handler.py b/bot/seasons/evergreen/error_handler.py
index 6de35e60..7774f06e 100644
--- a/bot/seasons/evergreen/error_handler.py
+++ b/bot/seasons/evergreen/error_handler.py
@@ -8,102 +8,97 @@ from discord.ext import commands
log = logging.getLogger(__name__)
-class CommandErrorHandler:
+class CommandErrorHandler(commands.Cog):
"""A error handler for the PythonDiscord server!"""
def __init__(self, bot):
self.bot = bot
+ @staticmethod
+ def revert_cooldown_counter(command, message):
+ """Undoes the last cooldown counter for user-error cases."""
+ if command._buckets.valid:
+ bucket = command._buckets.get_bucket(message)
+ bucket._tokens = min(bucket.rate, bucket._tokens + 1)
+ logging.debug(
+ "Cooldown counter reverted as the command was not used correctly."
+ )
+
+ @commands.Cog.listener()
async def on_command_error(self, ctx, error):
"""Activates when a command opens an error"""
if hasattr(ctx.command, 'on_error'):
return logging.debug(
- "A command error occured but "
- "the command had it's own error handler"
+ "A command error occured but the command had it's own error handler."
)
+
error = getattr(error, 'original', error)
+
if isinstance(error, commands.CommandNotFound):
return logging.debug(
- f"{ctx.author} called '{ctx.message.content}' "
- "but no command was found"
+ f"{ctx.author} called '{ctx.message.content}' but no command was found."
)
+
if isinstance(error, commands.UserInputError):
logging.debug(
- f"{ctx.author} called the command '{ctx.command}' "
- "but entered invalid input!"
+ f"{ctx.author} called the command '{ctx.command}' but entered invalid input!"
)
+
+ self.revert_cooldown_counter(ctx.command, ctx.message)
+
return await ctx.send(
- ":no_entry: The command you specified failed to run."
+ ":no_entry: The command you specified failed to run. "
"This is because the arguments you provided were invalid."
)
+
if isinstance(error, commands.CommandOnCooldown):
logging.debug(
- f"{ctx.author} called the command '{ctx.command}' "
- "but they were on cooldown!"
+ f"{ctx.author} called the command '{ctx.command}' but they were on cooldown!"
)
+ remaining_minutes, remaining_seconds = divmod(error.retry_after, 60)
+
return await ctx.send(
- "This command is on cooldown,"
- f" please retry in {math.ceil(error.retry_after)}s."
+ "This command is on cooldown, please retry in "
+ f"{int(remaining_minutes)} minutes {math.ceil(remaining_seconds)} seconds."
)
+
if isinstance(error, commands.DisabledCommand):
logging.debug(
- f"{ctx.author} called the command '{ctx.command}' "
- "but the command was disabled!"
- )
- return await ctx.send(
- ":no_entry: This command has been disabled."
+ f"{ctx.author} called the command '{ctx.command}' but the command was disabled!"
)
+ return await ctx.send(":no_entry: This command has been disabled.")
+
if isinstance(error, commands.NoPrivateMessage):
logging.debug(
f"{ctx.author} called the command '{ctx.command}' "
"in a private message however the command was guild only!"
)
- return await ctx.author.send(
- ":no_entry: This command can only be used inside a server."
- )
+ return await ctx.author.send(":no_entry: This command can only be used in the server.")
+
if isinstance(error, commands.BadArgument):
- if ctx.command.qualified_name == 'tag list':
- logging.debug(
- f"{ctx.author} called the command '{ctx.command}' "
- "but entered an invalid user!"
- )
- return await ctx.send(
- "I could not find that member. Please try again."
- )
- else:
- logging.debug(
- f"{ctx.author} called the command '{ctx.command}' "
- "but entered a bad argument!"
- )
- return await ctx.send(
- "The argument you provided was invalid."
- )
- if isinstance(error, commands.CheckFailure):
+ self.revert_cooldown_counter(ctx.command, ctx.message)
+
logging.debug(
- f"{ctx.author} called the command '{ctx.command}' "
- "but the checks failed!"
- )
- return await ctx.send(
- ":no_entry: You are not authorized to use this command."
+ f"{ctx.author} called the command '{ctx.command}' but entered a bad argument!"
)
- print(
- f"Ignoring exception in command {ctx.command}:",
- file=sys.stderr
- )
+ return await ctx.send("The argument you provided was invalid.")
+
+ if isinstance(error, commands.CheckFailure):
+ logging.debug(f"{ctx.author} called the command '{ctx.command}' but the checks failed!")
+ return await ctx.send(":no_entry: You are not authorized to use this command.")
+
+ print(f"Ignoring exception in command {ctx.command}:", file=sys.stderr)
+
logging.warning(
f"{ctx.author} called the command '{ctx.command}' "
"however the command failed to run with the error:"
f"-------------\n{error}"
)
- traceback.print_exception(
- type(error),
- error,
- error.__traceback__,
- file=sys.stderr
- )
+
+ traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr)
def setup(bot):
bot.add_cog(CommandErrorHandler(bot))
- log.debug("CommandErrorHandler cog loaded")
+ log.info("CommandErrorHandler cog loaded")
diff --git a/bot/seasons/evergreen/fun.py b/bot/seasons/evergreen/fun.py
new file mode 100644
index 00000000..9ef47331
--- /dev/null
+++ b/bot/seasons/evergreen/fun.py
@@ -0,0 +1,38 @@
+import logging
+import random
+
+from discord.ext import commands
+
+from bot.constants import Emojis
+
+log = logging.getLogger(__name__)
+
+
+class Fun(commands.Cog):
+ """
+ A collection of general commands for fun.
+ """
+
+ def __init__(self, bot):
+ self.bot = bot
+
+ @commands.command()
+ async def roll(self, ctx, num_rolls: int = 1):
+ """
+ Outputs a number of random dice emotes (up to 6)
+ """
+ output = ""
+ if num_rolls > 6:
+ num_rolls = 6
+ 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, '')
+ await ctx.send(output)
+
+
+# Required in order to load the cog, use the class name in the add_cog function.
+def setup(bot):
+ bot.add_cog(Fun(bot))
+ log.info("Fun cog loaded")
diff --git a/bot/seasons/evergreen/magic_8ball.py b/bot/seasons/evergreen/magic_8ball.py
new file mode 100644
index 00000000..88c9fd26
--- /dev/null
+++ b/bot/seasons/evergreen/magic_8ball.py
@@ -0,0 +1,36 @@
+import json
+import logging
+import random
+from pathlib import Path
+
+from discord.ext import commands
+
+log = logging.getLogger(__name__)
+
+
+class Magic8ball:
+ """
+ A Magic 8ball command to respond to a users question.
+ """
+
+ def __init__(self, bot):
+ self.bot = bot
+ with open(Path("bot", "resources", "evergreen", "magic8ball.json"), "r") as file:
+ self.answers = json.load(file)
+
+ @commands.command(name="8ball")
+ async def output_answer(self, ctx, *, question):
+ """
+ Return a magic 8 ball answer from answers list.
+ """
+ if len(question.split()) >= 3:
+ answer = random.choice(self.answers)
+ await ctx.send(answer)
+ else:
+ await ctx.send("Usage: .8ball <question> (minimum length of 3 eg: `will I win?`)")
+
+
+# Required in order to load the cog, use the class name in the add_cog function.
+def setup(bot):
+ bot.add_cog(Magic8ball(bot))
+ log.info("Magic 8ball cog loaded")
diff --git a/bot/seasons/evergreen/snakes/__init__.py b/bot/seasons/evergreen/snakes/__init__.py
new file mode 100644
index 00000000..6fb1f673
--- /dev/null
+++ b/bot/seasons/evergreen/snakes/__init__.py
@@ -0,0 +1,10 @@
+import logging
+
+from bot.seasons.evergreen.snakes.snakes_cog import Snakes
+
+log = logging.getLogger(__name__)
+
+
+def setup(bot):
+ bot.add_cog(Snakes(bot))
+ log.info("Snakes cog loaded")
diff --git a/bot/seasons/evergreen/snakes/converter.py b/bot/seasons/evergreen/snakes/converter.py
new file mode 100644
index 00000000..c091d9c1
--- /dev/null
+++ b/bot/seasons/evergreen/snakes/converter.py
@@ -0,0 +1,80 @@
+import json
+import logging
+import random
+
+import discord
+from discord.ext.commands import Converter
+from fuzzywuzzy import fuzz
+
+from bot.seasons.evergreen.snakes.utils import SNAKE_RESOURCES
+from bot.utils import disambiguate
+
+log = logging.getLogger(__name__)
+
+
+class Snake(Converter):
+ snakes = None
+ special_cases = None
+
+ async def convert(self, ctx, name):
+ await self.build_list()
+ name = name.lower()
+
+ if name == 'python':
+ return 'Python (programming language)'
+
+ def get_potential(iterable, *, threshold=80):
+ nonlocal name
+ potential = []
+
+ for item in iterable:
+ original, item = item, item.lower()
+
+ if name == item:
+ return [original]
+
+ a, b = fuzz.ratio(name, item), fuzz.partial_ratio(name, item)
+ if a >= threshold or b >= threshold:
+ potential.append(original)
+
+ return potential
+
+ # Handle special cases
+ if name.lower() in self.special_cases:
+ return self.special_cases.get(name.lower(), name.lower())
+
+ names = {snake['name']: snake['scientific'] for snake in self.snakes}
+ all_names = names.keys() | names.values()
+ timeout = len(all_names) * (3 / 4)
+
+ embed = discord.Embed(
+ title='Found multiple choices. Please choose the correct one.', colour=0x59982F)
+ embed.set_author(name=ctx.author.display_name, icon_url=ctx.author.avatar_url)
+
+ name = await disambiguate(ctx, get_potential(all_names), timeout=timeout, embed=embed)
+ return names.get(name, name)
+
+ @classmethod
+ async def build_list(cls):
+ # Get all the snakes
+ if cls.snakes is None:
+ with (SNAKE_RESOURCES / "snake_names.json").open() 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:
+ special_cases = json.load(snakefile)
+ cls.special_cases = {snake['name'].lower(): snake for snake in special_cases}
+
+ @classmethod
+ async def random(cls):
+ """
+ This is stupid. We should find a way to
+ somehow get the global session into a
+ global context, so I can get it from here.
+ :return:
+ """
+ await cls.build_list()
+ names = [snake['scientific'] for snake in cls.snakes]
+ return random.choice(names)
diff --git a/bot/seasons/evergreen/snakes/snakes_cog.py b/bot/seasons/evergreen/snakes/snakes_cog.py
new file mode 100644
index 00000000..74d2ab4f
--- /dev/null
+++ b/bot/seasons/evergreen/snakes/snakes_cog.py
@@ -0,0 +1,1189 @@
+import asyncio
+import colorsys
+import logging
+import os
+import random
+import re
+import string
+import textwrap
+import urllib
+from functools import partial
+from io import BytesIO
+from typing import Any, Dict
+
+import aiohttp
+import async_timeout
+from PIL import Image, ImageDraw, ImageFont
+from discord import Colour, Embed, File, Member, Message, Reaction
+from discord.ext.commands import BadArgument, Bot, Cog, Context, bot_has_permissions, group
+
+from bot.constants import ERROR_REPLIES, Tokens
+from bot.decorators import locked
+from bot.seasons.evergreen.snakes import utils
+from bot.seasons.evergreen.snakes.converter import Snake
+
+log = logging.getLogger(__name__)
+
+
+# region: Constants
+# Color
+SNAKE_COLOR = 0x399600
+
+# Antidote constants
+SYRINGE_EMOJI = "\U0001F489" # :syringe:
+PILL_EMOJI = "\U0001F48A" # :pill:
+HOURGLASS_EMOJI = "\u231B" # :hourglass:
+CROSSBONES_EMOJI = "\u2620" # :skull_crossbones:
+ALEMBIC_EMOJI = "\u2697" # :alembic:
+TICK_EMOJI = "\u2705" # :white_check_mark: - Correct peg, correct hole
+CROSS_EMOJI = "\u274C" # :x: - Wrong peg, wrong hole
+BLANK_EMOJI = "\u26AA" # :white_circle: - Correct peg, wrong hole
+HOLE_EMOJI = "\u2B1C" # :white_square: - Used in guesses
+EMPTY_UNICODE = "\u200b" # literally just an empty space
+
+ANTIDOTE_EMOJI = (
+ SYRINGE_EMOJI,
+ PILL_EMOJI,
+ HOURGLASS_EMOJI,
+ CROSSBONES_EMOJI,
+ ALEMBIC_EMOJI,
+)
+
+# Quiz constants
+ANSWERS_EMOJI = {
+ "a": "\U0001F1E6", # :regional_indicator_a: 🇦
+ "b": "\U0001F1E7", # :regional_indicator_b: 🇧
+ "c": "\U0001F1E8", # :regional_indicator_c: 🇨
+ "d": "\U0001F1E9", # :regional_indicator_d: 🇩
+}
+
+ANSWERS_EMOJI_REVERSE = {
+ "\U0001F1E6": "A", # :regional_indicator_a: 🇦
+ "\U0001F1E7": "B", # :regional_indicator_b: 🇧
+ "\U0001F1E8": "C", # :regional_indicator_c: 🇨
+ "\U0001F1E9": "D", # :regional_indicator_d: 🇩
+}
+
+# Zzzen of pythhhon constant
+ZEN = """
+Beautiful is better than ugly.
+Explicit is better than implicit.
+Simple is better than complex.
+Complex is better than complicated.
+Flat is better than nested.
+Sparse is better than dense.
+Readability counts.
+Special cases aren't special enough to break the rules.
+Although practicality beats purity.
+Errors should never pass silently.
+Unless explicitly silenced.
+In the face of ambiguity, refuse the temptation to guess.
+There should be one-- and preferably only one --obvious way to do it.
+Now is better than never.
+Although never is often better than *right* now.
+If the implementation is hard to explain, it's a bad idea.
+If the implementation is easy to explain, it may be a good idea.
+"""
+
+# Max messages to train snake_chat on
+MSG_MAX = 100
+
+# get_snek constants
+URL = "https://en.wikipedia.org/w/api.php?"
+
+# snake guess responses
+INCORRECT_GUESS = (
+ "Nope, that's not what it is.",
+ "Not quite.",
+ "Not even close.",
+ "Terrible guess.",
+ "Nnnno.",
+ "Dude. No.",
+ "I thought everyone knew this one.",
+ "Guess you suck at snakes.",
+ "Bet you feel stupid now.",
+ "Hahahaha, no.",
+ "Did you hit the wrong key?"
+)
+
+CORRECT_GUESS = (
+ "**WRONG**. Wait, no, actually you're right.",
+ "Yeah, you got it!",
+ "Yep, that's exactly what it is.",
+ "Uh-huh. Yep yep yep.",
+ "Yeah that's right.",
+ "Yup. How did you know that?",
+ "Are you a herpetologist?",
+ "Sure, okay, but I bet you can't pronounce it.",
+ "Are you cheating?"
+)
+
+# snake card consts
+CARD = {
+ "top": Image.open("bot/resources/snakes/snake_cards/card_top.png"),
+ "frame": Image.open("bot/resources/snakes/snake_cards/card_frame.png"),
+ "bottom": Image.open("bot/resources/snakes/snake_cards/card_bottom.png"),
+ "backs": [
+ Image.open(f"bot/resources/snakes/snake_cards/backs/{file}")
+ for file in os.listdir("bot/resources/snakes/snake_cards/backs")
+ ],
+ "font": ImageFont.truetype("bot/resources/snakes/snake_cards/expressway.ttf", 20)
+}
+# endregion
+
+
+class Snakes(Cog):
+ """
+ Commands related to snakes. These were created by our
+ community during the first code jam.
+
+ More information can be found in the code-jam-1 repo.
+
+ https://gitlab_bot_repo.com/discord-python/code-jams/code-jam-1
+ """
+
+ wiki_brief = re.compile(r'(.*?)(=+ (.*?) =+)', flags=re.DOTALL)
+ valid_image_extensions = ('gif', 'png', 'jpeg', 'jpg', 'webp')
+
+ def __init__(self, bot: Bot):
+ self.active_sal = {}
+ self.bot = bot
+ self.snake_names = utils.get_resource("snake_names")
+ self.snake_idioms = utils.get_resource("snake_idioms")
+ self.snake_quizzes = utils.get_resource("snake_quiz")
+ self.snake_facts = utils.get_resource("snake_facts")
+
+ # region: Helper methods
+ @staticmethod
+ def _beautiful_pastel(hue):
+ """
+ Returns random bright pastels.
+ """
+ light = random.uniform(0.7, 0.85)
+ saturation = 1
+
+ rgb = colorsys.hls_to_rgb(hue, light, saturation)
+ hex_rgb = ""
+
+ for part in rgb:
+ value = int(part * 0xFF)
+ hex_rgb += f"{value:02x}"
+
+ return int(hex_rgb, 16)
+
+ @staticmethod
+ def _generate_card(buffer: BytesIO, content: dict) -> BytesIO:
+ """
+ Generate a card from snake information.
+
+ Written by juan and Someone during the first code jam.
+ """
+ snake = Image.open(buffer)
+
+ # Get the size of the snake icon, configure the height of the image box (yes, it changes)
+ icon_width = 347 # Hardcoded, not much i can do about that
+ icon_height = int((icon_width / snake.width) * snake.height)
+ frame_copies = icon_height // CARD['frame'].height + 1
+ snake.thumbnail((icon_width, icon_height))
+
+ # Get the dimensions of the final image
+ main_height = icon_height + CARD['top'].height + CARD['bottom'].height
+ main_width = CARD['frame'].width
+
+ # Start creating the foreground
+ foreground = Image.new("RGBA", (main_width, main_height), (0, 0, 0, 0))
+ foreground.paste(CARD['top'], (0, 0))
+
+ # Generate the frame borders to the correct height
+ for offset in range(frame_copies):
+ position = (0, CARD['top'].height + offset * CARD['frame'].height)
+ foreground.paste(CARD['frame'], position)
+
+ # Add the image and bottom part of the image
+ foreground.paste(snake, (36, CARD['top'].height)) # Also hardcoded :(
+ foreground.paste(CARD['bottom'], (0, CARD['top'].height + icon_height))
+
+ # Setup the background
+ back = random.choice(CARD['backs'])
+ back_copies = main_height // back.height + 1
+ full_image = Image.new("RGBA", (main_width, main_height), (0, 0, 0, 0))
+
+ # Generate the tiled background
+ for offset in range(back_copies):
+ full_image.paste(back, (16, 16 + offset * back.height))
+
+ # Place the foreground onto the final image
+ full_image.paste(foreground, (0, 0), foreground)
+
+ # Get the first two sentences of the info
+ description = '.'.join(content['info'].split(".")[:2]) + '.'
+
+ # Setup positioning variables
+ margin = 36
+ offset = CARD['top'].height + icon_height + margin
+
+ # Create blank rectangle image which will be behind the text
+ rectangle = Image.new(
+ "RGBA",
+ (main_width, main_height),
+ (0, 0, 0, 0)
+ )
+
+ # Draw a semi-transparent rectangle on it
+ rect = ImageDraw.Draw(rectangle)
+ rect.rectangle(
+ (margin, offset, main_width - margin, main_height - margin),
+ fill=(63, 63, 63, 128)
+ )
+
+ # Paste it onto the final image
+ full_image.paste(rectangle, (0, 0), mask=rectangle)
+
+ # Draw the text onto the final image
+ draw = ImageDraw.Draw(full_image)
+ for line in textwrap.wrap(description, 36):
+ draw.text([margin + 4, offset], line, font=CARD['font'])
+ offset += CARD['font'].getsize(line)[1]
+
+ # Get the image contents as a BufferIO object
+ buffer = BytesIO()
+ full_image.save(buffer, 'PNG')
+ buffer.seek(0)
+
+ return buffer
+
+ @staticmethod
+ def _snakify(message):
+ """
+ Sssnakifffiesss a sstring.
+ """
+ # Replace fricatives with exaggerated snake fricatives.
+ simple_fricatives = [
+ "f", "s", "z", "h",
+ "F", "S", "Z", "H",
+ ]
+ complex_fricatives = [
+ "th", "sh", "Th", "Sh"
+ ]
+
+ for letter in simple_fricatives:
+ if letter.islower():
+ message = message.replace(letter, letter * random.randint(2, 4))
+ else:
+ message = message.replace(letter, (letter * random.randint(2, 4)).title())
+
+ for fricative in complex_fricatives:
+ message = message.replace(fricative, fricative[0] + fricative[1] * random.randint(2, 4))
+
+ return message
+
+ async def _fetch(self, session, url, params=None):
+ """
+ Asyncronous web request helper method.
+ """
+ if params is None:
+ params = {}
+
+ async with async_timeout.timeout(10):
+ async with session.get(url, params=params) as response:
+ return await response.json()
+
+ def _get_random_long_message(self, messages, retries=10):
+ """
+ Fetch a message that's at least 3 words long,
+ but only if it is possible to do so in retries
+ attempts. Else, just return whatever the last
+ message is.
+ """
+ long_message = random.choice(messages)
+ if len(long_message.split()) < 3 and retries > 0:
+ return self._get_random_long_message(
+ messages,
+ retries=retries - 1
+ )
+
+ return long_message
+
+ async def _get_snek(self, name: str) -> Dict[str, Any]:
+ """
+ Goes online and fetches all the data from a wikipedia article
+ about a snake. Builds a dict that the .get() method can use.
+
+ Created by Ava and eivl.
+
+ :param name: The name of the snake to get information for - omit for a random snake
+ :return: A dict containing information on a snake
+ """
+ snake_info = {}
+
+ async with aiohttp.ClientSession() as session:
+ params = {
+ 'format': 'json',
+ 'action': 'query',
+ 'list': 'search',
+ 'srsearch': name,
+ 'utf8': '',
+ 'srlimit': '1',
+ }
+
+ json = await self._fetch(session, URL, params=params)
+
+ # wikipedia does have a error page
+ try:
+ pageid = json["query"]["search"][0]["pageid"]
+ except KeyError:
+ # Wikipedia error page ID(?)
+ pageid = 41118
+ except IndexError:
+ return None
+
+ params = {
+ 'format': 'json',
+ 'action': 'query',
+ 'prop': 'extracts|images|info',
+ 'exlimit': 'max',
+ 'explaintext': '',
+ 'inprop': 'url',
+ 'pageids': pageid
+ }
+
+ json = await self._fetch(session, URL, params=params)
+
+ # constructing dict - handle exceptions later
+ try:
+ snake_info["title"] = json["query"]["pages"][f"{pageid}"]["title"]
+ snake_info["extract"] = json["query"]["pages"][f"{pageid}"]["extract"]
+ snake_info["images"] = json["query"]["pages"][f"{pageid}"]["images"]
+ snake_info["fullurl"] = json["query"]["pages"][f"{pageid}"]["fullurl"]
+ snake_info["pageid"] = json["query"]["pages"][f"{pageid}"]["pageid"]
+ except KeyError:
+ snake_info["error"] = True
+
+ if snake_info["images"]:
+ i_url = 'https://commons.wikimedia.org/wiki/Special:FilePath/'
+ image_list = []
+ map_list = []
+ thumb_list = []
+
+ # Wikipedia has arbitrary images that are not snakes
+ banned = [
+ 'Commons-logo.svg',
+ 'Red%20Pencil%20Icon.png',
+ 'distribution',
+ 'The%20Death%20of%20Cleopatra%20arthur.jpg',
+ 'Head%20of%20holotype',
+ 'locator',
+ 'Woma.png',
+ '-map.',
+ '.svg',
+ 'ange.',
+ 'Adder%20(PSF).png'
+ ]
+
+ for image in snake_info["images"]:
+ # images come in the format of `File:filename.extension`
+ file, sep, filename = image["title"].partition(':')
+ filename = filename.replace(" ", "%20") # Wikipedia returns good data!
+
+ if not filename.startswith('Map'):
+ if any(ban in filename for ban in banned):
+ pass
+ else:
+ image_list.append(f"{i_url}{filename}")
+ thumb_list.append(f"{i_url}{filename}?width=100")
+ else:
+ map_list.append(f"{i_url}{filename}")
+
+ snake_info["image_list"] = image_list
+ snake_info["map_list"] = map_list
+ snake_info["thumb_list"] = thumb_list
+ snake_info["name"] = name
+
+ match = self.wiki_brief.match(snake_info['extract'])
+ info = match.group(1) if match else None
+
+ if info:
+ info = info.replace("\n", "\n\n") # Give us some proper paragraphs.
+
+ snake_info["info"] = info
+
+ return snake_info
+
+ async def _get_snake_name(self) -> Dict[str, str]:
+ """
+ Gets a random snake name.
+ :return: A random snake name, as a string.
+ """
+ return random.choice(self.snake_names)
+
+ async def _validate_answer(self, ctx: Context, message: Message, answer: str, options: list):
+ """
+ Validate the answer using a reaction event loop
+ :return:
+ """
+
+ def predicate(reaction, user):
+ """
+ Test if the the answer is valid and can be evaluated.
+ """
+ return (
+ reaction.message.id == message.id # The reaction is attached to the question we asked.
+ and user == ctx.author # It's the user who triggered the quiz.
+ and str(reaction.emoji) in ANSWERS_EMOJI.values() # The reaction is one of the options.
+ )
+
+ for emoji in ANSWERS_EMOJI.values():
+ await message.add_reaction(emoji)
+
+ # Validate the answer
+ try:
+ reaction, user = await ctx.bot.wait_for("reaction_add", timeout=45.0, check=predicate)
+ except asyncio.TimeoutError:
+ await ctx.channel.send(f"You took too long. The correct answer was **{options[answer]}**.")
+ await message.clear_reactions()
+ return
+
+ if str(reaction.emoji) == ANSWERS_EMOJI[answer]:
+ await ctx.send(f"{random.choice(CORRECT_GUESS)} The correct answer was **{options[answer]}**.")
+ else:
+ await ctx.send(
+ f"{random.choice(INCORRECT_GUESS)} The correct answer was **{options[answer]}**."
+ )
+
+ await message.clear_reactions()
+ # endregion
+
+ # region: Commands
+ @group(name='snakes', aliases=('snake',), invoke_without_command=True)
+ async def snakes_group(self, ctx: Context):
+ """Commands from our first code jam."""
+
+ await ctx.invoke(self.bot.get_command("help"), "snake")
+
+ @bot_has_permissions(manage_messages=True)
+ @snakes_group.command(name='antidote')
+ @locked()
+ async def antidote_command(self, ctx: Context):
+ """
+ Antidote - Can you create the antivenom before the patient dies?
+
+ Rules: You have 4 ingredients for each antidote, you only have 10 attempts
+ Once you synthesize the antidote, you will be presented with 4 markers
+ Tick: This means you have a CORRECT ingredient in the CORRECT position
+ Circle: This means you have a CORRECT ingredient in the WRONG position
+ Cross: This means you have a WRONG ingredient in the WRONG position
+
+ Info: The game automatically ends after 5 minutes inactivity.
+ You should only use each ingredient once.
+
+ This game was created by Lord Bisk and Runew0lf.
+ """
+
+ def predicate(reaction_: Reaction, user_: Member):
+ """
+ Make sure that this reaction is what we want to operate on
+ """
+
+ return (
+ all((
+ # Reaction is on this message
+ reaction_.message.id == board_id.id,
+ # Reaction is one of the pagination emotes
+ reaction_.emoji in ANTIDOTE_EMOJI,
+ # Reaction was not made by the Bot
+ user_.id != self.bot.user.id,
+ # Reaction was made by author
+ user_.id == ctx.author.id
+ ))
+ )
+
+ # Initialize variables
+ antidote_tries = 0
+ antidote_guess_count = 0
+ antidote_guess_list = []
+ guess_result = []
+ board = []
+ page_guess_list = []
+ page_result_list = []
+ win = False
+
+ antidote_embed = Embed(color=SNAKE_COLOR, title="Antidote")
+ antidote_embed.set_author(name=ctx.author.name, icon_url=ctx.author.avatar_url)
+
+ # Generate answer
+ antidote_answer = list(ANTIDOTE_EMOJI) # Duplicate list, not reference it
+ random.shuffle(antidote_answer)
+ antidote_answer.pop()
+
+ # Begin initial board building
+ for i in range(0, 10):
+ page_guess_list.append(f"{HOLE_EMOJI} {HOLE_EMOJI} {HOLE_EMOJI} {HOLE_EMOJI}")
+ page_result_list.append(f"{CROSS_EMOJI} {CROSS_EMOJI} {CROSS_EMOJI} {CROSS_EMOJI}")
+ board.append(f"`{i+1:02d}` "
+ f"{page_guess_list[i]} - "
+ f"{page_result_list[i]}")
+ board.append(EMPTY_UNICODE)
+ antidote_embed.add_field(name="10 guesses remaining", value="\n".join(board))
+ board_id = await ctx.send(embed=antidote_embed) # Display board
+
+ # Add our player reactions
+ for emoji in ANTIDOTE_EMOJI:
+ await board_id.add_reaction(emoji)
+
+ # Begin main game loop
+ while not win and antidote_tries < 10:
+ try:
+ reaction, user = await ctx.bot.wait_for(
+ "reaction_add", timeout=300, check=predicate)
+ except asyncio.TimeoutError:
+ log.debug("Antidote timed out waiting for a reaction")
+ break # We're done, no reactions for the last 5 minutes
+
+ if antidote_tries < 10:
+ if antidote_guess_count < 4:
+ if reaction.emoji in ANTIDOTE_EMOJI:
+ antidote_guess_list.append(reaction.emoji)
+ antidote_guess_count += 1
+
+ if antidote_guess_count == 4: # Guesses complete
+ antidote_guess_count = 0
+ page_guess_list[antidote_tries] = " ".join(antidote_guess_list)
+
+ # Now check guess
+ for i in range(0, len(antidote_answer)):
+ if antidote_guess_list[i] == antidote_answer[i]:
+ guess_result.append(TICK_EMOJI)
+ elif antidote_guess_list[i] in antidote_answer:
+ guess_result.append(BLANK_EMOJI)
+ else:
+ guess_result.append(CROSS_EMOJI)
+ guess_result.sort()
+ page_result_list[antidote_tries] = " ".join(guess_result)
+
+ # Rebuild the board
+ board = []
+ for i in range(0, 10):
+ board.append(f"`{i+1:02d}` "
+ f"{page_guess_list[i]} - "
+ f"{page_result_list[i]}")
+ board.append(EMPTY_UNICODE)
+
+ # Remove Reactions
+ for emoji in antidote_guess_list:
+ await board_id.remove_reaction(emoji, user)
+
+ if antidote_guess_list == antidote_answer:
+ win = True
+
+ antidote_tries += 1
+ guess_result = []
+ antidote_guess_list = []
+
+ antidote_embed.clear_fields()
+ antidote_embed.add_field(name=f"{10 - antidote_tries} "
+ f"guesses remaining",
+ value="\n".join(board))
+ # Redisplay the board
+ await board_id.edit(embed=antidote_embed)
+
+ # Winning / Ending Screen
+ if win is True:
+ 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!",
+ value=f"The solution was: {' '.join(antidote_answer)}\n"
+ f"You had {10 - antidote_tries} tries remaining.")
+ await board_id.edit(embed=antidote_embed)
+ else:
+ 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://media.giphy.com/media/ceeN6U57leAhi/giphy.gif")
+ antidote_embed.add_field(name=EMPTY_UNICODE,
+ value=f"Sorry you didnt make the antidote in time.\n"
+ f"The formula was {' '.join(antidote_answer)}")
+ await board_id.edit(embed=antidote_embed)
+
+ log.debug("Ending pagination and removing all reactions...")
+ await board_id.clear_reactions()
+
+ @snakes_group.command(name='draw')
+ async def draw_command(self, ctx: Context):
+ """
+ Draws a random snek using Perlin noise
+
+ Written by Momo and kel.
+ Modified by juan and lemon.
+ """
+
+ with ctx.typing():
+
+ # Generate random snake attributes
+ width = random.randint(6, 10)
+ length = random.randint(15, 22)
+ random_hue = random.random()
+ snek_color = self._beautiful_pastel(random_hue)
+ text_color = self._beautiful_pastel((random_hue + 0.5) % 1)
+ bg_color = (
+ random.randint(32, 50),
+ random.randint(32, 50),
+ random.randint(50, 70),
+ )
+
+ # Build and send the snek
+ text = random.choice(self.snake_idioms)["idiom"]
+ factory = utils.PerlinNoiseFactory(dimension=1, octaves=2)
+ image_frame = utils.create_snek_frame(
+ factory,
+ snake_width=width,
+ snake_length=length,
+ snake_color=snek_color,
+ text=text,
+ text_color=text_color,
+ bg_color=bg_color
+ )
+ png_bytes = utils.frame_to_png_bytes(image_frame)
+ file = File(png_bytes, filename='snek.png')
+ await ctx.send(file=file)
+
+ @snakes_group.command(name='get')
+ @bot_has_permissions(manage_messages=True)
+ @locked()
+ async def get_command(self, ctx: Context, *, name: Snake = None):
+ """
+ Fetches information about a snake from Wikipedia.
+ :param ctx: Context object passed from discord.py
+ :param name: Optional, the name of the snake to get information
+ for - omit for a random snake
+
+ Created by Ava and eivl.
+ """
+ with ctx.typing():
+ if name is None:
+ name = await Snake.random()
+
+ if isinstance(name, dict):
+ data = name
+ else:
+ data = await self._get_snek(name)
+
+ if data.get('error'):
+ return await ctx.send('Could not fetch data from Wikipedia.')
+
+ description = data["info"]
+
+ # Shorten the description if needed
+ if len(description) > 1000:
+ description = description[:1000]
+ last_newline = description.rfind("\n")
+ if last_newline > 0:
+ description = description[:last_newline]
+
+ # Strip and add the Wiki link.
+ if "fullurl" in data:
+ description = description.strip("\n")
+ description += f"\n\nRead more on [Wikipedia]({data['fullurl']})"
+
+ # Build and send the embed.
+ embed = Embed(
+ title=data.get("title", data.get('name')),
+ description=description,
+ colour=0x59982F,
+ )
+
+ emoji = 'https://emojipedia-us.s3.amazonaws.com/thumbs/60/google/3/snake_1f40d.png'
+ image = next((url for url in data['image_list']
+ if url.endswith(self.valid_image_extensions)), emoji)
+ embed.set_image(url=image)
+
+ await ctx.send(embed=embed)
+
+ @snakes_group.command(name='guess', aliases=('identify',))
+ @locked()
+ async def guess_command(self, ctx):
+ """
+ Snake identifying game!
+
+ Made by Ava and eivl.
+ Modified by lemon.
+ """
+ with ctx.typing():
+
+ image = None
+
+ while image is None:
+ snakes = [await Snake.random() for _ in range(4)]
+ snake = random.choice(snakes)
+ answer = "abcd"[snakes.index(snake)]
+
+ data = await self._get_snek(snake)
+
+ image = next((url for url in data['image_list']
+ if url.endswith(self.valid_image_extensions)), None)
+
+ embed = Embed(
+ title='Which of the following is the snake in the image?',
+ description="\n".join(
+ f"{'ABCD'[snakes.index(snake)]}: {snake}" for snake in snakes),
+ colour=SNAKE_COLOR
+ )
+ embed.set_image(url=image)
+
+ guess = await ctx.send(embed=embed)
+ options = {f"{'abcd'[snakes.index(snake)]}": snake for snake in snakes}
+ await self._validate_answer(ctx, guess, answer, options)
+
+ @snakes_group.command(name='hatch')
+ async def hatch_command(self, ctx: Context):
+ """
+ Hatches your personal snake
+
+ Written by Momo and kel.
+ """
+ # Pick a random snake to hatch.
+ snake_name = random.choice(list(utils.snakes.keys()))
+ snake_image = utils.snakes[snake_name]
+
+ # Hatch the snake
+ message = await ctx.channel.send(embed=Embed(description="Hatching your snake :snake:..."))
+ await asyncio.sleep(1)
+
+ for stage in utils.stages:
+ hatch_embed = Embed(description=stage)
+ await message.edit(embed=hatch_embed)
+ await asyncio.sleep(1)
+ await asyncio.sleep(1)
+ await message.delete()
+
+ # Build and send the embed.
+ my_snake_embed = Embed(description=":tada: Congrats! You hatched: **{0}**".format(snake_name))
+ my_snake_embed.set_thumbnail(url=snake_image)
+ my_snake_embed.set_footer(
+ text=" Owner: {0}#{1}".format(ctx.message.author.name, ctx.message.author.discriminator)
+ )
+
+ await ctx.channel.send(embed=my_snake_embed)
+
+ @snakes_group.command(name='movie')
+ async def movie_command(self, ctx: Context):
+ """
+ Gets a random snake-related movie from OMDB.
+
+ Written by Samuel.
+ Modified by gdude.
+ """
+ url = "http://www.omdbapi.com/"
+ page = random.randint(1, 27)
+
+ response = await self.bot.http_session.get(
+ url,
+ params={
+ "s": "snake",
+ "page": page,
+ "type": "movie",
+ "apikey": Tokens.omdb
+ }
+ )
+ data = await response.json()
+ movie = random.choice(data["Search"])["imdbID"]
+
+ response = await self.bot.http_session.get(
+ url,
+ params={
+ "i": movie,
+ "apikey": Tokens.omdb
+ }
+ )
+ data = await response.json()
+
+ embed = Embed(
+ title=data["Title"],
+ color=SNAKE_COLOR
+ )
+
+ del data["Response"], data["imdbID"], data["Title"]
+
+ for key, value in data.items():
+ if not value or value == "N/A" or key in ("Response", "imdbID", "Title", "Type"):
+ continue
+
+ if key == "Ratings": # [{'Source': 'Internet Movie Database', 'Value': '7.6/10'}]
+ rating = random.choice(value)
+
+ if rating["Source"] != "Internet Movie Database":
+ embed.add_field(name=f"Rating: {rating['Source']}", value=rating["Value"])
+
+ continue
+
+ if key == "Poster":
+ embed.set_image(url=value)
+ continue
+
+ elif key == "imdbRating":
+ key = "IMDB Rating"
+
+ elif key == "imdbVotes":
+ key = "IMDB Votes"
+
+ embed.add_field(name=key, value=value, inline=True)
+
+ embed.set_footer(text="Data provided by the OMDB API")
+
+ await ctx.channel.send(
+ embed=embed
+ )
+
+ @snakes_group.command(name='quiz')
+ @locked()
+ async def quiz_command(self, ctx: Context):
+ """
+ Asks a snake-related question in the chat and validates the user's guess.
+
+ This was created by Mushy and Cardium,
+ and modified by Urthas and lemon.
+ """
+ # Prepare a question.
+ question = random.choice(self.snake_quizzes)
+ answer = question["answerkey"]
+ options = {key: question["options"][key] for key in ANSWERS_EMOJI.keys()}
+
+ # Build and send the embed.
+ embed = Embed(
+ color=SNAKE_COLOR,
+ title=question["question"],
+ description="\n".join(
+ [f"**{key.upper()}**: {answer}" for key, answer in options.items()]
+ )
+ )
+
+ quiz = await ctx.channel.send("", embed=embed)
+ await self._validate_answer(ctx, quiz, answer, options)
+
+ @snakes_group.command(name='name', aliases=('name_gen',))
+ async def name_command(self, ctx: Context, *, name: str = None):
+ """
+ Slices the users name at the last vowel (or second last if the name
+ ends with a vowel), and then combines it with a random snake name,
+ which is sliced at the first vowel (or second if the name starts with
+ a vowel).
+
+ If the name contains no vowels, it just appends the snakename
+ to the end of the name.
+
+ Examples:
+ lemon + anaconda = lemoconda
+ krzsn + anaconda = krzsnconda
+ gdude + anaconda = gduconda
+ aperture + anaconda = apertuconda
+ lucy + python = luthon
+ joseph + taipan = joseipan
+
+ This was written by Iceman, and modified for inclusion into the bot by lemon.
+ """
+ snake_name = await self._get_snake_name()
+ snake_name = snake_name['name']
+ snake_prefix = ""
+
+ # Set aside every word in the snake name except the last.
+ if " " in snake_name:
+ snake_prefix = " ".join(snake_name.split()[:-1])
+ snake_name = snake_name.split()[-1]
+
+ # If no name is provided, use whoever called the command.
+ if name:
+ user_name = name
+ else:
+ user_name = ctx.author.display_name
+
+ # Get the index of the vowel to slice the username at
+ user_slice_index = len(user_name)
+ for index, char in enumerate(reversed(user_name)):
+ if index == 0:
+ continue
+ if char.lower() in "aeiouy":
+ user_slice_index -= index
+ break
+
+ # Now, get the index of the vowel to slice the snake_name at
+ snake_slice_index = 0
+ for index, char in enumerate(snake_name):
+ if index == 0:
+ continue
+ if char.lower() in "aeiouy":
+ snake_slice_index = index + 1
+ break
+
+ # Combine!
+ snake_name = snake_name[snake_slice_index:]
+ user_name = user_name[:user_slice_index]
+ result = f"{snake_prefix} {user_name}{snake_name}"
+ result = string.capwords(result)
+
+ # Embed and send
+ embed = Embed(
+ title="Snake name",
+ description=f"Your snake-name is **{result}**",
+ color=SNAKE_COLOR
+ )
+
+ return await ctx.send(embed=embed)
+
+ @snakes_group.command(name='sal')
+ @locked()
+ async def sal_command(self, ctx: Context):
+ """
+ Play a game of Snakes and Ladders!
+
+ Written by Momo and kel.
+ Modified by lemon.
+ """
+ # check if there is already a game in this channel
+ if ctx.channel in self.active_sal:
+ await ctx.send(f"{ctx.author.mention} A game is already in progress in this channel.")
+ return
+
+ game = utils.SnakeAndLaddersGame(snakes=self, context=ctx)
+ self.active_sal[ctx.channel] = game
+
+ await game.open_game()
+
+ @snakes_group.command(name='about')
+ async def about_command(self, ctx: Context):
+ """
+ A command that shows an embed with information about the event,
+ it's participants, and its winners.
+ """
+ contributors = [
+ "<@!245270749919576066>",
+ "<@!396290259907903491>",
+ "<@!172395097705414656>",
+ "<@!361708843425726474>",
+ "<@!300302216663793665>",
+ "<@!210248051430916096>",
+ "<@!174588005745557505>",
+ "<@!87793066227822592>",
+ "<@!211619754039967744>",
+ "<@!97347867923976192>",
+ "<@!136081839474343936>",
+ "<@!263560579770220554>",
+ "<@!104749643715387392>",
+ "<@!303940835005825024>",
+ ]
+
+ embed = Embed(
+ 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"
+ "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."
+ )
+ )
+
+ embed.add_field(
+ name="Contributors",
+ value=(
+ ", ".join(contributors)
+ )
+ )
+
+ await ctx.channel.send(embed=embed)
+
+ @snakes_group.command(name='card')
+ async def card_command(self, ctx: Context, *, name: Snake = None):
+ """
+ Create an interesting little card from a snake!
+
+ Created by juan and Someone during the first code jam.
+ """
+ # Get the snake data we need
+ if not name:
+ name_obj = await self._get_snake_name()
+ name = name_obj['scientific']
+ content = await self._get_snek(name)
+
+ elif isinstance(name, dict):
+ content = name
+
+ else:
+ content = await self._get_snek(name)
+
+ # Make the card
+ async with ctx.typing():
+
+ stream = BytesIO()
+ async with async_timeout.timeout(10):
+ async with self.bot.http_session.get(content['image_list'][0]) as response:
+ stream.write(await response.read())
+
+ stream.seek(0)
+
+ func = partial(self._generate_card, stream, content)
+ final_buffer = await self.bot.loop.run_in_executor(None, func)
+
+ # Send it!
+ await ctx.send(
+ f"A wild {content['name'].title()} appears!",
+ file=File(final_buffer, filename=content['name'].replace(" ", "") + ".png")
+ )
+
+ @snakes_group.command(name='fact')
+ async def fact_command(self, ctx: Context):
+ """
+ Gets a snake-related fact
+
+ Written by Andrew and Prithaj.
+ Modified by lemon.
+ """
+ question = random.choice(self.snake_facts)["fact"]
+ embed = Embed(
+ title="Snake fact",
+ color=SNAKE_COLOR,
+ description=question
+ )
+ await ctx.channel.send(embed=embed)
+
+ @snakes_group.command(name='help')
+ async def help_command(self, ctx: Context):
+ """
+ This just invokes the help command on this cog.
+ """
+ log.debug(f"{ctx.author} requested info about the snakes cog")
+ return await ctx.invoke(self.bot.get_command("help"), "Snakes")
+
+ @snakes_group.command(name='snakify')
+ async def snakify_command(self, ctx: Context, *, message: str = None):
+ """
+ How would I talk if I were a snake?
+ :param ctx: context
+ :param message: If this is passed, it will snakify the message.
+ If not, it will snakify a random message from
+ the users history.
+
+ Written by Momo and kel.
+ Modified by lemon.
+ """
+ with ctx.typing():
+ embed = Embed()
+ user = ctx.message.author
+
+ if not message:
+
+ # Get a random message from the users history
+ messages = []
+ async for message in ctx.channel.history(limit=500).filter(
+ lambda msg: msg.author == ctx.message.author # Message was sent by author.
+ ):
+ messages.append(message.content)
+
+ message = self._get_random_long_message(messages)
+
+ # Set the avatar
+ if user.avatar is not None:
+ avatar = f"https://cdn.discordapp.com/avatars/{user.id}/{user.avatar}"
+ else:
+ avatar = ctx.author.default_avatar_url
+
+ # Build and send the embed
+ embed.set_author(
+ name=f"{user.name}#{user.discriminator}",
+ icon_url=avatar,
+ )
+ embed.description = f"*{self._snakify(message)}*"
+
+ await ctx.channel.send(embed=embed)
+
+ @snakes_group.command(name='video', aliases=('get_video',))
+ async def video_command(self, ctx: Context, *, search: str = None):
+ """
+ Gets a YouTube video about snakes
+
+ :param ctx: Context object passed from discord.py
+ :param search: Optional, a name of a snake. Used to search for videos with that name
+
+ Written by Andrew and Prithaj.
+ """
+ # Are we searching for anything specific?
+ if search:
+ query = search + ' snake'
+ else:
+ snake = await self._get_snake_name()
+ query = snake['name']
+
+ # Build the URL and make the request
+ url = f'https://www.googleapis.com/youtube/v3/search'
+ response = await self.bot.http_session.get(
+ url,
+ params={
+ "part": "snippet",
+ "q": urllib.parse.quote(query),
+ "type": "video",
+ "key": Tokens.youtube
+ }
+ )
+ response = await response.json()
+ data = response['items']
+
+ # Send the user a video
+ if len(data) > 0:
+ num = random.randint(0, len(data) - 1)
+ youtube_base_url = 'https://www.youtube.com/watch?v='
+ await ctx.channel.send(
+ content=f"{youtube_base_url}{data[num]['id']['videoId']}"
+ )
+ else:
+ log.warning(f"YouTube API error. Full response looks like {response}")
+
+ @snakes_group.command(name='zen')
+ async def zen_command(self, ctx: Context):
+ """
+ Gets a random quote from the Zen of Python,
+ except as if spoken by a snake.
+
+ Written by Prithaj and Andrew.
+ Modified by lemon.
+ """
+ embed = Embed(
+ title="Zzzen of Pythhon",
+ color=SNAKE_COLOR
+ )
+
+ # Get the zen quote and snakify it
+ zen_quote = random.choice(ZEN.splitlines())
+ zen_quote = self._snakify(zen_quote)
+
+ # Embed and send
+ embed.description = zen_quote
+ await ctx.channel.send(
+ embed=embed
+ )
+ # endregion
+
+ # region: Error handlers
+ @get_command.error
+ @card_command.error
+ @video_command.error
+ async def command_error(self, ctx, error):
+
+ embed = Embed()
+ embed.colour = Colour.red()
+
+ if isinstance(error, BadArgument):
+ embed.description = str(error)
+ embed.title = random.choice(ERROR_REPLIES)
+
+ elif isinstance(error, OSError):
+ log.error(f"snake_card encountered an OSError: {error} ({error.original})")
+ embed.description = "Could not generate the snake card! Please try again."
+ embed.title = random.choice(ERROR_REPLIES)
+
+ else:
+ log.error(f"Unhandled tag command error: {error} ({error.original})")
+ return
+
+ await ctx.send(embed=embed)
+ # endregion
diff --git a/bot/seasons/evergreen/snakes/utils.py b/bot/seasons/evergreen/snakes/utils.py
new file mode 100644
index 00000000..ec280223
--- /dev/null
+++ b/bot/seasons/evergreen/snakes/utils.py
@@ -0,0 +1,700 @@
+"""
+Perlin noise implementation.
+Taken from: https://gist.github.com/eevee/26f547457522755cb1fb8739d0ea89a1
+Licensed under ISC
+"""
+import asyncio
+import io
+import json
+import logging
+import math
+import random
+from itertools import product
+from pathlib import Path
+from typing import List, Tuple
+
+import aiohttp
+from PIL import Image
+from PIL.ImageDraw import ImageDraw
+from discord import File, Member, Reaction
+from discord.ext.commands import Context
+
+SNAKE_RESOURCES = Path('bot', 'resources', 'snakes').absolute()
+
+h1 = r'''```
+ ----
+ ------
+ /--------\
+ |--------|
+ |--------|
+ \------/
+ ----```'''
+h2 = r'''```
+ ----
+ ------
+ /---\-/--\
+ |-----\--|
+ |--------|
+ \------/
+ ----```'''
+h3 = r'''```
+ ----
+ ------
+ /---\-/--\
+ |-----\--|
+ |-----/--|
+ \----\-/
+ ----```'''
+h4 = r'''```
+ -----
+ ----- \
+ /--| /---\
+ |--\ -\---|
+ |--\--/-- /
+ \------- /
+ ------```'''
+stages = [h1, h2, h3, h4]
+snakes = {
+ "Baby Python": "https://i.imgur.com/SYOcmSa.png",
+ "Baby Rattle Snake": "https://i.imgur.com/i5jYA8f.png",
+ "Baby Dragon Snake": "https://i.imgur.com/SuMKM4m.png",
+ "Baby Garden Snake": "https://i.imgur.com/5vYx3ah.png",
+ "Baby Cobra": "https://i.imgur.com/jk14ryt.png"
+}
+
+BOARD_TILE_SIZE = 56 # the size of each board tile
+BOARD_PLAYER_SIZE = 20 # the size of each player icon
+BOARD_MARGIN = (10, 0) # margins, in pixels (for player icons)
+# The size of the image to download
+# Should a power of 2 and higher than BOARD_PLAYER_SIZE
+PLAYER_ICON_IMAGE_SIZE = 32
+MAX_PLAYERS = 4 # depends on the board size/quality, 4 is for the default board
+
+# board definition (from, to)
+BOARD = {
+ # ladders
+ 2: 38,
+ 7: 14,
+ 8: 31,
+ 15: 26,
+ 21: 42,
+ 28: 84,
+ 36: 44,
+ 51: 67,
+ 71: 91,
+ 78: 98,
+ 87: 94,
+
+ # snakes
+ 99: 80,
+ 95: 75,
+ 92: 88,
+ 89: 68,
+ 74: 53,
+ 64: 60,
+ 62: 19,
+ 49: 11,
+ 46: 25,
+ 16: 6
+}
+
+DEFAULT_SNAKE_COLOR: int = 0x15c7ea
+DEFAULT_BACKGROUND_COLOR: int = 0
+DEFAULT_IMAGE_DIMENSIONS: Tuple[int] = (200, 200)
+DEFAULT_SNAKE_LENGTH: int = 22
+DEFAULT_SNAKE_WIDTH: int = 8
+DEFAULT_SEGMENT_LENGTH_RANGE: Tuple[int] = (7, 10)
+DEFAULT_IMAGE_MARGINS: Tuple[int] = (50, 50)
+DEFAULT_TEXT: str = "snek\nit\nup"
+DEFAULT_TEXT_POSITION: Tuple[int] = (
+ 10,
+ 10
+)
+DEFAULT_TEXT_COLOR: int = 0xf2ea15
+X = 0
+Y = 1
+ANGLE_RANGE = math.pi * 2
+
+
+def get_resource(file: str) -> List[dict]:
+ with (SNAKE_RESOURCES / f"{file}.json").open(encoding="utf-8") as snakefile:
+ return json.load(snakefile)
+
+
+def smoothstep(t):
+ """Smooth curve with a zero derivative at 0 and 1, making it useful for
+ interpolating.
+ """
+ return t * t * (3. - 2. * t)
+
+
+def lerp(t, a, b):
+ """Linear interpolation between a and b, given a fraction t."""
+ return a + t * (b - a)
+
+
+class PerlinNoiseFactory(object):
+ """Callable that produces Perlin noise for an arbitrary point in an
+ arbitrary number of dimensions. The underlying grid is aligned with the
+ integers.
+ There is no limit to the coordinates used; new gradients are generated on
+ the fly as necessary.
+ """
+
+ def __init__(self, dimension, octaves=1, tile=(), unbias=False):
+ """Create a new Perlin noise factory in the given number of dimensions,
+ which should be an integer and at least 1.
+ More octaves create a foggier and more-detailed noise pattern. More
+ than 4 octaves is rather excessive.
+ ``tile`` can be used to make a seamlessly tiling pattern. For example:
+ pnf = PerlinNoiseFactory(2, tile=(0, 3))
+ This will produce noise that tiles every 3 units vertically, but never
+ tiles horizontally.
+ If ``unbias`` is true, the smoothstep function will be applied to the
+ output before returning it, to counteract some of Perlin noise's
+ significant bias towards the center of its output range.
+ """
+ self.dimension = dimension
+ self.octaves = octaves
+ self.tile = tile + (0,) * dimension
+ self.unbias = unbias
+
+ # For n dimensions, the range of Perlin noise is ±sqrt(n)/2; multiply
+ # by this to scale to ±1
+ self.scale_factor = 2 * dimension ** -0.5
+
+ self.gradient = {}
+
+ def _generate_gradient(self):
+ # Generate a random unit vector at each grid point -- this is the
+ # "gradient" vector, in that the grid tile slopes towards it
+
+ # 1 dimension is special, since the only unit vector is trivial;
+ # instead, use a slope between -1 and 1
+ if self.dimension == 1:
+ return (random.uniform(-1, 1),)
+
+ # Generate a random point on the surface of the unit n-hypersphere;
+ # this is the same as a random unit vector in n dimensions. Thanks
+ # to: http://mathworld.wolfram.com/SpherePointPicking.html
+ # Pick n normal random variables with stddev 1
+ random_point = [random.gauss(0, 1) for _ in range(self.dimension)]
+ # Then scale the result to a unit vector
+ scale = sum(n * n for n in random_point) ** -0.5
+ return tuple(coord * scale for coord in random_point)
+
+ def get_plain_noise(self, *point):
+ """Get plain noise for a single point, without taking into account
+ either octaves or tiling.
+ """
+ if len(point) != self.dimension:
+ raise ValueError("Expected {0} values, got {1}".format(
+ self.dimension, len(point)))
+
+ # Build a list of the (min, max) bounds in each dimension
+ grid_coords = []
+ for coord in point:
+ min_coord = math.floor(coord)
+ max_coord = min_coord + 1
+ grid_coords.append((min_coord, max_coord))
+
+ # Compute the dot product of each gradient vector and the point's
+ # distance from the corresponding grid point. This gives you each
+ # gradient's "influence" on the chosen point.
+ dots = []
+ for grid_point in product(*grid_coords):
+ if grid_point not in self.gradient:
+ self.gradient[grid_point] = self._generate_gradient()
+ gradient = self.gradient[grid_point]
+
+ dot = 0
+ for i in range(self.dimension):
+ dot += gradient[i] * (point[i] - grid_point[i])
+ dots.append(dot)
+
+ # Interpolate all those dot products together. The interpolation is
+ # done with smoothstep to smooth out the slope as you pass from one
+ # grid cell into the next.
+ # Due to the way product() works, dot products are ordered such that
+ # the last dimension alternates: (..., min), (..., max), etc. So we
+ # can interpolate adjacent pairs to "collapse" that last dimension. Then
+ # the results will alternate in their second-to-last dimension, and so
+ # forth, until we only have a single value left.
+ dim = self.dimension
+ while len(dots) > 1:
+ dim -= 1
+ s = smoothstep(point[dim] - grid_coords[dim][0])
+
+ next_dots = []
+ while dots:
+ next_dots.append(lerp(s, dots.pop(0), dots.pop(0)))
+
+ dots = next_dots
+
+ return dots[0] * self.scale_factor
+
+ def __call__(self, *point):
+ """Get the value of this Perlin noise function at the given point. The
+ number of values given should match the number of dimensions.
+ """
+ ret = 0
+ for o in range(self.octaves):
+ o2 = 1 << o
+ new_point = []
+ for i, coord in enumerate(point):
+ coord *= o2
+ if self.tile[i]:
+ coord %= self.tile[i] * o2
+ new_point.append(coord)
+ ret += self.get_plain_noise(*new_point) / o2
+
+ # Need to scale n back down since adding all those extra octaves has
+ # probably expanded it beyond ±1
+ # 1 octave: ±1
+ # 2 octaves: ±1½
+ # 3 octaves: ±1¾
+ ret /= 2 - 2 ** (1 - self.octaves)
+
+ if self.unbias:
+ # The output of the plain Perlin noise algorithm has a fairly
+ # strong bias towards the center due to the central limit theorem
+ # -- in fact the top and bottom 1/8 virtually never happen. That's
+ # a quarter of our entire output range! If only we had a function
+ # in [0..1] that could introduce a bias towards the endpoints...
+ r = (ret + 1) / 2
+ # Doing it this many times is a completely made-up heuristic.
+ for _ in range(int(self.octaves / 2 + 0.5)):
+ r = smoothstep(r)
+ ret = r * 2 - 1
+
+ return ret
+
+
+def create_snek_frame(
+ perlin_factory: PerlinNoiseFactory, perlin_lookup_vertical_shift: float = 0,
+ image_dimensions: Tuple[int] = DEFAULT_IMAGE_DIMENSIONS, image_margins: Tuple[int] = DEFAULT_IMAGE_MARGINS,
+ snake_length: int = DEFAULT_SNAKE_LENGTH,
+ snake_color: int = DEFAULT_SNAKE_COLOR, bg_color: int = DEFAULT_BACKGROUND_COLOR,
+ segment_length_range: Tuple[int] = DEFAULT_SEGMENT_LENGTH_RANGE, snake_width: int = DEFAULT_SNAKE_WIDTH,
+ text: str = DEFAULT_TEXT, text_position: Tuple[int] = DEFAULT_TEXT_POSITION,
+ text_color: Tuple[int] = DEFAULT_TEXT_COLOR
+) -> Image:
+ """
+ Creates a single random snek frame using Perlin noise.
+ :param perlin_factory: the perlin noise factory used. Required.
+ :param perlin_lookup_vertical_shift: the Perlin noise shift in the Y-dimension for this frame
+ :param image_dimensions: the size of the output image.
+ :param image_margins: the margins to respect inside of the image.
+ :param snake_length: the length of the snake, in segments.
+ :param snake_color: the color of the snake.
+ :param bg_color: the background color.
+ :param segment_length_range: the range of the segment length. Values will be generated inside this range, including
+ the bounds.
+ :param snake_width: the width of the snek, in pixels.
+ :param text: the text to display with the snek. Set to None for no text.
+ :param text_position: the position of the text.
+ :param text_color: the color of the text.
+ :return: a PIL image, representing a single frame.
+ """
+ start_x = random.randint(image_margins[X], image_dimensions[X] - image_margins[X])
+ start_y = random.randint(image_margins[Y], image_dimensions[Y] - image_margins[Y])
+ points = [(start_x, start_y)]
+
+ for index in range(0, snake_length):
+ angle = perlin_factory.get_plain_noise(
+ ((1 / (snake_length + 1)) * (index + 1)) + perlin_lookup_vertical_shift
+ ) * ANGLE_RANGE
+ current_point = points[index]
+ segment_length = random.randint(segment_length_range[0], segment_length_range[1])
+ points.append((
+ current_point[X] + segment_length * math.cos(angle),
+ current_point[Y] + segment_length * math.sin(angle)
+ ))
+
+ # normalize bounds
+ min_dimensions = [start_x, start_y]
+ max_dimensions = [start_x, start_y]
+ for point in points:
+ min_dimensions[X] = min(point[X], min_dimensions[X])
+ min_dimensions[Y] = min(point[Y], min_dimensions[Y])
+ max_dimensions[X] = max(point[X], max_dimensions[X])
+ max_dimensions[Y] = max(point[Y], max_dimensions[Y])
+
+ # shift towards middle
+ dimension_range = (max_dimensions[X] - min_dimensions[X], max_dimensions[Y] - min_dimensions[Y])
+ shift = (
+ image_dimensions[X] / 2 - (dimension_range[X] / 2 + min_dimensions[X]),
+ image_dimensions[Y] / 2 - (dimension_range[Y] / 2 + min_dimensions[Y])
+ )
+
+ image = Image.new(mode='RGB', size=image_dimensions, color=bg_color)
+ draw = ImageDraw(image)
+ for index in range(1, len(points)):
+ point = points[index]
+ previous = points[index - 1]
+ draw.line(
+ (
+ shift[X] + previous[X],
+ shift[Y] + previous[Y],
+ shift[X] + point[X],
+ shift[Y] + point[Y]
+ ),
+ width=snake_width,
+ fill=snake_color
+ )
+ if text is not None:
+ draw.multiline_text(text_position, text, fill=text_color)
+ del draw
+ return image
+
+
+def frame_to_png_bytes(image: Image):
+ stream = io.BytesIO()
+ image.save(stream, format='PNG')
+ return stream.getvalue()
+
+
+log = logging.getLogger(__name__)
+START_EMOJI = "\u2611" # :ballot_box_with_check: - Start the game
+CANCEL_EMOJI = "\u274C" # :x: - Cancel or leave the game
+ROLL_EMOJI = "\U0001F3B2" # :game_die: - Roll the die!
+JOIN_EMOJI = "\U0001F64B" # :raising_hand: - Join the game.
+STARTUP_SCREEN_EMOJI = [
+ JOIN_EMOJI,
+ START_EMOJI,
+ CANCEL_EMOJI
+]
+GAME_SCREEN_EMOJI = [
+ ROLL_EMOJI,
+ CANCEL_EMOJI
+]
+
+
+class SnakeAndLaddersGame:
+ def __init__(self, snakes, context: Context):
+ self.snakes = snakes
+ self.ctx = context
+ self.channel = self.ctx.channel
+ self.state = 'booting'
+ self.started = False
+ self.author = self.ctx.author
+ self.players = []
+ self.player_tiles = {}
+ self.round_has_rolled = {}
+ self.avatar_images = {}
+ self.board = None
+ self.positions = None
+ self.rolls = []
+
+ async def open_game(self):
+ """
+ Create a new Snakes and Ladders game.
+
+ Listen for reactions until players have joined,
+ and the game has been started.
+ """
+ def startup_event_check(reaction_: Reaction, user_: Member):
+ """
+ Make sure that this reaction is what we want to operate on
+ """
+ return (
+ all((
+ reaction_.message.id == startup.id, # Reaction is on startup message
+ reaction_.emoji in STARTUP_SCREEN_EMOJI, # Reaction is one of the startup emotes
+ user_.id != self.ctx.bot.user.id, # Reaction was not made by the bot
+ ))
+ )
+
+ # Check to see if the bot can remove reactions
+ if not self.channel.permissions_for(self.ctx.guild.me).manage_messages:
+ log.warning(
+ "Unable to start Snakes and Ladders - "
+ f"Missing manage_messages permissions in {self.channel}"
+ )
+ return
+
+ await self._add_player(self.author)
+ await self.channel.send(
+ "**Snakes and Ladders**: A new game is about to start!",
+ file=File(
+ str(SNAKE_RESOURCES / "snakes_and_ladders" / "banner.jpg"),
+ # os.path.join("bot", "resources", "snakes", "snakes_and_ladders", "banner.jpg"),
+ filename='Snakes and Ladders.jpg'
+ )
+ )
+ startup = await self.channel.send(
+ f"Press {JOIN_EMOJI} to participate, and press "
+ f"{START_EMOJI} to start the game"
+ )
+ for emoji in STARTUP_SCREEN_EMOJI:
+ await startup.add_reaction(emoji)
+
+ self.state = 'waiting'
+
+ while not self.started:
+ try:
+ reaction, user = await self.ctx.bot.wait_for(
+ "reaction_add",
+ timeout=300,
+ check=startup_event_check
+ )
+ if reaction.emoji == JOIN_EMOJI:
+ await self.player_join(user)
+ elif reaction.emoji == CANCEL_EMOJI:
+ if self.ctx.author == user:
+ await self.cancel_game(user)
+ return
+ else:
+ await self.player_leave(user)
+ elif reaction.emoji == START_EMOJI:
+ if self.ctx.author == user:
+ self.started = True
+ await self.start_game(user)
+ await startup.delete()
+ break
+
+ await startup.remove_reaction(reaction.emoji, user)
+
+ except asyncio.TimeoutError:
+ log.debug("Snakes and Ladders timed out waiting for a reaction")
+ self.cancel_game(self.author)
+ return # We're done, no reactions for the last 5 minutes
+
+ async def _add_player(self, user: Member):
+ self.players.append(user)
+ self.player_tiles[user.id] = 1
+ avatar_url = user.avatar_url_as(format='jpeg', size=PLAYER_ICON_IMAGE_SIZE)
+ async with aiohttp.ClientSession() as session:
+ async with session.get(avatar_url) as res:
+ avatar_bytes = await res.read()
+ im = Image.open(io.BytesIO(avatar_bytes)).resize((BOARD_PLAYER_SIZE, BOARD_PLAYER_SIZE))
+ self.avatar_images[user.id] = im
+
+ async def player_join(self, user: Member):
+ for p in self.players:
+ if user == p:
+ await self.channel.send(user.mention + " You are already in the game.", delete_after=10)
+ return
+ if self.state != 'waiting':
+ await self.channel.send(user.mention + " You cannot join at this time.", delete_after=10)
+ return
+ if len(self.players) is MAX_PLAYERS:
+ await self.channel.send(user.mention + " The game is full!", delete_after=10)
+ return
+
+ await self._add_player(user)
+
+ await self.channel.send(
+ f"**Snakes and Ladders**: {user.mention} has joined the game.\n"
+ f"There are now {str(len(self.players))} players in the game.",
+ delete_after=10
+ )
+
+ async def player_leave(self, user: Member):
+ if user == self.author:
+ await self.channel.send(
+ user.mention + " You are the author, and cannot leave the game. Execute "
+ "`sal cancel` to cancel the game.",
+ delete_after=10
+ )
+ return
+ for p in self.players:
+ if user == p:
+ self.players.remove(p)
+ self.player_tiles.pop(p.id, None)
+ self.round_has_rolled.pop(p.id, None)
+ await self.channel.send(
+ "**Snakes and Ladders**: " + user.mention + " has left the game.",
+ delete_after=10
+ )
+
+ if self.state != 'waiting' and len(self.players) == 1:
+ await self.channel.send("**Snakes and Ladders**: The game has been surrendered!")
+ self._destruct()
+ return
+ await self.channel.send(user.mention + " You are not in the match.", delete_after=10)
+
+ async def cancel_game(self, user: Member):
+ if not user == self.author:
+ await self.channel.send(user.mention + " Only the author of the game can cancel it.", delete_after=10)
+ return
+ await self.channel.send("**Snakes and Ladders**: Game has been canceled.")
+ self._destruct()
+
+ async def start_game(self, user: Member):
+ if not user == self.author:
+ await self.channel.send(user.mention + " Only the author of the game can start it.", delete_after=10)
+ return
+ if len(self.players) < 1:
+ await self.channel.send(
+ user.mention + " A minimum of 2 players is required to start the game.",
+ delete_after=10
+ )
+ return
+ if not self.state == 'waiting':
+ await self.channel.send(user.mention + " The game cannot be started at this time.", delete_after=10)
+ return
+ self.state = 'starting'
+ player_list = ', '.join(user.mention for user in self.players)
+ await self.channel.send("**Snakes and Ladders**: The game is starting!\nPlayers: " + player_list)
+ await self.start_round()
+
+ async def start_round(self):
+ def game_event_check(reaction_: Reaction, user_: Member):
+ """
+ Make sure that this reaction is what we want to operate on
+ """
+ return (
+ all((
+ reaction_.message.id == self.positions.id, # Reaction is on positions message
+ reaction_.emoji in GAME_SCREEN_EMOJI, # Reaction is one of the game emotes
+ user_.id != self.ctx.bot.user.id, # Reaction was not made by the bot
+ ))
+ )
+
+ self.state = 'roll'
+ for user in self.players:
+ self.round_has_rolled[user.id] = False
+ # board_img = Image.open(os.path.join(
+ # "bot", "resources", "snakes", "snakes_and_ladders", "board.jpg"))
+ board_img = Image.open(str(SNAKE_RESOURCES / "snakes_and_ladders" / "board.jpg"))
+ player_row_size = math.ceil(MAX_PLAYERS / 2)
+
+ for i, player in enumerate(self.players):
+ tile = self.player_tiles[player.id]
+ tile_coordinates = self._board_coordinate_from_index(tile)
+ x_offset = BOARD_MARGIN[0] + tile_coordinates[0] * BOARD_TILE_SIZE
+ y_offset = \
+ BOARD_MARGIN[1] + (
+ (10 * BOARD_TILE_SIZE) - (9 - tile_coordinates[1]) * BOARD_TILE_SIZE - BOARD_PLAYER_SIZE)
+ x_offset += BOARD_PLAYER_SIZE * (i % player_row_size)
+ y_offset -= BOARD_PLAYER_SIZE * math.floor(i / player_row_size)
+ board_img.paste(self.avatar_images[player.id],
+ box=(x_offset, y_offset))
+ stream = io.BytesIO()
+ board_img.save(stream, format='JPEG')
+ board_file = File(stream.getvalue(), filename='Board.jpg')
+ player_list = '\n'.join((user.mention + ": Tile " + str(self.player_tiles[user.id])) for user in self.players)
+
+ # Store and send new messages
+ temp_board = await self.channel.send(
+ "**Snakes and Ladders**: A new round has started! Current board:",
+ file=board_file
+ )
+ temp_positions = await self.channel.send(
+ f"**Current positions**:\n{player_list}\n\nUse {ROLL_EMOJI} to roll the dice!"
+ )
+
+ # Delete the previous messages
+ if self.board and self.positions:
+ await self.board.delete()
+ await self.positions.delete()
+
+ # remove the roll messages
+ for roll in self.rolls:
+ await roll.delete()
+ self.rolls = []
+
+ # Save new messages
+ self.board = temp_board
+ self.positions = temp_positions
+
+ # Wait for rolls
+ for emoji in GAME_SCREEN_EMOJI:
+ await self.positions.add_reaction(emoji)
+
+ while True:
+ try:
+ reaction, user = await self.ctx.bot.wait_for(
+ "reaction_add",
+ timeout=300,
+ check=game_event_check
+ )
+
+ if reaction.emoji == ROLL_EMOJI:
+ await self.player_roll(user)
+ elif reaction.emoji == CANCEL_EMOJI:
+ if self.ctx.author == user:
+ await self.cancel_game(user)
+ return
+ else:
+ await self.player_leave(user)
+
+ await self.positions.remove_reaction(reaction.emoji, user)
+
+ if self._check_all_rolled():
+ break
+
+ except asyncio.TimeoutError:
+ log.debug("Snakes and Ladders timed out waiting for a reaction")
+ await self.cancel_game(self.author)
+ return # We're done, no reactions for the last 5 minutes
+
+ # Round completed
+ await self._complete_round()
+
+ async def player_roll(self, user: Member):
+ if user.id not in self.player_tiles:
+ await self.channel.send(user.mention + " You are not in the match.", delete_after=10)
+ return
+ if self.state != 'roll':
+ await self.channel.send(user.mention + " You may not roll at this time.", delete_after=10)
+ return
+ if self.round_has_rolled[user.id]:
+ return
+ roll = random.randint(1, 6)
+ self.rolls.append(await self.channel.send(f"{user.mention} rolled a **{roll}**!"))
+ next_tile = self.player_tiles[user.id] + roll
+
+ # apply snakes and ladders
+ if next_tile in BOARD:
+ target = BOARD[next_tile]
+ if target < next_tile:
+ await self.channel.send(
+ f"{user.mention} slips on a snake and falls back to **{target}**",
+ delete_after=15
+ )
+ else:
+ await self.channel.send(
+ f"{user.mention} climbs a ladder to **{target}**",
+ delete_after=15
+ )
+ next_tile = target
+
+ self.player_tiles[user.id] = min(100, next_tile)
+ self.round_has_rolled[user.id] = True
+
+ async def _complete_round(self):
+ self.state = 'post_round'
+
+ # check for winner
+ winner = self._check_winner()
+ if winner is None:
+ # there is no winner, start the next round
+ await self.start_round()
+ return
+
+ # announce winner and exit
+ await self.channel.send("**Snakes and Ladders**: " + winner.mention + " has won the game! :tada:")
+ self._destruct()
+
+ def _check_winner(self) -> Member:
+ if self.state != 'post_round':
+ return None
+ return next((player for player in self.players if self.player_tiles[player.id] == 100),
+ None)
+
+ def _check_all_rolled(self):
+ return all(rolled for rolled in self.round_has_rolled.values())
+
+ def _destruct(self):
+ del self.snakes.active_sal[self.channel]
+
+ def _board_coordinate_from_index(self, index: int):
+ # converts the tile number to the x/y coordinates for graphical purposes
+ y_level = 9 - math.floor((index - 1) / 10)
+ is_reversed = math.floor((index - 1) / 10) % 2 != 0
+ x_level = (index - 1) % 10
+ if is_reversed:
+ x_level = 9 - x_level
+ return x_level, y_level
diff --git a/bot/seasons/evergreen/uptime.py b/bot/seasons/evergreen/uptime.py
index 1321da19..3d2c7d03 100644
--- a/bot/seasons/evergreen/uptime.py
+++ b/bot/seasons/evergreen/uptime.py
@@ -9,7 +9,7 @@ from bot import start_time
log = logging.getLogger(__name__)
-class Uptime:
+class Uptime(commands.Cog):
"""
A cog for posting the bots uptime.
"""
@@ -35,4 +35,4 @@ class Uptime:
# Required in order to load the cog, use the class name in the add_cog function.
def setup(bot):
bot.add_cog(Uptime(bot))
- log.debug("Uptime cog loaded")
+ log.info("Uptime cog loaded")
diff --git a/bot/seasons/halloween/candy_collection.py b/bot/seasons/halloween/candy_collection.py
index 80f30a1b..6932097c 100644
--- a/bot/seasons/halloween/candy_collection.py
+++ b/bot/seasons/halloween/candy_collection.py
@@ -20,7 +20,7 @@ ADD_SKULL_REACTION_CHANCE = 50 # 2%
ADD_SKULL_EXISTING_REACTION_CHANCE = 20 # 5%
-class CandyCollection:
+class CandyCollection(commands.Cog):
def __init__(self, bot):
self.bot = bot
with open(json_location) as candy:
@@ -31,6 +31,7 @@ class CandyCollection:
userid = userinfo['userid']
self.get_candyinfo[userid] = userinfo
+ @commands.Cog.listener()
async def on_message(self, message):
"""
Randomly adds candy or skull to certain messages
@@ -54,6 +55,7 @@ class CandyCollection:
self.msg_reacted.append(d)
return await message.add_reaction('\N{CANDY}')
+ @commands.Cog.listener()
async def on_reaction_add(self, reaction, user):
"""
Add/remove candies from a person if the reaction satisfies criteria
@@ -231,4 +233,4 @@ class CandyCollection:
def setup(bot):
bot.add_cog(CandyCollection(bot))
- log.debug("CandyCollection cog loaded")
+ log.info("CandyCollection cog loaded")
diff --git a/bot/seasons/halloween/hacktoberstats.py b/bot/seasons/halloween/hacktoberstats.py
index 41cf10ee..81f11455 100644
--- a/bot/seasons/halloween/hacktoberstats.py
+++ b/bot/seasons/halloween/hacktoberstats.py
@@ -13,7 +13,7 @@ from discord.ext import commands
log = logging.getLogger(__name__)
-class HacktoberStats:
+class HacktoberStats(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.link_json = Path("bot", "resources", "github_links.json")
@@ -332,4 +332,4 @@ class HacktoberStats:
def setup(bot):
bot.add_cog(HacktoberStats(bot))
- log.debug("HacktoberStats cog loaded")
+ log.info("HacktoberStats cog loaded")
diff --git a/bot/seasons/halloween/halloween_facts.py b/bot/seasons/halloween/halloween_facts.py
index 098ee432..9224cc57 100644
--- a/bot/seasons/halloween/halloween_facts.py
+++ b/bot/seasons/halloween/halloween_facts.py
@@ -25,7 +25,7 @@ PUMPKIN_ORANGE = discord.Color(0xFF7518)
INTERVAL = timedelta(hours=6).total_seconds()
-class HalloweenFacts:
+class HalloweenFacts(commands.Cog):
def __init__(self, bot):
self.bot = bot
@@ -35,6 +35,7 @@ class HalloweenFacts:
self.facts = list(enumerate(self.halloween_facts))
random.shuffle(self.facts)
+ @commands.Cog.listener()
async def on_ready(self):
self.channel = self.bot.get_channel(Hacktoberfest.channel_id)
self.bot.loop.create_task(self._fact_publisher_task())
@@ -63,4 +64,4 @@ class HalloweenFacts:
def setup(bot):
bot.add_cog(HalloweenFacts(bot))
- log.debug("HalloweenFacts cog loaded")
+ log.info("HalloweenFacts cog loaded")
diff --git a/bot/seasons/halloween/halloweenify.py b/bot/seasons/halloween/halloweenify.py
index cda07472..0d6964a5 100644
--- a/bot/seasons/halloween/halloweenify.py
+++ b/bot/seasons/halloween/halloweenify.py
@@ -10,7 +10,7 @@ from discord.ext.commands.cooldowns import BucketType
log = logging.getLogger(__name__)
-class Halloweenify:
+class Halloweenify(commands.Cog):
"""
A cog to change a invokers nickname to a spooky one!
"""
@@ -52,4 +52,4 @@ class Halloweenify:
def setup(bot):
bot.add_cog(Halloweenify(bot))
- log.debug("Halloweenify cog loaded")
+ log.info("Halloweenify cog loaded")
diff --git a/bot/seasons/halloween/monstersurvey.py b/bot/seasons/halloween/monstersurvey.py
index 08873f24..2b251b90 100644
--- a/bot/seasons/halloween/monstersurvey.py
+++ b/bot/seasons/halloween/monstersurvey.py
@@ -4,7 +4,7 @@ import os
from discord import Embed
from discord.ext import commands
-from discord.ext.commands import Bot, Context
+from discord.ext.commands import Bot, Cog, Context
log = logging.getLogger(__name__)
@@ -14,7 +14,7 @@ EMOJIS = {
}
-class MonsterSurvey:
+class MonsterSurvey(Cog):
"""
Vote for your favorite monster!
This command allows users to vote for their favorite listed monster.
@@ -215,4 +215,4 @@ class MonsterSurvey:
def setup(bot):
bot.add_cog(MonsterSurvey(bot))
- log.debug("MonsterSurvey cog loaded")
+ log.info("MonsterSurvey cog loaded")
diff --git a/bot/seasons/halloween/scarymovie.py b/bot/seasons/halloween/scarymovie.py
index b280781e..dcff4f58 100644
--- a/bot/seasons/halloween/scarymovie.py
+++ b/bot/seasons/halloween/scarymovie.py
@@ -13,7 +13,7 @@ TMDB_API_KEY = environ.get('TMDB_API_KEY')
TMDB_TOKEN = environ.get('TMDB_TOKEN')
-class ScaryMovie:
+class ScaryMovie(commands.Cog):
"""
Selects a random scary movie and embeds info into discord chat
"""
@@ -138,4 +138,4 @@ class ScaryMovie:
def setup(bot):
bot.add_cog(ScaryMovie(bot))
- log.debug("ScaryMovie cog loaded")
+ log.info("ScaryMovie cog loaded")
diff --git a/bot/seasons/halloween/spookyavatar.py b/bot/seasons/halloween/spookyavatar.py
index b37a03f9..032ad352 100644
--- a/bot/seasons/halloween/spookyavatar.py
+++ b/bot/seasons/halloween/spookyavatar.py
@@ -4,15 +4,15 @@ from io import BytesIO
import aiohttp
import discord
-from discord.ext import commands
from PIL import Image
+from discord.ext import commands
from bot.utils.halloween import spookifications
log = logging.getLogger(__name__)
-class SpookyAvatar:
+class SpookyAvatar(commands.Cog):
"""
A cog that spookifies an avatar.
@@ -55,4 +55,4 @@ class SpookyAvatar:
def setup(bot):
bot.add_cog(SpookyAvatar(bot))
- log.debug("SpookyAvatar cog loaded")
+ log.info("SpookyAvatar cog loaded")
diff --git a/bot/seasons/halloween/spookygif.py b/bot/seasons/halloween/spookygif.py
index 1233773b..c11d5ecb 100644
--- a/bot/seasons/halloween/spookygif.py
+++ b/bot/seasons/halloween/spookygif.py
@@ -9,7 +9,7 @@ from bot.constants import Tokens
log = logging.getLogger(__name__)
-class SpookyGif:
+class SpookyGif(commands.Cog):
"""
A cog to fetch a random spooky gif from the web!
"""
@@ -40,4 +40,4 @@ class SpookyGif:
def setup(bot):
bot.add_cog(SpookyGif(bot))
- log.debug("SpookyGif cog loaded")
+ log.info("SpookyGif cog loaded")
diff --git a/bot/seasons/halloween/spookyreact.py b/bot/seasons/halloween/spookyreact.py
index f63cd7e5..3b4e3fdf 100644
--- a/bot/seasons/halloween/spookyreact.py
+++ b/bot/seasons/halloween/spookyreact.py
@@ -2,6 +2,7 @@ import logging
import re
import discord
+from discord.ext.commands import Cog
log = logging.getLogger(__name__)
@@ -16,7 +17,7 @@ SPOOKY_TRIGGERS = {
}
-class SpookyReact:
+class SpookyReact(Cog):
"""
A cog that makes the bot react to message triggers.
@@ -25,6 +26,7 @@ class SpookyReact:
def __init__(self, bot):
self.bot = bot
+ @Cog.listener()
async def on_message(self, ctx: discord.Message):
"""
A command to send the seasonalbot github project
@@ -69,4 +71,4 @@ class SpookyReact:
def setup(bot):
bot.add_cog(SpookyReact(bot))
- log.debug("SpookyReact cog loaded")
+ log.info("SpookyReact cog loaded")
diff --git a/bot/seasons/halloween/spookysound.py b/bot/seasons/halloween/spookysound.py
index 4cab1239..1e430dab 100644
--- a/bot/seasons/halloween/spookysound.py
+++ b/bot/seasons/halloween/spookysound.py
@@ -10,7 +10,7 @@ from bot.constants import Hacktoberfest
log = logging.getLogger(__name__)
-class SpookySound:
+class SpookySound(commands.Cog):
"""
A cog that plays a spooky sound in a voice channel on command.
"""
@@ -47,4 +47,4 @@ class SpookySound:
def setup(bot):
bot.add_cog(SpookySound(bot))
- log.debug("SpookySound cog loaded")
+ log.info("SpookySound cog loaded")
diff --git a/bot/seasons/pride/__init__.py b/bot/seasons/pride/__init__.py
new file mode 100644
index 00000000..d8a7e34b
--- /dev/null
+++ b/bot/seasons/pride/__init__.py
@@ -0,0 +1,17 @@
+from bot.seasons import SeasonBase
+
+
+class Pride(SeasonBase):
+ """
+ No matter your origin, identity or sexuality, we come together to celebrate each and everyone's individuality.
+ Feature contributions to ProudBot is encouraged to commemorate the history and challenges of the LGBTQ+ community.
+ Happy Pride Month
+ """
+
+ name = "pride"
+ bot_name = "ProudBot"
+ greeting = "Happy Pride Month!"
+
+ # Duration of season
+ start_date = "01/06"
+ end_date = "30/06"
diff --git a/bot/seasons/season.py b/bot/seasons/season.py
index e59949d7..b7892606 100644
--- a/bot/seasons/season.py
+++ b/bot/seasons/season.py
@@ -90,6 +90,7 @@ class SeasonBase:
colour: Optional[int] = None
icon: str = "/logos/logo_full/logo_full.png"
+ bot_icon: Optional[str] = None
date_format: str = "%d/%m/%Y"
@@ -151,10 +152,11 @@ class SeasonBase:
return f"New Season, {self.name_clean}!"
- async def get_icon(self) -> bytes:
+ async def get_icon(self, avatar: bool = False) -> bytes:
"""
Retrieves the icon image from the branding repository, using the
- defined icon attribute for the season.
+ defined icon attribute for the season. If `avatar` is True, uses
+ optional bot-only avatar icon if present.
The icon attribute must provide the url path, starting from the master
branch base url, including the starting slash:
@@ -162,7 +164,11 @@ class SeasonBase:
"""
base_url = "https://raw.githubusercontent.com/python-discord/branding/master"
- full_url = base_url + self.icon
+ if avatar:
+ icon = self.bot_icon or self.icon
+ else:
+ icon = self.icon
+ full_url = base_url + icon
log.debug(f"Getting icon from: {full_url}")
async with bot.http_session.get(full_url) as resp:
return await resp.read()
@@ -217,17 +223,17 @@ class SeasonBase:
old_avatar = bot.user.avatar
# attempt the change
- log.debug(f"Changing avatar to {self.icon}")
- icon = await self.get_icon()
+ log.debug(f"Changing avatar to {self.bot_icon or self.icon}")
+ icon = await self.get_icon(avatar=True)
with contextlib.suppress(discord.HTTPException, asyncio.TimeoutError):
async with async_timeout.timeout(5):
await bot.user.edit(avatar=icon)
if bot.user.avatar != old_avatar:
- log.debug(f"Avatar changed to {self.icon}")
+ log.debug(f"Avatar changed to {self.bot_icon or self.icon}")
return True
- log.warning(f"Changing avatar failed: {self.icon}")
+ log.warning(f"Changing avatar failed: {self.bot_icon or self.icon}")
return False
async def apply_server_icon(self) -> bool:
@@ -334,18 +340,19 @@ class SeasonBase:
if not Client.debug:
log.info("Applying avatar.")
await self.apply_avatar()
- log.info("Applying server icon.")
- await self.apply_server_icon()
if username_changed:
+ log.info("Applying server icon.")
+ await self.apply_server_icon()
log.info(f"Announcing season {self.name}.")
await self.announce_season()
else:
+ log.info(f"Skipping server icon change due to username not being changed.")
log.info(f"Skipping season announcement due to username not being changed.")
await bot.send_log("SeasonalBot Loaded!", f"Active Season: **{self.name_clean}**")
-class SeasonManager:
+class SeasonManager(commands.Cog):
"""
A cog for managing seasons.
"""
@@ -465,7 +472,7 @@ class SeasonManager:
# report back details
season_name = type(self.season).__name__
embed = discord.Embed(
- description=f"**Season:** {season_name}\n**Avatar:** {self.season.icon}",
+ description=f"**Season:** {season_name}\n**Avatar:** {self.season.bot_icon or self.season.icon}",
colour=colour
)
embed.set_author(name=title)
diff --git a/bot/seasons/valentines/be_my_valentine.py b/bot/seasons/valentines/be_my_valentine.py
new file mode 100644
index 00000000..4e2182c3
--- /dev/null
+++ b/bot/seasons/valentines/be_my_valentine.py
@@ -0,0 +1,241 @@
+import logging
+import random
+import typing
+from json import load
+from pathlib import Path
+
+import discord
+from discord.ext import commands
+from discord.ext.commands.cooldowns import BucketType
+
+from bot.constants import Client, Colours, Lovefest
+
+log = logging.getLogger(__name__)
+
+HEART_EMOJIS = [":heart:", ":gift_heart:", ":revolving_hearts:", ":sparkling_heart:", ":two_hearts:"]
+
+
+class BeMyValentine(commands.Cog):
+ """
+ A cog that sends valentines to other users !
+ """
+
+ def __init__(self, bot):
+ self.bot = bot
+ self.valentines = self.load_json()
+
+ @staticmethod
+ def load_json():
+ p = Path('bot', 'resources', 'valentines', 'bemyvalentine_valentines.json')
+ with p.open() as json_data:
+ valentines = load(json_data)
+ return valentines
+
+ @commands.group(name="lovefest", invoke_without_command=True)
+ async def lovefest_role(self, ctx):
+ """
+ You can have yourself the lovefest role or remove it.
+ The lovefest role makes you eligible to receive anonymous valentines from other users.
+
+ 1) use the command \".lovefest sub\" to get the lovefest role.
+ 2) use the command \".lovefest unsub\" to get rid of the lovefest role.
+ """
+ await ctx.invoke(self.bot.get_command("help"), "lovefest")
+
+ @lovefest_role.command(name="sub")
+ async def add_role(self, ctx):
+ """
+ This command adds the lovefest role.
+ """
+ user = ctx.author
+ role = discord.utils.get(ctx.guild.roles, id=Lovefest.role_id)
+ if Lovefest.role_id not in [role.id for role in ctx.message.author.roles]:
+ await user.add_roles(role)
+ await ctx.send("The Lovefest role has been added !")
+ else:
+ await ctx.send("You already have the role !")
+
+ @lovefest_role.command(name="unsub")
+ async def remove_role(self, ctx):
+ """
+ This command removes the lovefest role.
+ """
+ user = ctx.author
+ role = discord.utils.get(ctx.guild.roles, id=Lovefest.role_id)
+ if Lovefest.role_id not in [role.id for role in ctx.message.author.roles]:
+ await ctx.send("You dont have the lovefest role.")
+ else:
+ await user.remove_roles(role)
+ await ctx.send("The lovefest role has been successfully removed !")
+
+ @commands.cooldown(1, 1800, BucketType.user)
+ @commands.group(name='bemyvalentine', invoke_without_command=True)
+ async def send_valentine(self, ctx, user: typing.Optional[discord.Member] = None, *, valentine_type=None):
+ """
+ This command sends valentine to user if specified or a random user having lovefest role.
+
+ syntax: .bemyvalentine [user](optional) [p/poem/c/compliment/or you can type your own valentine message]
+ (optional)
+
+ example: .bemyvalentine (sends valentine as a poem or a compliment to a random user)
+ example: .bemyvalentine Iceman#6508 p (sends a poem to Iceman)
+ example: .bemyvalentine Iceman Hey I love you, wanna hang around ? (sends the custom message to Iceman)
+ NOTE : AVOID TAGGING THE USER MOST OF THE TIMES.JUST TRIM THE '@' when using this command.
+ """
+
+ if ctx.guild is None:
+ # This command should only be used in the server
+ msg = "You are supposed to use this command in the server."
+ return await ctx.send(msg)
+
+ if user:
+ if Lovefest.role_id not in [role.id for role in user.roles]:
+ message = f"You cannot send a valentine to {user} as he/she does not have the lovefest role!"
+ return await ctx.send(message)
+
+ if user == ctx.author:
+ # Well a user can't valentine himself/herself.
+ return await ctx.send("Come on dude, you can't send a valentine to yourself :expressionless:")
+
+ emoji_1, emoji_2 = self.random_emoji()
+ lovefest_role = discord.utils.get(ctx.guild.roles, id=Lovefest.role_id)
+ channel = self.bot.get_channel(Lovefest.channel_id)
+ valentine, title = self.valentine_check(valentine_type)
+
+ if user is None:
+ author = ctx.author
+ user = self.random_user(author, lovefest_role.members)
+ if user is None:
+ return await ctx.send("There are no users avilable to whome your valentine can be sent.")
+
+ embed = discord.Embed(
+ title=f'{emoji_1} {title} {user.display_name} {emoji_2}',
+ description=f'{valentine} \n **{emoji_2}From {ctx.author}{emoji_1}**',
+ color=Colours.pink
+ )
+ await channel.send(user.mention, embed=embed)
+
+ @commands.cooldown(1, 1800, BucketType.user)
+ @send_valentine.command(name='secret')
+ async def anonymous(self, ctx, user: typing.Optional[discord.Member] = None, *, valentine_type=None):
+ """
+ This command DMs a valentine to be given anonymous to a user if specified or a random user having lovefest role.
+
+ **This command should be DMed to the bot.**
+
+ syntax : .bemyvalentine secret [user](optional) [p/poem/c/compliment/or you can type your own valentine message]
+ (optional)
+
+ example : .bemyvalentine secret (sends valentine as a poem or a compliment to a random user in DM making you
+ anonymous)
+ example : .bemyvalentine secret Iceman#6508 p (sends a poem to Iceman in DM making you anonymous)
+ example : .bemyvalentine secret Iceman#6508 Hey I love you, wanna hang around ? (sends the custom message to
+ Iceman in DM making you anonymous)
+ """
+
+ if ctx.guild is not None:
+ # This command is only DM specific
+ msg = "You are not supposed to use this command in the server, DM the command to the bot."
+ return await ctx.send(msg)
+
+ if user:
+ if Lovefest.role_id not in [role.id for role in user.roles]:
+ message = f"You cannot send a valentine to {user} as he/she does not have the lovefest role!"
+ return await ctx.send(message)
+
+ if user == ctx.author:
+ # Well a user cant valentine himself/herself.
+ return await ctx.send('Come on dude, you cant send a valentine to yourself :expressionless:')
+
+ guild = self.bot.get_guild(id=Client.guild)
+ emoji_1, emoji_2 = self.random_emoji()
+ lovefest_role = discord.utils.get(guild.roles, id=Lovefest.role_id)
+ valentine, title = self.valentine_check(valentine_type)
+
+ if user is None:
+ author = ctx.author
+ user = self.random_user(author, lovefest_role.members)
+ if user is None:
+ return await ctx.send("There are no users avilable to whome your valentine can be sent.")
+
+ embed = discord.Embed(
+ title=f'{emoji_1}{title} {user.display_name}{emoji_2}',
+ description=f'{valentine} \n **{emoji_2}From anonymous{emoji_1}**',
+ color=Colours.pink
+ )
+ try:
+ await user.send(embed=embed)
+ except discord.Forbidden:
+ await ctx.author.send(f"{user} has DMs disabled, so I couldn't send the message. Sorry!")
+ else:
+ await ctx.author.send(f"Your message has been sent to {user}")
+
+ def valentine_check(self, valentine_type):
+ if valentine_type is None:
+ valentine, title = self.random_valentine()
+
+ elif valentine_type.lower() in ['p', 'poem']:
+ valentine = self.valentine_poem()
+ title = 'A poem dedicated to'
+
+ elif valentine_type.lower() in ['c', 'compliment']:
+ valentine = self.valentine_compliment()
+ title = 'A compliment for'
+
+ else:
+ # in this case, the user decides to type his own valentine.
+ valentine = valentine_type
+ title = 'A message for'
+ return valentine, title
+
+ @staticmethod
+ def random_user(author, members):
+ """
+ Picks a random member from the list provided in `members`, ensuring
+ the author is not one of the options.
+
+ :param author: member who invoked the command
+ :param members: list of discord.Member objects
+ """
+ if author in members:
+ members.remove(author)
+
+ return random.choice(members) if members else None
+
+ @staticmethod
+ def random_emoji():
+ EMOJI_1 = random.choice(HEART_EMOJIS)
+ EMOJI_2 = random.choice(HEART_EMOJIS)
+ return EMOJI_1, EMOJI_2
+
+ def random_valentine(self):
+ """
+ Grabs a random poem or a compliment (any message).
+ """
+ valentine_poem = random.choice(self.valentines['valentine_poems'])
+ valentine_compliment = random.choice(self.valentines['valentine_compliments'])
+ random_valentine = random.choice([valentine_compliment, valentine_poem])
+ if random_valentine == valentine_poem:
+ title = 'A poem dedicated to'
+ else:
+ title = 'A compliment for '
+ return random_valentine, title
+
+ def valentine_poem(self):
+ """
+ Grabs a random poem.
+ """
+ valentine_poem = random.choice(self.valentines['valentine_poems'])
+ return valentine_poem
+
+ def valentine_compliment(self):
+ """
+ Grabs a random compliment.
+ """
+ valentine_compliment = random.choice(self.valentines['valentine_compliments'])
+ return valentine_compliment
+
+
+def setup(bot):
+ bot.add_cog(BeMyValentine(bot))
+ log.info("BeMyValentine cog loaded")
diff --git a/bot/seasons/valentines/lovecalculator.py b/bot/seasons/valentines/lovecalculator.py
new file mode 100644
index 00000000..0662cf5b
--- /dev/null
+++ b/bot/seasons/valentines/lovecalculator.py
@@ -0,0 +1,107 @@
+import bisect
+import hashlib
+import json
+import logging
+import random
+from pathlib import Path
+from typing import Union
+
+import discord
+from discord import Member
+from discord.ext import commands
+from discord.ext.commands import BadArgument, Cog, clean_content
+
+from bot.constants import Roles
+
+log = logging.getLogger(__name__)
+
+with Path('bot', 'resources', 'valentines', 'love_matches.json').open() as file:
+ LOVE_DATA = json.load(file)
+ LOVE_DATA = sorted((int(key), value) for key, value in LOVE_DATA.items())
+
+
+class LoveCalculator(Cog):
+ """
+ A cog for calculating the love between two people
+ """
+
+ def __init__(self, bot):
+ self.bot = bot
+
+ @commands.command(aliases=('love_calculator', 'love_calc'))
+ @commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
+ async def love(self, ctx, who: Union[Member, str], whom: Union[Member, str] = None):
+ """
+ Tells you how much the two love each other.
+
+ This command accepts users or arbitrary strings as arguments.
+ Users are converted from:
+ - User ID
+ - Mention
+ - name#discrim
+ - name
+ - nickname
+
+ Any two arguments will always yield the same result, though the order of arguments matters:
+ Running .love joseph erlang will always yield the same result.
+ Running .love erlang joseph won't yield the same result as .love joseph erlang
+
+ If you want to use multiple words for one argument, you must include quotes.
+ .love "Zes Vappa" "morning coffee"
+
+ If only one argument is provided, the subject will become one of the helpers at random.
+ """
+
+ if whom is None:
+ staff = ctx.guild.get_role(Roles.helpers).members
+ whom = random.choice(staff)
+
+ def normalize(arg):
+ if isinstance(arg, Member):
+ # if we are given a member, return name#discrim without any extra changes
+ arg = str(arg)
+ else:
+ # otherwise normalise case and remove any leading/trailing whitespace
+ arg = arg.strip().title()
+ # this has to be done manually to be applied to usernames
+ return clean_content(escape_markdown=True).convert(ctx, arg)
+
+ who, whom = [await normalize(arg) for arg in (who, whom)]
+
+ # make sure user didn't provide something silly such as 10 spaces
+ if not (who and whom):
+ raise BadArgument('Arguments be non-empty strings.')
+
+ # hash inputs to guarantee consistent results (hashing algorithm choice arbitrary)
+ #
+ # hashlib is used over the builtin hash() function
+ # to guarantee same result over multiple runtimes
+ m = hashlib.sha256(who.encode() + whom.encode())
+ # mod 101 for [0, 100]
+ love_percent = sum(m.digest()) % 101
+
+ # We need the -1 due to how bisect returns the point
+ # see the documentation for further detail
+ # https://docs.python.org/3/library/bisect.html#bisect.bisect
+ index = bisect.bisect(LOVE_DATA, (love_percent,)) - 1
+ # we already have the nearest "fit" love level
+ # we only need the dict, so we can ditch the first element
+ _, data = LOVE_DATA[index]
+
+ status = random.choice(data['titles'])
+ embed = discord.Embed(
+ title=status,
+ description=f'{who} \N{HEAVY BLACK HEART} {whom} scored {love_percent}%!\n\u200b',
+ color=discord.Color.dark_magenta()
+ )
+ embed.add_field(
+ name='A letter from Dr. Love:',
+ value=data['text']
+ )
+
+ await ctx.send(embed=embed)
+
+
+def setup(bot):
+ bot.add_cog(LoveCalculator(bot))
+ log.info("LoveCalculator cog loaded")
diff --git a/bot/seasons/valentines/movie_generator.py b/bot/seasons/valentines/movie_generator.py
new file mode 100644
index 00000000..8fce011b
--- /dev/null
+++ b/bot/seasons/valentines/movie_generator.py
@@ -0,0 +1,66 @@
+import logging
+import random
+from os import environ
+from urllib import parse
+
+import discord
+from discord.ext import commands
+
+TMDB_API_KEY = environ.get("TMDB_API_KEY")
+
+log = logging.getLogger(__name__)
+
+
+class RomanceMovieFinder(commands.Cog):
+ """
+ A cog that returns a random romance movie suggestion to a user
+ """
+
+ def __init__(self, bot):
+ self.bot = bot
+
+ @commands.command(name="romancemovie")
+ async def romance_movie(self, ctx):
+ """
+ Randomly selects a romance movie and displays information about it
+ """
+ # selecting a random int to parse it to the page parameter
+ random_page = random.randint(0, 20)
+ # TMDB api params
+ params = {
+ "api_key": TMDB_API_KEY,
+ "language": "en-US",
+ "sort_by": "popularity.desc",
+ "include_adult": "false",
+ "include_video": "false",
+ "page": random_page,
+ "with_genres": "10749"
+ }
+ # the api request url
+ request_url = "https://api.themoviedb.org/3/discover/movie?" + parse.urlencode(params)
+ async with self.bot.http_session.get(request_url) as resp:
+ # trying to load the json file returned from the api
+ try:
+ data = await resp.json()
+ # selecting random result from results object in the json file
+ selected_movie = random.choice(data["results"])
+
+ embed = discord.Embed(
+ title=f":sparkling_heart: {selected_movie['title']} :sparkling_heart:",
+ description=selected_movie["overview"],
+ )
+ embed.set_image(url=f"http://image.tmdb.org/t/p/w200/{selected_movie['poster_path']}")
+ embed.add_field(name="Release date :clock1:", value=selected_movie["release_date"])
+ embed.add_field(name="Rating :star2:", value=selected_movie["vote_average"])
+ await ctx.send(embed=embed)
+ except KeyError:
+ warning_message = "A KeyError was raised while fetching information on the movie. The API service" \
+ " could be unavailable or the API key could be set incorrectly."
+ embed = discord.Embed(title=warning_message)
+ log.warning(warning_message)
+ await ctx.send(embed=embed)
+
+
+def setup(bot):
+ bot.add_cog(RomanceMovieFinder(bot))
+ log.info("RomanceMovieFinder cog loaded")
diff --git a/bot/seasons/valentines/myvalenstate.py b/bot/seasons/valentines/myvalenstate.py
new file mode 100644
index 00000000..7d9f3a59
--- /dev/null
+++ b/bot/seasons/valentines/myvalenstate.py
@@ -0,0 +1,85 @@
+import collections
+import json
+import logging
+from pathlib import Path
+from random import choice
+
+import discord
+from discord.ext import commands
+
+from bot.constants import Colours
+
+log = logging.getLogger(__name__)
+
+with open(Path('bot', 'resources', 'valentines', 'valenstates.json'), 'r') as file:
+ STATES = json.load(file)
+
+
+class MyValenstate(commands.Cog):
+ def __init__(self, bot):
+ self.bot = bot
+
+ def levenshtein(self, source, goal):
+ """
+ Calculates the Levenshtein Distance between source and goal.
+ """
+ if len(source) < len(goal):
+ return self.levenshtein(goal, source)
+ if len(source) == 0:
+ return len(goal)
+ if len(goal) == 0:
+ return len(source)
+
+ pre_row = list(range(0, len(source) + 1))
+ for i, source_c in enumerate(source):
+ cur_row = [i + 1]
+ for j, goal_c in enumerate(goal):
+ if source_c != goal_c:
+ cur_row.append(min(pre_row[j], pre_row[j + 1], cur_row[j]) + 1)
+ else:
+ cur_row.append(min(pre_row[j], pre_row[j + 1], cur_row[j]))
+ pre_row = cur_row
+ return pre_row[-1]
+
+ @commands.command()
+ async def myvalenstate(self, ctx, *, name=None):
+ eq_chars = collections.defaultdict(int)
+ if name is None:
+ author = ctx.message.author.name.lower().replace(' ', '')
+ else:
+ author = name.lower().replace(' ', '')
+
+ for state in STATES.keys():
+ lower_state = state.lower().replace(' ', '')
+ eq_chars[state] = self.levenshtein(author, lower_state)
+
+ matches = [x for x, y in eq_chars.items() if y == min(eq_chars.values())]
+ valenstate = choice(matches)
+ matches.remove(valenstate)
+
+ embed_title = "But there are more!"
+ if len(matches) > 1:
+ leftovers = f"{', '.join(matches[:len(matches)-2])}, and {matches[len(matches)-1]}"
+ embed_text = f"You have {len(matches)} more matches, these being {leftovers}."
+ elif len(matches) == 1:
+ embed_title = "But there's another one!"
+ leftovers = str(matches)
+ embed_text = f"You have another match, this being {leftovers}."
+ else:
+ embed_title = "You have a true match!"
+ embed_text = "This state is your true Valenstate! There are no states that would suit" \
+ " you better"
+
+ embed = discord.Embed(
+ title=f'Your Valenstate is {valenstate} \u2764',
+ description=f'{STATES[valenstate]["text"]}',
+ colour=Colours.pink
+ )
+ embed.add_field(name=embed_title, value=embed_text)
+ embed.set_image(url=STATES[valenstate]["flag"])
+ await ctx.channel.send(embed=embed)
+
+
+def setup(bot):
+ bot.add_cog(MyValenstate(bot))
+ log.info("MyValenstate cog loaded")
diff --git a/bot/seasons/valentines/pickuplines.py b/bot/seasons/valentines/pickuplines.py
new file mode 100644
index 00000000..e1abb4e5
--- /dev/null
+++ b/bot/seasons/valentines/pickuplines.py
@@ -0,0 +1,44 @@
+import logging
+import random
+from json import load
+from pathlib import Path
+
+import discord
+from discord.ext import commands
+
+from bot.constants import Colours
+
+log = logging.getLogger(__name__)
+
+with open(Path('bot', 'resources', 'valentines', 'pickup_lines.json'), 'r', encoding="utf8") as f:
+ pickup_lines = load(f)
+
+
+class PickupLine(commands.Cog):
+ """
+ A cog that gives random cheesy pickup lines.
+ """
+
+ def __init__(self, bot):
+ self.bot = bot
+
+ @commands.command()
+ async def pickupline(self, ctx):
+ """
+ Gives you a random pickup line. Note that most of them are very cheesy!
+ """
+ random_line = random.choice(pickup_lines['lines'])
+ embed = discord.Embed(
+ title=':cheese: Your pickup line :cheese:',
+ description=random_line['line'],
+ color=Colours.pink
+ )
+ embed.set_thumbnail(
+ url=random_line.get('image', pickup_lines['placeholder'])
+ )
+ await ctx.send(embed=embed)
+
+
+def setup(bot):
+ bot.add_cog(PickupLine(bot))
+ log.info('PickupLine cog loaded')
diff --git a/bot/seasons/valentines/savethedate.py b/bot/seasons/valentines/savethedate.py
new file mode 100644
index 00000000..fbc9eb82
--- /dev/null
+++ b/bot/seasons/valentines/savethedate.py
@@ -0,0 +1,45 @@
+import logging
+import random
+from json import load
+from pathlib import Path
+
+import discord
+from discord.ext import commands
+
+from bot.constants import Colours
+
+log = logging.getLogger(__name__)
+
+HEART_EMOJIS = [":heart:", ":gift_heart:", ":revolving_hearts:", ":sparkling_heart:", ":two_hearts:"]
+
+with open(Path('bot', 'resources', 'valentines', 'date_ideas.json'), 'r', encoding="utf8") as f:
+ VALENTINES_DATES = load(f)
+
+
+class SaveTheDate(commands.Cog):
+ """
+ A cog that gives random suggestion, for a valentines date !
+ """
+
+ def __init__(self, bot):
+ self.bot = bot
+
+ @commands.command()
+ async def savethedate(self, ctx):
+ """
+ Gives you ideas for what to do on a date with your valentine.
+ """
+ random_date = random.choice(VALENTINES_DATES['ideas'])
+ emoji_1 = random.choice(HEART_EMOJIS)
+ emoji_2 = random.choice(HEART_EMOJIS)
+ embed = discord.Embed(
+ title=f"{emoji_1}{random_date['name']}{emoji_2}",
+ description=f"{random_date['description']}",
+ colour=Colours.pink
+ )
+ await ctx.send(embed=embed)
+
+
+def setup(bot):
+ bot.add_cog(SaveTheDate(bot))
+ log.info("SaveTheDate cog loaded")
diff --git a/bot/seasons/valentines/valentine_zodiac.py b/bot/seasons/valentines/valentine_zodiac.py
new file mode 100644
index 00000000..33fc739a
--- /dev/null
+++ b/bot/seasons/valentines/valentine_zodiac.py
@@ -0,0 +1,59 @@
+import logging
+import random
+from json import load
+from pathlib import Path
+
+import discord
+from discord.ext import commands
+
+from bot.constants import Colours
+
+log = logging.getLogger(__name__)
+
+LETTER_EMOJI = ':love_letter:'
+HEART_EMOJIS = [":heart:", ":gift_heart:", ":revolving_hearts:", ":sparkling_heart:", ":two_hearts:"]
+
+
+class ValentineZodiac(commands.Cog):
+ """
+ A cog that returns a counter compatible zodiac sign to the given user's zodiac sign.
+ """
+ def __init__(self, bot):
+ self.bot = bot
+ self.zodiacs = self.load_json()
+
+ @staticmethod
+ def load_json():
+ p = Path('bot', 'resources', 'valentines', 'zodiac_compatibility.json')
+ with p.open() as json_data:
+ zodiacs = load(json_data)
+ return zodiacs
+
+ @commands.command(name="partnerzodiac")
+ async def counter_zodiac(self, ctx, zodiac_sign):
+ """
+ Provides a counter compatible zodiac sign to the given user's zodiac sign.
+ """
+ try:
+ compatible_zodiac = random.choice(self.zodiacs[zodiac_sign.lower()])
+ except KeyError:
+ return await ctx.send(zodiac_sign.capitalize() + " zodiac sign does not exist.")
+
+ emoji1 = random.choice(HEART_EMOJIS)
+ emoji2 = random.choice(HEART_EMOJIS)
+ embed = discord.Embed(
+ title="Zodic Compatibility",
+ description=f'{zodiac_sign.capitalize()}{emoji1}{compatible_zodiac["Zodiac"]}\n'
+ f'{emoji2}Compatibility meter : {compatible_zodiac["compatibility_score"]}{emoji2}',
+ color=Colours.pink
+ )
+ embed.add_field(
+ name=f'A letter from Dr.Zodiac {LETTER_EMOJI}',
+ value=compatible_zodiac['description']
+ )
+ await ctx.send(embed=embed)
+
+
+def setup(bot):
+ bot.add_cog(ValentineZodiac(bot))
+ log.info("ValentineZodiac cog loaded")
diff --git a/bot/seasons/valentines/whoisvalentine.py b/bot/seasons/valentines/whoisvalentine.py
new file mode 100644
index 00000000..59a13ca3
--- /dev/null
+++ b/bot/seasons/valentines/whoisvalentine.py
@@ -0,0 +1,54 @@
+import json
+import logging
+from pathlib import Path
+from random import choice
+
+import discord
+from discord.ext import commands
+
+from bot.constants import Colours
+
+log = logging.getLogger(__name__)
+
+with open(Path("bot", "resources", "valentines", "valentine_facts.json"), "r") as file:
+ FACTS = json.load(file)
+
+
+class ValentineFacts(commands.Cog):
+ def __init__(self, bot):
+ self.bot = bot
+
+ @commands.command(aliases=('whoisvalentine', 'saint_valentine'))
+ async def who_is_valentine(self, ctx):
+ """
+ Displays info about Saint Valentine.
+ """
+ embed = discord.Embed(
+ title="Who is Saint Valentine?",
+ description=FACTS['whois'],
+ color=Colours.pink
+ )
+ embed.set_thumbnail(
+ url='https://upload.wikimedia.org/wikipedia/commons/thumb/f/f1/Saint_Valentine_-_'
+ 'facial_reconstruction.jpg/1024px-Saint_Valentine_-_facial_reconstruction.jpg'
+ )
+
+ await ctx.channel.send(embed=embed)
+
+ @commands.command()
+ async def valentine_fact(self, ctx):
+ """
+ Shows a random fact about Valentine's Day.
+ """
+ embed = discord.Embed(
+ title=choice(FACTS['titles']),
+ description=choice(FACTS['text']),
+ color=Colours.pink
+ )
+
+ await ctx.channel.send(embed=embed)
+
+
+def setup(bot):
+ bot.add_cog(ValentineFacts(bot))
+ log.info("ValentineFacts cog loaded")
diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py
index e69de29b..ef18a1b9 100644
--- a/bot/utils/__init__.py
+++ b/bot/utils/__init__.py
@@ -0,0 +1,79 @@
+import asyncio
+from typing import List
+
+import discord
+from discord.ext.commands import BadArgument, Context
+
+from bot.pagination import LinePaginator
+
+
+async def disambiguate(
+ ctx: Context, entries: List[str], *, timeout: float = 30,
+ per_page: int = 20, empty: bool = False, embed: discord.Embed = None
+):
+ """
+ Has the user choose between multiple entries in case one could not be chosen automatically.
+
+ This will raise a BadArgument if entries is empty, if the disambiguation event times out,
+ or if the user makes an invalid choice.
+
+ :param ctx: Context object from discord.py
+ :param entries: List of items for user to choose from
+ :param timeout: Number of seconds to wait before canceling disambiguation
+ :param per_page: Entries per embed page
+ :param empty: Whether the paginator should have an extra line between items
+ :param embed: The embed that the paginator will use.
+ :return: Users choice for correct entry.
+ """
+ if len(entries) == 0:
+ raise BadArgument('No matches found.')
+
+ if len(entries) == 1:
+ return entries[0]
+
+ choices = (f'{index}: {entry}' for index, entry in enumerate(entries, start=1))
+
+ def check(message):
+ return (message.content.isdigit()
+ and message.author == ctx.author
+ and message.channel == ctx.channel)
+
+ try:
+ if embed is None:
+ embed = discord.Embed()
+
+ coro1 = ctx.bot.wait_for('message', check=check, timeout=timeout)
+ coro2 = LinePaginator.paginate(choices, ctx, embed=embed, max_lines=per_page,
+ empty=empty, max_size=6000, timeout=9000)
+
+ # wait_for timeout will go to except instead of the wait_for thing as I expected
+ futures = [asyncio.ensure_future(coro1), asyncio.ensure_future(coro2)]
+ done, pending = await asyncio.wait(futures, return_when=asyncio.FIRST_COMPLETED, loop=ctx.bot.loop)
+
+ # :yert:
+ result = list(done)[0].result()
+
+ # Pagination was canceled - result is None
+ if result is None:
+ for coro in pending:
+ coro.cancel()
+ raise BadArgument('Canceled.')
+
+ # Pagination was not initiated, only one page
+ if result.author == ctx.bot.user:
+ # Continue the wait_for
+ result = await list(pending)[0]
+
+ # Love that duplicate code
+ for coro in pending:
+ coro.cancel()
+ except asyncio.TimeoutError:
+ raise BadArgument('Timed out.')
+
+ # Guaranteed to not error because of isdigit() in check
+ index = int(result.content)
+
+ try:
+ return entries[index - 1]
+ except IndexError:
+ raise BadArgument('Invalid choice.')