aboutsummaryrefslogtreecommitdiffstats
path: root/bot/seasons
diff options
context:
space:
mode:
Diffstat (limited to 'bot/seasons')
-rw-r--r--bot/seasons/__init__.py14
-rw-r--r--bot/seasons/christmas/__init__.py33
-rw-r--r--bot/seasons/christmas/adventofcode.py742
-rw-r--r--bot/seasons/christmas/hanukkah_embed.py113
-rw-r--r--bot/seasons/easter/__init__.py35
-rw-r--r--bot/seasons/easter/april_fools_vids.py39
-rw-r--r--bot/seasons/easter/avatar_easterifier.py129
-rw-r--r--bot/seasons/easter/bunny_name_generator.py93
-rw-r--r--bot/seasons/easter/conversationstarters.py29
-rw-r--r--bot/seasons/easter/easter_riddle.py101
-rw-r--r--bot/seasons/easter/egg_decorating.py119
-rw-r--r--bot/seasons/easter/egg_facts.py62
-rw-r--r--bot/seasons/easter/egghead_quiz.py120
-rw-r--r--bot/seasons/easter/traditions.py31
-rw-r--r--bot/seasons/evergreen/8bitify.py54
-rw-r--r--bot/seasons/evergreen/__init__.py17
-rw-r--r--bot/seasons/evergreen/battleship.py444
-rw-r--r--bot/seasons/evergreen/bookmark.py65
-rw-r--r--bot/seasons/evergreen/error_handler.py125
-rw-r--r--bot/seasons/evergreen/fun.py148
-rw-r--r--bot/seasons/evergreen/game.py395
-rw-r--r--bot/seasons/evergreen/issues.py77
-rw-r--r--bot/seasons/evergreen/magic_8ball.py32
-rw-r--r--bot/seasons/evergreen/minesweeper.py285
-rw-r--r--bot/seasons/evergreen/movie.py198
-rw-r--r--bot/seasons/evergreen/recommend_game.py51
-rw-r--r--bot/seasons/evergreen/reddit.py130
-rw-r--r--bot/seasons/evergreen/showprojects.py34
-rw-r--r--bot/seasons/evergreen/snakes/__init__.py13
-rw-r--r--bot/seasons/evergreen/snakes/converter.py85
-rw-r--r--bot/seasons/evergreen/snakes/snakes_cog.py1149
-rw-r--r--bot/seasons/evergreen/snakes/utils.py716
-rw-r--r--bot/seasons/evergreen/speedrun.py28
-rw-r--r--bot/seasons/evergreen/trivia_quiz.py303
-rw-r--r--bot/seasons/evergreen/uptime.py34
-rw-r--r--bot/seasons/halloween/8ball.py34
-rw-r--r--bot/seasons/halloween/__init__.py24
-rw-r--r--bot/seasons/halloween/candy_collection.py221
-rw-r--r--bot/seasons/halloween/hacktober-issue-finder.py107
-rw-r--r--bot/seasons/halloween/hacktoberstats.py340
-rw-r--r--bot/seasons/halloween/halloween_facts.py68
-rw-r--r--bot/seasons/halloween/halloweenify.py52
-rw-r--r--bot/seasons/halloween/monsterbio.py56
-rw-r--r--bot/seasons/halloween/monstersurvey.py206
-rw-r--r--bot/seasons/halloween/scarymovie.py132
-rw-r--r--bot/seasons/halloween/spookyavatar.py53
-rw-r--r--bot/seasons/halloween/spookygif.py39
-rw-r--r--bot/seasons/halloween/spookyrating.py67
-rw-r--r--bot/seasons/halloween/spookyreact.py72
-rw-r--r--bot/seasons/halloween/spookysound.py48
-rw-r--r--bot/seasons/halloween/timeleft.py60
-rw-r--r--bot/seasons/pride/__init__.py36
-rw-r--r--bot/seasons/pride/drag_queen_name.py33
-rw-r--r--bot/seasons/pride/pride_anthem.py58
-rw-r--r--bot/seasons/pride/pride_avatar.py145
-rw-r--r--bot/seasons/pride/pride_facts.py106
-rw-r--r--bot/seasons/season.py560
-rw-r--r--bot/seasons/valentines/__init__.py22
-rw-r--r--bot/seasons/valentines/be_my_valentine.py234
-rw-r--r--bot/seasons/valentines/lovecalculator.py104
-rw-r--r--bot/seasons/valentines/movie_generator.py63
-rw-r--r--bot/seasons/valentines/myvalenstate.py87
-rw-r--r--bot/seasons/valentines/pickuplines.py45
-rw-r--r--bot/seasons/valentines/savethedate.py42
-rw-r--r--bot/seasons/valentines/valentine_zodiac.py58
-rw-r--r--bot/seasons/valentines/whoisvalentine.py53
-rw-r--r--bot/seasons/wildcard/__init__.py31
67 files changed, 0 insertions, 9299 deletions
diff --git a/bot/seasons/__init__.py b/bot/seasons/__init__.py
deleted file mode 100644
index 7faf9164..00000000
--- a/bot/seasons/__init__.py
+++ /dev/null
@@ -1,14 +0,0 @@
-import logging
-
-from discord.ext import commands
-
-from bot.seasons.season import SeasonBase, SeasonManager, get_season
-
-__all__ = ("SeasonBase", "get_season")
-
-log = logging.getLogger(__name__)
-
-
-def setup(bot: commands.Bot) -> None:
- bot.add_cog(SeasonManager(bot))
- log.info("SeasonManager cog loaded")
diff --git a/bot/seasons/christmas/__init__.py b/bot/seasons/christmas/__init__.py
deleted file mode 100644
index 4287efb7..00000000
--- a/bot/seasons/christmas/__init__.py
+++ /dev/null
@@ -1,33 +0,0 @@
-import datetime
-
-from bot.constants import Colours
-from bot.seasons import SeasonBase
-
-
-class Christmas(SeasonBase):
- """
- Christmas seasonal event attributes.
-
- We are getting into the festive spirit with a new server icon, new bot name and avatar, and some
- new commands for you to check out!
-
- No matter who you are, where you are or what beliefs you may follow, we hope every one of you
- enjoy this festive season!
- """
-
- name = "christmas"
- bot_name = "Merrybot"
- greeting = "Happy Holidays!"
-
- start_date = "01/12"
- end_date = "01/01"
-
- colour = Colours.dark_green
- icon = (
- "/logos/logo_seasonal/christmas/2019/festive_512.gif",
- )
-
- @classmethod
- def end(cls) -> datetime.datetime:
- """Overload the `SeasonBase` method to account for the event ending in the next year."""
- return datetime.datetime.strptime(f"{cls.end_date}/{cls.current_year() + 1}", cls.date_format)
diff --git a/bot/seasons/christmas/adventofcode.py b/bot/seasons/christmas/adventofcode.py
deleted file mode 100644
index 8caf43bd..00000000
--- a/bot/seasons/christmas/adventofcode.py
+++ /dev/null
@@ -1,742 +0,0 @@
-import asyncio
-import json
-import logging
-import math
-import re
-from datetime import datetime, timedelta
-from pathlib import Path
-from typing import List, Tuple
-
-import aiohttp
-import discord
-from bs4 import BeautifulSoup
-from discord.ext import commands
-from pytz import timezone
-
-from bot.constants import AdventOfCode as AocConfig, Channels, Colours, Emojis, Tokens, WHITELISTED_CHANNELS
-from bot.decorators import override_in_channel
-from bot.utils import unlocked_role
-
-log = logging.getLogger(__name__)
-
-AOC_REQUEST_HEADER = {"user-agent": "PythonDiscord AoC Event Bot"}
-AOC_SESSION_COOKIE = {"session": Tokens.aoc_session_cookie}
-
-EST = timezone("EST")
-COUNTDOWN_STEP = 60 * 5
-
-AOC_WHITELIST = WHITELISTED_CHANNELS + (Channels.advent_of_code,)
-
-
-def is_in_advent() -> bool:
- """Utility function to check if we are between December 1st and December 25th."""
- # Run the code from the 1st to the 24th
- return datetime.now(EST).day in range(1, 25) and datetime.now(EST).month == 12
-
-
-def time_left_to_aoc_midnight() -> Tuple[datetime, timedelta]:
- """Calculates the amount of time left until midnight in UTC-5 (Advent of Code maintainer timezone)."""
- # Change all time properties back to 00:00
- todays_midnight = datetime.now(EST).replace(microsecond=0,
- second=0,
- minute=0,
- hour=0)
-
- # We want tomorrow so add a day on
- tomorrow = todays_midnight + timedelta(days=1)
-
- # Calculate the timedelta between the current time and midnight
- return tomorrow, tomorrow - datetime.now(EST)
-
-
-async def countdown_status(bot: commands.Bot) -> None:
- """Set the playing status of the bot to the minutes & hours left until the next day's challenge."""
- while is_in_advent():
- _, time_left = time_left_to_aoc_midnight()
-
- aligned_seconds = int(math.ceil(time_left.seconds / COUNTDOWN_STEP)) * COUNTDOWN_STEP
- hours, minutes = aligned_seconds // 3600, aligned_seconds // 60 % 60
-
- if aligned_seconds == 0:
- playing = f"right now!"
- elif aligned_seconds == COUNTDOWN_STEP:
- playing = f"in less than {minutes} minutes"
- elif hours == 0:
- playing = f"in {minutes} minutes"
- elif hours == 23:
- playing = f"since {60 - minutes} minutes ago"
- else:
- playing = f"in {hours} hours and {minutes} minutes"
-
- # Status will look like "Playing in 5 hours and 30 minutes"
- await bot.change_presence(activity=discord.Game(playing))
-
- # Sleep until next aligned time or a full step if already aligned
- delay = time_left.seconds % COUNTDOWN_STEP or COUNTDOWN_STEP
- await asyncio.sleep(delay)
-
-
-async def day_countdown(bot: commands.Bot) -> None:
- """
- Calculate the number of seconds left until the next day of Advent.
-
- Once we have calculated this we should then sleep that number and when the time is reached, ping
- the Advent of Code role notifying them that the new challenge is ready.
- """
- while is_in_advent():
- tomorrow, time_left = time_left_to_aoc_midnight()
-
- # Correct `time_left.seconds` for the sleep we have after unlocking the role (-5) and adding
- # a second (+1) as the bot is consistently ~0.5 seconds early in announcing the puzzles.
- await asyncio.sleep(time_left.seconds - 4)
-
- channel = bot.get_channel(Channels.advent_of_code)
-
- if not channel:
- log.error("Could not find the AoC channel to send notification in")
- break
-
- aoc_role = channel.guild.get_role(AocConfig.role_id)
- if not aoc_role:
- log.error("Could not find the AoC role to announce the daily puzzle")
- break
-
- async with unlocked_role(aoc_role, delay=5):
- puzzle_url = f"https://adventofcode.com/{AocConfig.year}/day/{tomorrow.day}"
-
- # Check if the puzzle is already available to prevent our members from spamming
- # the puzzle page before it's available by making a small HEAD request.
- for retry in range(1, 5):
- log.debug(f"Checking if the puzzle is already available (attempt {retry}/4)")
- async with bot.http_session.head(puzzle_url, raise_for_status=False) as resp:
- if resp.status == 200:
- log.debug("Puzzle is available; let's send an announcement message.")
- break
- log.debug(f"The puzzle is not yet available (status={resp.status})")
- await asyncio.sleep(10)
- else:
- log.error("The puzzle does does not appear to be available at this time, canceling announcement")
- break
-
- await channel.send(
- f"{aoc_role.mention} Good morning! Day {tomorrow.day} is ready to be attempted. "
- f"View it online now at {puzzle_url}. Good luck!"
- )
-
- # Wait a couple minutes so that if our sleep didn't sleep enough
- # time we don't end up announcing twice.
- await asyncio.sleep(120)
-
-
-class AdventOfCode(commands.Cog):
- """Advent of Code festivities! Ho Ho Ho!"""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- self._base_url = f"https://adventofcode.com/{AocConfig.year}"
- self.global_leaderboard_url = f"https://adventofcode.com/{AocConfig.year}/leaderboard"
- self.private_leaderboard_url = f"{self._base_url}/leaderboard/private/view/{AocConfig.leaderboard_id}"
-
- self.about_aoc_filepath = Path("./bot/resources/advent_of_code/about.json")
- self.cached_about_aoc = self._build_about_embed()
-
- self.cached_global_leaderboard = None
- self.cached_private_leaderboard = None
-
- self.countdown_task = None
- self.status_task = None
-
- countdown_coro = day_countdown(self.bot)
- self.countdown_task = self.bot.loop.create_task(countdown_coro)
-
- status_coro = countdown_status(self.bot)
- self.status_task = self.bot.loop.create_task(status_coro)
-
- @commands.group(name="adventofcode", aliases=("aoc",), invoke_without_command=True)
- @override_in_channel(AOC_WHITELIST)
- async def adventofcode_group(self, ctx: commands.Context) -> None:
- """All of the Advent of Code commands."""
- await ctx.send_help(ctx.command)
-
- @adventofcode_group.command(
- name="subscribe",
- aliases=("sub", "notifications", "notify", "notifs"),
- brief="Notifications for new days"
- )
- @override_in_channel(AOC_WHITELIST)
- async def aoc_subscribe(self, ctx: commands.Context) -> None:
- """Assign the role for notifications about new days being ready."""
- role = ctx.guild.get_role(AocConfig.role_id)
- unsubscribe_command = f"{ctx.prefix}{ctx.command.root_parent} unsubscribe"
-
- if role not in ctx.author.roles:
- await ctx.author.add_roles(role)
- await ctx.send("Okay! You have been __subscribed__ to notifications about new Advent of Code tasks. "
- f"You can run `{unsubscribe_command}` to disable them again for you.")
- else:
- await ctx.send("Hey, you already are receiving notifications about new Advent of Code tasks. "
- f"If you don't want them any more, run `{unsubscribe_command}` instead.")
-
- @adventofcode_group.command(name="unsubscribe", aliases=("unsub",), brief="Notifications for new days")
- @override_in_channel(AOC_WHITELIST)
- async def aoc_unsubscribe(self, ctx: commands.Context) -> None:
- """Remove the role for notifications about new days being ready."""
- role = ctx.guild.get_role(AocConfig.role_id)
-
- if role in ctx.author.roles:
- await ctx.author.remove_roles(role)
- await ctx.send("Okay! You have been __unsubscribed__ from notifications about new Advent of Code tasks.")
- else:
- await ctx.send("Hey, you don't even get any notifications about new Advent of Code tasks currently anyway.")
-
- @adventofcode_group.command(name="countdown", aliases=("count", "c"), brief="Return time left until next day")
- @override_in_channel(AOC_WHITELIST)
- async def aoc_countdown(self, ctx: commands.Context) -> None:
- """Return time left until next day."""
- if not is_in_advent():
- datetime_now = datetime.now(EST)
-
- # Calculate the delta to this & next year's December 1st to see which one is closest and not in the past
- this_year = datetime(datetime_now.year, 12, 1, tzinfo=EST)
- next_year = datetime(datetime_now.year + 1, 12, 1, tzinfo=EST)
- deltas = (dec_first - datetime_now for dec_first in (this_year, next_year))
- delta = min(delta for delta in deltas if delta >= timedelta()) # timedelta() gives 0 duration delta
-
- # Add a finer timedelta if there's less than a day left
- if delta.days == 0:
- delta_str = f"approximately {delta.seconds // 3600} hours"
- else:
- delta_str = f"{delta.days} days"
-
- await ctx.send(f"The Advent of Code event is not currently running. "
- f"The next event will start in {delta_str}.")
- return
-
- tomorrow, time_left = time_left_to_aoc_midnight()
-
- hours, minutes = time_left.seconds // 3600, time_left.seconds // 60 % 60
-
- await ctx.send(f"There are {hours} hours and {minutes} minutes left until day {tomorrow.day}.")
-
- @adventofcode_group.command(name="about", aliases=("ab", "info"), brief="Learn about Advent of Code")
- @override_in_channel(AOC_WHITELIST)
- async def about_aoc(self, ctx: commands.Context) -> None:
- """Respond with an explanation of all things Advent of Code."""
- await ctx.send("", embed=self.cached_about_aoc)
-
- @adventofcode_group.command(name="join", aliases=("j",), brief="Learn how to join the leaderboard (via DM)")
- @override_in_channel(AOC_WHITELIST)
- async def join_leaderboard(self, ctx: commands.Context) -> None:
- """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!"
- )
- 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")
- else:
- await ctx.message.add_reaction(Emojis.envelope)
-
- @adventofcode_group.command(
- name="leaderboard",
- aliases=("board", "lb"),
- brief="Get a snapshot of the PyDis private AoC leaderboard",
- )
- @override_in_channel(AOC_WHITELIST)
- async def aoc_leaderboard(self, ctx: commands.Context, number_of_people_to_display: int = 10) -> None:
- """
- Pull the top number_of_people_to_display members from the PyDis leaderboard and post an embed.
-
- For readability, number_of_people_to_display defaults to 10. A maximum value is configured in the
- Advent of Code section of the bot constants. number_of_people_to_display values greater than this
- limit will default to this maximum and provide feedback to the user.
- """
- async with ctx.typing():
- await self._check_leaderboard_cache(ctx)
-
- if not self.cached_private_leaderboard:
- # Feedback on issues with leaderboard caching are sent by _check_leaderboard_cache()
- # Short circuit here if there's an issue
- return
-
- number_of_people_to_display = await self._check_n_entries(ctx, number_of_people_to_display)
-
- # Generate leaderboard table for embed
- members_to_print = self.cached_private_leaderboard.top_n(number_of_people_to_display)
- table = AocPrivateLeaderboard.build_leaderboard_embed(members_to_print)
-
- # Build embed
- aoc_embed = discord.Embed(
- description=f"Total members: {len(self.cached_private_leaderboard.members)}",
- colour=Colours.soft_green,
- timestamp=self.cached_private_leaderboard.last_updated
- )
- aoc_embed.set_author(name="Advent of Code", url=self.private_leaderboard_url)
- aoc_embed.set_footer(text="Last Updated")
-
- await ctx.send(
- content=f"Here's the current Top {number_of_people_to_display}! {Emojis.christmas_tree*3}\n\n{table}",
- embed=aoc_embed,
- )
-
- @adventofcode_group.command(
- name="stats",
- aliases=("dailystats", "ds"),
- brief="Get daily statistics for the PyDis private leaderboard"
- )
- @override_in_channel(AOC_WHITELIST)
- async def private_leaderboard_daily_stats(self, ctx: commands.Context) -> None:
- """
- Respond with a table of the daily completion statistics for the PyDis private leaderboard.
-
- Embed will display the total members and the number of users who have completed each day's puzzle
- """
- async with ctx.typing():
- await self._check_leaderboard_cache(ctx)
-
- if not self.cached_private_leaderboard:
- # Feedback on issues with leaderboard caching are sent by _check_leaderboard_cache()
- # Short circuit here if there's an issue
- return
-
- # Build ASCII table
- total_members = len(self.cached_private_leaderboard.members)
- _star = Emojis.star
- header = f"{'Day':4}{_star:^8}{_star*2:^4}{'% ' + _star:^8}{'% ' + _star*2:^4}\n{'='*35}"
- table = ""
- for day, completions in enumerate(self.cached_private_leaderboard.daily_completion_summary):
- per_one_star = f"{(completions[0]/total_members)*100:.2f}"
- per_two_star = f"{(completions[1]/total_members)*100:.2f}"
-
- table += f"{day+1:3}){completions[0]:^8}{completions[1]:^6}{per_one_star:^10}{per_two_star:^6}\n"
-
- table = f"```\n{header}\n{table}```"
-
- # Build embed
- daily_stats_embed = discord.Embed(
- colour=Colours.soft_green, timestamp=self.cached_private_leaderboard.last_updated
- )
- daily_stats_embed.set_author(name="Advent of Code", url=self._base_url)
- daily_stats_embed.set_footer(text="Last Updated")
-
- await ctx.send(
- content=f"Here's the current daily statistics!\n\n{table}", embed=daily_stats_embed
- )
-
- @adventofcode_group.command(
- name="global",
- aliases=("globalboard", "gb"),
- brief="Get a snapshot of the global AoC leaderboard",
- )
- @override_in_channel(AOC_WHITELIST)
- async def global_leaderboard(self, ctx: commands.Context, number_of_people_to_display: int = 10) -> None:
- """
- Pull the top number_of_people_to_display members from the global AoC leaderboard and post an embed.
-
- For readability, number_of_people_to_display defaults to 10. A maximum value is configured in the
- Advent of Code section of the bot constants. number_of_people_to_display values greater than this
- limit will default to this maximum and provide feedback to the user.
- """
- async with ctx.typing():
- await self._check_leaderboard_cache(ctx, global_board=True)
-
- if not self.cached_global_leaderboard:
- # Feedback on issues with leaderboard caching are sent by _check_leaderboard_cache()
- # Short circuit here if there's an issue
- return
-
- number_of_people_to_display = await self._check_n_entries(ctx, number_of_people_to_display)
-
- # Generate leaderboard table for embed
- members_to_print = self.cached_global_leaderboard.top_n(number_of_people_to_display)
- table = AocGlobalLeaderboard.build_leaderboard_embed(members_to_print)
-
- # Build embed
- aoc_embed = discord.Embed(colour=Colours.soft_green, timestamp=self.cached_global_leaderboard.last_updated)
- aoc_embed.set_author(name="Advent of Code", url=self._base_url)
- aoc_embed.set_footer(text="Last Updated")
-
- await ctx.send(
- f"Here's the current global Top {number_of_people_to_display}! {Emojis.christmas_tree*3}\n\n{table}",
- embed=aoc_embed,
- )
-
- async def _check_leaderboard_cache(self, ctx: commands.Context, global_board: bool = False) -> None:
- """
- Check age of current leaderboard & pull a new one if the board is too old.
-
- global_board is a boolean to toggle between the global board and the Pydis private board
- """
- # Toggle between global & private leaderboards
- if global_board:
- log.debug("Checking global leaderboard cache")
- leaderboard_str = "cached_global_leaderboard"
- _shortstr = "global"
- else:
- log.debug("Checking private leaderboard cache")
- leaderboard_str = "cached_private_leaderboard"
- _shortstr = "private"
-
- leaderboard = getattr(self, leaderboard_str)
- if not leaderboard:
- log.debug(f"No cached {_shortstr} leaderboard found")
- await self._boardgetter(global_board)
- else:
- leaderboard_age = datetime.utcnow() - leaderboard.last_updated
- age_seconds = leaderboard_age.total_seconds()
- if age_seconds < AocConfig.leaderboard_cache_age_threshold_seconds:
- log.debug(f"Cached {_shortstr} leaderboard age less than threshold ({age_seconds} seconds old)")
- else:
- log.debug(f"Cached {_shortstr} leaderboard age greater than threshold ({age_seconds} seconds old)")
- await self._boardgetter(global_board)
-
- leaderboard = getattr(self, leaderboard_str)
- if not leaderboard:
- await ctx.send(
- "",
- embed=_error_embed_helper(
- title=f"Something's gone wrong and there's no cached {_shortstr} leaderboard!",
- description="Please check in with a staff member.",
- ),
- )
-
- async def _check_n_entries(self, ctx: commands.Context, number_of_people_to_display: int) -> int:
- """Check for n > max_entries and n <= 0."""
- max_entries = AocConfig.leaderboard_max_displayed_members
- author = ctx.message.author
- if not 0 <= number_of_people_to_display <= max_entries:
- log.debug(
- f"{author.name} ({author.id}) attempted to fetch an invalid number "
- f" of entries from the AoC leaderboard ({number_of_people_to_display})"
- )
- await ctx.send(
- f":x: {author.mention}, number of entries to display must be a positive "
- f"integer less than or equal to {max_entries}\n\n"
- f"Head to {self.private_leaderboard_url} to view the entire leaderboard"
- )
- number_of_people_to_display = max_entries
-
- return number_of_people_to_display
-
- def _build_about_embed(self) -> discord.Embed:
- """Build and return the informational "About AoC" embed from the resources file."""
- with self.about_aoc_filepath.open("r") as f:
- embed_fields = json.load(f)
-
- about_embed = discord.Embed(title=self._base_url, colour=Colours.soft_green, url=self._base_url)
- about_embed.set_author(name="Advent of Code", url=self._base_url)
- for field in embed_fields:
- about_embed.add_field(**field)
-
- about_embed.set_footer(text=f"Last Updated (UTC): {datetime.utcnow()}")
-
- return about_embed
-
- async def _boardgetter(self, global_board: bool) -> None:
- """Invoke the proper leaderboard getter based on the global_board boolean."""
- if global_board:
- self.cached_global_leaderboard = await AocGlobalLeaderboard.from_url()
- else:
- self.cached_private_leaderboard = await AocPrivateLeaderboard.from_url()
-
- def cog_unload(self) -> None:
- """Cancel season-related tasks on cog unload."""
- log.debug("Unloading the cog and canceling the background task.")
- self.countdown_task.cancel()
- self.status_task.cancel()
-
-
-class AocMember:
- """Object representing the Advent of Code user."""
-
- def __init__(self, name: str, aoc_id: int, stars: int, starboard: list, local_score: int, global_score: int):
- self.name = name
- self.aoc_id = aoc_id
- self.stars = stars
- self.starboard = starboard
- self.local_score = local_score
- self.global_score = global_score
- self.completions = self._completions_from_starboard(self.starboard)
-
- def __repr__(self):
- """Generate a user-friendly representation of the AocMember & their score."""
- return f"<{self.name} ({self.aoc_id}): {self.local_score}>"
-
- @classmethod
- def member_from_json(cls, injson: dict) -> "AocMember":
- """
- Generate an AocMember from AoC's private leaderboard API JSON.
-
- injson is expected to be the dict contained in:
-
- AoC_APIjson['members'][<member id>:str]
-
- Returns an AocMember object
- """
- return cls(
- name=injson["name"] if injson["name"] else "Anonymous User",
- aoc_id=int(injson["id"]),
- stars=injson["stars"],
- starboard=cls._starboard_from_json(injson["completion_day_level"]),
- local_score=injson["local_score"],
- global_score=injson["global_score"],
- )
-
- @staticmethod
- def _starboard_from_json(injson: dict) -> list:
- """
- Generate starboard from AoC's private leaderboard API JSON.
-
- injson is expected to be the dict contained in:
-
- AoC_APIjson['members'][<member id>:str]['completion_day_level']
-
- Returns a list of 25 lists, where each nested list contains a pair of booleans representing
- the code challenge completion status for that day
- """
- # Basic input validation
- if not isinstance(injson, dict):
- raise ValueError
-
- # Initialize starboard
- starboard = []
- for _i in range(25):
- starboard.append([False, False])
-
- # Iterate over days, which are the keys of injson (as str)
- for day in injson:
- idx = int(day) - 1
- # If there is a second star, the first star must be completed
- if "2" in injson[day].keys():
- starboard[idx] = [True, True]
- # If the day exists in injson, then at least the first star is completed
- else:
- starboard[idx] = [True, False]
-
- return starboard
-
- @staticmethod
- def _completions_from_starboard(starboard: list) -> tuple:
- """Return days completed, as a (1 star, 2 star) tuple, from starboard."""
- completions = [0, 0]
- for day in starboard:
- if day[0]:
- completions[0] += 1
- if day[1]:
- completions[1] += 1
-
- return tuple(completions)
-
-
-class AocPrivateLeaderboard:
- """Object representing the Advent of Code private leaderboard."""
-
- def __init__(self, members: list, owner_id: int, event_year: int):
- self.members = members
- self._owner_id = owner_id
- self._event_year = event_year
- self.last_updated = datetime.utcnow()
-
- self.daily_completion_summary = self.calculate_daily_completion()
-
- def top_n(self, n: int = 10) -> dict:
- """
- Return the top n participants on the leaderboard.
-
- If n is not specified, default to the top 10
- """
- return self.members[:n]
-
- def calculate_daily_completion(self) -> List[tuple]:
- """
- Calculate member completion rates by day.
-
- Return a list of tuples for each day containing the number of users who completed each part
- of the challenge
- """
- daily_member_completions = []
- for day in range(25):
- one_star_count = 0
- two_star_count = 0
- for member in self.members:
- if member.starboard[day][1]:
- one_star_count += 1
- two_star_count += 1
- elif member.starboard[day][0]:
- one_star_count += 1
- else:
- daily_member_completions.append((one_star_count, two_star_count))
-
- return(daily_member_completions)
-
- @staticmethod
- async def json_from_url(
- leaderboard_id: int = AocConfig.leaderboard_id, year: int = AocConfig.year
- ) -> "AocPrivateLeaderboard":
- """
- Request the API JSON from Advent of Code for leaderboard_id for the specified year's event.
-
- If no year is input, year defaults to the current year
- """
- api_url = f"https://adventofcode.com/{year}/leaderboard/private/view/{leaderboard_id}.json"
-
- log.debug("Querying Advent of Code Private Leaderboard API")
- async with aiohttp.ClientSession(cookies=AOC_SESSION_COOKIE, headers=AOC_REQUEST_HEADER) as session:
- async with session.get(api_url) as resp:
- if resp.status == 200:
- raw_dict = await resp.json()
- else:
- log.warning(f"Bad response received from AoC ({resp.status}), check session cookie")
- resp.raise_for_status()
-
- return raw_dict
-
- @classmethod
- def from_json(cls, injson: dict) -> "AocPrivateLeaderboard":
- """Generate an AocPrivateLeaderboard object from AoC's private leaderboard API JSON."""
- return cls(
- members=cls._sorted_members(injson["members"]), owner_id=injson["owner_id"], event_year=injson["event"]
- )
-
- @classmethod
- async def from_url(cls) -> "AocPrivateLeaderboard":
- """Helper wrapping of AocPrivateLeaderboard.json_from_url and AocPrivateLeaderboard.from_json."""
- api_json = await cls.json_from_url()
- return cls.from_json(api_json)
-
- @staticmethod
- def _sorted_members(injson: dict) -> list:
- """
- Generate a sorted list of AocMember objects from AoC's private leaderboard API JSON.
-
- Output list is sorted based on the AocMember.local_score
- """
- members = [AocMember.member_from_json(injson[member]) for member in injson]
- members.sort(key=lambda x: x.local_score, reverse=True)
-
- return members
-
- @staticmethod
- def build_leaderboard_embed(members_to_print: List[AocMember]) -> str:
- """
- Build a text table from members_to_print, a list of AocMember objects.
-
- Returns a string to be used as the content of the bot's leaderboard response
- """
- stargroup = f"{Emojis.star}, {Emojis.star*2}"
- header = f"{' '*3}{'Score'} {'Name':^25} {stargroup:^7}\n{'-'*44}"
- table = ""
- for i, member in enumerate(members_to_print):
- if member.name == "Anonymous User":
- name = f"{member.name} #{member.aoc_id}"
- else:
- name = member.name
-
- table += (
- f"{i+1:2}) {member.local_score:4} {name:25.25} "
- f"({member.completions[0]:2}, {member.completions[1]:2})\n"
- )
- else:
- table = f"```{header}\n{table}```"
-
- return table
-
-
-class AocGlobalLeaderboard:
- """Object representing the Advent of Code global leaderboard."""
-
- def __init__(self, members: List[tuple]):
- self.members = members
- self.last_updated = datetime.utcnow()
-
- def top_n(self, n: int = 10) -> dict:
- """
- Return the top n participants on the leaderboard.
-
- If n is not specified, default to the top 10
- """
- return self.members[:n]
-
- @classmethod
- async def from_url(cls) -> "AocGlobalLeaderboard":
- """
- Generate an list of tuples for the entries on AoC's global leaderboard.
-
- Because there is no API for this, web scraping needs to be used
- """
- aoc_url = f"https://adventofcode.com/{AocConfig.year}/leaderboard"
-
- async with aiohttp.ClientSession(headers=AOC_REQUEST_HEADER) as session:
- async with session.get(aoc_url) as resp:
- if resp.status == 200:
- raw_html = await resp.text()
- else:
- log.warning(f"Bad response received from AoC ({resp.status}), check session cookie")
- resp.raise_for_status()
-
- soup = BeautifulSoup(raw_html, "html.parser")
- ele = soup.find_all("div", class_="leaderboard-entry")
-
- exp = r"(?:[ ]{,2}(\d+)\))?[ ]+(\d+)\s+([\w\(\)\#\@\-\d ]+)"
-
- lb_list = []
- for entry in ele:
- # Strip off the AoC++ decorator
- raw_str = entry.text.replace("(AoC++)", "").rstrip()
-
- # Use a regex to extract the info from the string to unify formatting
- # Group 1: Rank
- # Group 2: Global Score
- # Group 3: Member string
- r = re.match(exp, raw_str)
-
- rank = int(r.group(1)) if r.group(1) else None
- global_score = int(r.group(2))
-
- member = r.group(3)
- if member.lower().startswith("(anonymous"):
- # Normalize anonymous user string by stripping () and title casing
- member = re.sub(r"[\(\)]", "", member).title()
-
- lb_list.append((rank, global_score, member))
-
- return cls(lb_list)
-
- @staticmethod
- def build_leaderboard_embed(members_to_print: List[tuple]) -> str:
- """
- Build a text table from members_to_print, a list of tuples.
-
- Returns a string to be used as the content of the bot's leaderboard response
- """
- header = f"{' '*4}{'Score'} {'Name':^25}\n{'-'*36}"
- table = ""
- for member in members_to_print:
- # In the event of a tie, rank is None
- if member[0]:
- rank = f"{member[0]:3})"
- else:
- rank = f"{' ':4}"
- table += f"{rank} {member[1]:4} {member[2]:25.25}\n"
- else:
- table = f"```{header}\n{table}```"
-
- return table
-
-
-def _error_embed_helper(title: str, description: str) -> discord.Embed:
- """Return a red-colored Embed with the given title and description."""
- return discord.Embed(title=title, description=description, colour=discord.Colour.red())
-
-
-def setup(bot: commands.Bot) -> None:
- """Advent of Code Cog load."""
- bot.add_cog(AdventOfCode(bot))
- log.info("AdventOfCode cog loaded")
diff --git a/bot/seasons/christmas/hanukkah_embed.py b/bot/seasons/christmas/hanukkah_embed.py
deleted file mode 100644
index aaa02b27..00000000
--- a/bot/seasons/christmas/hanukkah_embed.py
+++ /dev/null
@@ -1,113 +0,0 @@
-import datetime
-import logging
-from typing import List
-
-from discord import Embed
-from discord.ext import commands
-
-from bot.constants import Colours
-
-
-log = logging.getLogger(__name__)
-
-
-class HanukkahEmbed(commands.Cog):
- """A cog that returns information about Hanukkah festival."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.url = ("https://www.hebcal.com/hebcal/?v=1&cfg=json&maj=on&min=on&mod=on&nx=on&"
- "year=now&month=x&ss=on&mf=on&c=on&geo=geoname&geonameid=3448439&m=50&s=on")
- self.hanukkah_days = []
- self.hanukkah_months = []
- self.hanukkah_years = []
-
- async def get_hanukkah_dates(self) -> List[str]:
- """Gets the dates for hanukkah festival."""
- hanukkah_dates = []
- async with self.bot.http_session.get(self.url) as response:
- json_data = await response.json()
- festivals = json_data['items']
- for festival in festivals:
- if festival['title'].startswith('Chanukah'):
- date = festival['date']
- hanukkah_dates.append(date)
- return hanukkah_dates
-
- @commands.command(name='hanukkah', aliases=['chanukah'])
- async def hanukkah_festival(self, ctx: commands.Context) -> None:
- """Tells you about the Hanukkah Festivaltime of festival, festival day, etc)."""
- hanukkah_dates = await self.get_hanukkah_dates()
- self.hanukkah_dates_split(hanukkah_dates)
- hanukkah_start_day = int(self.hanukkah_days[0])
- hanukkah_start_month = int(self.hanukkah_months[0])
- hanukkah_start_year = int(self.hanukkah_years[0])
- hanukkah_end_day = int(self.hanukkah_days[8])
- hanukkah_end_month = int(self.hanukkah_months[8])
- hanukkah_end_year = int(self.hanukkah_years[8])
-
- hanukkah_start = datetime.date(hanukkah_start_year, hanukkah_start_month, hanukkah_start_day)
- hanukkah_end = datetime.date(hanukkah_end_year, hanukkah_end_month, hanukkah_end_day)
- today = datetime.date.today()
- # today = datetime.date(2019, 12, 24) (for testing)
- day = str(today.day)
- month = str(today.month)
- year = str(today.year)
- embed = Embed()
- embed.title = 'Hanukkah'
- embed.colour = Colours.blue
- if day in self.hanukkah_days and month in self.hanukkah_months and year in self.hanukkah_years:
- if int(day) == hanukkah_start_day:
- now = datetime.datetime.utcnow()
- now = str(now)
- hours = int(now[11:13]) + 4 # using only hours
- hanukkah_start_hour = 18
- if hours < hanukkah_start_hour:
- embed.description = (f"Hanukkah hasnt started yet, "
- f"it will start in about {hanukkah_start_hour-hours} hour/s.")
- return await ctx.send(embed=embed)
- elif hours > hanukkah_start_hour:
- embed.description = (f'It is the starting day of Hanukkah ! '
- f'Its been {hours-hanukkah_start_hour} hours hanukkah started !')
- return await ctx.send(embed=embed)
- festival_day = self.hanukkah_days.index(day)
- number_suffixes = ['st', 'nd', 'rd', 'th']
- suffix = ''
- if int(festival_day) == 1:
- suffix = number_suffixes[0]
- if int(festival_day) == 2:
- suffix = number_suffixes[1]
- if int(festival_day) == 3:
- suffix = number_suffixes[2]
- if int(festival_day) > 3:
- suffix = number_suffixes[3]
- message = ''
- for _ in range(1, festival_day + 1):
- message += ':menorah:'
- embed.description = f'It is the {festival_day}{suffix} day of Hanukkah ! \n {message}'
- await ctx.send(embed=embed)
- else:
- if today < hanukkah_start:
- festival_starting_month = hanukkah_start.strftime('%B')
- embed.description = (f"Hanukkah has not started yet. "
- f"Hanukkah will start at sundown on {hanukkah_start_day}th "
- f"of {festival_starting_month}.")
- else:
- festival_end_month = hanukkah_end.strftime('%B')
- embed.description = (f"Looks like you missed Hanukkah !"
- f"Hanukkah ended on {hanukkah_end_day}th of {festival_end_month}.")
-
- await ctx.send(embed=embed)
-
- def hanukkah_dates_split(self, hanukkah_dates: List[str]) -> None:
- """We are splitting the dates for hanukkah into days, months and years."""
- for date in hanukkah_dates:
- self.hanukkah_days.append(date[8:10])
- self.hanukkah_months.append(date[5:7])
- self.hanukkah_years.append(date[0:4])
-
-
-def setup(bot: commands.Bot) -> None:
- """Cog load."""
- bot.add_cog(HanukkahEmbed(bot))
- log.info("Hanukkah embed cog loaded")
diff --git a/bot/seasons/easter/__init__.py b/bot/seasons/easter/__init__.py
deleted file mode 100644
index dd60bf5c..00000000
--- a/bot/seasons/easter/__init__.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from bot.constants import Colours
-from bot.seasons import SeasonBase
-
-
-class Easter(SeasonBase):
- """
- Here at Python Discord, we celebrate our version of Easter during the entire month of April.
-
- While this celebration takes place, you'll notice a few changes:
-
- • The server icon has changed to our Easter icon. Thanks to <@140605665772175361> for the
- design!
-
- • [Easter issues now available for SeasonalBot on the repo](https://git.io/fjkvQ).
-
- • You may see stuff like an Easter themed esoteric challenge, a celebration of Earth Day, or
- Easter-related micro-events for you to join. Stay tuned!
-
- If you'd like to contribute, head on over to <#635950537262759947> and we will help you get
- started. It doesn't matter if you're new to open source or Python, if you'd like to help, we
- will find you a task and teach you what you need to know.
- """
-
- name = "easter"
- bot_name = "BunnyBot"
- greeting = "Happy Easter!"
-
- # Duration of season
- start_date = "02/04"
- end_date = "30/04"
-
- colour = Colours.pink
- icon = (
- "/logos/logo_seasonal/easter/easter.png",
- )
diff --git a/bot/seasons/easter/april_fools_vids.py b/bot/seasons/easter/april_fools_vids.py
deleted file mode 100644
index 4869f510..00000000
--- a/bot/seasons/easter/april_fools_vids.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import logging
-import random
-from json import load
-from pathlib import Path
-
-from discord.ext import commands
-
-log = logging.getLogger(__name__)
-
-
-class AprilFoolVideos(commands.Cog):
- """A cog for April Fools' that gets a random April Fools' video from Youtube."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.yt_vids = self.load_json()
- self.youtubers = ['google'] # will add more in future
-
- @staticmethod
- def load_json() -> dict:
- """A function to load JSON data."""
- p = Path('bot/resources/easter/april_fools_vids.json')
- with p.open() as json_file:
- all_vids = load(json_file)
- return all_vids
-
- @commands.command(name='fool')
- async def april_fools(self, ctx: commands.Context) -> None:
- """Get a random April Fools' video from Youtube."""
- random_youtuber = random.choice(self.youtubers)
- category = self.yt_vids[random_youtuber]
- random_vid = random.choice(category)
- await ctx.send(f"Check out this April Fools' video by {random_youtuber}.\n\n{random_vid['link']}")
-
-
-def setup(bot: commands.Bot) -> None:
- """April Fools' Cog load."""
- bot.add_cog(AprilFoolVideos(bot))
- log.info('April Fools videos cog loaded!')
diff --git a/bot/seasons/easter/avatar_easterifier.py b/bot/seasons/easter/avatar_easterifier.py
deleted file mode 100644
index e21e35fc..00000000
--- a/bot/seasons/easter/avatar_easterifier.py
+++ /dev/null
@@ -1,129 +0,0 @@
-import asyncio
-import logging
-from io import BytesIO
-from pathlib import Path
-from typing import Tuple, Union
-
-import discord
-from PIL import Image
-from PIL.ImageOps import posterize
-from discord.ext import commands
-
-log = logging.getLogger(__name__)
-
-COLOURS = [
- (255, 247, 0), (255, 255, 224), (0, 255, 127), (189, 252, 201), (255, 192, 203),
- (255, 160, 122), (181, 115, 220), (221, 160, 221), (200, 162, 200), (238, 130, 238),
- (135, 206, 235), (0, 204, 204), (64, 224, 208)
-] # Pastel colours - Easter-like
-
-
-class AvatarEasterifier(commands.Cog):
- """Put an Easter spin on your avatar or image!"""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @staticmethod
- def closest(x: Tuple[int, int, int]) -> Tuple[int, int, int]:
- """
- Finds the closest easter colour to a given pixel.
-
- Returns a merge between the original colour and the closest colour
- """
- r1, g1, b1 = x
-
- def distance(point: Tuple[int, int, int]) -> Tuple[int, int, int]:
- """Finds the difference between a pastel colour and the original pixel colour."""
- r2, g2, b2 = point
- return ((r1 - r2)**2 + (g1 - g2)**2 + (b1 - b2)**2)
-
- closest_colours = sorted(COLOURS, key=lambda point: distance(point))
- r2, g2, b2 = closest_colours[0]
- r = (r1 + r2) // 2
- g = (g1 + g2) // 2
- b = (b1 + b2) // 2
-
- return (r, g, b)
-
- @commands.command(pass_context=True, aliases=["easterify"])
- async def avatareasterify(self, ctx: commands.Context, *colours: Union[discord.Colour, str]) -> None:
- """
- This "Easterifies" the user's avatar.
-
- Given colours will produce a personalised egg in the corner, similar to the egg_decorate command.
- If colours are not given, a nice little chocolate bunny will sit in the corner.
- Colours are split by spaces, unless you wrap the colour name in double quotes.
- Discord colour names, HTML colour names, XKCD colour names and hex values are accepted.
- """
- async def send(*args, **kwargs) -> str:
- """
- This replaces the original ctx.send.
-
- When invoking the egg decorating command, the egg itself doesn't print to to the channel.
- Returns the message content so that if any errors occur, the error message can be output.
- """
- if args:
- return args[0]
-
- async with ctx.typing():
-
- # Grabs image of avatar
- image_bytes = await ctx.author.avatar_url_as(size=256).read()
-
- old = Image.open(BytesIO(image_bytes))
- old = old.convert("RGBA")
-
- # Grabs alpha channel since posterize can't be used with an RGBA image.
- alpha = old.getchannel("A").getdata()
- old = old.convert("RGB")
- old = posterize(old, 6)
-
- data = old.getdata()
- setted_data = set(data)
- new_d = {}
-
- for x in setted_data:
- new_d[x] = self.closest(x)
- await asyncio.sleep(0) # Ensures discord doesn't break in the background.
- new_data = [(*new_d[x], alpha[i]) if x in new_d else x for i, x in enumerate(data)]
-
- im = Image.new("RGBA", old.size)
- im.putdata(new_data)
-
- if colours:
- send_message = ctx.send
- ctx.send = send # Assigns ctx.send to a fake send
- egg = await ctx.invoke(self.bot.get_command("eggdecorate"), *colours)
- if isinstance(egg, str): # When an error message occurs in eggdecorate.
- return await send_message(egg)
-
- ratio = 64 / egg.height
- egg = egg.resize((round(egg.width * ratio), round(egg.height * ratio)))
- egg = egg.convert("RGBA")
- im.alpha_composite(egg, (im.width - egg.width, (im.height - egg.height)//2)) # Right centre.
- ctx.send = send_message # Reassigns ctx.send
- else:
- bunny = Image.open(Path("bot/resources/easter/chocolate_bunny.png"))
- im.alpha_composite(bunny, (im.width - bunny.width, (im.height - bunny.height)//2)) # Right centre.
-
- bufferedio = BytesIO()
- im.save(bufferedio, format="PNG")
-
- bufferedio.seek(0)
-
- file = discord.File(bufferedio, filename="easterified_avatar.png") # Creates file to be used in embed
- embed = discord.Embed(
- name="Your Lovely Easterified Avatar",
- description="Here is your lovely avatar, all bright and colourful\nwith Easter pastel colours. Enjoy :D"
- )
- embed.set_image(url="attachment://easterified_avatar.png")
- embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url)
-
- await ctx.send(file=file, embed=embed)
-
-
-def setup(bot: commands.Bot) -> None:
- """Avatar Easterifier Cog load."""
- bot.add_cog(AvatarEasterifier(bot))
- log.info("AvatarEasterifier cog loaded")
diff --git a/bot/seasons/easter/bunny_name_generator.py b/bot/seasons/easter/bunny_name_generator.py
deleted file mode 100644
index 97c467e1..00000000
--- a/bot/seasons/easter/bunny_name_generator.py
+++ /dev/null
@@ -1,93 +0,0 @@
-import json
-import logging
-import random
-import re
-from pathlib import Path
-from typing import List, Union
-
-from discord.ext import commands
-
-log = logging.getLogger(__name__)
-
-with Path("bot/resources/easter/bunny_names.json").open("r", encoding="utf8") as f:
- BUNNY_NAMES = json.load(f)
-
-
-class BunnyNameGenerator(commands.Cog):
- """Generate a random bunny name, or bunnify your Discord username!"""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- def find_separators(self, displayname: str) -> Union[List[str], None]:
- """Check if Discord name contains spaces so we can bunnify an individual word in the name."""
- new_name = re.split(r'[_.\s]', displayname)
- if displayname not in new_name:
- return new_name
-
- def find_vowels(self, displayname: str) -> str:
- """
- Finds vowels in the user's display name.
-
- If the Discord name contains a vowel and the letter y, it will match one or more of these patterns.
-
- Only the most recently matched pattern will apply the changes.
- """
- expressions = [
- (r'a.+y', 'patchy'),
- (r'e.+y', 'ears'),
- (r'i.+y', 'ditsy'),
- (r'o.+y', 'oofy'),
- (r'u.+y', 'uffy'),
- ]
-
- for exp, vowel_sub in expressions:
- new_name = re.sub(exp, vowel_sub, displayname)
- if new_name != displayname:
- return new_name
-
- def append_name(self, displayname: str) -> str:
- """Adds a suffix to the end of the Discord name."""
- extensions = ['foot', 'ear', 'nose', 'tail']
- suffix = random.choice(extensions)
- appended_name = displayname + suffix
-
- return appended_name
-
- @commands.command()
- async def bunnyname(self, ctx: commands.Context) -> None:
- """Picks a random bunny name from a JSON file."""
- await ctx.send(random.choice(BUNNY_NAMES["names"]))
-
- @commands.command()
- async def bunnifyme(self, ctx: commands.Context) -> None:
- """Gets your Discord username and bunnifies it."""
- username = ctx.message.author.display_name
-
- # If name contains spaces or other separators, get the individual words to randomly bunnify
- spaces_in_name = self.find_separators(username)
-
- # If name contains vowels, see if it matches any of the patterns in this function
- # If there are matches, the bunnified name is returned.
- vowels_in_name = self.find_vowels(username)
-
- # Default if the checks above return None
- unmatched_name = self.append_name(username)
-
- if spaces_in_name is not None:
- replacements = ['Cotton', 'Fluff', 'Floof' 'Bounce', 'Snuffle', 'Nibble', 'Cuddle', 'Velvetpaw', 'Carrot']
- word_to_replace = random.choice(spaces_in_name)
- substitute = random.choice(replacements)
- bunnified_name = username.replace(word_to_replace, substitute)
- elif vowels_in_name is not None:
- bunnified_name = vowels_in_name
- elif unmatched_name:
- bunnified_name = unmatched_name
-
- await ctx.send(bunnified_name)
-
-
-def setup(bot: commands.Bot) -> None:
- """Bunny Name Generator Cog load."""
- bot.add_cog(BunnyNameGenerator(bot))
- log.info("BunnyNameGenerator cog loaded.")
diff --git a/bot/seasons/easter/conversationstarters.py b/bot/seasons/easter/conversationstarters.py
deleted file mode 100644
index 3f38ae82..00000000
--- a/bot/seasons/easter/conversationstarters.py
+++ /dev/null
@@ -1,29 +0,0 @@
-import json
-import logging
-import random
-from pathlib import Path
-
-from discord.ext import commands
-
-log = logging.getLogger(__name__)
-
-with open(Path("bot/resources/easter/starter.json"), "r", encoding="utf8") as f:
- starters = json.load(f)
-
-
-class ConvoStarters(commands.Cog):
- """Easter conversation topics."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command()
- async def topic(self, ctx: commands.Context) -> None:
- """Responds with a random topic to start a conversation."""
- await ctx.send(random.choice(starters['starters']))
-
-
-def setup(bot: commands.Bot) -> None:
- """Conversation starters Cog load."""
- bot.add_cog(ConvoStarters(bot))
- log.info("ConvoStarters cog loaded")
diff --git a/bot/seasons/easter/easter_riddle.py b/bot/seasons/easter/easter_riddle.py
deleted file mode 100644
index f5b1aac7..00000000
--- a/bot/seasons/easter/easter_riddle.py
+++ /dev/null
@@ -1,101 +0,0 @@
-import asyncio
-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 Path("bot/resources/easter/easter_riddle.json").open("r", encoding="utf8") as f:
- RIDDLE_QUESTIONS = load(f)
-
-TIMELIMIT = 10
-
-
-class EasterRiddle(commands.Cog):
- """This cog contains the command for the Easter quiz!"""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.winners = []
- self.correct = ""
- self.current_channel = None
-
- @commands.command(aliases=["riddlemethis", "riddleme"])
- async def riddle(self, ctx: commands.Context) -> None:
- """
- Gives a random riddle, then provides 2 hints at certain intervals before revealing the answer.
-
- The duration of the hint interval can be configured by changing the TIMELIMIT constant in this file.
- """
- if self.current_channel:
- return await ctx.send(f"A riddle is already being solved in {self.current_channel.mention}!")
-
- self.current_channel = ctx.message.channel
-
- random_question = random.choice(RIDDLE_QUESTIONS)
- question = random_question["question"]
- hints = random_question["riddles"]
- self.correct = random_question["correct_answer"]
-
- description = f"You have {TIMELIMIT} seconds before the first hint."
-
- riddle_embed = discord.Embed(title=question, description=description, colour=Colours.pink)
-
- await ctx.send(embed=riddle_embed)
- await asyncio.sleep(TIMELIMIT)
-
- hint_embed = discord.Embed(
- title=f"Here's a hint: {hints[0]}!",
- colour=Colours.pink
- )
-
- await ctx.send(embed=hint_embed)
- await asyncio.sleep(TIMELIMIT)
-
- hint_embed = discord.Embed(
- title=f"Here's a hint: {hints[1]}!",
- colour=Colours.pink
- )
-
- await ctx.send(embed=hint_embed)
- await asyncio.sleep(TIMELIMIT)
-
- if self.winners:
- win_list = " ".join(self.winners)
- content = f"Well done {win_list} for getting it right!"
- else:
- content = "Nobody got it right..."
-
- answer_embed = discord.Embed(
- title=f"The answer is: {self.correct}!",
- colour=Colours.pink
- )
-
- await ctx.send(content, embed=answer_embed)
-
- self.winners = []
- self.current_channel = None
-
- @commands.Cog.listener()
- async def on_message(self, message: discord.Message) -> None:
- """If a non-bot user enters a correct answer, their username gets added to self.winners."""
- if self.current_channel != message.channel:
- return
-
- if self.bot.user == message.author:
- return
-
- if message.content.lower() == self.correct.lower():
- self.winners.append(message.author.mention)
-
-
-def setup(bot: commands.Bot) -> None:
- """Easter Riddle Cog load."""
- bot.add_cog(EasterRiddle(bot))
- log.info("Easter Riddle bot loaded")
diff --git a/bot/seasons/easter/egg_decorating.py b/bot/seasons/easter/egg_decorating.py
deleted file mode 100644
index 23df95f1..00000000
--- a/bot/seasons/easter/egg_decorating.py
+++ /dev/null
@@ -1,119 +0,0 @@
-import json
-import logging
-import random
-from contextlib import suppress
-from io import BytesIO
-from pathlib import Path
-from typing import Union
-
-import discord
-from PIL import Image
-from discord.ext import commands
-
-log = logging.getLogger(__name__)
-
-with open(Path("bot/resources/evergreen/html_colours.json")) as f:
- HTML_COLOURS = json.load(f)
-
-with open(Path("bot/resources/evergreen/xkcd_colours.json")) as f:
- XKCD_COLOURS = json.load(f)
-
-COLOURS = [
- (255, 0, 0, 255), (255, 128, 0, 255), (255, 255, 0, 255), (0, 255, 0, 255),
- (0, 255, 255, 255), (0, 0, 255, 255), (255, 0, 255, 255), (128, 0, 128, 255)
-] # Colours to be replaced - Red, Orange, Yellow, Green, Light Blue, Dark Blue, Pink, Purple
-
-IRREPLACEABLE = [
- (0, 0, 0, 0), (0, 0, 0, 255)
-] # Colours that are meant to stay the same - Transparent and Black
-
-
-class EggDecorating(commands.Cog):
- """Decorate some easter eggs!"""
-
- def __init__(self, bot: commands.Bot) -> None:
- self.bot = bot
-
- @staticmethod
- def replace_invalid(colour: str) -> Union[int, None]:
- """Attempts to match with HTML or XKCD colour names, returning the int value."""
- with suppress(KeyError):
- return int(HTML_COLOURS[colour], 16)
- with suppress(KeyError):
- return int(XKCD_COLOURS[colour], 16)
- return None
-
- @commands.command(aliases=["decorateegg"])
- async def eggdecorate(
- self, ctx: commands.Context, *colours: Union[discord.Colour, str]
- ) -> Union[Image.Image, discord.Message]:
- """
- Picks a random egg design and decorates it using the given colours.
-
- Colours are split by spaces, unless you wrap the colour name in double quotes.
- Discord colour names, HTML colour names, XKCD colour names and hex values are accepted.
- """
- if len(colours) < 2:
- return await ctx.send("You must include at least 2 colours!")
-
- invalid = []
- colours = list(colours)
- for idx, colour in enumerate(colours):
- if isinstance(colour, discord.Colour):
- continue
- value = self.replace_invalid(colour)
- if value:
- colours[idx] = discord.Colour(value)
- else:
- invalid.append(colour)
-
- if len(invalid) > 1:
- return await ctx.send(f"Sorry, I don't know these colours: {' '.join(invalid)}")
- elif len(invalid) == 1:
- return await ctx.send(f"Sorry, I don't know the colour {invalid[0]}!")
-
- async with ctx.typing():
- # Expand list to 8 colours
- colours_n = len(colours)
- if colours_n < 8:
- q, r = divmod(8, colours_n)
- colours = colours * q + colours[:r]
- num = random.randint(1, 6)
- im = Image.open(Path(f"bot/resources/easter/easter_eggs/design{num}.png"))
- data = list(im.getdata())
-
- replaceable = {x for x in data if x not in IRREPLACEABLE}
- replaceable = sorted(replaceable, key=COLOURS.index)
-
- replacing_colours = {colour: colours[i] for i, colour in enumerate(replaceable)}
- new_data = []
- for x in data:
- if x in replacing_colours:
- new_data.append((*replacing_colours[x].to_rgb(), 255))
- # Also ensures that the alpha channel has a value
- else:
- new_data.append(x)
- new_im = Image.new(im.mode, im.size)
- new_im.putdata(new_data)
-
- bufferedio = BytesIO()
- new_im.save(bufferedio, format="PNG")
-
- bufferedio.seek(0)
-
- file = discord.File(bufferedio, filename="egg.png") # Creates file to be used in embed
- embed = discord.Embed(
- title="Your Colourful Easter Egg",
- description="Here is your pretty little egg. Hope you like it!"
- )
- embed.set_image(url="attachment://egg.png")
- embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url)
-
- await ctx.send(file=file, embed=embed)
- return new_im
-
-
-def setup(bot: commands.bot) -> None:
- """Egg decorating Cog load."""
- bot.add_cog(EggDecorating(bot))
- log.info("EggDecorating cog loaded.")
diff --git a/bot/seasons/easter/egg_facts.py b/bot/seasons/easter/egg_facts.py
deleted file mode 100644
index e66e25a3..00000000
--- a/bot/seasons/easter/egg_facts.py
+++ /dev/null
@@ -1,62 +0,0 @@
-import asyncio
-import logging
-import random
-from json import load
-from pathlib import Path
-
-import discord
-from discord.ext import commands
-
-from bot.constants import Channels
-from bot.constants import Colours
-
-
-log = logging.getLogger(__name__)
-
-
-class EasterFacts(commands.Cog):
- """
- A cog contains a command that will return an easter egg fact when called.
-
- It also contains a background task which sends an easter egg fact in the event channel everyday.
- """
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.facts = self.load_json()
-
- @staticmethod
- def load_json() -> dict:
- """Load a list of easter egg facts from the resource JSON file."""
- p = Path("bot/resources/easter/easter_egg_facts.json")
- with p.open(encoding="utf8") as f:
- return load(f)
-
- async def send_egg_fact_daily(self) -> None:
- """A background task that sends an easter egg fact in the event channel everyday."""
- channel = self.bot.get_channel(Channels.seasonalbot_commands)
- while True:
- embed = self.make_embed()
- await channel.send(embed=embed)
- await asyncio.sleep(24 * 60 * 60)
-
- @commands.command(name='eggfact', aliases=['fact'])
- async def easter_facts(self, ctx: commands.Context) -> None:
- """Get easter egg facts."""
- embed = self.make_embed()
- await ctx.send(embed=embed)
-
- def make_embed(self) -> discord.Embed:
- """Makes a nice embed for the message to be sent."""
- return discord.Embed(
- colour=Colours.soft_red,
- title="Easter Egg Fact",
- description=random.choice(self.facts)
- )
-
-
-def setup(bot: commands.Bot) -> None:
- """Easter Egg facts cog load."""
- bot.loop.create_task(EasterFacts(bot).send_egg_fact_daily())
- bot.add_cog(EasterFacts(bot))
- log.info("EasterFacts cog loaded")
diff --git a/bot/seasons/easter/egghead_quiz.py b/bot/seasons/easter/egghead_quiz.py
deleted file mode 100644
index bd179fe2..00000000
--- a/bot/seasons/easter/egghead_quiz.py
+++ /dev/null
@@ -1,120 +0,0 @@
-import asyncio
-import logging
-import random
-from json import load
-from pathlib import Path
-from typing import Union
-
-import discord
-from discord.ext import commands
-
-from bot.constants import Colours
-
-log = logging.getLogger(__name__)
-
-with open(Path("bot/resources/easter/egghead_questions.json"), "r", encoding="utf8") as f:
- EGGHEAD_QUESTIONS = load(f)
-
-
-EMOJIS = [
- '\U0001f1e6', '\U0001f1e7', '\U0001f1e8', '\U0001f1e9', '\U0001f1ea',
- '\U0001f1eb', '\U0001f1ec', '\U0001f1ed', '\U0001f1ee', '\U0001f1ef',
- '\U0001f1f0', '\U0001f1f1', '\U0001f1f2', '\U0001f1f3', '\U0001f1f4',
- '\U0001f1f5', '\U0001f1f6', '\U0001f1f7', '\U0001f1f8', '\U0001f1f9',
- '\U0001f1fa', '\U0001f1fb', '\U0001f1fc', '\U0001f1fd', '\U0001f1fe',
- '\U0001f1ff'
-] # Regional Indicators A-Z (used for voting)
-
-TIMELIMIT = 30
-
-
-class EggheadQuiz(commands.Cog):
- """This cog contains the command for the Easter quiz!"""
-
- def __init__(self, bot: commands.Bot) -> None:
- self.bot = bot
- self.quiz_messages = {}
-
- @commands.command(aliases=["eggheadquiz", "easterquiz"])
- async def eggquiz(self, ctx: commands.Context) -> None:
- """
- Gives a random quiz question, waits 30 seconds and then outputs the answer.
-
- Also informs of the percentages and votes of each option
- """
- random_question = random.choice(EGGHEAD_QUESTIONS)
- question, answers = random_question["question"], random_question["answers"]
- answers = [(EMOJIS[i], a) for i, a in enumerate(answers)]
- correct = EMOJIS[random_question["correct_answer"]]
-
- valid_emojis = [emoji for emoji, _ in answers]
-
- description = f"You have {TIMELIMIT} seconds to vote.\n\n"
- description += "\n".join([f"{emoji} -> **{answer}**" for emoji, answer in answers])
-
- q_embed = discord.Embed(title=question, description=description, colour=Colours.pink)
-
- msg = await ctx.send(embed=q_embed)
- for emoji in valid_emojis:
- await msg.add_reaction(emoji)
-
- self.quiz_messages[msg.id] = valid_emojis
-
- await asyncio.sleep(TIMELIMIT)
-
- del self.quiz_messages[msg.id]
-
- msg = await ctx.channel.fetch_message(msg.id) # Refreshes message
-
- total_no = sum([len(await r.users().flatten()) for r in msg.reactions]) - len(valid_emojis) # - bot's reactions
-
- if total_no == 0:
- return await msg.delete() # To avoid ZeroDivisionError if nobody reacts
-
- results = ["**VOTES:**"]
- for emoji, _ in answers:
- num = [len(await r.users().flatten()) for r in msg.reactions if str(r.emoji) == emoji][0] - 1
- percent = round(100 * num / total_no)
- s = "" if num == 1 else "s"
- string = f"{emoji} - {num} vote{s} ({percent}%)"
- results.append(string)
-
- mentions = " ".join([
- u.mention for u in [
- await r.users().flatten() for r in msg.reactions if str(r.emoji) == correct
- ][0] if not u.bot
- ])
-
- content = f"Well done {mentions} for getting it correct!" if mentions else "Nobody got it right..."
-
- a_embed = discord.Embed(
- title=f"The correct answer was {correct}!",
- description="\n".join(results),
- colour=Colours.pink
- )
-
- await ctx.send(content, embed=a_embed)
-
- @staticmethod
- async def already_reacted(message: discord.Message, user: Union[discord.Member, discord.User]) -> bool:
- """Returns whether a given user has reacted more than once to a given message."""
- users = [u.id for reaction in [await r.users().flatten() for r in message.reactions] for u in reaction]
- return users.count(user.id) > 1 # Old reaction plus new reaction
-
- @commands.Cog.listener()
- async def on_reaction_add(self, reaction: discord.Reaction, user: Union[discord.Member, discord.User]) -> None:
- """Listener to listen specifically for reactions of quiz messages."""
- if user.bot:
- return
- if reaction.message.id not in self.quiz_messages:
- return
- if str(reaction.emoji) not in self.quiz_messages[reaction.message.id]:
- return await reaction.message.remove_reaction(reaction, user)
- if await self.already_reacted(reaction.message, user):
- return await reaction.message.remove_reaction(reaction, user)
-
-
-def setup(bot: commands.Bot) -> None:
- """Egghead Quiz Cog load."""
- bot.add_cog(EggheadQuiz(bot))
- log.info("EggheadQuiz bot loaded")
diff --git a/bot/seasons/easter/traditions.py b/bot/seasons/easter/traditions.py
deleted file mode 100644
index 9529823f..00000000
--- a/bot/seasons/easter/traditions.py
+++ /dev/null
@@ -1,31 +0,0 @@
-import json
-import logging
-import random
-from pathlib import Path
-
-from discord.ext import commands
-
-log = logging.getLogger(__name__)
-
-with open(Path("bot/resources/easter/traditions.json"), "r", encoding="utf8") as f:
- traditions = json.load(f)
-
-
-class Traditions(commands.Cog):
- """A cog which allows users to get a random easter tradition or custom from a random country."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(aliases=('eastercustoms',))
- async def easter_tradition(self, ctx: commands.Context) -> None:
- """Responds with a random tradition or custom."""
- random_country = random.choice(list(traditions))
-
- await ctx.send(f"{random_country}:\n{traditions[random_country]}")
-
-
-def setup(bot: commands.Bot) -> None:
- """Traditions Cog load."""
- bot.add_cog(Traditions(bot))
- log.info("Traditions cog loaded")
diff --git a/bot/seasons/evergreen/8bitify.py b/bot/seasons/evergreen/8bitify.py
deleted file mode 100644
index 60062fc1..00000000
--- a/bot/seasons/evergreen/8bitify.py
+++ /dev/null
@@ -1,54 +0,0 @@
-from io import BytesIO
-
-import discord
-from PIL import Image
-from discord.ext import commands
-
-
-class EightBitify(commands.Cog):
- """Make your avatar 8bit!"""
-
- def __init__(self, bot: commands.Bot) -> None:
- self.bot = bot
-
- @staticmethod
- def pixelate(image: Image) -> Image:
- """Takes an image and pixelates it."""
- return image.resize((32, 32)).resize((1024, 1024))
-
- @staticmethod
- def quantize(image: Image) -> Image:
- """Reduces colour palette to 256 colours."""
- return image.quantize(colors=32)
-
- @commands.command(name="8bitify")
- async def eightbit_command(self, ctx: commands.Context) -> None:
- """Pixelates your avatar and changes the palette to an 8bit one."""
- async with ctx.typing():
- image_bytes = await ctx.author.avatar_url.read()
- avatar = Image.open(BytesIO(image_bytes))
- avatar = avatar.convert("RGBA").resize((1024, 1024))
-
- eightbit = self.pixelate(avatar)
- eightbit = self.quantize(eightbit)
-
- bufferedio = BytesIO()
- eightbit.save(bufferedio, format="PNG")
- bufferedio.seek(0)
-
- file = discord.File(bufferedio, filename="8bitavatar.png")
-
- embed = discord.Embed(
- title="Your 8-bit avatar",
- description='Here is your avatar. I think it looks all cool and "retro"'
- )
-
- embed.set_image(url="attachment://8bitavatar.png")
- embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url)
-
- await ctx.send(file=file, embed=embed)
-
-
-def setup(bot: commands.Bot) -> None:
- """Cog load."""
- bot.add_cog(EightBitify(bot))
diff --git a/bot/seasons/evergreen/__init__.py b/bot/seasons/evergreen/__init__.py
deleted file mode 100644
index b3d0dc63..00000000
--- a/bot/seasons/evergreen/__init__.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from bot.seasons import SeasonBase
-
-
-class Evergreen(SeasonBase):
- """Evergreen Seasonal event attributes."""
-
- bot_icon = "/logos/logo_seasonal/evergreen/logo_evergreen.png"
- icon = (
- "/logos/logo_animated/heartbeat/heartbeat_512.gif",
- "/logos/logo_animated/spinner/spinner_512.gif",
- "/logos/logo_animated/tongues/tongues_512.gif",
- "/logos/logo_animated/winky/winky_512.gif",
- "/logos/logo_animated/jumper/jumper_512.gif",
- "/logos/logo_animated/apple/apple_512.gif",
- "/logos/logo_animated/blinky/blinky_512.gif",
- "/logos/logo_animated/runner/runner_512.gif",
- )
diff --git a/bot/seasons/evergreen/battleship.py b/bot/seasons/evergreen/battleship.py
deleted file mode 100644
index 9b8aaa48..00000000
--- a/bot/seasons/evergreen/battleship.py
+++ /dev/null
@@ -1,444 +0,0 @@
-import asyncio
-import logging
-import random
-import re
-import typing
-from dataclasses import dataclass
-from functools import partial
-
-import discord
-from discord.ext import commands
-
-from bot.constants import Colours
-
-log = logging.getLogger(__name__)
-
-
-@dataclass
-class Square:
- """Each square on the battleship grid - if they contain a boat and if they've been aimed at."""
-
- boat: typing.Optional[str]
- aimed: bool
-
-
-Grid = typing.List[typing.List[Square]]
-EmojiSet = typing.Dict[typing.Tuple[bool, bool], str]
-
-
-@dataclass
-class Player:
- """Each player in the game - their messages for the boards and their current grid."""
-
- user: discord.Member
- board: discord.Message
- opponent_board: discord.Message
- grid: Grid
-
-
-# The name of the ship and its size
-SHIPS = {
- "Carrier": 5,
- "Battleship": 4,
- "Cruiser": 3,
- "Submarine": 3,
- "Destroyer": 2,
-}
-
-
-# For these two variables, the first boolean is whether the square is a ship (True) or not (False).
-# The second boolean is whether the player has aimed for that square (True) or not (False)
-
-# This is for the player's own board which shows the location of their own ships.
-SHIP_EMOJIS = {
- (True, True): ":fire:",
- (True, False): ":ship:",
- (False, True): ":anger:",
- (False, False): ":ocean:",
-}
-
-# This is for the opposing player's board which only shows aimed locations.
-HIDDEN_EMOJIS = {
- (True, True): ":red_circle:",
- (True, False): ":black_circle:",
- (False, True): ":white_circle:",
- (False, False): ":black_circle:",
-}
-
-# For the top row of the board
-LETTERS = (
- ":stop_button::regional_indicator_a::regional_indicator_b::regional_indicator_c::regional_indicator_d:"
- ":regional_indicator_e::regional_indicator_f::regional_indicator_g::regional_indicator_h:"
- ":regional_indicator_i::regional_indicator_j:"
-)
-
-# For the first column of the board
-NUMBERS = [
- ":one:",
- ":two:",
- ":three:",
- ":four:",
- ":five:",
- ":six:",
- ":seven:",
- ":eight:",
- ":nine:",
- ":keycap_ten:",
-]
-
-CROSS_EMOJI = "\u274e"
-HAND_RAISED_EMOJI = "\U0001f64b"
-
-
-class Game:
- """A Battleship Game."""
-
- def __init__(
- self,
- bot: commands.Bot,
- channel: discord.TextChannel,
- player1: discord.Member,
- player2: discord.Member
- ) -> None:
-
- self.bot = bot
- self.public_channel = channel
-
- self.p1 = Player(player1, None, None, self.generate_grid())
- self.p2 = Player(player2, None, None, self.generate_grid())
-
- self.gameover: bool = False
-
- self.turn: typing.Optional[discord.Member] = None
- self.next: typing.Optional[discord.Member] = None
-
- self.match: typing.Optional[typing.Match] = None
- self.surrender: bool = False
-
- self.setup_grids()
-
- @staticmethod
- def generate_grid() -> Grid:
- """Generates a grid by instantiating the Squares."""
- return [[Square(None, False) for _ in range(10)] for _ in range(10)]
-
- @staticmethod
- def format_grid(player: Player, emojiset: EmojiSet) -> str:
- """
- Gets and formats the grid as a list into a string to be output to the DM.
-
- Also adds the Letter and Number indexes.
- """
- grid = [
- [emojiset[bool(square.boat), square.aimed] for square in row]
- for row in player.grid
- ]
-
- rows = ["".join([number] + row) for number, row in zip(NUMBERS, grid)]
- return "\n".join([LETTERS] + rows)
-
- @staticmethod
- def get_square(grid: Grid, square: str) -> Square:
- """Grabs a square from a grid with an inputted key."""
- index = ord(square[0]) - ord("A")
- number = int(square[1:])
-
- return grid[number-1][index] # -1 since lists are indexed from 0
-
- async def game_over(
- self,
- *,
- winner: discord.Member,
- loser: discord.Member
- ) -> None:
- """Removes games from list of current games and announces to public chat."""
- await self.public_channel.send(f"Game Over! {winner.mention} won against {loser.mention}")
-
- for player in (self.p1, self.p2):
- grid = self.format_grid(player, SHIP_EMOJIS)
- await self.public_channel.send(f"{player.user}'s Board:\n{grid}")
-
- @staticmethod
- def check_sink(grid: Grid, boat: str) -> bool:
- """Checks if all squares containing a given boat have sunk."""
- return all(square.aimed for row in grid for square in row if square.boat == boat)
-
- @staticmethod
- def check_gameover(grid: Grid) -> bool:
- """Checks if all boats have been sunk."""
- return all(square.aimed for row in grid for square in row if square.boat)
-
- def setup_grids(self) -> None:
- """Places the boats on the grids to initialise the game."""
- for player in (self.p1, self.p2):
- for name, size in SHIPS.items():
- while True: # Repeats if about to overwrite another boat
- ship_collision = False
- coords = []
-
- coord1 = random.randint(0, 9)
- coord2 = random.randint(0, 10 - size)
-
- if random.choice((True, False)): # Vertical or Horizontal
- x, y = coord1, coord2
- xincr, yincr = 0, 1
- else:
- x, y = coord2, coord1
- xincr, yincr = 1, 0
-
- for i in range(size):
- new_x = x + (xincr * i)
- new_y = y + (yincr * i)
- if player.grid[new_x][new_y].boat: # Check if there's already a boat
- ship_collision = True
- break
- coords.append((new_x, new_y))
- if not ship_collision: # If not overwriting any other boat spaces, break loop
- break
-
- for x, y in coords:
- player.grid[x][y].boat = name
-
- async def print_grids(self) -> None:
- """Prints grids to the DM channels."""
- # Convert squares into Emoji
-
- boards = [
- self.format_grid(player, emojiset)
- for emojiset in (HIDDEN_EMOJIS, SHIP_EMOJIS)
- for player in (self.p1, self.p2)
- ]
-
- locations = (
- (self.p2, "opponent_board"), (self.p1, "opponent_board"),
- (self.p1, "board"), (self.p2, "board")
- )
-
- for board, location in zip(boards, locations):
- player, attr = location
- if getattr(player, attr):
- await getattr(player, attr).edit(content=board)
- else:
- setattr(player, attr, await player.user.send(board))
-
- def predicate(self, message: discord.Message) -> bool:
- """Predicate checking the message typed for each turn."""
- if message.author == self.turn.user and message.channel == self.turn.user.dm_channel:
- if message.content.lower() == "surrender":
- self.surrender = True
- return True
- self.match = re.match("([A-J]|[a-j]) ?((10)|[1-9])", message.content.strip())
- if not self.match:
- self.bot.loop.create_task(message.add_reaction(CROSS_EMOJI))
- return bool(self.match)
-
- async def take_turn(self) -> typing.Optional[Square]:
- """Lets the player who's turn it is choose a square."""
- square = None
- turn_message = await self.turn.user.send(
- "It's your turn! Type the square you want to fire at. Format it like this: A1\n"
- "Type `surrender` to give up"
- )
- await self.next.user.send("Their turn", delete_after=3.0)
- while True:
- try:
- await self.bot.wait_for("message", check=self.predicate, timeout=60.0)
- except asyncio.TimeoutError:
- await self.turn.user.send("You took too long. Game over!")
- await self.next.user.send(f"{self.turn.user} took too long. Game over!")
- await self.public_channel.send(
- f"Game over! {self.turn.user.mention} timed out so {self.next.user.mention} wins!"
- )
- self.gameover = True
- break
- else:
- if self.surrender:
- await self.next.user.send(f"{self.turn.user} surrendered. Game over!")
- await self.public_channel.send(
- f"Game over! {self.turn.user.mention} surrendered to {self.next.user.mention}!"
- )
- self.gameover = True
- break
- square = self.get_square(self.next.grid, self.match.string)
- if square.aimed:
- await self.turn.user.send("You've already aimed at this square!", delete_after=3.0)
- else:
- break
- await turn_message.delete()
- return square
-
- async def hit(self, square: Square, alert_messages: typing.List[discord.Message]) -> None:
- """Occurs when a player successfully aims for a ship."""
- await self.turn.user.send("Hit!", delete_after=3.0)
- alert_messages.append(await self.next.user.send("Hit!"))
- if self.check_sink(self.next.grid, square.boat):
- await self.turn.user.send(f"You've sunk their {square.boat} ship!", delete_after=3.0)
- alert_messages.append(await self.next.user.send(f"Oh no! Your {square.boat} ship sunk!"))
- if self.check_gameover(self.next.grid):
- await self.turn.user.send("You win!")
- await self.next.user.send("You lose!")
- self.gameover = True
- await self.game_over(winner=self.turn.user, loser=self.next.user)
-
- async def start_game(self) -> None:
- """Begins the game."""
- await self.p1.user.send(f"You're playing battleship with {self.p2.user}.")
- await self.p2.user.send(f"You're playing battleship with {self.p1.user}.")
-
- alert_messages = []
-
- self.turn = self.p1
- self.next = self.p2
-
- while True:
- await self.print_grids()
-
- if self.gameover:
- return
-
- square = await self.take_turn()
- if not square:
- return
- square.aimed = True
-
- for message in alert_messages:
- await message.delete()
-
- alert_messages = []
- alert_messages.append(await self.next.user.send(f"{self.turn.user} aimed at {self.match.string}!"))
-
- if square.boat:
- await self.hit(square, alert_messages)
- if self.gameover:
- return
- else:
- await self.turn.user.send("Miss!", delete_after=3.0)
- alert_messages.append(await self.next.user.send("Miss!"))
-
- self.turn, self.next = self.next, self.turn
-
-
-class Battleship(commands.Cog):
- """Play the classic game Battleship!"""
-
- def __init__(self, bot: commands.Bot) -> None:
- self.bot = bot
- self.games: typing.List[Game] = []
- self.waiting: typing.List[discord.Member] = []
-
- def predicate(
- self,
- ctx: commands.Context,
- announcement: discord.Message,
- reaction: discord.Reaction,
- user: discord.Member
- ) -> bool:
- """Predicate checking the criteria for the announcement message."""
- if self.already_playing(ctx.author): # If they've joined a game since requesting a player 2
- return True # Is dealt with later on
- if (
- user.id not in (ctx.me.id, ctx.author.id)
- and str(reaction.emoji) == HAND_RAISED_EMOJI
- and reaction.message.id == announcement.id
- ):
- if self.already_playing(user):
- self.bot.loop.create_task(ctx.send(f"{user.mention} You're already playing a game!"))
- self.bot.loop.create_task(announcement.remove_reaction(reaction, user))
- return False
-
- if user in self.waiting:
- self.bot.loop.create_task(ctx.send(
- f"{user.mention} Please cancel your game first before joining another one."
- ))
- self.bot.loop.create_task(announcement.remove_reaction(reaction, user))
- return False
-
- return True
-
- if (
- user.id == ctx.author.id
- and str(reaction.emoji) == CROSS_EMOJI
- and reaction.message.id == announcement.id
- ):
- return True
- return False
-
- def already_playing(self, player: discord.Member) -> bool:
- """Check if someone is already in a game."""
- return any(player in (game.p1.user, game.p2.user) for game in self.games)
-
- @commands.group(invoke_without_command=True)
- @commands.guild_only()
- async def battleship(self, ctx: commands.Context) -> None:
- """
- Play a game of Battleship with someone else!
-
- This will set up a message waiting for someone else to react and play along.
- The game takes place entirely in DMs.
- Make sure you have your DMs open so that the bot can message you.
- """
- if self.already_playing(ctx.author):
- return await ctx.send("You're already playing a game!")
-
- if ctx.author in self.waiting:
- return await ctx.send("You've already sent out a request for a player 2")
-
- announcement = await ctx.send(
- "**Battleship**: A new game is about to start!\n"
- f"Press {HAND_RAISED_EMOJI} to play against {ctx.author.mention}!\n"
- f"(Cancel the game with {CROSS_EMOJI}.)"
- )
- self.waiting.append(ctx.author)
- await announcement.add_reaction(HAND_RAISED_EMOJI)
- await announcement.add_reaction(CROSS_EMOJI)
-
- try:
- reaction, user = await self.bot.wait_for(
- "reaction_add",
- check=partial(self.predicate, ctx, announcement),
- timeout=60.0
- )
- except asyncio.TimeoutError:
- self.waiting.remove(ctx.author)
- await announcement.delete()
- return await ctx.send(f"{ctx.author.mention} Seems like there's no one here to play...")
-
- if str(reaction.emoji) == CROSS_EMOJI:
- self.waiting.remove(ctx.author)
- await announcement.delete()
- return await ctx.send(f"{ctx.author.mention} Game cancelled.")
-
- await announcement.delete()
- self.waiting.remove(ctx.author)
- if self.already_playing(ctx.author):
- return
- try:
- game = Game(self.bot, ctx.channel, ctx.author, user)
- self.games.append(game)
- await game.start_game()
- self.games.remove(game)
- except discord.Forbidden:
- await ctx.send(
- f"{ctx.author.mention} {user.mention} "
- "Game failed. This is likely due to you not having your DMs open. Check and try again."
- )
- self.games.remove(game)
- except Exception:
- # End the game in the event of an unforseen error so the players aren't stuck in a game
- await ctx.send(f"{ctx.author.mention} {user.mention} An error occurred. Game failed")
- self.games.remove(game)
- raise
-
- @battleship.command(name="ships", aliases=["boats"])
- async def battleship_ships(self, ctx: commands.Context) -> None:
- """Lists the ships that are found on the battleship grid."""
- embed = discord.Embed(colour=Colours.blue)
- embed.add_field(name="Name", value="\n".join(SHIPS))
- embed.add_field(name="Size", value="\n".join(str(size) for size in SHIPS.values()))
- await ctx.send(embed=embed)
-
-
-def setup(bot: commands.Bot) -> None:
- """Cog load."""
- bot.add_cog(Battleship(bot))
- log.info("Battleship cog loaded")
diff --git a/bot/seasons/evergreen/bookmark.py b/bot/seasons/evergreen/bookmark.py
deleted file mode 100644
index bd7d5c11..00000000
--- a/bot/seasons/evergreen/bookmark.py
+++ /dev/null
@@ -1,65 +0,0 @@
-import logging
-import random
-
-import discord
-from discord.ext import commands
-
-from bot.constants import Colours, ERROR_REPLIES, Emojis, bookmark_icon_url
-
-log = logging.getLogger(__name__)
-
-
-class Bookmark(commands.Cog):
- """Creates personal bookmarks by relaying a message link to the user's DMs."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(name="bookmark", aliases=("bm", "pin"))
- async def bookmark(
- self,
- ctx: commands.Context,
- target_message: discord.Message,
- *,
- title: str = "Bookmark"
- ) -> None:
- """Send the author a link to `target_message` via DMs."""
- # Prevent users from bookmarking a message in a channel they don't have access to
- permissions = ctx.author.permissions_in(target_message.channel)
- if not permissions.read_messages:
- log.info(f"{ctx.author} tried to bookmark a message in #{target_message.channel} but has no permissions")
- embed = discord.Embed(
- title=random.choice(ERROR_REPLIES),
- color=Colours.soft_red,
- description="You don't have permission to view this channel."
- )
- await ctx.send(embed=embed)
- return
-
- embed = discord.Embed(
- title=title,
- colour=Colours.soft_green,
- description=target_message.content
- )
- embed.add_field(name="Wanna give it a visit?", value=f"[Visit original message]({target_message.jump_url})")
- embed.set_author(name=target_message.author, icon_url=target_message.author.avatar_url)
- embed.set_thumbnail(url=bookmark_icon_url)
-
- try:
- await ctx.author.send(embed=embed)
- except discord.Forbidden:
- error_embed = discord.Embed(
- title=random.choice(ERROR_REPLIES),
- description=f"{ctx.author.mention}, please enable your DMs to receive the bookmark",
- colour=Colours.soft_red
- )
- await ctx.send(embed=error_embed)
- else:
- log.info(f"{ctx.author} bookmarked {target_message.jump_url} with title '{title}'")
- await ctx.message.add_reaction(Emojis.envelope)
-
-
-def setup(bot: commands.Bot) -> None:
- """Load the Bookmark cog."""
- bot.add_cog(Bookmark(bot))
- log.info("Bookmark cog loaded")
diff --git a/bot/seasons/evergreen/error_handler.py b/bot/seasons/evergreen/error_handler.py
deleted file mode 100644
index 2753a6df..00000000
--- a/bot/seasons/evergreen/error_handler.py
+++ /dev/null
@@ -1,125 +0,0 @@
-import logging
-import math
-import random
-from typing import Iterable, Union
-
-from discord import Embed, Message
-from discord.ext import commands
-from sentry_sdk import push_scope
-
-from bot.constants import Colours, ERROR_REPLIES, NEGATIVE_REPLIES
-from bot.decorators import InChannelCheckFailure
-
-log = logging.getLogger(__name__)
-
-
-class CommandErrorHandler(commands.Cog):
- """A error handler for the PythonDiscord server."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @staticmethod
- def revert_cooldown_counter(command: commands.Command, message: Message) -> None:
- """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.")
-
- @staticmethod
- def error_embed(message: str, title: Union[Iterable, str] = ERROR_REPLIES) -> Embed:
- """Build a basic embed with red colour and either a random error title or a title provided."""
- embed = Embed(colour=Colours.soft_red)
- if isinstance(title, str):
- embed.title = title
- else:
- embed.title = random.choice(title)
- embed.description = message
- return embed
-
- @commands.Cog.listener()
- async def on_command_error(self, ctx: commands.Context, error: commands.CommandError) -> None:
- """Activates when a command opens an error."""
- if hasattr(ctx.command, 'on_error'):
- logging.debug("A command error occured but the command had it's own error handler.")
- return
-
- error = getattr(error, 'original', error)
- logging.debug(
- f"Error Encountered: {type(error).__name__} - {str(error)}, "
- f"Command: {ctx.command}, "
- f"Author: {ctx.author}, "
- f"Channel: {ctx.channel}"
- )
-
- if isinstance(error, commands.CommandNotFound):
- return
-
- if isinstance(error, InChannelCheckFailure):
- await ctx.send(embed=self.error_embed(str(error), NEGATIVE_REPLIES), delete_after=7.5)
- return
-
- if isinstance(error, commands.UserInputError):
- self.revert_cooldown_counter(ctx.command, ctx.message)
- embed = self.error_embed(
- f"Your input was invalid: {error}\n\nUsage:\n```{ctx.prefix}{ctx.command} {ctx.command.signature}```"
- )
- await ctx.send(embed=embed)
- return
-
- if isinstance(error, commands.CommandOnCooldown):
- mins, secs = divmod(math.ceil(error.retry_after), 60)
- embed = self.error_embed(
- f"This command is on cooldown:\nPlease retry in {mins} minutes {secs} seconds.",
- NEGATIVE_REPLIES
- )
- await ctx.send(embed=embed, delete_after=7.5)
- return
-
- if isinstance(error, commands.DisabledCommand):
- await ctx.send(embed=self.error_embed("This command has been disabled.", NEGATIVE_REPLIES))
- return
-
- if isinstance(error, commands.NoPrivateMessage):
- await ctx.send(embed=self.error_embed("This command can only be used in the server.", NEGATIVE_REPLIES))
- return
-
- if isinstance(error, commands.BadArgument):
- self.revert_cooldown_counter(ctx.command, ctx.message)
- embed = self.error_embed(
- "The argument you provided was invalid: "
- f"{error}\n\nUsage:\n```{ctx.prefix}{ctx.command} {ctx.command.signature}```"
- )
- await ctx.send(embed=embed)
- return
-
- if isinstance(error, commands.CheckFailure):
- await ctx.send(embed=self.error_embed("You are not authorized to use this command.", NEGATIVE_REPLIES))
- return
-
- with push_scope() as scope:
- scope.user = {
- "id": ctx.author.id,
- "username": str(ctx.author)
- }
-
- scope.set_tag("command", ctx.command.qualified_name)
- scope.set_tag("message_id", ctx.message.id)
- scope.set_tag("channel_id", ctx.channel.id)
-
- scope.set_extra("full_message", ctx.message.content)
-
- if ctx.guild is not None:
- scope.set_extra(
- "jump_to",
- f"https://discordapp.com/channels/{ctx.guild.id}/{ctx.channel.id}/{ctx.message.id}"
- )
-
- log.exception(f"Unhandled command error: {str(error)}", exc_info=error)
-
-
-def setup(bot: commands.Bot) -> None:
- """Error handler Cog load."""
- bot.add_cog(CommandErrorHandler(bot))
- log.info("CommandErrorHandler cog loaded")
diff --git a/bot/seasons/evergreen/fun.py b/bot/seasons/evergreen/fun.py
deleted file mode 100644
index 889ae079..00000000
--- a/bot/seasons/evergreen/fun.py
+++ /dev/null
@@ -1,148 +0,0 @@
-import functools
-import logging
-import random
-from typing import Callable, Tuple, Union
-
-from discord import Embed, Message
-from discord.ext import commands
-from discord.ext.commands import Bot, Cog, Context, MessageConverter
-
-from bot import utils
-from bot.constants import Emojis
-
-log = logging.getLogger(__name__)
-
-UWU_WORDS = {
- "fi": "fwi",
- "l": "w",
- "r": "w",
- "some": "sum",
- "th": "d",
- "thing": "fing",
- "tho": "fo",
- "you're": "yuw'we",
- "your": "yur",
- "you": "yuw",
-}
-
-
-class Fun(Cog):
- """A collection of general commands for fun."""
-
- def __init__(self, bot: Bot) -> None:
- self.bot = bot
-
- @commands.command()
- async def roll(self, ctx: Context, num_rolls: int = 1) -> None:
- """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)
-
- @commands.command(name="uwu", aliases=("uwuwize", "uwuify",))
- async def uwu_command(self, ctx: Context, *, text: str) -> None:
- """
- Converts a given `text` into it's uwu equivalent.
-
- Also accepts a valid discord Message ID or link.
- """
- conversion_func = functools.partial(
- utils.replace_many, replacements=UWU_WORDS, ignore_case=True, match_case=True
- )
- text, embed = await Fun._get_text_and_embed(ctx, text)
- # Convert embed if it exists
- if embed is not None:
- embed = Fun._convert_embed(conversion_func, embed)
- converted_text = conversion_func(text)
- # Don't put >>> if only embed present
- if converted_text:
- converted_text = f">>> {converted_text.lstrip('> ')}"
- await ctx.send(content=converted_text, embed=embed)
-
- @commands.command(name="randomcase", aliases=("rcase", "randomcaps", "rcaps",))
- async def randomcase_command(self, ctx: Context, *, text: str) -> None:
- """
- Randomly converts the casing of a given `text`.
-
- Also accepts a valid discord Message ID or link.
- """
- def conversion_func(text: str) -> str:
- """Randomly converts the casing of a given string."""
- return "".join(
- char.upper() if round(random.random()) else char.lower() for char in text
- )
- text, embed = await Fun._get_text_and_embed(ctx, text)
- # Convert embed if it exists
- if embed is not None:
- embed = Fun._convert_embed(conversion_func, embed)
- converted_text = conversion_func(text)
- # Don't put >>> if only embed present
- if converted_text:
- converted_text = f">>> {converted_text.lstrip('> ')}"
- await ctx.send(content=converted_text, embed=embed)
-
- @staticmethod
- async def _get_text_and_embed(ctx: Context, text: str) -> Tuple[str, Union[Embed, None]]:
- """
- Attempts to extract the text and embed from a possible link to a discord Message.
-
- Returns a tuple of:
- str: If `text` is a valid discord Message, the contents of the message, else `text`.
- Union[Embed, None]: The embed if found in the valid Message, else None
- """
- embed = None
- message = await Fun._get_discord_message(ctx, text)
- if isinstance(message, Message):
- text = message.content
- # Take first embed because we can't send multiple embeds
- if message.embeds:
- embed = message.embeds[0]
- return (text, embed)
-
- @staticmethod
- async def _get_discord_message(ctx: Context, text: str) -> Union[Message, str]:
- """
- Attempts to convert a given `text` to a discord Message object and return it.
-
- Conversion will succeed if given a discord Message ID or link.
- Returns `text` if the conversion fails.
- """
- try:
- text = await MessageConverter().convert(ctx, text)
- except commands.BadArgument:
- log.debug(f"Input '{text:.20}...' is not a valid Discord Message")
- return text
-
- @staticmethod
- def _convert_embed(func: Callable[[str, ], str], embed: Embed) -> Embed:
- """
- Converts the text in an embed using a given conversion function, then return the embed.
-
- Only modifies the following fields: title, description, footer, fields
- """
- embed_dict = embed.to_dict()
-
- embed_dict["title"] = func(embed_dict.get("title", ""))
- embed_dict["description"] = func(embed_dict.get("description", ""))
-
- if "footer" in embed_dict:
- embed_dict["footer"]["text"] = func(embed_dict["footer"].get("text", ""))
-
- if "fields" in embed_dict:
- for field in embed_dict["fields"]:
- field["name"] = func(field.get("name", ""))
- field["value"] = func(field.get("value", ""))
-
- return Embed.from_dict(embed_dict)
-
-
-def setup(bot: commands.Bot) -> None:
- """Fun Cog load."""
- bot.add_cog(Fun(bot))
- log.info("Fun cog loaded")
diff --git a/bot/seasons/evergreen/game.py b/bot/seasons/evergreen/game.py
deleted file mode 100644
index e6700937..00000000
--- a/bot/seasons/evergreen/game.py
+++ /dev/null
@@ -1,395 +0,0 @@
-import difflib
-import logging
-import random
-from datetime import datetime as dt
-from enum import IntEnum
-from typing import Any, Dict, List, Optional, Tuple
-
-from aiohttp import ClientSession
-from discord import Embed
-from discord.ext import tasks
-from discord.ext.commands import Cog, Context, group
-
-from bot.bot import SeasonalBot
-from bot.constants import STAFF_ROLES, Tokens
-from bot.decorators import with_role
-from bot.pagination import ImagePaginator, LinePaginator
-
-# Base URL of IGDB API
-BASE_URL = "https://api-v3.igdb.com"
-
-HEADERS = {
- "user-key": Tokens.igdb,
- "Accept": "application/json"
-}
-
-logger = logging.getLogger(__name__)
-
-# ---------
-# TEMPLATES
-# ---------
-
-# Body templates
-# Request body template for get_games_list
-GAMES_LIST_BODY = (
- "fields cover.image_id, first_release_date, total_rating, name, storyline, url, platforms.name, status,"
- "involved_companies.company.name, summary, age_ratings.category, age_ratings.rating, total_rating_count;"
- "{sort} {limit} {offset} {genre} {additional}"
-)
-
-# Request body template for get_companies_list
-COMPANIES_LIST_BODY = (
- "fields name, url, start_date, logo.image_id, developed.name, published.name, description;"
- "offset {offset}; limit {limit};"
-)
-
-# Request body template for games search
-SEARCH_BODY = 'fields name, url, storyline, total_rating, total_rating_count; limit 50; search "{term}";'
-
-# Pages templates
-# Game embed layout
-GAME_PAGE = (
- "**[{name}]({url})**\n"
- "{description}"
- "**Release Date:** {release_date}\n"
- "**Rating:** {rating}/100 :star: (based on {rating_count} ratings)\n"
- "**Platforms:** {platforms}\n"
- "**Status:** {status}\n"
- "**Age Ratings:** {age_ratings}\n"
- "**Made by:** {made_by}\n\n"
- "{storyline}"
-)
-
-# .games company command page layout
-COMPANY_PAGE = (
- "**[{name}]({url})**\n"
- "{description}"
- "**Founded:** {founded}\n"
- "**Developed:** {developed}\n"
- "**Published:** {published}"
-)
-
-# For .games search command line layout
-GAME_SEARCH_LINE = (
- "**[{name}]({url})**\n"
- "{rating}/100 :star: (based on {rating_count} ratings)\n"
-)
-
-# URL templates
-COVER_URL = "https://images.igdb.com/igdb/image/upload/t_cover_big/{image_id}.jpg"
-LOGO_URL = "https://images.igdb.com/igdb/image/upload/t_logo_med/{image_id}.png"
-
-# Create aliases for complex genre names
-ALIASES = {
- "Role-playing (rpg)": ["Role playing", "Rpg"],
- "Turn-based strategy (tbs)": ["Turn based strategy", "Tbs"],
- "Real time strategy (rts)": ["Real time strategy", "Rts"],
- "Hack and slash/beat 'em up": ["Hack and slash"]
-}
-
-
-class GameStatus(IntEnum):
- """Game statuses in IGDB API."""
-
- Released = 0
- Alpha = 2
- Beta = 3
- Early = 4
- Offline = 5
- Cancelled = 6
- Rumored = 7
-
-
-class AgeRatingCategories(IntEnum):
- """IGDB API Age Rating categories IDs."""
-
- ESRB = 1
- PEGI = 2
-
-
-class AgeRatings(IntEnum):
- """PEGI/ESRB ratings IGDB API IDs."""
-
- Three = 1
- Seven = 2
- Twelve = 3
- Sixteen = 4
- Eighteen = 5
- RP = 6
- EC = 7
- E = 8
- E10 = 9
- T = 10
- M = 11
- AO = 12
-
-
-class Games(Cog):
- """Games Cog contains commands that collect data from IGDB."""
-
- def __init__(self, bot: SeasonalBot):
- self.bot = bot
- self.http_session: ClientSession = bot.http_session
-
- self.genres: Dict[str, int] = {}
-
- self.refresh_genres_task.start()
-
- @tasks.loop(hours=1.0)
- async def refresh_genres_task(self) -> None:
- """Refresh genres in every hour."""
- try:
- await self._get_genres()
- except Exception as e:
- logger.warning(f"There was error while refreshing genres: {e}")
- return
- logger.info("Successfully refreshed genres.")
-
- def cog_unload(self) -> None:
- """Cancel genres refreshing start when unloading Cog."""
- self.refresh_genres_task.cancel()
- logger.info("Successfully stopped Genres Refreshing task.")
-
- async def _get_genres(self) -> None:
- """Create genres variable for games command."""
- body = "fields name; limit 100;"
- async with self.http_session.get(f"{BASE_URL}/genres", data=body, headers=HEADERS) as resp:
- result = await resp.json()
-
- genres = {genre["name"].capitalize(): genre["id"] for genre in result}
-
- # Replace complex names with names from ALIASES
- for genre_name, genre in genres.items():
- if genre_name in ALIASES:
- for alias in ALIASES[genre_name]:
- self.genres[alias] = genre
- else:
- self.genres[genre_name] = genre
-
- @group(name="games", aliases=["game"], invoke_without_command=True)
- async def games(self, ctx: Context, amount: Optional[int] = 5, *, genre: Optional[str] = None) -> None:
- """
- Get random game(s) by genre from IGDB. Use .games genres command to get all available genres.
-
- Also support amount parameter, what max is 25 and min 1, default 5. Supported formats:
- - .games <genre>
- - .games <amount> <genre>
- """
- # When user didn't specified genre, send help message
- if genre is None:
- await ctx.send_help("games")
- return
-
- # Capitalize genre for check
- genre = "".join(genre).capitalize()
-
- # Check for amounts, max is 25 and min 1
- if not 1 <= amount <= 25:
- await ctx.send("Your provided amount is out of range. Our minimum is 1 and maximum 25.")
- return
-
- # Get games listing, if genre don't exist, show error message with possibilities.
- # Offset must be random, due otherwise we will get always same result (offset show in which position should
- # API start returning result)
- try:
- games = await self.get_games_list(amount, self.genres[genre], offset=random.randint(0, 150))
- except KeyError:
- possibilities = "`, `".join(difflib.get_close_matches(genre, self.genres))
- await ctx.send(f"Invalid genre `{genre}`. {f'Maybe you meant `{possibilities}`?' if possibilities else ''}")
- return
-
- # Create pages and paginate
- pages = [await self.create_page(game) for game in games]
-
- await ImagePaginator.paginate(pages, ctx, Embed(title=f"Random {genre.title()} Games"))
-
- @games.command(name="top", aliases=["t"])
- async def top(self, ctx: Context, amount: int = 10) -> None:
- """
- Get current Top games in IGDB.
-
- Support amount parameter. Max is 25, min is 1.
- """
- if not 1 <= amount <= 25:
- await ctx.send("Your provided amount is out of range. Our minimum is 1 and maximum 25.")
- return
-
- games = await self.get_games_list(amount, sort="total_rating desc",
- additional_body="where total_rating >= 90; sort total_rating_count desc;")
-
- pages = [await self.create_page(game) for game in games]
- await ImagePaginator.paginate(pages, ctx, Embed(title=f"Top {amount} Games"))
-
- @games.command(name="genres", aliases=["genre", "g"])
- async def genres(self, ctx: Context) -> None:
- """Get all available genres."""
- await ctx.send(f"Currently available genres: {', '.join(f'`{genre}`' for genre in self.genres)}")
-
- @games.command(name="search", aliases=["s"])
- async def search(self, ctx: Context, *, search_term: str) -> None:
- """Find games by name."""
- lines = await self.search_games(search_term)
-
- await LinePaginator.paginate(lines, ctx, Embed(title=f"Game Search Results: {search_term}"), empty=False)
-
- @games.command(name="company", aliases=["companies"])
- async def company(self, ctx: Context, amount: int = 5) -> None:
- """
- Get random Game Companies companies from IGDB API.
-
- Support amount parameter. Max is 25, min is 1.
- """
- if not 1 <= amount <= 25:
- await ctx.send("Your provided amount is out of range. Our minimum is 1 and maximum 25.")
- return
-
- # Get companies listing. Provide limit for limiting how much companies will be returned. Get random offset to
- # get (almost) every time different companies (offset show in which position should API start returning result)
- companies = await self.get_companies_list(limit=amount, offset=random.randint(0, 150))
- pages = [await self.create_company_page(co) for co in companies]
-
- await ImagePaginator.paginate(pages, ctx, Embed(title="Random Game Companies"))
-
- @with_role(*STAFF_ROLES)
- @games.command(name="refresh", aliases=["r"])
- async def refresh_genres_command(self, ctx: Context) -> None:
- """Refresh .games command genres."""
- try:
- await self._get_genres()
- except Exception as e:
- await ctx.send(f"There was error while refreshing genres: `{e}`")
- return
- await ctx.send("Successfully refreshed genres.")
-
- async def get_games_list(self,
- amount: int,
- genre: Optional[str] = None,
- sort: Optional[str] = None,
- additional_body: str = "",
- offset: int = 0
- ) -> List[Dict[str, Any]]:
- """
- Get list of games from IGDB API by parameters that is provided.
-
- Amount param show how much games this get, genre is genre ID and at least one genre in game must this when
- provided. Sort is sorting by specific field and direction, ex. total_rating desc/asc (total_rating is field,
- desc/asc is direction). Additional_body is field where you can pass extra search parameters. Offset show start
- position in API.
- """
- # Create body of IGDB API request, define fields, sorting, offset, limit and genre
- params = {
- "sort": f"sort {sort};" if sort else "",
- "limit": f"limit {amount};",
- "offset": f"offset {offset};" if offset else "",
- "genre": f"where genres = ({genre});" if genre else "",
- "additional": additional_body
- }
- body = GAMES_LIST_BODY.format(**params)
-
- # Do request to IGDB API, create headers, URL, define body, return result
- async with self.http_session.get(url=f"{BASE_URL}/games", data=body, headers=HEADERS) as resp:
- return await resp.json()
-
- async def create_page(self, data: Dict[str, Any]) -> Tuple[str, str]:
- """Create content of Game Page."""
- # Create cover image URL from template
- url = COVER_URL.format(**{"image_id": data["cover"]["image_id"] if "cover" in data else ""})
-
- # Get release date separately with checking
- release_date = dt.utcfromtimestamp(data["first_release_date"]).date() if "first_release_date" in data else "?"
-
- # Create Age Ratings value
- rating = ", ".join(f"{AgeRatingCategories(age['category']).name} {AgeRatings(age['rating']).name}"
- for age in data["age_ratings"]) if "age_ratings" in data else "?"
-
- companies = [c["company"]["name"] for c in data["involved_companies"]] if "involved_companies" in data else "?"
-
- # Create formatting for template page
- formatting = {
- "name": data["name"],
- "url": data["url"],
- "description": f"{data['summary']}\n\n" if "summary" in data else "\n",
- "release_date": release_date,
- "rating": round(data["total_rating"] if "total_rating" in data else 0, 2),
- "rating_count": data["total_rating_count"] if "total_rating_count" in data else "?",
- "platforms": ", ".join(platform["name"] for platform in data["platforms"]) if "platforms" in data else "?",
- "status": GameStatus(data["status"]).name if "status" in data else "?",
- "age_ratings": rating,
- "made_by": ", ".join(companies),
- "storyline": data["storyline"] if "storyline" in data else ""
- }
- page = GAME_PAGE.format(**formatting)
-
- return page, url
-
- async def search_games(self, search_term: str) -> List[str]:
- """Search game from IGDB API by string, return listing of pages."""
- lines = []
-
- # Define request body of IGDB API request and do request
- body = SEARCH_BODY.format(**{"term": search_term})
-
- async with self.http_session.get(url=f"{BASE_URL}/games", data=body, headers=HEADERS) as resp:
- data = await resp.json()
-
- # Loop over games, format them to good format, make line and append this to total lines
- for game in data:
- formatting = {
- "name": game["name"],
- "url": game["url"],
- "rating": round(game["total_rating"] if "total_rating" in game else 0, 2),
- "rating_count": game["total_rating_count"] if "total_rating" in game else "?"
- }
- line = GAME_SEARCH_LINE.format(**formatting)
- lines.append(line)
-
- return lines
-
- async def get_companies_list(self, limit: int, offset: int = 0) -> List[Dict[str, Any]]:
- """
- Get random Game Companies from IGDB API.
-
- Limit is parameter, that show how much movies this should return, offset show in which position should API start
- returning results.
- """
- # Create request body from template
- body = COMPANIES_LIST_BODY.format(**{
- "limit": limit,
- "offset": offset
- })
-
- async with self.http_session.get(url=f"{BASE_URL}/companies", data=body, headers=HEADERS) as resp:
- return await resp.json()
-
- async def create_company_page(self, data: Dict[str, Any]) -> Tuple[str, str]:
- """Create good formatted Game Company page."""
- # Generate URL of company logo
- url = LOGO_URL.format(**{"image_id": data["logo"]["image_id"] if "logo" in data else ""})
-
- # Try to get found date of company
- founded = dt.utcfromtimestamp(data["start_date"]).date() if "start_date" in data else "?"
-
- # Generate list of games, that company have developed or published
- developed = ", ".join(game["name"] for game in data["developed"]) if "developed" in data else "?"
- published = ", ".join(game["name"] for game in data["published"]) if "published" in data else "?"
-
- formatting = {
- "name": data["name"],
- "url": data["url"],
- "description": f"{data['description']}\n\n" if "description" in data else "\n",
- "founded": founded,
- "developed": developed,
- "published": published
- }
- page = COMPANY_PAGE.format(**formatting)
-
- return page, url
-
-
-def setup(bot: SeasonalBot) -> None:
- """Add/Load Games cog."""
- # Check does IGDB API key exist, if not, log warning and don't load cog
- if not Tokens.igdb:
- logger.warning("No IGDB API key. Not loading Games cog.")
- return
- bot.add_cog(Games(bot))
diff --git a/bot/seasons/evergreen/issues.py b/bot/seasons/evergreen/issues.py
deleted file mode 100644
index fba5b174..00000000
--- a/bot/seasons/evergreen/issues.py
+++ /dev/null
@@ -1,77 +0,0 @@
-import logging
-
-import discord
-from discord.ext import commands
-
-from bot.constants import Colours, Emojis, WHITELISTED_CHANNELS
-from bot.decorators import override_in_channel
-
-log = logging.getLogger(__name__)
-
-BAD_RESPONSE = {
- 404: "Issue/pull request not located! Please enter a valid number!",
- 403: "Rate limit has been hit! Please try again later!"
-}
-
-
-class Issues(commands.Cog):
- """Cog that allows users to retrieve issues from GitHub."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(aliases=("pr",))
- @override_in_channel(WHITELISTED_CHANNELS)
- async def issue(
- self, ctx: commands.Context, number: int, repository: str = "seasonalbot", user: str = "python-discord"
- ) -> None:
- """Command to retrieve issues from a GitHub repository."""
- url = f"https://api.github.com/repos/{user}/{repository}/issues/{number}"
- merge_url = f"https://api.github.com/repos/{user}/{repository}/pulls/{number}/merge"
-
- log.trace(f"Querying GH issues API: {url}")
- async with self.bot.http_session.get(url) as r:
- json_data = await r.json()
-
- if r.status in BAD_RESPONSE:
- log.warning(f"Received response {r.status} from: {url}")
- return await ctx.send(f"[{str(r.status)}] {BAD_RESPONSE.get(r.status)}")
-
- # The initial API request is made to the issues API endpoint, which will return information
- # if the issue or PR is present. However, the scope of information returned for PRs differs
- # from issues: if the 'issues' key is present in the response then we can pull the data we
- # need from the initial API call.
- if "issues" in json_data.get("html_url"):
- if json_data.get("state") == "open":
- icon_url = Emojis.issue
- else:
- icon_url = Emojis.issue_closed
-
- # If the 'issues' key is not contained in the API response and there is no error code, then
- # we know that a PR has been requested and a call to the pulls API endpoint is necessary
- # to get the desired information for the PR.
- else:
- log.trace(f"PR provided, querying GH pulls API for additional information: {merge_url}")
- async with self.bot.http_session.get(merge_url) as m:
- if json_data.get("state") == "open":
- icon_url = Emojis.pull_request
- # When the status is 204 this means that the state of the PR is merged
- elif m.status == 204:
- icon_url = Emojis.merge
- else:
- icon_url = Emojis.pull_request_closed
-
- issue_url = json_data.get("html_url")
- description_text = f"[{repository}] #{number} {json_data.get('title')}"
- resp = discord.Embed(
- colour=Colours.bright_green,
- description=f"{icon_url} [{description_text}]({issue_url})"
- )
- resp.set_author(name="GitHub", url=issue_url)
- await ctx.send(embed=resp)
-
-
-def setup(bot: commands.Bot) -> None:
- """Cog Retrieves Issues From Github."""
- bot.add_cog(Issues(bot))
- log.info("Issues cog loaded")
diff --git a/bot/seasons/evergreen/magic_8ball.py b/bot/seasons/evergreen/magic_8ball.py
deleted file mode 100644
index e47ef454..00000000
--- a/bot/seasons/evergreen/magic_8ball.py
+++ /dev/null
@@ -1,32 +0,0 @@
-import json
-import logging
-import random
-from pathlib import Path
-
-from discord.ext import commands
-
-log = logging.getLogger(__name__)
-
-
-class Magic8ball(commands.Cog):
- """A Magic 8ball command to respond to a user's question."""
-
- def __init__(self, bot: commands.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: commands.Context, *, question: str) -> None:
- """Return a Magic 8ball 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?`)")
-
-
-def setup(bot: commands.Bot) -> None:
- """Magic 8ball Cog load."""
- bot.add_cog(Magic8ball(bot))
- log.info("Magic8ball cog loaded")
diff --git a/bot/seasons/evergreen/minesweeper.py b/bot/seasons/evergreen/minesweeper.py
deleted file mode 100644
index b0ba8145..00000000
--- a/bot/seasons/evergreen/minesweeper.py
+++ /dev/null
@@ -1,285 +0,0 @@
-import logging
-import typing
-from dataclasses import dataclass
-from random import randint, random
-
-import discord
-from discord.ext import commands
-
-from bot.constants import Client
-
-MESSAGE_MAPPING = {
- 0: ":stop_button:",
- 1: ":one:",
- 2: ":two:",
- 3: ":three:",
- 4: ":four:",
- 5: ":five:",
- 6: ":six:",
- 7: ":seven:",
- 8: ":eight:",
- 9: ":nine:",
- 10: ":keycap_ten:",
- "bomb": ":bomb:",
- "hidden": ":grey_question:",
- "flag": ":flag_black:",
- "x": ":x:"
-}
-
-log = logging.getLogger(__name__)
-
-
-class CoordinateConverter(commands.Converter):
- """Converter for Coordinates."""
-
- async def convert(self, ctx: commands.Context, coordinate: str) -> typing.Tuple[int, int]:
- """Take in a coordinate string and turn it into an (x, y) tuple."""
- if not 2 <= len(coordinate) <= 3:
- raise commands.BadArgument('Invalid co-ordinate provided')
-
- coordinate = coordinate.lower()
- if coordinate[0].isalpha():
- digit = coordinate[1:]
- letter = coordinate[0]
- else:
- digit = coordinate[:-1]
- letter = coordinate[-1]
-
- if not digit.isdigit():
- raise commands.BadArgument
-
- x = ord(letter) - ord('a')
- y = int(digit) - 1
-
- if (not 0 <= x <= 9) or (not 0 <= y <= 9):
- raise commands.BadArgument
- return x, y
-
-
-GameBoard = typing.List[typing.List[typing.Union[str, int]]]
-
-
-@dataclass
-class Game:
- """The data for a game."""
-
- board: GameBoard
- revealed: GameBoard
- dm_msg: discord.Message
- chat_msg: discord.Message
- activated_on_server: bool
-
-
-GamesDict = typing.Dict[int, Game]
-
-
-class Minesweeper(commands.Cog):
- """Play a game of Minesweeper."""
-
- def __init__(self, bot: commands.Bot) -> None:
- self.games: GamesDict = {} # Store the currently running games
-
- @commands.group(name='minesweeper', aliases=('ms',), invoke_without_command=True)
- async def minesweeper_group(self, ctx: commands.Context) -> None:
- """Commands for Playing Minesweeper."""
- await ctx.send_help(ctx.command)
-
- @staticmethod
- def get_neighbours(x: int, y: int) -> typing.Generator[typing.Tuple[int, int], None, None]:
- """Get all the neighbouring x and y including it self."""
- for x_ in [x - 1, x, x + 1]:
- for y_ in [y - 1, y, y + 1]:
- if x_ != -1 and x_ != 10 and y_ != -1 and y_ != 10:
- yield x_, y_
-
- def generate_board(self, bomb_chance: float) -> GameBoard:
- """Generate a 2d array for the board."""
- board: GameBoard = [
- [
- "bomb" if random() <= bomb_chance else "number"
- for _ in range(10)
- ] for _ in range(10)
- ]
-
- # make sure there is always a free cell
- board[randint(0, 9)][randint(0, 9)] = "number"
-
- for y, row in enumerate(board):
- for x, cell in enumerate(row):
- if cell == "number":
- # calculate bombs near it
- bombs = 0
- for x_, y_ in self.get_neighbours(x, y):
- if board[y_][x_] == "bomb":
- bombs += 1
- board[y][x] = bombs
- return board
-
- @staticmethod
- def format_for_discord(board: GameBoard) -> str:
- """Format the board as a string for Discord."""
- discord_msg = (
- ":stop_button: :regional_indicator_a::regional_indicator_b::regional_indicator_c:"
- ":regional_indicator_d::regional_indicator_e::regional_indicator_f::regional_indicator_g:"
- ":regional_indicator_h::regional_indicator_i::regional_indicator_j:\n\n"
- )
- rows = []
- for row_number, row in enumerate(board):
- new_row = f"{MESSAGE_MAPPING[row_number + 1]} "
- new_row += "".join(MESSAGE_MAPPING[cell] for cell in row)
- rows.append(new_row)
-
- discord_msg += "\n".join(rows)
- return discord_msg
-
- @minesweeper_group.command(name="start")
- async def start_command(self, ctx: commands.Context, bomb_chance: float = .2) -> None:
- """Start a game of Minesweeper."""
- if ctx.author.id in self.games: # Player is already playing
- await ctx.send(f"{ctx.author.mention} you already have a game running!", delete_after=2)
- await ctx.message.delete(delay=2)
- return
-
- # Add game to list
- board: GameBoard = self.generate_board(bomb_chance)
- revealed_board: GameBoard = [["hidden"] * 10 for _ in range(10)]
-
- if ctx.guild:
- await ctx.send(f"{ctx.author.mention} is playing Minesweeper")
- chat_msg = await ctx.send(f"Here's there board!\n{self.format_for_discord(revealed_board)}")
- else:
- chat_msg = None
-
- await ctx.author.send(
- f"Play by typing: `{Client.prefix}ms reveal xy [xy]` or `{Client.prefix}ms flag xy [xy]` \n"
- f"Close the game with `{Client.prefix}ms end`\n"
- )
- dm_msg = await ctx.author.send(f"Here's your board!\n{self.format_for_discord(revealed_board)}")
-
- self.games[ctx.author.id] = Game(
- board=board,
- revealed=revealed_board,
- dm_msg=dm_msg,
- chat_msg=chat_msg,
- activated_on_server=ctx.guild is not None
- )
-
- async def update_boards(self, ctx: commands.Context) -> None:
- """Update both playing boards."""
- game = self.games[ctx.author.id]
- await game.dm_msg.delete()
- game.dm_msg = await ctx.author.send(f"Here's your board!\n{self.format_for_discord(game.revealed)}")
- if game.activated_on_server:
- await game.chat_msg.edit(content=f"Here's there board!\n{self.format_for_discord(game.revealed)}")
-
- @commands.dm_only()
- @minesweeper_group.command(name="flag")
- async def flag_command(self, ctx: commands.Context, *coordinates: CoordinateConverter) -> None:
- """Place multiple flags on the board."""
- board: GameBoard = self.games[ctx.author.id].revealed
- for x, y in coordinates:
- if board[y][x] == "hidden":
- board[y][x] = "flag"
-
- await self.update_boards(ctx)
-
- @staticmethod
- def reveal_bombs(revealed: GameBoard, board: GameBoard) -> None:
- """Reveals all the bombs."""
- for y, row in enumerate(board):
- for x, cell in enumerate(row):
- if cell == "bomb":
- revealed[y][x] = cell
-
- async def lost(self, ctx: commands.Context) -> None:
- """The player lost the game."""
- game = self.games[ctx.author.id]
- self.reveal_bombs(game.revealed, game.board)
- await ctx.author.send(":fire: You lost! :fire:")
- if game.activated_on_server:
- await game.chat_msg.channel.send(f":fire: {ctx.author.mention} just lost Minesweeper! :fire:")
-
- async def won(self, ctx: commands.Context) -> None:
- """The player won the game."""
- game = self.games[ctx.author.id]
- await ctx.author.send(":tada: You won! :tada:")
- if game.activated_on_server:
- await game.chat_msg.channel.send(f":tada: {ctx.author.mention} just won Minesweeper! :tada:")
-
- def reveal_zeros(self, revealed: GameBoard, board: GameBoard, x: int, y: int) -> None:
- """Recursively reveal adjacent cells when a 0 cell is encountered."""
- for x_, y_ in self.get_neighbours(x, y):
- if revealed[y_][x_] != "hidden":
- continue
- revealed[y_][x_] = board[y_][x_]
- if board[y_][x_] == 0:
- self.reveal_zeros(revealed, board, x_, y_)
-
- async def check_if_won(self, ctx: commands.Context, revealed: GameBoard, board: GameBoard) -> bool:
- """Checks if a player has won."""
- if any(
- revealed[y][x] in ["hidden", "flag"] and board[y][x] != "bomb"
- for x in range(10)
- for y in range(10)
- ):
- return False
- else:
- await self.won(ctx)
- return True
-
- async def reveal_one(
- self,
- ctx: commands.Context,
- revealed: GameBoard,
- board: GameBoard,
- x: int,
- y: int
- ) -> bool:
- """
- Reveal one square.
-
- return is True if the game ended, breaking the loop in `reveal_command` and deleting the game
- """
- revealed[y][x] = board[y][x]
- if board[y][x] == "bomb":
- await self.lost(ctx)
- revealed[y][x] = "x" # mark bomb that made you lose with a x
- return True
- elif board[y][x] == 0:
- self.reveal_zeros(revealed, board, x, y)
- return await self.check_if_won(ctx, revealed, board)
-
- @commands.dm_only()
- @minesweeper_group.command(name="reveal")
- async def reveal_command(self, ctx: commands.Context, *coordinates: CoordinateConverter) -> None:
- """Reveal multiple cells."""
- game = self.games[ctx.author.id]
- revealed: GameBoard = game.revealed
- board: GameBoard = game.board
-
- for x, y in coordinates:
- # reveal_one returns True if the revealed cell is a bomb or the player won, ending the game
- if await self.reveal_one(ctx, revealed, board, x, y):
- await self.update_boards(ctx)
- del self.games[ctx.author.id]
- break
- else:
- await self.update_boards(ctx)
-
- @minesweeper_group.command(name="end")
- async def end_command(self, ctx: commands.Context) -> None:
- """End your current game."""
- game = self.games[ctx.author.id]
- game.revealed = game.board
- await self.update_boards(ctx)
- new_msg = f":no_entry: Game canceled :no_entry:\n{game.dm_msg.content}"
- await game.dm_msg.edit(content=new_msg)
- if game.activated_on_server:
- await game.chat_msg.edit(content=new_msg)
- del self.games[ctx.author.id]
-
-
-def setup(bot: commands.Bot) -> None:
- """Load the Minesweeper cog."""
- bot.add_cog(Minesweeper(bot))
- log.info("Minesweeper cog loaded")
diff --git a/bot/seasons/evergreen/movie.py b/bot/seasons/evergreen/movie.py
deleted file mode 100644
index 3c5a312d..00000000
--- a/bot/seasons/evergreen/movie.py
+++ /dev/null
@@ -1,198 +0,0 @@
-import logging
-import random
-from enum import Enum
-from typing import Any, Dict, List, Tuple
-from urllib.parse import urlencode
-
-from aiohttp import ClientSession
-from discord import Embed
-from discord.ext.commands import Bot, Cog, Context, group
-
-from bot.constants import Tokens
-from bot.pagination import ImagePaginator
-
-# Define base URL of TMDB
-BASE_URL = "https://api.themoviedb.org/3/"
-
-logger = logging.getLogger(__name__)
-
-# Define movie params, that will be used for every movie request
-MOVIE_PARAMS = {
- "api_key": Tokens.tmdb,
- "language": "en-US"
-}
-
-
-class MovieGenres(Enum):
- """Movies Genre names and IDs."""
-
- Action = "28"
- Adventure = "12"
- Animation = "16"
- Comedy = "35"
- Crime = "80"
- Documentary = "99"
- Drama = "18"
- Family = "10751"
- Fantasy = "14"
- History = "36"
- Horror = "27"
- Music = "10402"
- Mystery = "9648"
- Romance = "10749"
- Science = "878"
- Thriller = "53"
- Western = "37"
-
-
-class Movie(Cog):
- """Movie Cog contains movies command that grab random movies from TMDB."""
-
- def __init__(self, bot: Bot):
- self.bot = bot
- self.http_session: ClientSession = bot.http_session
-
- @group(name='movies', aliases=['movie'], invoke_without_command=True)
- async def movies(self, ctx: Context, genre: str = "", amount: int = 5) -> None:
- """
- Get random movies by specifying genre. Also support amount parameter, that define how much movies will be shown.
-
- Default 5. Use .movies genres to get all available genres.
- """
- # Check is there more than 20 movies specified, due TMDB return 20 movies
- # per page, so this is max. Also you can't get less movies than 1, just logic
- if amount > 20:
- await ctx.send("You can't get more than 20 movies at once. (TMDB limits)")
- return
- elif amount < 1:
- await ctx.send("You can't get less than 1 movie.")
- return
-
- # Capitalize genre for getting data from Enum, get random page, send help when genre don't exist.
- genre = genre.capitalize()
- try:
- result = await self.get_movies_list(self.http_session, MovieGenres[genre].value, 1)
- except KeyError:
- await ctx.send_help('movies')
- return
-
- # Check if "results" is in result. If not, throw error.
- if "results" not in result.keys():
- err_msg = f"There is problem while making TMDB API request. Response Code: {result['status_code']}, " \
- f"{result['status_message']}."
- await ctx.send(err_msg)
- logger.warning(err_msg)
-
- # Get random page. Max page is last page where is movies with this genre.
- page = random.randint(1, result["total_pages"])
-
- # Get movies list from TMDB, check if results key in result. When not, raise error.
- movies = await self.get_movies_list(self.http_session, MovieGenres[genre].value, page)
- if 'results' not in movies.keys():
- err_msg = f"There is problem while making TMDB API request. Response Code: {result['status_code']}, " \
- f"{result['status_message']}."
- await ctx.send(err_msg)
- logger.warning(err_msg)
-
- # Get all pages and embed
- pages = await self.get_pages(self.http_session, movies, amount)
- embed = await self.get_embed(genre)
-
- await ImagePaginator.paginate(pages, ctx, embed)
-
- @movies.command(name='genres', aliases=['genre', 'g'])
- async def genres(self, ctx: Context) -> None:
- """Show all currently available genres for .movies command."""
- await ctx.send(f"Current available genres: {', '.join('`' + genre.name + '`' for genre in MovieGenres)}")
-
- async def get_movies_list(self, client: ClientSession, genre_id: str, page: int) -> Dict[str, Any]:
- """Return JSON of TMDB discover request."""
- # Define params of request
- params = {
- "api_key": Tokens.tmdb,
- "language": "en-US",
- "sort_by": "popularity.desc",
- "include_adult": "false",
- "include_video": "false",
- "page": page,
- "with_genres": genre_id
- }
-
- url = BASE_URL + "discover/movie?" + urlencode(params)
-
- # Make discover request to TMDB, return result
- async with client.get(url) as resp:
- return await resp.json()
-
- async def get_pages(self, client: ClientSession, movies: Dict[str, Any], amount: int) -> List[Tuple[str, str]]:
- """Fetch all movie pages from movies dictionary. Return list of pages."""
- pages = []
-
- for i in range(amount):
- movie_id = movies['results'][i]['id']
- movie = await self.get_movie(client, movie_id)
-
- page, img = await self.create_page(movie)
- pages.append((page, img))
-
- return pages
-
- async def get_movie(self, client: ClientSession, movie: int) -> Dict:
- """Get Movie by movie ID from TMDB. Return result dictionary."""
- url = BASE_URL + f"movie/{movie}?" + urlencode(MOVIE_PARAMS)
-
- async with client.get(url) as resp:
- return await resp.json()
-
- async def create_page(self, movie: Dict[str, Any]) -> Tuple[str, str]:
- """Create page from TMDB movie request result. Return formatted page + image."""
- text = ""
-
- # Add title + tagline (if not empty)
- text += f"**{movie['title']}**\n"
- if movie['tagline']:
- text += f"{movie['tagline']}\n\n"
- else:
- text += "\n"
-
- # Add other information
- text += f"**Rating:** {movie['vote_average']}/10 :star:\n"
- text += f"**Release Date:** {movie['release_date']}\n\n"
-
- text += "__**Production Information**__\n"
-
- companies = movie['production_companies']
- countries = movie['production_countries']
-
- text += f"**Made by:** {', '.join(company['name'] for company in companies)}\n"
- text += f"**Made in:** {', '.join(country['name'] for country in countries)}\n\n"
-
- text += "__**Some Numbers**__\n"
-
- budget = f"{movie['budget']:,d}" if movie['budget'] else "?"
- revenue = f"{movie['revenue']:,d}" if movie['revenue'] else "?"
-
- if movie['runtime'] is not None:
- duration = divmod(movie['runtime'], 60)
- else:
- duration = ("?", "?")
-
- text += f"**Budget:** ${budget}\n"
- text += f"**Revenue:** ${revenue}\n"
- text += f"**Duration:** {f'{duration[0]} hour(s) {duration[1]} minute(s)'}\n\n"
-
- text += movie['overview']
-
- img = f"http://image.tmdb.org/t/p/w200{movie['poster_path']}"
-
- # Return page content and image
- return text, img
-
- async def get_embed(self, name: str) -> Embed:
- """Return embed of random movies. Uses name in title."""
- return Embed(title=f'Random {name} Movies').set_footer(text='Powered by TMDB (themoviedb.org)')
-
-
-def setup(bot: Bot) -> None:
- """Load Movie Cog."""
- bot.add_cog(Movie(bot))
diff --git a/bot/seasons/evergreen/recommend_game.py b/bot/seasons/evergreen/recommend_game.py
deleted file mode 100644
index 835a4e53..00000000
--- a/bot/seasons/evergreen/recommend_game.py
+++ /dev/null
@@ -1,51 +0,0 @@
-import json
-import logging
-from pathlib import Path
-from random import shuffle
-
-import discord
-from discord.ext import commands
-
-log = logging.getLogger(__name__)
-game_recs = []
-
-# Populate the list `game_recs` with resource files
-for rec_path in Path("bot/resources/evergreen/game_recs").glob("*.json"):
- with rec_path.open(encoding='utf-8') as file:
- data = json.load(file)
- game_recs.append(data)
-shuffle(game_recs)
-
-
-class RecommendGame(commands.Cog):
- """Commands related to recommending games."""
-
- def __init__(self, bot: commands.Bot) -> None:
- self.bot = bot
- self.index = 0
-
- @commands.command(name="recommendgame", aliases=['gamerec'])
- async def recommend_game(self, ctx: commands.Context) -> None:
- """Sends an Embed of a random game recommendation."""
- if self.index >= len(game_recs):
- self.index = 0
- shuffle(game_recs)
- game = game_recs[self.index]
- self.index += 1
-
- author = self.bot.get_user(int(game['author']))
-
- # Creating and formatting Embed
- embed = discord.Embed(color=discord.Colour.blue())
- if author is not None:
- embed.set_author(name=author.name, icon_url=author.avatar_url)
- embed.set_image(url=game['image'])
- embed.add_field(name='Recommendation: ' + game['title'] + '\n' + game['link'], value=game['description'])
-
- await ctx.send(embed=embed)
-
-
-def setup(bot: commands.Bot) -> None:
- """Loads the RecommendGame cog."""
- bot.add_cog(RecommendGame(bot))
- log.info("RecommendGame cog loaded")
diff --git a/bot/seasons/evergreen/reddit.py b/bot/seasons/evergreen/reddit.py
deleted file mode 100644
index 32ca419a..00000000
--- a/bot/seasons/evergreen/reddit.py
+++ /dev/null
@@ -1,130 +0,0 @@
-import logging
-import random
-
-import discord
-from discord.ext import commands
-from discord.ext.commands.cooldowns import BucketType
-
-from bot.pagination import ImagePaginator
-
-
-log = logging.getLogger(__name__)
-
-
-class Reddit(commands.Cog):
- """Fetches reddit posts."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- async def fetch(self, url: str) -> dict:
- """Send a get request to the reddit API and get json response."""
- session = self.bot.http_session
- params = {
- 'limit': 50
- }
- headers = {
- 'User-Agent': 'Iceman'
- }
-
- async with session.get(url=url, params=params, headers=headers) as response:
- return await response.json()
-
- @commands.command(name='reddit')
- @commands.cooldown(1, 10, BucketType.user)
- async def get_reddit(self, ctx: commands.Context, subreddit: str = 'python', sort: str = "hot") -> None:
- """
- Fetch reddit posts by using this command.
-
- Gets a post from r/python by default.
- Usage:
- --> .reddit [subreddit_name] [hot/top/new]
- """
- pages = []
- sort_list = ["hot", "new", "top", "rising"]
- if sort.lower() not in sort_list:
- await ctx.send(f"Invalid sorting: {sort}\nUsing default sorting: `Hot`")
- sort = "hot"
-
- data = await self.fetch(f'https://www.reddit.com/r/{subreddit}/{sort}/.json')
-
- try:
- posts = data["data"]["children"]
- except KeyError:
- return await ctx.send('Subreddit not found!')
- if not posts:
- return await ctx.send('No posts available!')
-
- if posts[1]["data"]["over_18"] is True:
- return await ctx.send(
- "You cannot access this Subreddit as it is ment for those who "
- "are 18 years or older."
- )
-
- embed_titles = ""
-
- # Chooses k unique random elements from a population sequence or set.
- random_posts = random.sample(posts, k=5)
-
- # -----------------------------------------------------------
- # This code below is bound of change when the emojis are added.
-
- upvote_emoji = self.bot.get_emoji(638729835245731840)
- comment_emoji = self.bot.get_emoji(638729835073765387)
- user_emoji = self.bot.get_emoji(638729835442602003)
- text_emoji = self.bot.get_emoji(676030265910493204)
- video_emoji = self.bot.get_emoji(676030265839190047)
- image_emoji = self.bot.get_emoji(676030265734201344)
- reddit_emoji = self.bot.get_emoji(676030265734332427)
-
- # ------------------------------------------------------------
-
- for i, post in enumerate(random_posts, start=1):
- post_title = post["data"]["title"][0:50]
- post_url = post['data']['url']
- if post_title == "":
- post_title = "No Title."
- elif post_title == post_url:
- post_title = "Title is itself a link."
-
- # ------------------------------------------------------------------
- # Embed building.
-
- embed_titles += f"**{i}.[{post_title}]({post_url})**\n"
- image_url = " "
- post_stats = f"{text_emoji}" # Set default content type to text.
-
- if post["data"]["is_video"] is True or "youtube" in post_url.split("."):
- # This means the content type in the post is a video.
- post_stats = f"{video_emoji} "
-
- elif post_url.endswith("jpg") or post_url.endswith("png") or post_url.endswith("gif"):
- # This means the content type in the post is an image.
- post_stats = f"{image_emoji} "
- image_url = post_url
-
- votes = f'{upvote_emoji}{post["data"]["ups"]}'
- comments = f'{comment_emoji}\u2002{ post["data"]["num_comments"]}'
- post_stats += (
- f"\u2002{votes}\u2003"
- f"{comments}"
- f'\u2003{user_emoji}\u2002{post["data"]["author"]}\n'
- )
- embed_titles += f"{post_stats}\n"
- page_text = f"**[{post_title}]({post_url})**\n{post_stats}\n{post['data']['selftext'][0:200]}"
-
- embed = discord.Embed()
- page_tuple = (page_text, image_url)
- pages.append(page_tuple)
-
- # ------------------------------------------------------------------
-
- pages.insert(0, (embed_titles, " "))
- embed.set_author(name=f"r/{posts[0]['data']['subreddit']} - {sort}", icon_url=reddit_emoji.url)
- await ImagePaginator.paginate(pages, ctx, embed)
-
-
-def setup(bot: commands.Bot) -> None:
- """Load the Cog."""
- bot.add_cog(Reddit(bot))
- log.debug('Loaded')
diff --git a/bot/seasons/evergreen/showprojects.py b/bot/seasons/evergreen/showprojects.py
deleted file mode 100644
index a943e548..00000000
--- a/bot/seasons/evergreen/showprojects.py
+++ /dev/null
@@ -1,34 +0,0 @@
-import logging
-
-from discord import Message
-from discord.ext import commands
-
-from bot.constants import Channels
-
-log = logging.getLogger(__name__)
-
-
-class ShowProjects(commands.Cog):
- """Cog that reacts to posts in the #show-your-projects."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.lastPoster = 0 # Given 0 as the default last poster ID as no user can actually have 0 assigned to them
-
- @commands.Cog.listener()
- async def on_message(self, message: Message) -> None:
- """Adds reactions to posts in #show-your-projects."""
- reactions = ["\U0001f44d", "\U00002764", "\U0001f440", "\U0001f389", "\U0001f680", "\U00002b50", "\U0001f6a9"]
- if (message.channel.id == Channels.show_your_projects
- and message.author.bot is False
- and message.author.id != self.lastPoster):
- for reaction in reactions:
- await message.add_reaction(reaction)
-
- self.lastPoster = message.author.id
-
-
-def setup(bot: commands.Bot) -> None:
- """Show Projects Reaction Cog."""
- bot.add_cog(ShowProjects(bot))
- log.info("ShowProjects cog loaded")
diff --git a/bot/seasons/evergreen/snakes/__init__.py b/bot/seasons/evergreen/snakes/__init__.py
deleted file mode 100644
index d7f9f20c..00000000
--- a/bot/seasons/evergreen/snakes/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-import logging
-
-from discord.ext import commands
-
-from bot.seasons.evergreen.snakes.snakes_cog import Snakes
-
-log = logging.getLogger(__name__)
-
-
-def setup(bot: commands.Bot) -> None:
- """Snakes Cog load."""
- 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
deleted file mode 100644
index 57103b57..00000000
--- a/bot/seasons/evergreen/snakes/converter.py
+++ /dev/null
@@ -1,85 +0,0 @@
-import json
-import logging
-import random
-from typing import Iterable, List
-
-import discord
-from discord.ext.commands import Context, 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):
- """Snake converter for the Snakes Cog."""
-
- snakes = None
- special_cases = None
-
- async def convert(self, ctx: Context, name: str) -> str:
- """Convert the input snake name to the closest matching Snake object."""
- await self.build_list()
- name = name.lower()
-
- if name == 'python':
- return 'Python (programming language)'
-
- def get_potential(iterable: Iterable, *, threshold: int = 80) -> List[str]:
- 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) -> None:
- """Build list of snakes from the static snake resources."""
- # Get all the snakes
- if cls.snakes is None:
- with (SNAKE_RESOURCES / "snake_names.json").open() as snakefile:
- 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) -> str:
- """
- Get a random Snake from the loaded resources.
-
- 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.
- """
- 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
deleted file mode 100644
index 09f5e250..00000000
--- a/bot/seasons/evergreen/snakes/snakes_cog.py
+++ /dev/null
@@ -1,1149 +0,0 @@
-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, List
-
-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, CommandError, 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, created by our community during the first code jam.
-
- More information can be found in the code-jam-1 repo.
-
- https://github.com/python-discord/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: float) -> int:
- """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: str) -> str:
- """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: aiohttp.ClientSession, url: str, params: dict = None) -> dict:
- """Asynchronous 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: List[str], retries: int = 10) -> str:
- """
- Fetch a message that's at least 3 words long, if 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]:
- """
- 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.
- """
- 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 random.choice(self.snake_names)
-
- async def _validate_answer(self, ctx: Context, message: Message, answer: str, options: list) -> None:
- """Validate the answer using a reaction event loop."""
- def predicate(reaction: Reaction, user: Member) -> bool:
- """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) -> None:
- """Commands from our first code jam."""
- await ctx.send_help(ctx.command)
-
- @bot_has_permissions(manage_messages=True)
- @snakes_group.command(name='antidote')
- @locked()
- async def antidote_command(self, ctx: Context) -> None:
- """
- 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) -> bool:
- """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) -> None:
- """
- 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) -> None:
- """
- Fetches information about a snake from Wikipedia.
-
- 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: Context) -> None:
- """
- 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) -> None:
- """
- 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) -> None:
- """
- 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) -> None:
- """
- 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) -> None:
- """
- Snakifies a username.
-
- 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) -> None:
- """
- 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) -> None:
- """Show an embed with information about the event, its 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) -> 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) -> None:
- """
- 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='snakify')
- async def snakify_command(self, ctx: Context, *, message: str = None) -> None:
- """
- How would I talk if I were a snake?
-
- If `message` is passed, the bot will snakify the message.
- Otherwise, a random message from the user's history is snakified.
-
- 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) -> None:
- """
- Gets a YouTube video about snakes.
-
- If `search` is given, a snake with that name will be searched on Youtube.
-
- 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) -> None:
- """
- 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: Context, error: CommandError) -> None:
- """Local error handler for the Snake Cog."""
- 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
deleted file mode 100644
index 7d6caf04..00000000
--- a/bot/seasons/evergreen/snakes/utils.py
+++ /dev/null
@@ -1,716 +0,0 @@
-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
-
-from PIL import Image
-from PIL.ImageDraw import ImageDraw
-from discord import File, Member, Reaction
-from discord.ext.commands import Cog, Context
-
-from bot.constants import Roles
-
-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]:
- """Load Snake resources JSON."""
- with (SNAKE_RESOURCES / f"{file}.json").open(encoding="utf-8") as snakefile:
- return json.load(snakefile)
-
-
-def smoothstep(t: float) -> float:
- """Smooth curve with a zero derivative at 0 and 1, making it useful for interpolating."""
- return t * t * (3. - 2. * t)
-
-
-def lerp(t: float, a: float, b: float) -> float:
- """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.
-
- Taken from: https://gist.github.com/eevee/26f547457522755cb1fb8739d0ea89a1
- Licensed under ISC
- """
-
- def __init__(self, dimension: int, octaves: int = 1, tile: Tuple[int] = (), unbias: bool = False):
- """
- Create a new Perlin noise factory in the given number of dimensions.
-
- dimension 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) -> Tuple[float, ...]:
- """
- 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) -> float:
- """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) -> float:
- """
- 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.
-
- `perlin_lookup_vertical_shift` represents the Perlin noise shift in the Y-dimension for this frame.
- If `text` is given, display the given text with the snek.
- """
- 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) -> io.BytesIO:
- """Convert image to byte stream."""
- stream = io.BytesIO()
- image.save(stream, format='PNG')
- stream.seek(0)
- return stream
-
-
-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:
- """Snakes and Ladders game Cog."""
-
- def __init__(self, snakes: Cog, 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) -> None:
- """
- 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) -> bool:
- """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"),
- 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 user == self.author or (self._is_moderator(user) and user not in self.players):
- # Allow game author or non-playing moderation staff to cancel a waiting game
- await self.cancel_game()
- 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")
- await self.cancel_game()
- return # We're done, no reactions for the last 5 minutes
-
- async def _add_player(self, user: Member) -> None:
- """Add player to game."""
- self.players.append(user)
- self.player_tiles[user.id] = 1
-
- avatar_bytes = await user.avatar_url_as(format='jpeg', size=PLAYER_ICON_IMAGE_SIZE).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) -> None:
- """
- Handle players joining the game.
-
- Prevent player joining if they have already joined, if the game is full, or if the game is
- in a waiting state.
- """
- 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) -> bool:
- """
- Handle players leaving the game.
-
- Leaving is prevented if the user wasn't part of the game.
-
- If the number of players reaches 0, the game is terminated. In this case, a sentinel boolean
- is returned True to prevent a game from continuing after it's destroyed.
- """
- is_surrendered = False # Sentinel value to assist with stopping a surrendered game
- 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) == 0:
- await self.channel.send("**Snakes and Ladders**: The game has been surrendered!")
- is_surrendered = True
- self._destruct()
-
- return is_surrendered
- else:
- await self.channel.send(user.mention + " You are not in the match.", delete_after=10)
- return is_surrendered
-
- async def cancel_game(self) -> None:
- """Cancel the running game."""
- await self.channel.send("**Snakes and Ladders**: Game has been canceled.")
- self._destruct()
-
- async def start_game(self, user: Member) -> None:
- """
- Allow the game author to begin the game.
-
- The game cannot be started if the game is in a waiting state.
- """
- 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 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) -> None:
- """Begin the round."""
- def game_event_check(reaction_: Reaction, user_: Member) -> bool:
- """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(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))
-
- board_file = File(frame_to_png_bytes(board_img), 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)
-
- is_surrendered = False
- 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._is_moderator(user) and user not in self.players:
- # Only allow non-playing moderation staff to cancel a running game
- await self.cancel_game()
- return
- else:
- is_surrendered = 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()
- return # We're done, no reactions for the last 5 minutes
-
- # Round completed
- # Check to see if the game was surrendered before completing the round, without this
- # sentinel, the game object would be deleted but the next round still posted into purgatory
- if not is_surrendered:
- await self._complete_round()
-
- async def player_roll(self, user: Member) -> None:
- """Handle the player's roll."""
- 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) -> None:
- """At the conclusion of a round check to see if there's been a winner."""
- 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:
- """Return a winning member if we're in the post-round state and there's a winner."""
- 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) -> bool:
- """Check if all members have made their roll."""
- return all(rolled for rolled in self.round_has_rolled.values())
-
- def _destruct(self) -> None:
- """Clean up the finished game object."""
- del self.snakes.active_sal[self.channel]
-
- def _board_coordinate_from_index(self, index: int) -> Tuple[int, int]:
- """Convert 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
-
- @staticmethod
- def _is_moderator(user: Member) -> bool:
- """Return True if the user is a Moderator."""
- return any(Roles.moderator == role.id for role in user.roles)
diff --git a/bot/seasons/evergreen/speedrun.py b/bot/seasons/evergreen/speedrun.py
deleted file mode 100644
index 76c5e8d3..00000000
--- a/bot/seasons/evergreen/speedrun.py
+++ /dev/null
@@ -1,28 +0,0 @@
-import json
-import logging
-from pathlib import Path
-from random import choice
-
-from discord.ext import commands
-
-log = logging.getLogger(__name__)
-with Path('bot/resources/evergreen/speedrun_links.json').open(encoding="utf-8") as file:
- LINKS = json.load(file)
-
-
-class Speedrun(commands.Cog):
- """Commands about the video game speedrunning community."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(name="speedrun")
- async def get_speedrun(self, ctx: commands.Context) -> None:
- """Sends a link to a video of a random speedrun."""
- await ctx.send(choice(LINKS))
-
-
-def setup(bot: commands.Bot) -> None:
- """Load the Speedrun cog."""
- bot.add_cog(Speedrun(bot))
- log.info("Speedrun cog loaded")
diff --git a/bot/seasons/evergreen/trivia_quiz.py b/bot/seasons/evergreen/trivia_quiz.py
deleted file mode 100644
index 99b64497..00000000
--- a/bot/seasons/evergreen/trivia_quiz.py
+++ /dev/null
@@ -1,303 +0,0 @@
-import asyncio
-import json
-import logging
-import random
-from pathlib import Path
-
-import discord
-from discord.ext import commands
-from fuzzywuzzy import fuzz
-
-from bot.constants import Roles
-
-
-logger = logging.getLogger(__name__)
-
-
-WRONG_ANS_RESPONSE = [
- "No one answered correctly!",
- "Better luck next time"
-]
-
-
-class TriviaQuiz(commands.Cog):
- """A cog for all quiz commands."""
-
- def __init__(self, bot: commands.Bot) -> None:
- self.bot = bot
- self.questions = self.load_questions()
- self.game_status = {} # A variable to store the game status: either running or not running.
- self.game_owners = {} # A variable to store the person's ID who started the quiz game in a channel.
- self.question_limit = 4
- self.player_scores = {} # A variable to store all player's scores for a bot session.
- self.game_player_scores = {} # A variable to store temporary game player's scores.
- self.categories = {
- "general": "Test your general knowledge"
- # "retro": "Questions related to retro gaming."
- }
-
- @staticmethod
- def load_questions() -> dict:
- """Load the questions from the JSON file."""
- p = Path("bot", "resources", "evergreen", "trivia_quiz.json")
- with p.open() as json_data:
- questions = json.load(json_data)
- return questions
-
- @commands.group(name="quiz", aliases=["trivia"], invoke_without_command=True)
- async def quiz_game(self, ctx: commands.Context, category: str = None) -> None:
- """
- Start a quiz!
-
- Questions for the quiz can be selected from the following categories:
- - general : Test your general knowledge. (default)
- (More to come!)
- """
- if ctx.channel.id not in self.game_status:
- self.game_status[ctx.channel.id] = False
-
- if ctx.channel.id not in self.game_player_scores:
- self.game_player_scores[ctx.channel.id] = {}
-
- # Stop game if running.
- if self.game_status[ctx.channel.id] is True:
- return await ctx.send(
- f"Game is already running..."
- f"do `{self.bot.command_prefix}quiz stop`"
- )
-
- # Send embed showing available categories if inputted category is invalid.
- if category is None:
- category = random.choice(list(self.categories))
-
- category = category.lower()
- if category not in self.categories:
- embed = self.category_embed()
- await ctx.send(embed=embed)
- return
-
- # Start game if not running.
- if self.game_status[ctx.channel.id] is False:
- self.game_owners[ctx.channel.id] = ctx.author
- self.game_status[ctx.channel.id] = True
- start_embed = self.make_start_embed(category)
-
- await ctx.send(embed=start_embed) # send an embed with the rules
- await asyncio.sleep(1)
-
- topic = self.questions[category]
-
- done_question = []
- hint_no = 0
- answer = None
- while self.game_status[ctx.channel.id]:
- # Exit quiz if number of questions for a round are already sent.
- if len(done_question) > self.question_limit and hint_no == 0:
- await ctx.send("The round has ended.")
- await self.declare_winner(ctx.channel, self.game_player_scores[ctx.channel.id])
-
- self.game_status[ctx.channel.id] = False
- del self.game_owners[ctx.channel.id]
- self.game_player_scores[ctx.channel.id] = {}
-
- break
-
- # If no hint has been sent or any time alert. Basically if hint_no = 0 means it is a new question.
- if hint_no == 0:
- # Select a random question which has not been used yet.
- while True:
- question_dict = random.choice(topic)
- if question_dict["id"] not in done_question:
- done_question.append(question_dict["id"])
- break
-
- q = question_dict["question"]
- answer = question_dict["answer"]
-
- embed = discord.Embed(colour=discord.Colour.gold())
- embed.title = f"Question #{len(done_question)}"
- embed.description = q
- await ctx.send(embed=embed) # Send question embed.
-
- # A function to check whether user input is the correct answer(close to the right answer)
- def check(m: discord.Message) -> bool:
- ratio = fuzz.ratio(answer.lower(), m.content.lower())
- return ratio > 85 and m.channel == ctx.channel
-
- try:
- msg = await self.bot.wait_for('message', check=check, timeout=10)
- except asyncio.TimeoutError:
- # In case of TimeoutError and the game has been stopped, then do nothing.
- if self.game_status[ctx.channel.id] is False:
- break
-
- # if number of hints sent or time alerts sent is less than 2, then send one.
- if hint_no < 2:
- hint_no += 1
- if "hints" in question_dict:
- hints = question_dict["hints"]
- await ctx.send(f"**Hint #{hint_no+1}\n**{hints[hint_no]}")
- else:
- await ctx.send(f"{30 - hint_no * 10}s left!")
-
- # Once hint or time alerts has been sent 2 times, the hint_no value will be 3
- # If hint_no > 2, then it means that all hints/time alerts have been sent.
- # Also means that the answer is not yet given and the bot sends the answer and the next question.
- else:
- if self.game_status[ctx.channel.id] is False:
- break
-
- response = random.choice(WRONG_ANS_RESPONSE)
- await ctx.send(response)
- await self.send_answer(ctx.channel, question_dict)
- await asyncio.sleep(1)
-
- hint_no = 0 # init hint_no = 0 so that 2 hints/time alerts can be sent for the new question.
-
- await self.send_score(ctx.channel, self.game_player_scores[ctx.channel.id])
- await asyncio.sleep(2)
- else:
- if self.game_status[ctx.channel.id] is False:
- break
-
- # Reduce points by 25 for every hint/time alert that has been sent.
- points = 100 - 25*hint_no
- if msg.author in self.game_player_scores[ctx.channel.id]:
- self.game_player_scores[ctx.channel.id][msg.author] += points
- else:
- self.game_player_scores[ctx.channel.id][msg.author] = points
-
- # Also updating the overall scoreboard.
- if msg.author in self.player_scores:
- self.player_scores[msg.author] += points
- else:
- self.player_scores[msg.author] = points
-
- hint_no = 0
-
- await ctx.send(f"{msg.author.mention} got the correct answer :tada: {points} points!")
- await self.send_answer(ctx.channel, question_dict)
- await self.send_score(ctx.channel, self.game_player_scores[ctx.channel.id])
- await asyncio.sleep(2)
-
- @staticmethod
- def make_start_embed(category: str) -> discord.Embed:
- """Generate a starting/introduction embed for the quiz."""
- start_embed = discord.Embed(colour=discord.Colour.red())
- start_embed.title = "Quiz game Starting!!"
- start_embed.description = "Each game consists of 5 questions.\n"
- start_embed.description += "**Rules :**\nNo cheating and have fun!"
- start_embed.description += f"\n **Category** : {category}"
- start_embed.set_footer(
- text="Points for each question reduces by 25 after 10s or after a hint. Total time is 30s per question"
- )
- return start_embed
-
- @quiz_game.command(name="stop")
- async def stop_quiz(self, ctx: commands.Context) -> None:
- """
- Stop a quiz game if its running in the channel.
-
- Note: Only mods or the owner of the quiz can stop it.
- """
- if self.game_status[ctx.channel.id] is True:
- # Check if the author is the game starter or a moderator.
- if (
- ctx.author == self.game_owners[ctx.channel.id]
- or any(Roles.moderator == role.id for role in ctx.author.roles)
- ):
- await ctx.send("Quiz stopped.")
- await self.declare_winner(ctx.channel, self.game_player_scores[ctx.channel.id])
-
- self.game_status[ctx.channel.id] = False
- del self.game_owners[ctx.channel.id]
- self.game_player_scores[ctx.channel.id] = {}
- else:
- await ctx.send(f"{ctx.author.mention}, you are not authorised to stop this game :ghost:!")
- else:
- await ctx.send("No quiz running.")
-
- @quiz_game.command(name="leaderboard")
- async def leaderboard(self, ctx: commands.Context) -> None:
- """View everyone's score for this bot session."""
- await self.send_score(ctx.channel, self.player_scores)
-
- @staticmethod
- async def send_score(channel: discord.TextChannel, player_data: dict) -> None:
- """A function which sends the score."""
- if len(player_data) == 0:
- await channel.send("No one has made it onto the leaderboard yet.")
- return
-
- embed = discord.Embed(colour=discord.Colour.blue())
- embed.title = "Score Board"
- embed.description = ""
-
- sorted_dict = sorted(player_data.items(), key=lambda a: a[1], reverse=True)
- for item in sorted_dict:
- embed.description += f"{item[0]} : {item[1]}\n"
-
- await channel.send(embed=embed)
-
- @staticmethod
- async def declare_winner(channel: discord.TextChannel, player_data: dict) -> None:
- """Announce the winner of the quiz in the game channel."""
- if player_data:
- highest_points = max(list(player_data.values()))
- no_of_winners = list(player_data.values()).count(highest_points)
-
- # Check if more than 1 player has highest points.
- if no_of_winners > 1:
- word = "You guys"
- winners = []
- points_copy = list(player_data.values()).copy()
-
- for _ in range(no_of_winners):
- index = points_copy.index(highest_points)
- winners.append(list(player_data.keys())[index])
- points_copy[index] = 0
-
- winners_mention = " ".join(winner.mention for winner in winners)
- else:
- word = "You"
- author_index = list(player_data.values()).index(highest_points)
- winner = list(player_data.keys())[author_index]
- winners_mention = winner.mention
-
- await channel.send(
- f"Congratulations {winners_mention} :tada: "
- f"{word} have won this quiz game with a grand total of {highest_points} points!"
- )
-
- def category_embed(self) -> discord.Embed:
- """Build an embed showing all available trivia categories."""
- embed = discord.Embed(colour=discord.Colour.blue())
- embed.title = "The available question categories are:"
- embed.set_footer(text="If a category is not chosen, a random one will be selected.")
- embed.description = ""
-
- for cat, description in self.categories.items():
- embed.description += f"**- {cat.capitalize()}**\n{description.capitalize()}\n"
-
- return embed
-
- @staticmethod
- async def send_answer(channel: discord.TextChannel, question_dict: dict) -> None:
- """Send the correct answer of a question to the game channel."""
- answer = question_dict["answer"]
- info = question_dict["info"]
- embed = discord.Embed(color=discord.Colour.red())
- embed.title = f"The correct answer is **{answer}**\n"
- embed.description = ""
-
- if info != "":
- embed.description += f"**Information**\n{info}\n\n"
-
- embed.description += "Let's move to the next question.\nRemaining questions: "
- await channel.send(embed=embed)
-
-
-def setup(bot: commands.Bot) -> None:
- """Load the cog."""
- bot.add_cog(TriviaQuiz(bot))
- logger.debug("TriviaQuiz cog loaded")
diff --git a/bot/seasons/evergreen/uptime.py b/bot/seasons/evergreen/uptime.py
deleted file mode 100644
index 6f24f545..00000000
--- a/bot/seasons/evergreen/uptime.py
+++ /dev/null
@@ -1,34 +0,0 @@
-import logging
-
-import arrow
-from dateutil.relativedelta import relativedelta
-from discord.ext import commands
-
-from bot import start_time
-
-log = logging.getLogger(__name__)
-
-
-class Uptime(commands.Cog):
- """A cog for posting the bot's uptime."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(name="uptime")
- async def uptime(self, ctx: commands.Context) -> None:
- """Responds with the uptime of the bot."""
- difference = relativedelta(start_time - arrow.utcnow())
- uptime_string = start_time.shift(
- seconds=-difference.seconds,
- minutes=-difference.minutes,
- hours=-difference.hours,
- days=-difference.days
- ).humanize()
- await ctx.send(f"I started up {uptime_string}.")
-
-
-def setup(bot: commands.Bot) -> None:
- """Uptime Cog load."""
- bot.add_cog(Uptime(bot))
- log.info("Uptime cog loaded")
diff --git a/bot/seasons/halloween/8ball.py b/bot/seasons/halloween/8ball.py
deleted file mode 100644
index 2e1c2804..00000000
--- a/bot/seasons/halloween/8ball.py
+++ /dev/null
@@ -1,34 +0,0 @@
-import asyncio
-import json
-import logging
-import random
-from pathlib import Path
-
-from discord.ext import commands
-
-log = logging.getLogger(__name__)
-
-with open(Path("bot/resources/halloween/responses.json"), "r", encoding="utf8") as f:
- responses = json.load(f)
-
-
-class SpookyEightBall(commands.Cog):
- """Spooky Eightball answers."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(aliases=('spooky8ball',))
- async def spookyeightball(self, ctx: commands.Context, *, question: str) -> None:
- """Responds with a random response to a question."""
- choice = random.choice(responses['responses'])
- msg = await ctx.send(choice[0])
- if len(choice) > 1:
- await asyncio.sleep(random.randint(2, 5))
- await msg.edit(content=f"{choice[0]} \n{choice[1]}")
-
-
-def setup(bot: commands.Bot) -> None:
- """Spooky Eight Ball Cog Load."""
- bot.add_cog(SpookyEightBall(bot))
- log.info("SpookyEightBall cog loaded")
diff --git a/bot/seasons/halloween/__init__.py b/bot/seasons/halloween/__init__.py
deleted file mode 100644
index c81879d7..00000000
--- a/bot/seasons/halloween/__init__.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from bot.constants import Colours
-from bot.seasons import SeasonBase
-
-
-class Halloween(SeasonBase):
- """
- Halloween Seasonal event attributes.
-
- Announcement for this cog temporarily disabled, since we're doing a custom
- Hacktoberfest announcement. If you're enabling the announcement again,
- make sure to update this docstring accordingly.
- """
-
- name = "halloween"
- bot_name = "NeonBot"
- greeting = "Happy Halloween!"
-
- start_date = "01/10"
- end_date = "01/11"
-
- colour = Colours.pink
- icon = (
- "/logos/logo_seasonal/hacktober/hacktoberfest.png",
- )
diff --git a/bot/seasons/halloween/candy_collection.py b/bot/seasons/halloween/candy_collection.py
deleted file mode 100644
index 490609dd..00000000
--- a/bot/seasons/halloween/candy_collection.py
+++ /dev/null
@@ -1,221 +0,0 @@
-import functools
-import json
-import logging
-import os
-import random
-from typing import List, Union
-
-import discord
-from discord.ext import commands
-
-from bot.constants import Channels
-
-log = logging.getLogger(__name__)
-
-json_location = os.path.join("bot", "resources", "halloween", "candy_collection.json")
-
-# chance is 1 in x range, so 1 in 20 range would give 5% chance (for add candy)
-ADD_CANDY_REACTION_CHANCE = 20 # 5%
-ADD_CANDY_EXISTING_REACTION_CHANCE = 10 # 10%
-ADD_SKULL_REACTION_CHANCE = 50 # 2%
-ADD_SKULL_EXISTING_REACTION_CHANCE = 20 # 5%
-
-
-class CandyCollection(commands.Cog):
- """Candy collection game Cog."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- with open(json_location) as candy:
- self.candy_json = json.load(candy)
- self.msg_reacted = self.candy_json['msg_reacted']
- self.get_candyinfo = dict()
- for userinfo in self.candy_json['records']:
- userid = userinfo['userid']
- self.get_candyinfo[userid] = userinfo
-
- @commands.Cog.listener()
- async def on_message(self, message: discord.Message) -> None:
- """Randomly adds candy or skull reaction to non-bot messages in the Event channel."""
- # make sure its a human message
- if message.author.bot:
- return
- # ensure it's hacktober channel
- if message.channel.id != Channels.seasonalbot_commands:
- return
-
- # do random check for skull first as it has the lower chance
- if random.randint(1, ADD_SKULL_REACTION_CHANCE) == 1:
- d = {"reaction": '\N{SKULL}', "msg_id": message.id, "won": False}
- self.msg_reacted.append(d)
- return await message.add_reaction('\N{SKULL}')
- # check for the candy chance next
- if random.randint(1, ADD_CANDY_REACTION_CHANCE) == 1:
- d = {"reaction": '\N{CANDY}', "msg_id": message.id, "won": False}
- self.msg_reacted.append(d)
- return await message.add_reaction('\N{CANDY}')
-
- @commands.Cog.listener()
- async def on_reaction_add(self, reaction: discord.Reaction, user: discord.Member) -> None:
- """Add/remove candies from a person if the reaction satisfies criteria."""
- message = reaction.message
- # check to ensure the reactor is human
- if user.bot:
- return
-
- # check to ensure it is in correct channel
- if message.channel.id != Channels.seasonalbot_commands:
- return
-
- # if its not a candy or skull, and it is one of 10 most recent messages,
- # proceed to add a skull/candy with higher chance
- if str(reaction.emoji) not in ('\N{SKULL}', '\N{CANDY}'):
- if message.id in await self.ten_recent_msg():
- await self.reacted_msg_chance(message)
- return
-
- for react in self.msg_reacted:
- # check to see if the message id of a message we added a
- # reaction to is in json file, and if nobody has won/claimed it yet
- if react['msg_id'] == message.id and react['won'] is False:
- react['user_reacted'] = user.id
- react['won'] = True
- try:
- # if they have record/candies in json already it will do this
- user_records = self.get_candyinfo[user.id]
- if str(reaction.emoji) == '\N{CANDY}':
- user_records['record'] += 1
- if str(reaction.emoji) == '\N{SKULL}':
- if user_records['record'] <= 3:
- user_records['record'] = 0
- lost = 'all of your'
- else:
- lost = random.randint(1, 3)
- user_records['record'] -= lost
- await self.send_spook_msg(message.author, message.channel, lost)
-
- except KeyError:
- # otherwise it will raise KeyError so we need to add them to file
- if str(reaction.emoji) == '\N{CANDY}':
- print('ok')
- d = {"userid": user.id, "record": 1}
- self.candy_json['records'].append(d)
- await self.remove_reactions(reaction)
-
- async def reacted_msg_chance(self, message: discord.Message) -> None:
- """
- Randomly add a skull or candy reaction to a message if there is a reaction there already.
-
- This event has a higher probability of occurring than a reaction add to a message without an
- existing reaction.
- """
- if random.randint(1, ADD_SKULL_EXISTING_REACTION_CHANCE) == 1:
- d = {"reaction": '\N{SKULL}', "msg_id": message.id, "won": False}
- self.msg_reacted.append(d)
- return await message.add_reaction('\N{SKULL}')
-
- if random.randint(1, ADD_CANDY_EXISTING_REACTION_CHANCE) == 1:
- d = {"reaction": '\N{CANDY}', "msg_id": message.id, "won": False}
- self.msg_reacted.append(d)
- return await message.add_reaction('\N{CANDY}')
-
- async def ten_recent_msg(self) -> List[int]:
- """Get the last 10 messages sent in the channel."""
- ten_recent = []
- recent_msg_id = max(
- message.id for message in self.bot._connection._messages
- if message.channel.id == Channels.seasonalbot_commands
- )
-
- channel = await self.hacktober_channel()
- ten_recent.append(recent_msg_id)
-
- for i in range(9):
- o = discord.Object(id=recent_msg_id + i)
- msg = await next(channel.history(limit=1, before=o))
- ten_recent.append(msg.id)
-
- return ten_recent
-
- async def get_message(self, msg_id: int) -> Union[discord.Message, None]:
- """Get the message from its ID."""
- try:
- o = discord.Object(id=msg_id + 1)
- # Use history rather than get_message due to
- # poor ratelimit (50/1s vs 1/1s)
- msg = await next(self.hacktober_channel.history(limit=1, before=o))
-
- if msg.id != msg_id:
- return None
-
- return msg
-
- except Exception:
- return None
-
- async def hacktober_channel(self) -> discord.TextChannel:
- """Get #hacktoberbot channel from its ID."""
- return self.bot.get_channel(id=Channels.seasonalbot_commands)
-
- async def remove_reactions(self, reaction: discord.Reaction) -> None:
- """Remove all candy/skull reactions."""
- try:
- async for user in reaction.users():
- await reaction.message.remove_reaction(reaction.emoji, user)
-
- except discord.HTTPException:
- pass
-
- async def send_spook_msg(self, author: discord.Member, channel: discord.TextChannel, candies: int) -> None:
- """Send a spooky message."""
- e = discord.Embed(colour=author.colour)
- e.set_author(name="Ghosts and Ghouls and Jack o' lanterns at night; "
- f"I took {candies} candies and quickly took flight.")
- await channel.send(embed=e)
-
- def save_to_json(self) -> None:
- """Save JSON to a local file."""
- with open(json_location, 'w') as outfile:
- json.dump(self.candy_json, outfile)
-
- @commands.command()
- async def candy(self, ctx: commands.Context) -> None:
- """Get the candy leaderboard and save to JSON."""
- # Use run_in_executor to prevent blocking
- thing = functools.partial(self.save_to_json)
- await self.bot.loop.run_in_executor(None, thing)
-
- emoji = (
- '\N{FIRST PLACE MEDAL}',
- '\N{SECOND PLACE MEDAL}',
- '\N{THIRD PLACE MEDAL}',
- '\N{SPORTS MEDAL}',
- '\N{SPORTS MEDAL}'
- )
-
- top_sorted = sorted(self.candy_json['records'], key=lambda k: k.get('record', 0), reverse=True)
- top_five = top_sorted[:5]
-
- usersid = []
- records = []
- for record in top_five:
- usersid.append(record['userid'])
- records.append(record['record'])
-
- value = '\n'.join(f'{emoji[index]} <@{usersid[index]}>: {records[index]}'
- for index in range(0, len(usersid))) or 'No Candies'
-
- e = discord.Embed(colour=discord.Colour.blurple())
- e.add_field(name="Top Candy Records", value=value, inline=False)
- e.add_field(name='\u200b',
- value=f"Candies will randomly appear on messages sent. "
- f"\nHit the candy when it appears as fast as possible to get the candy! "
- f"\nBut beware the ghosts...",
- inline=False)
- await ctx.send(embed=e)
-
-
-def setup(bot: commands.Bot) -> None:
- """Candy Collection game Cog load."""
- bot.add_cog(CandyCollection(bot))
- log.info("CandyCollection cog loaded")
diff --git a/bot/seasons/halloween/hacktober-issue-finder.py b/bot/seasons/halloween/hacktober-issue-finder.py
deleted file mode 100644
index 10732374..00000000
--- a/bot/seasons/halloween/hacktober-issue-finder.py
+++ /dev/null
@@ -1,107 +0,0 @@
-import datetime
-import logging
-import random
-from typing import Dict, Optional
-
-import aiohttp
-import discord
-from discord.ext import commands
-
-log = logging.getLogger(__name__)
-
-URL = "https://api.github.com/search/issues?per_page=100&q=is:issue+label:hacktoberfest+language:python+state:open"
-HEADERS = {"Accept": "application / vnd.github.v3 + json"}
-
-
-class HacktoberIssues(commands.Cog):
- """Find a random hacktober python issue on GitHub."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.cache_normal = None
- self.cache_timer_normal = datetime.datetime(1, 1, 1)
- self.cache_beginner = None
- self.cache_timer_beginner = datetime.datetime(1, 1, 1)
-
- @commands.command()
- async def hacktoberissues(self, ctx: commands.Context, option: str = "") -> None:
- """
- Get a random python hacktober issue from Github.
-
- If the command is run with beginner (`.hacktoberissues beginner`):
- It will also narrow it down to the "first good issue" label.
- """
- with ctx.typing():
- issues = await self.get_issues(ctx, option)
- if issues is None:
- return
- issue = random.choice(issues["items"])
- embed = self.format_embed(issue)
- await ctx.send(embed=embed)
-
- async def get_issues(self, ctx: commands.Context, option: str) -> Optional[Dict]:
- """Get a list of the python issues with the label 'hacktoberfest' from the Github api."""
- if option == "beginner":
- if (ctx.message.created_at - self.cache_timer_beginner).seconds <= 60:
- log.debug("using cache")
- return self.cache_beginner
- elif (ctx.message.created_at - self.cache_timer_normal).seconds <= 60:
- log.debug("using cache")
- return self.cache_normal
-
- async with aiohttp.ClientSession() as session:
- if option == "beginner":
- url = URL + '+label:"good first issue"'
- if self.cache_beginner is not None:
- page = random.randint(1, min(1000, self.cache_beginner["total_count"]) // 100)
- url += f"&page={page}"
- else:
- url = URL
- if self.cache_normal is not None:
- page = random.randint(1, min(1000, self.cache_normal["total_count"]) // 100)
- url += f"&page={page}"
-
- log.debug(f"making api request to url: {url}")
- async with session.get(url, headers=HEADERS) as response:
- if response.status != 200:
- log.error(f"expected 200 status (got {response.status}) from the GitHub api.")
- await ctx.send(f"ERROR: expected 200 status (got {response.status}) from the GitHub api.")
- await ctx.send(await response.text())
- return None
- data = await response.json()
-
- if len(data["items"]) == 0:
- log.error(f"no issues returned from GitHub api. with url: {response.url}")
- await ctx.send(f"ERROR: no issues returned from GitHub api. with url: {response.url}")
- return None
-
- if option == "beginner":
- self.cache_beginner = data
- self.cache_timer_beginner = ctx.message.created_at
- else:
- self.cache_normal = data
- self.cache_timer_normal = ctx.message.created_at
-
- return data
-
- @staticmethod
- def format_embed(issue: Dict) -> discord.Embed:
- """Format the issue data into a embed."""
- title = issue["title"]
- issue_url = issue["url"].replace("api.", "").replace("/repos/", "/")
- body = issue["body"]
- labels = [label["name"] for label in issue["labels"]]
-
- embed = discord.Embed(title=title)
- embed.description = body
- embed.add_field(name="labels", value="\n".join(labels))
- embed.url = issue_url
- embed.set_footer(text=issue_url)
-
- return embed
-
-
-def setup(bot: commands.Bot) -> None:
- """Hacktober issue finder Cog Load."""
- bot.add_cog(HacktoberIssues(bot))
- log.info("hacktober-issue-finder cog loaded")
diff --git a/bot/seasons/halloween/hacktoberstats.py b/bot/seasons/halloween/hacktoberstats.py
deleted file mode 100644
index d61e048b..00000000
--- a/bot/seasons/halloween/hacktoberstats.py
+++ /dev/null
@@ -1,340 +0,0 @@
-import json
-import logging
-import re
-from collections import Counter
-from datetime import datetime
-from pathlib import Path
-from typing import List, Tuple
-
-import aiohttp
-import discord
-from discord.ext import commands
-
-from bot.constants import Channels, WHITELISTED_CHANNELS
-from bot.decorators import override_in_channel
-from bot.utils.persist import make_persistent
-
-
-log = logging.getLogger(__name__)
-
-CURRENT_YEAR = datetime.now().year # Used to construct GH API query
-PRS_FOR_SHIRT = 4 # Minimum number of PRs before a shirt is awarded
-HACKTOBER_WHITELIST = WHITELISTED_CHANNELS + (Channels.hacktoberfest_2019,)
-
-
-class HacktoberStats(commands.Cog):
- """Hacktoberfest statistics Cog."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.link_json = make_persistent(Path("bot", "resources", "halloween", "github_links.json"))
- self.linked_accounts = self.load_linked_users()
-
- @commands.group(name="hacktoberstats", aliases=("hackstats",), invoke_without_command=True)
- @override_in_channel(HACKTOBER_WHITELIST)
- async def hacktoberstats_group(self, ctx: commands.Context, github_username: str = None) -> None:
- """
- Display an embed for a user's Hacktoberfest contributions.
-
- If invoked without a subcommand or github_username, get the invoking user's stats if they've
- linked their Discord name to GitHub using .stats link. If invoked with a github_username,
- get that user's contributions
- """
- if not github_username:
- author_id, author_mention = HacktoberStats._author_mention_from_context(ctx)
-
- if str(author_id) in self.linked_accounts.keys():
- github_username = self.linked_accounts[author_id]["github_username"]
- logging.info(f"Getting stats for {author_id} linked GitHub account '{github_username}'")
- else:
- msg = (
- f"{author_mention}, you have not linked a GitHub account\n\n"
- f"You can link your GitHub account using:\n```{ctx.prefix}hackstats link github_username```\n"
- f"Or query GitHub stats directly using:\n```{ctx.prefix}hackstats github_username```"
- )
- await ctx.send(msg)
- return
-
- await self.get_stats(ctx, github_username)
-
- @hacktoberstats_group.command(name="link")
- @override_in_channel(HACKTOBER_WHITELIST)
- async def link_user(self, ctx: commands.Context, github_username: str = None) -> None:
- """
- Link the invoking user's Github github_username to their Discord ID.
-
- Linked users are stored as a nested dict:
- {
- Discord_ID: {
- "github_username": str
- "date_added": datetime
- }
- }
- """
- author_id, author_mention = HacktoberStats._author_mention_from_context(ctx)
- if github_username:
- if str(author_id) in self.linked_accounts.keys():
- old_username = self.linked_accounts[author_id]["github_username"]
- logging.info(f"{author_id} has changed their github link from '{old_username}' to '{github_username}'")
- await ctx.send(f"{author_mention}, your GitHub username has been updated to: '{github_username}'")
- else:
- logging.info(f"{author_id} has added a github link to '{github_username}'")
- await ctx.send(f"{author_mention}, your GitHub username has been added")
-
- self.linked_accounts[author_id] = {
- "github_username": github_username,
- "date_added": datetime.now()
- }
-
- self.save_linked_users()
- else:
- logging.info(f"{author_id} tried to link a GitHub account but didn't provide a username")
- await ctx.send(f"{author_mention}, a GitHub username is required to link your account")
-
- @hacktoberstats_group.command(name="unlink")
- @override_in_channel(HACKTOBER_WHITELIST)
- async def unlink_user(self, ctx: commands.Context) -> None:
- """Remove the invoking user's account link from the log."""
- author_id, author_mention = HacktoberStats._author_mention_from_context(ctx)
-
- stored_user = self.linked_accounts.pop(author_id, None)
- if stored_user:
- await ctx.send(f"{author_mention}, your GitHub profile has been unlinked")
- logging.info(f"{author_id} has unlinked their GitHub account")
- else:
- await ctx.send(f"{author_mention}, you do not currently have a linked GitHub account")
- logging.info(f"{author_id} tried to unlink their GitHub account but no account was linked")
-
- self.save_linked_users()
-
- def load_linked_users(self) -> dict:
- """
- Load list of linked users from local JSON file.
-
- Linked users are stored as a nested dict:
- {
- Discord_ID: {
- "github_username": str
- "date_added": datetime
- }
- }
- """
- if self.link_json.exists():
- logging.info(f"Loading linked GitHub accounts from '{self.link_json}'")
- with open(self.link_json, 'r') as file:
- linked_accounts = json.load(file)
-
- logging.info(f"Loaded {len(linked_accounts)} linked GitHub accounts from '{self.link_json}'")
- return linked_accounts
- else:
- logging.info(f"Linked account log: '{self.link_json}' does not exist")
- return {}
-
- def save_linked_users(self) -> None:
- """
- Save list of linked users to local JSON file.
-
- Linked users are stored as a nested dict:
- {
- Discord_ID: {
- "github_username": str
- "date_added": datetime
- }
- }
- """
- logging.info(f"Saving linked_accounts to '{self.link_json}'")
- with open(self.link_json, 'w') as file:
- json.dump(self.linked_accounts, file, default=str)
- logging.info(f"linked_accounts saved to '{self.link_json}'")
-
- async def get_stats(self, ctx: commands.Context, github_username: str) -> None:
- """
- Query GitHub's API for PRs created by a GitHub user during the month of October.
-
- PRs with the 'invalid' tag are ignored
-
- If a valid github_username is provided, an embed is generated and posted to the channel
-
- Otherwise, post a helpful error message
- """
- async with ctx.typing():
- prs = await self.get_october_prs(github_username)
-
- if prs:
- stats_embed = self.build_embed(github_username, prs)
- await ctx.send('Here are some stats!', embed=stats_embed)
- else:
- await ctx.send(f"No October GitHub contributions found for '{github_username}'")
-
- def build_embed(self, github_username: str, prs: List[dict]) -> discord.Embed:
- """Return a stats embed built from github_username's PRs."""
- logging.info(f"Building Hacktoberfest embed for GitHub user: '{github_username}'")
- pr_stats = self._summarize_prs(prs)
-
- n = pr_stats['n_prs']
- if n >= PRS_FOR_SHIRT:
- shirtstr = f"**{github_username} has earned a tshirt!**"
- elif n == PRS_FOR_SHIRT - 1:
- shirtstr = f"**{github_username} is 1 PR away from a tshirt!**"
- else:
- shirtstr = f"**{github_username} is {PRS_FOR_SHIRT - n} PRs away from a tshirt!**"
-
- stats_embed = discord.Embed(
- title=f"{github_username}'s Hacktoberfest",
- color=discord.Color(0x9c4af7),
- description=(
- f"{github_username} has made {n} "
- f"{HacktoberStats._contributionator(n)} in "
- f"October\n\n"
- f"{shirtstr}\n\n"
- )
- )
-
- stats_embed.set_thumbnail(url=f"https://www.github.com/{github_username}.png")
- stats_embed.set_author(
- name="Hacktoberfest",
- url="https://hacktoberfest.digitalocean.com",
- icon_url="https://hacktoberfest.digitalocean.com/pretty_logo.png"
- )
- stats_embed.add_field(
- name="Top 5 Repositories:",
- value=self._build_top5str(pr_stats)
- )
-
- logging.info(f"Hacktoberfest PR built for GitHub user '{github_username}'")
- return stats_embed
-
- @staticmethod
- async def get_october_prs(github_username: str) -> List[dict]:
- """
- Query GitHub's API for PRs created during the month of October by github_username.
-
- PRs with an 'invalid' tag are ignored
-
- If PRs are found, return a list of dicts with basic PR information
-
- For each PR:
- {
- "repo_url": str
- "repo_shortname": str (e.g. "python-discord/seasonalbot")
- "created_at": datetime.datetime
- }
-
- Otherwise, return None
- """
- logging.info(f"Generating Hacktoberfest PR query for GitHub user: '{github_username}'")
- base_url = "https://api.github.com/search/issues?q="
- not_label = "invalid"
- action_type = "pr"
- is_query = f"public+author:{github_username}"
- not_query = "draft"
- date_range = f"{CURRENT_YEAR}-10-01T00:00:00%2B14:00..{CURRENT_YEAR}-10-31T23:59:59-11:00"
- per_page = "300"
- query_url = (
- f"{base_url}"
- f"-label:{not_label}"
- f"+type:{action_type}"
- f"+is:{is_query}"
- f"+-is:{not_query}"
- f"+created:{date_range}"
- f"&per_page={per_page}"
- )
-
- headers = {"user-agent": "Discord Python Hacktoberbot"}
- async with aiohttp.ClientSession() as session:
- async with session.get(query_url, headers=headers) as resp:
- jsonresp = await resp.json()
-
- if "message" in jsonresp.keys():
- # One of the parameters is invalid, short circuit for now
- api_message = jsonresp["errors"][0]["message"]
- logging.error(f"GitHub API request for '{github_username}' failed with message: {api_message}")
- return
- else:
- if jsonresp["total_count"] == 0:
- # Short circuit if there aren't any PRs
- logging.info(f"No Hacktoberfest PRs found for GitHub user: '{github_username}'")
- return
- else:
- logging.info(f"Found {len(jsonresp['items'])} Hacktoberfest PRs for GitHub user: '{github_username}'")
- outlist = []
- for item in jsonresp["items"]:
- shortname = HacktoberStats._get_shortname(item["repository_url"])
- itemdict = {
- "repo_url": f"https://www.github.com/{shortname}",
- "repo_shortname": shortname,
- "created_at": datetime.strptime(
- item["created_at"], r"%Y-%m-%dT%H:%M:%SZ"
- ),
- }
- outlist.append(itemdict)
- return outlist
-
- @staticmethod
- def _get_shortname(in_url: str) -> str:
- """
- Extract shortname from https://api.github.com/repos/* URL.
-
- e.g. "https://api.github.com/repos/python-discord/seasonalbot"
- |
- V
- "python-discord/seasonalbot"
- """
- exp = r"https?:\/\/api.github.com\/repos\/([/\-\_\.\w]+)"
- return re.findall(exp, in_url)[0]
-
- @staticmethod
- def _summarize_prs(prs: List[dict]) -> dict:
- """
- Generate statistics from an input list of PR dictionaries, as output by get_october_prs.
-
- Return a dictionary containing:
- {
- "n_prs": int
- "top5": [(repo_shortname, ncontributions), ...]
- }
- """
- contributed_repos = [pr["repo_shortname"] for pr in prs]
- return {"n_prs": len(prs), "top5": Counter(contributed_repos).most_common(5)}
-
- @staticmethod
- def _build_top5str(stats: List[tuple]) -> str:
- """
- Build a string from the Top 5 contributions that is compatible with a discord.Embed field.
-
- Top 5 contributions should be a list of tuples, as output in the stats dictionary by
- _summarize_prs
-
- String is of the form:
- n contribution(s) to [shortname](url)
- ...
- """
- base_url = "https://www.github.com/"
- contributionstrs = []
- for repo in stats['top5']:
- n = repo[1]
- contributionstrs.append(f"{n} {HacktoberStats._contributionator(n)} to [{repo[0]}]({base_url}{repo[0]})")
-
- return "\n".join(contributionstrs)
-
- @staticmethod
- def _contributionator(n: int) -> str:
- """Return "contribution" or "contributions" based on the value of n."""
- if n == 1:
- return "contribution"
- else:
- return "contributions"
-
- @staticmethod
- def _author_mention_from_context(ctx: commands.Context) -> Tuple:
- """Return stringified Message author ID and mentionable string from commands.Context."""
- author_id = str(ctx.message.author.id)
- author_mention = ctx.message.author.mention
-
- return author_id, author_mention
-
-
-def setup(bot: commands.Bot) -> None:
- """Hacktoberstats Cog load."""
- bot.add_cog(HacktoberStats(bot))
- log.info("HacktoberStats cog loaded")
diff --git a/bot/seasons/halloween/halloween_facts.py b/bot/seasons/halloween/halloween_facts.py
deleted file mode 100644
index 94730d9e..00000000
--- a/bot/seasons/halloween/halloween_facts.py
+++ /dev/null
@@ -1,68 +0,0 @@
-import json
-import logging
-import random
-from datetime import timedelta
-from pathlib import Path
-from typing import Tuple
-
-import discord
-from discord.ext import commands
-
-from bot.constants import Channels
-
-log = logging.getLogger(__name__)
-
-SPOOKY_EMOJIS = [
- "\N{BAT}",
- "\N{DERELICT HOUSE BUILDING}",
- "\N{EXTRATERRESTRIAL ALIEN}",
- "\N{GHOST}",
- "\N{JACK-O-LANTERN}",
- "\N{SKULL}",
- "\N{SKULL AND CROSSBONES}",
- "\N{SPIDER WEB}",
-]
-PUMPKIN_ORANGE = discord.Color(0xFF7518)
-INTERVAL = timedelta(hours=6).total_seconds()
-
-
-class HalloweenFacts(commands.Cog):
- """A Cog for displaying interesting facts about Halloween."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- with open(Path("bot/resources/halloween/halloween_facts.json"), "r") as file:
- self.halloween_facts = json.load(file)
- self.channel = None
- self.facts = list(enumerate(self.halloween_facts))
- random.shuffle(self.facts)
-
- @commands.Cog.listener()
- async def on_ready(self) -> None:
- """Get event Channel object and initialize fact task loop."""
- self.channel = self.bot.get_channel(Channels.seasonalbot_commands)
- self.bot.loop.create_task(self._fact_publisher_task())
-
- def random_fact(self) -> Tuple[int, str]:
- """Return a random fact from the loaded facts."""
- return random.choice(self.facts)
-
- @commands.command(name="spookyfact", aliases=("halloweenfact",), brief="Get the most recent Halloween fact")
- async def get_random_fact(self, ctx: commands.Context) -> None:
- """Reply with the most recent Halloween fact."""
- index, fact = self.random_fact()
- embed = self._build_embed(index, fact)
- await ctx.send(embed=embed)
-
- @staticmethod
- def _build_embed(index: int, fact: str) -> discord.Embed:
- """Builds a Discord embed from the given fact and its index."""
- emoji = random.choice(SPOOKY_EMOJIS)
- title = f"{emoji} Halloween Fact #{index + 1}"
- return discord.Embed(title=title, description=fact, color=PUMPKIN_ORANGE)
-
-
-def setup(bot: commands.Bot) -> None:
- """Halloween facts Cog load."""
- bot.add_cog(HalloweenFacts(bot))
- log.info("HalloweenFacts cog loaded")
diff --git a/bot/seasons/halloween/halloweenify.py b/bot/seasons/halloween/halloweenify.py
deleted file mode 100644
index dfcc2b1e..00000000
--- a/bot/seasons/halloween/halloweenify.py
+++ /dev/null
@@ -1,52 +0,0 @@
-import logging
-from json import load
-from pathlib import Path
-from random import choice
-
-import discord
-from discord.ext import commands
-from discord.ext.commands.cooldowns import BucketType
-
-log = logging.getLogger(__name__)
-
-
-class Halloweenify(commands.Cog):
- """A cog to change a invokers nickname to a spooky one!"""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.cooldown(1, 300, BucketType.user)
- @commands.command()
- async def halloweenify(self, ctx: commands.Context) -> None:
- """Change your nickname into a much spookier one!"""
- async with ctx.typing():
- with open(Path("bot/resources/halloween/halloweenify.json"), "r") as f:
- data = load(f)
-
- # Choose a random character from our list we loaded above and set apart the nickname and image url.
- character = choice(data["characters"])
- nickname = ''.join([nickname for nickname in character])
- image = ''.join([character[nickname] for nickname in character])
-
- # Build up a Embed
- embed = discord.Embed()
- embed.colour = discord.Colour.dark_orange()
- embed.title = "Not spooky enough?"
- embed.description = (
- f"**{ctx.author.display_name}** wasn\'t spooky enough for you? That\'s understandable, "
- f"{ctx.author.display_name} isn\'t scary at all! "
- "Let me think of something better. Hmm... I got it!\n\n "
- f"Your new nickname will be: \n :ghost: **{nickname}** :jack_o_lantern:"
- )
- embed.set_image(url=image)
-
- await ctx.author.edit(nick=nickname)
-
- await ctx.send(embed=embed)
-
-
-def setup(bot: commands.Bot) -> None:
- """Halloweenify Cog load."""
- bot.add_cog(Halloweenify(bot))
- log.info("Halloweenify cog loaded")
diff --git a/bot/seasons/halloween/monsterbio.py b/bot/seasons/halloween/monsterbio.py
deleted file mode 100644
index bfa8a026..00000000
--- a/bot/seasons/halloween/monsterbio.py
+++ /dev/null
@@ -1,56 +0,0 @@
-import json
-import logging
-import random
-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/halloween/monster.json"), "r", encoding="utf8") as f:
- TEXT_OPTIONS = json.load(f) # Data for a mad-lib style generation of text
-
-
-class MonsterBio(commands.Cog):
- """A cog that generates a spooky monster biography."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- def generate_name(self, seeded_random: random.Random) -> str:
- """Generates a name (for either monster species or monster name)."""
- n_candidate_strings = seeded_random.randint(2, len(TEXT_OPTIONS["monster_type"]))
- return "".join(seeded_random.choice(TEXT_OPTIONS["monster_type"][i]) for i in range(n_candidate_strings))
-
- @commands.command(brief="Sends your monster bio!")
- async def monsterbio(self, ctx: commands.Context) -> None:
- """Sends a description of a monster."""
- seeded_random = random.Random(ctx.message.author.id) # Seed a local Random instance rather than the system one
-
- name = self.generate_name(seeded_random)
- species = self.generate_name(seeded_random)
- biography_text = seeded_random.choice(TEXT_OPTIONS["biography_text"])
- words = {"monster_name": name, "monster_species": species}
- for key, value in biography_text.items():
- if key == "text":
- continue
-
- options = seeded_random.sample(TEXT_OPTIONS[key], value)
- words[key] = ' '.join(options)
-
- embed = discord.Embed(
- title=f"{name}'s Biography",
- color=seeded_random.choice([Colours.orange, Colours.purple]),
- description=biography_text["text"].format_map(words),
- )
-
- await ctx.send(embed=embed)
-
-
-def setup(bot: commands.Bot) -> None:
- """Monster bio Cog load."""
- bot.add_cog(MonsterBio(bot))
- log.info("MonsterBio cog loaded.")
diff --git a/bot/seasons/halloween/monstersurvey.py b/bot/seasons/halloween/monstersurvey.py
deleted file mode 100644
index 12e1d022..00000000
--- a/bot/seasons/halloween/monstersurvey.py
+++ /dev/null
@@ -1,206 +0,0 @@
-import json
-import logging
-import os
-
-from discord import Embed
-from discord.ext import commands
-from discord.ext.commands import Bot, Cog, Context
-
-log = logging.getLogger(__name__)
-
-EMOJIS = {
- 'SUCCESS': u'\u2705',
- 'ERROR': u'\u274C'
-}
-
-
-class MonsterSurvey(Cog):
- """
- Vote for your favorite monster.
-
- This Cog allows users to vote for their favorite listed monster.
-
- Users may change their vote, but only their current vote will be counted.
- """
-
- def __init__(self, bot: Bot):
- """Initializes values for the bot to use within the voting commands."""
- self.bot = bot
- self.registry_location = os.path.join(os.getcwd(), 'bot', 'resources', 'halloween', 'monstersurvey.json')
- with open(self.registry_location, 'r') as jason:
- self.voter_registry = json.load(jason)
-
- def json_write(self) -> None:
- """Write voting results to a local JSON file."""
- log.info("Saved Monster Survey Results")
- with open(self.registry_location, 'w') as jason:
- json.dump(self.voter_registry, jason, indent=2)
-
- def cast_vote(self, id: int, monster: str) -> None:
- """
- Cast a user's vote for the specified monster.
-
- If the user has already voted, their existing vote is removed.
- """
- vr = self.voter_registry
- for m in vr.keys():
- if id not in vr[m]['votes'] and m == monster:
- vr[m]['votes'].append(id)
- else:
- if id in vr[m]['votes'] and m != monster:
- vr[m]['votes'].remove(id)
-
- def get_name_by_leaderboard_index(self, n: int) -> str:
- """Return the monster at the specified leaderboard index."""
- n = n - 1
- vr = self.voter_registry
- top = sorted(vr, key=lambda k: len(vr[k]['votes']), reverse=True)
- name = top[n] if n >= 0 else None
- return name
-
- @commands.group(
- name='monster',
- aliases=('mon',)
- )
- async def monster_group(self, ctx: Context) -> None:
- """The base voting command. If nothing is called, then it will return an embed."""
- if ctx.invoked_subcommand is None:
- async with ctx.typing():
- default_embed = Embed(
- title='Monster Voting',
- color=0xFF6800,
- description='Vote for your favorite monster!'
- )
- default_embed.add_field(
- name='.monster show monster_name(optional)',
- value='Show a specific monster. If none is listed, it will give you an error with valid choices.',
- inline=False)
- default_embed.add_field(
- name='.monster vote monster_name',
- value='Vote for a specific monster. You get one vote, but can change it at any time.',
- inline=False
- )
- default_embed.add_field(
- name='.monster leaderboard',
- value='Which monster has the most votes? This command will tell you.',
- inline=False
- )
- default_embed.set_footer(text=f"Monsters choices are: {', '.join(self.voter_registry.keys())}")
-
- await ctx.send(embed=default_embed)
-
- @monster_group.command(
- name='vote'
- )
- async def monster_vote(self, ctx: Context, name: str = None) -> None:
- """
- Cast a vote for a particular monster.
-
- Displays a list of monsters that can be voted for if one is not specified.
- """
- if name is None:
- await ctx.invoke(self.monster_leaderboard)
- return
-
- async with ctx.typing():
- # Check to see if user used a numeric (leaderboard) index to vote
- try:
- idx = int(name)
- name = self.get_name_by_leaderboard_index(idx)
- except ValueError:
- name = name.lower()
-
- vote_embed = Embed(
- name='Monster Voting',
- color=0xFF6800
- )
-
- m = self.voter_registry.get(name)
- if m is None:
- vote_embed.description = f'You cannot vote for {name} because it\'s not in the running.'
- vote_embed.add_field(
- name='Use `.monster show {monster_name}` for more information on a specific monster',
- value='or use `.monster vote {monster}` to cast your vote for said monster.',
- inline=False
- )
- vote_embed.add_field(
- name='You may vote for or show the following monsters:',
- value=f"{', '.join(self.voter_registry.keys())}"
- )
- else:
- self.cast_vote(ctx.author.id, name)
- vote_embed.add_field(
- name='Vote successful!',
- value=f'You have successfully voted for {m["full_name"]}!',
- inline=False
- )
- vote_embed.set_thumbnail(url=m['image'])
- vote_embed.set_footer(text="Please note that any previous votes have been removed.")
- self.json_write()
-
- await ctx.send(embed=vote_embed)
-
- @monster_group.command(
- name='show'
- )
- async def monster_show(self, ctx: Context, name: str = None) -> None:
- """Shows the named monster. If one is not named, it sends the default voting embed instead."""
- if name is None:
- await ctx.invoke(self.monster_leaderboard)
- return
-
- async with ctx.typing():
- # Check to see if user used a numeric (leaderboard) index to vote
- try:
- idx = int(name)
- name = self.get_name_by_leaderboard_index(idx)
- except ValueError:
- name = name.lower()
-
- m = self.voter_registry.get(name)
- if not m:
- await ctx.send('That monster does not exist.')
- await ctx.invoke(self.monster_vote)
- return
-
- embed = Embed(title=m['full_name'], color=0xFF6800)
- embed.add_field(name='Summary', value=m['summary'])
- embed.set_image(url=m['image'])
- embed.set_footer(text=f'To vote for this monster, type .monster vote {name}')
-
- await ctx.send(embed=embed)
-
- @monster_group.command(
- name='leaderboard',
- aliases=('lb',)
- )
- async def monster_leaderboard(self, ctx: Context) -> None:
- """Shows the current standings."""
- async with ctx.typing():
- vr = self.voter_registry
- top = sorted(vr, key=lambda k: len(vr[k]['votes']), reverse=True)
- total_votes = sum(len(m['votes']) for m in self.voter_registry.values())
-
- embed = Embed(title="Monster Survey Leader Board", color=0xFF6800)
- for rank, m in enumerate(top):
- votes = len(vr[m]['votes'])
- percentage = ((votes / total_votes) * 100) if total_votes > 0 else 0
- embed.add_field(name=f"{rank+1}. {vr[m]['full_name']}",
- value=(
- f"{votes} votes. {percentage:.1f}% of total votes.\n"
- f"Vote for this monster by typing "
- f"'.monster vote {m}'\n"
- f"Get more information on this monster by typing "
- f"'.monster show {m}'"
- ),
- inline=False)
-
- embed.set_footer(text="You can also vote by their rank number. '.monster vote {number}' ")
-
- await ctx.send(embed=embed)
-
-
-def setup(bot: Bot) -> None:
- """Monster survey Cog load."""
- bot.add_cog(MonsterSurvey(bot))
- log.info("MonsterSurvey cog loaded")
diff --git a/bot/seasons/halloween/scarymovie.py b/bot/seasons/halloween/scarymovie.py
deleted file mode 100644
index 3823a3e4..00000000
--- a/bot/seasons/halloween/scarymovie.py
+++ /dev/null
@@ -1,132 +0,0 @@
-import logging
-import random
-from os import environ
-
-import aiohttp
-from discord import Embed
-from discord.ext import commands
-
-log = logging.getLogger(__name__)
-
-
-TMDB_API_KEY = environ.get('TMDB_API_KEY')
-TMDB_TOKEN = environ.get('TMDB_TOKEN')
-
-
-class ScaryMovie(commands.Cog):
- """Selects a random scary movie and embeds info into Discord chat."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(name='scarymovie', alias=['smovie'])
- async def random_movie(self, ctx: commands.Context) -> None:
- """Randomly select a scary movie and display information about it."""
- async with ctx.typing():
- selection = await self.select_movie()
- movie_details = await self.format_metadata(selection)
-
- await ctx.send(embed=movie_details)
-
- @staticmethod
- async def select_movie() -> dict:
- """Selects a random movie and returns a JSON of movie details from TMDb."""
- url = 'https://api.themoviedb.org/4/discover/movie'
- params = {
- 'with_genres': '27',
- 'vote_count.gte': '5'
- }
- headers = {
- 'Authorization': 'Bearer ' + TMDB_TOKEN,
- 'Content-Type': 'application/json;charset=utf-8'
- }
-
- # Get total page count of horror movies
- async with aiohttp.ClientSession() as session:
- response = await session.get(url=url, params=params, headers=headers)
- total_pages = await response.json()
- total_pages = total_pages.get('total_pages')
-
- # Get movie details from one random result on a random page
- params['page'] = random.randint(1, total_pages)
- response = await session.get(url=url, params=params, headers=headers)
- response = await response.json()
- selection_id = random.choice(response.get('results')).get('id')
-
- # Get full details and credits
- selection = await session.get(
- url='https://api.themoviedb.org/3/movie/' + str(selection_id),
- params={'api_key': TMDB_API_KEY, 'append_to_response': 'credits'}
- )
-
- return await selection.json()
-
- @staticmethod
- async def format_metadata(movie: dict) -> Embed:
- """Formats raw TMDb data to be embedded in Discord chat."""
- # Build the relevant URLs.
- movie_id = movie.get("id")
- poster_path = movie.get("poster_path")
- tmdb_url = f'https://www.themoviedb.org/movie/{movie_id}' if movie_id else None
- poster = f'https://image.tmdb.org/t/p/original{poster_path}' if poster_path else None
-
- # Get cast names
- cast = []
- for actor in movie.get('credits', {}).get('cast', [])[:3]:
- cast.append(actor.get('name'))
-
- # Get director name
- director = movie.get('credits', {}).get('crew', [])
- if director:
- director = director[0].get('name')
-
- # Determine the spookiness rating
- rating = ''
- rating_count = movie.get('vote_average', 0)
-
- if rating_count:
- rating_count /= 2
-
- for _ in range(int(rating_count)):
- rating += ':skull:'
- if (rating_count % 1) >= .5:
- rating += ':bat:'
-
- # Try to get year of release and runtime
- year = movie.get('release_date', [])[:4]
- runtime = movie.get('runtime')
- runtime = f"{runtime} minutes" if runtime else None
-
- # Not all these attributes will always be present
- movie_attributes = {
- "Directed by": director,
- "Starring": ', '.join(cast),
- "Running time": runtime,
- "Release year": year,
- "Spookiness rating": rating,
- }
-
- embed = Embed(
- colour=0x01d277,
- title='**' + movie.get('title') + '**',
- url=tmdb_url,
- description=movie.get('overview')
- )
-
- if poster:
- embed.set_image(url=poster)
-
- # Add the attributes that we actually have data for, but not the others.
- for name, value in movie_attributes.items():
- if value:
- embed.add_field(name=name, value=value)
-
- embed.set_footer(text='powered by themoviedb.org')
-
- return embed
-
-
-def setup(bot: commands.Bot) -> None:
- """Scary movie Cog load."""
- bot.add_cog(ScaryMovie(bot))
- log.info("ScaryMovie cog loaded")
diff --git a/bot/seasons/halloween/spookyavatar.py b/bot/seasons/halloween/spookyavatar.py
deleted file mode 100644
index 268de3fb..00000000
--- a/bot/seasons/halloween/spookyavatar.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import logging
-import os
-from io import BytesIO
-
-import aiohttp
-import discord
-from PIL import Image
-from discord.ext import commands
-
-from bot.utils.halloween import spookifications
-
-log = logging.getLogger(__name__)
-
-
-class SpookyAvatar(commands.Cog):
- """A cog that spookifies an avatar."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- async def get(self, url: str) -> bytes:
- """Returns the contents of the supplied URL."""
- async with aiohttp.ClientSession() as session:
- async with session.get(url) as resp:
- return await resp.read()
-
- @commands.command(name='savatar', aliases=('spookyavatar', 'spookify'),
- brief='Spookify an user\'s avatar.')
- async def spooky_avatar(self, ctx: commands.Context, user: discord.Member = None) -> None:
- """A command to print the user's spookified avatar."""
- if user is None:
- user = ctx.message.author
-
- async with ctx.typing():
- embed = discord.Embed(colour=0xFF0000)
- embed.title = "Is this you or am I just really paranoid?"
- embed.set_author(name=str(user.name), icon_url=user.avatar_url)
-
- image_bytes = await ctx.author.avatar_url.read()
- im = Image.open(BytesIO(image_bytes))
- modified_im = spookifications.get_random_effect(im)
- modified_im.save(str(ctx.message.id)+'.png')
- f = discord.File(str(ctx.message.id)+'.png')
- embed.set_image(url='attachment://'+str(ctx.message.id)+'.png')
-
- await ctx.send(file=f, embed=embed)
- os.remove(str(ctx.message.id)+'.png')
-
-
-def setup(bot: commands.Bot) -> None:
- """Spooky avatar Cog load."""
- bot.add_cog(SpookyAvatar(bot))
- log.info("SpookyAvatar cog loaded")
diff --git a/bot/seasons/halloween/spookygif.py b/bot/seasons/halloween/spookygif.py
deleted file mode 100644
index 818de8cd..00000000
--- a/bot/seasons/halloween/spookygif.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import logging
-
-import aiohttp
-import discord
-from discord.ext import commands
-
-from bot.constants import Tokens
-
-log = logging.getLogger(__name__)
-
-
-class SpookyGif(commands.Cog):
- """A cog to fetch a random spooky gif from the web!"""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(name="spookygif", aliases=("sgif", "scarygif"))
- async def spookygif(self, ctx: commands.Context) -> None:
- """Fetches a random gif from the GIPHY API and responds with it."""
- async with ctx.typing():
- async with aiohttp.ClientSession() as session:
- params = {'api_key': Tokens.giphy, 'tag': 'halloween', 'rating': 'g'}
- # Make a GET request to the Giphy API to get a random halloween gif.
- async with session.get('http://api.giphy.com/v1/gifs/random', params=params) as resp:
- data = await resp.json()
- url = data['data']['image_url']
-
- embed = discord.Embed(colour=0x9b59b6)
- embed.title = "A spooooky gif!"
- embed.set_image(url=url)
-
- await ctx.send(embed=embed)
-
-
-def setup(bot: commands.Bot) -> None:
- """Spooky GIF Cog load."""
- bot.add_cog(SpookyGif(bot))
- log.info("SpookyGif cog loaded")
diff --git a/bot/seasons/halloween/spookyrating.py b/bot/seasons/halloween/spookyrating.py
deleted file mode 100644
index 7f78f536..00000000
--- a/bot/seasons/halloween/spookyrating.py
+++ /dev/null
@@ -1,67 +0,0 @@
-import bisect
-import json
-import logging
-import random
-from pathlib import Path
-
-import discord
-from discord.ext import commands
-
-from bot.constants import Colours
-
-log = logging.getLogger(__name__)
-
-with Path("bot/resources/halloween/spooky_rating.json").open() as file:
- SPOOKY_DATA = json.load(file)
- SPOOKY_DATA = sorted((int(key), value) for key, value in SPOOKY_DATA.items())
-
-
-class SpookyRating(commands.Cog):
- """A cog for calculating one's spooky rating."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.local_random = random.Random()
-
- @commands.command()
- @commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
- async def spookyrating(self, ctx: commands.Context, who: discord.Member = None) -> None:
- """
- Calculates the spooky rating of someone.
-
- Any user will always yield the same result, no matter who calls the command
- """
- if who is None:
- who = ctx.author
-
- # This ensures that the same result over multiple runtimes
- self.local_random.seed(who.id)
- spooky_percent = self.local_random.randint(1, 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(SPOOKY_DATA, (spooky_percent,)) - 1
-
- _, data = SPOOKY_DATA[index]
-
- embed = discord.Embed(
- title=data['title'],
- description=f'{who} scored {spooky_percent}%!',
- color=Colours.orange
- )
- embed.add_field(
- name='A whisper from Satan',
- value=data['text']
- )
- embed.set_thumbnail(
- url=data['image']
- )
-
- await ctx.send(embed=embed)
-
-
-def setup(bot: commands.Bot) -> None:
- """Spooky Rating Cog load."""
- bot.add_cog(SpookyRating(bot))
- log.info("SpookyRating cog loaded")
diff --git a/bot/seasons/halloween/spookyreact.py b/bot/seasons/halloween/spookyreact.py
deleted file mode 100644
index 90b1254d..00000000
--- a/bot/seasons/halloween/spookyreact.py
+++ /dev/null
@@ -1,72 +0,0 @@
-import logging
-import re
-
-import discord
-from discord.ext.commands import Bot, Cog
-
-log = logging.getLogger(__name__)
-
-SPOOKY_TRIGGERS = {
- 'spooky': (r"\bspo{2,}ky\b", "\U0001F47B"),
- 'skeleton': (r"\bskeleton\b", "\U0001F480"),
- 'doot': (r"\bdo{2,}t\b", "\U0001F480"),
- 'pumpkin': (r"\bpumpkin\b", "\U0001F383"),
- 'halloween': (r"\bhalloween\b", "\U0001F383"),
- 'jack-o-lantern': (r"\bjack-o-lantern\b", "\U0001F383"),
- 'danger': (r"\bdanger\b", "\U00002620")
-}
-
-
-class SpookyReact(Cog):
- """A cog that makes the bot react to message triggers."""
-
- def __init__(self, bot: Bot):
- self.bot = bot
-
- @Cog.listener()
- async def on_message(self, ctx: discord.Message) -> None:
- """
- A command to send the seasonalbot github project.
-
- Lines that begin with the bot's command prefix are ignored
-
- Seasonalbot's own messages are ignored
- """
- for trigger in SPOOKY_TRIGGERS.keys():
- trigger_test = re.search(SPOOKY_TRIGGERS[trigger][0], ctx.content.lower())
- if trigger_test:
- # Check message for bot replies and/or command invocations
- # Short circuit if they're found, logging is handled in _short_circuit_check
- if await self._short_circuit_check(ctx):
- return
- else:
- await ctx.add_reaction(SPOOKY_TRIGGERS[trigger][1])
- logging.info(f"Added '{trigger}' reaction to message ID: {ctx.id}")
-
- async def _short_circuit_check(self, ctx: discord.Message) -> bool:
- """
- Short-circuit helper check.
-
- Return True if:
- * author is the bot
- * prefix is not None
- """
- # Check for self reaction
- if ctx.author == self.bot.user:
- logging.debug(f"Ignoring reactions on self message. Message ID: {ctx.id}")
- return True
-
- # Check for command invocation
- # Because on_message doesn't give a full Context object, generate one first
- tmp_ctx = await self.bot.get_context(ctx)
- if tmp_ctx.prefix:
- logging.debug(f"Ignoring reactions on command invocation. Message ID: {ctx.id}")
- return True
-
- return False
-
-
-def setup(bot: Bot) -> None:
- """Spooky reaction Cog load."""
- bot.add_cog(SpookyReact(bot))
- log.info("SpookyReact cog loaded")
diff --git a/bot/seasons/halloween/spookysound.py b/bot/seasons/halloween/spookysound.py
deleted file mode 100644
index e0676d0a..00000000
--- a/bot/seasons/halloween/spookysound.py
+++ /dev/null
@@ -1,48 +0,0 @@
-import logging
-import random
-from pathlib import Path
-
-import discord
-from discord.ext import commands
-
-from bot.constants import Hacktoberfest
-
-log = logging.getLogger(__name__)
-
-
-class SpookySound(commands.Cog):
- """A cog that plays a spooky sound in a voice channel on command."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.sound_files = list(Path("bot/resources/halloween/spookysounds").glob("*.mp3"))
- self.channel = None
-
- @commands.cooldown(rate=1, per=1)
- @commands.command(brief="Play a spooky sound, restricted to once per 2 mins")
- async def spookysound(self, ctx: commands.Context) -> None:
- """
- Connect to the Hacktoberbot voice channel, play a random spooky sound, then disconnect.
-
- Cannot be used more than once in 2 minutes.
- """
- if not self.channel:
- await self.bot.wait_until_ready()
- self.channel = self.bot.get_channel(Hacktoberfest.voice_id)
-
- await ctx.send("Initiating spooky sound...")
- file_path = random.choice(self.sound_files)
- src = discord.FFmpegPCMAudio(str(file_path.resolve()))
- voice = await self.channel.connect()
- voice.play(src, after=lambda e: self.bot.loop.create_task(self.disconnect(voice)))
-
- @staticmethod
- async def disconnect(voice: discord.VoiceClient) -> None:
- """Helper method to disconnect a given voice client."""
- await voice.disconnect()
-
-
-def setup(bot: commands.Bot) -> None:
- """Spooky sound Cog load."""
- bot.add_cog(SpookySound(bot))
- log.info("SpookySound cog loaded")
diff --git a/bot/seasons/halloween/timeleft.py b/bot/seasons/halloween/timeleft.py
deleted file mode 100644
index 8cb3f4f6..00000000
--- a/bot/seasons/halloween/timeleft.py
+++ /dev/null
@@ -1,60 +0,0 @@
-import logging
-from datetime import datetime
-from typing import Tuple
-
-from discord.ext import commands
-
-log = logging.getLogger(__name__)
-
-
-class TimeLeft(commands.Cog):
- """A Cog that tells you how long left until Hacktober is over!"""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @staticmethod
- def in_october() -> bool:
- """Return True if the current month is October."""
- return datetime.utcnow().month == 10
-
- @staticmethod
- def load_date() -> Tuple[int, datetime, datetime]:
- """Return of a tuple of the current time and the end and start times of the next October."""
- now = datetime.utcnow()
- year = now.year
- if now.month > 10:
- year += 1
- end = datetime(year, 11, 1, 11, 59, 59)
- start = datetime(year, 10, 1)
- return now, end, start
-
- @commands.command()
- async def timeleft(self, ctx: commands.Context) -> None:
- """
- Calculates the time left until the end of Hacktober.
-
- Whilst in October, displays the days, hours and minutes left.
- Only displays the days left until the beginning and end whilst in a different month
- """
- now, end, start = self.load_date()
- diff = end - now
- days, seconds = diff.days, diff.seconds
- if self.in_october():
- minutes = seconds // 60
- hours, minutes = divmod(minutes, 60)
- await ctx.send(f"There is currently only {days} days, {hours} hours and {minutes}"
- "minutes left until the end of Hacktober.")
- else:
- start_diff = start - now
- start_days = start_diff.days
- await ctx.send(
- f"It is not currently Hacktober. However, the next one will start in {start_days} days "
- f"and will finish in {days} days."
- )
-
-
-def setup(bot: commands.Bot) -> None:
- """Cog load."""
- bot.add_cog(TimeLeft(bot))
- log.info("TimeLeft cog loaded")
diff --git a/bot/seasons/pride/__init__.py b/bot/seasons/pride/__init__.py
deleted file mode 100644
index 08df2fa1..00000000
--- a/bot/seasons/pride/__init__.py
+++ /dev/null
@@ -1,36 +0,0 @@
-from bot.constants import Colours
-from bot.seasons import SeasonBase
-
-
-class Pride(SeasonBase):
- """
- The month of June is a special month for us at Python Discord.
-
- It is very important to us that everyone feels welcome here, no matter their origin,
- identity or sexuality. During the month of June, while some of you are participating in Pride
- festivals across the world, we will be celebrating individuality and commemorating the history
- and challenges of the LGBTQ+ community with a Pride event of our own!
-
- While this celebration takes place, you'll notice a few changes:
- • The server icon has changed to our Pride icon. Thanks to <@98694745760481280> for the design!
- • [Pride issues are now available for SeasonalBot on the repo](https://git.io/pythonpride).
- • You may see Pride-themed esoteric challenges and other microevents.
-
- If you'd like to contribute, head on over to <#635950537262759947> and we will help you get
- started. It doesn't matter if you're new to open source or Python, if you'd like to help, we
- will find you a task and teach you what you need to know.
- """
-
- name = "pride"
- bot_name = "ProudBot"
- greeting = "Happy Pride Month!"
-
- # Duration of season
- start_date = "01/06"
- end_date = "01/07"
-
- # Season logo
- colour = Colours.soft_red
- icon = (
- "/logos/logo_seasonal/pride/logo_pride.png",
- )
diff --git a/bot/seasons/pride/drag_queen_name.py b/bot/seasons/pride/drag_queen_name.py
deleted file mode 100644
index 43813fbd..00000000
--- a/bot/seasons/pride/drag_queen_name.py
+++ /dev/null
@@ -1,33 +0,0 @@
-import json
-import logging
-import random
-from pathlib import Path
-
-from discord.ext import commands
-
-log = logging.getLogger(__name__)
-
-
-class DragNames(commands.Cog):
- """Gives a random drag queen name!"""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.names = self.load_names()
-
- @staticmethod
- def load_names() -> list:
- """Loads a list of drag queen names."""
- with open(Path("bot/resources/pride/drag_queen_names.json"), "r", encoding="utf-8") as f:
- return json.load(f)
-
- @commands.command(name="dragname", aliases=["dragqueenname", "queenme"])
- async def dragname(self, ctx: commands.Context) -> None:
- """Sends a message with a drag queen name."""
- await ctx.send(random.choice(self.names))
-
-
-def setup(bot: commands.Bot) -> None:
- """Cog loader for drag queen name generator."""
- bot.add_cog(DragNames(bot))
- log.info("Drag queen name generator cog loaded!")
diff --git a/bot/seasons/pride/pride_anthem.py b/bot/seasons/pride/pride_anthem.py
deleted file mode 100644
index b0c6d34e..00000000
--- a/bot/seasons/pride/pride_anthem.py
+++ /dev/null
@@ -1,58 +0,0 @@
-import json
-import logging
-import random
-from pathlib import Path
-
-from discord.ext import commands
-
-log = logging.getLogger(__name__)
-
-
-class PrideAnthem(commands.Cog):
- """Embed a random youtube video for a gay anthem!"""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.anthems = self.load_vids()
-
- def get_video(self, genre: str = None) -> dict:
- """
- Picks a random anthem from the list.
-
- If `genre` is supplied, it will pick from videos attributed with that genre.
- If none can be found, it will log this as well as provide that information to the user.
- """
- if not genre:
- return random.choice(self.anthems)
- else:
- songs = [song for song in self.anthems if genre.casefold() in song["genre"]]
- try:
- return random.choice(songs)
- except IndexError:
- log.info("No videos for that genre.")
-
- @staticmethod
- def load_vids() -> list:
- """Loads a list of videos from the resources folder as dictionaries."""
- with open(Path("bot/resources/pride/anthems.json"), "r", encoding="utf-8") as f:
- anthems = json.load(f)
- return anthems
-
- @commands.command(name="prideanthem", aliases=["anthem", "pridesong"])
- async def prideanthem(self, ctx: commands.Context, genre: str = None) -> None:
- """
- Sends a message with a video of a random pride anthem.
-
- If `genre` is supplied, it will select from that genre only.
- """
- anthem = self.get_video(genre)
- if anthem:
- await ctx.send(anthem["url"])
- else:
- await ctx.send("I couldn't find a video, sorry!")
-
-
-def setup(bot: commands.Bot) -> None:
- """Cog loader for pride anthem."""
- bot.add_cog(PrideAnthem(bot))
- log.info("Pride anthems cog loaded!")
diff --git a/bot/seasons/pride/pride_avatar.py b/bot/seasons/pride/pride_avatar.py
deleted file mode 100644
index 85e49d5c..00000000
--- a/bot/seasons/pride/pride_avatar.py
+++ /dev/null
@@ -1,145 +0,0 @@
-import logging
-from io import BytesIO
-from pathlib import Path
-
-import discord
-from PIL import Image, ImageDraw
-from discord.ext import commands
-
-from bot.constants import Colours
-
-log = logging.getLogger(__name__)
-
-OPTIONS = {
- "agender": "agender",
- "androgyne": "androgyne",
- "androgynous": "androgyne",
- "aromantic": "aromantic",
- "aro": "aromantic",
- "ace": "asexual",
- "asexual": "asexual",
- "bigender": "bigender",
- "bisexual": "bisexual",
- "bi": "bisexual",
- "demiboy": "demiboy",
- "demigirl": "demigirl",
- "demi": "demisexual",
- "demisexual": "demisexual",
- "gay": "gay",
- "lgbt": "gay",
- "queer": "gay",
- "homosexual": "gay",
- "fluid": "genderfluid",
- "genderfluid": "genderfluid",
- "genderqueer": "genderqueer",
- "intersex": "intersex",
- "lesbian": "lesbian",
- "non-binary": "nonbinary",
- "enby": "nonbinary",
- "nb": "nonbinary",
- "nonbinary": "nonbinary",
- "omnisexual": "omnisexual",
- "omni": "omnisexual",
- "pansexual": "pansexual",
- "pan": "pansexual",
- "pangender": "pangender",
- "poly": "polysexual",
- "polysexual": "polysexual",
- "polyamory": "polyamory",
- "polyamorous": "polyamory",
- "transgender": "transgender",
- "trans": "transgender",
- "trigender": "trigender"
-}
-
-
-class PrideAvatar(commands.Cog):
- """Put an LGBT spin on your avatar!"""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @staticmethod
- def crop_avatar(avatar: Image) -> Image:
- """This crops the avatar into a circle."""
- mask = Image.new("L", avatar.size, 0)
- draw = ImageDraw.Draw(mask)
- draw.ellipse((0, 0) + avatar.size, fill=255)
- avatar.putalpha(mask)
- return avatar
-
- @staticmethod
- def crop_ring(ring: Image, px: int) -> Image:
- """This crops the ring into a circle."""
- mask = Image.new("L", ring.size, 0)
- draw = ImageDraw.Draw(mask)
- draw.ellipse((0, 0) + ring.size, fill=255)
- draw.ellipse((px, px, 1024-px, 1024-px), fill=0)
- ring.putalpha(mask)
- return ring
-
- @commands.group(aliases=["avatarpride", "pridepfp", "prideprofile"], invoke_without_command=True)
- async def prideavatar(self, ctx: commands.Context, option: str = "lgbt", pixels: int = 64) -> None:
- """
- This surrounds an avatar with a border of a specified LGBT flag.
-
- This defaults to the LGBT rainbow flag if none is given.
- The amount of pixels can be given which determines the thickness of the flag border.
- This has a maximum of 512px and defaults to a 64px border.
- The full image is 1024x1024.
- """
- pixels = 0 if pixels < 0 else 512 if pixels > 512 else pixels
-
- option = option.lower()
-
- if option not in OPTIONS.keys():
- return await ctx.send("I don't have that flag!")
-
- flag = OPTIONS[option]
-
- async with ctx.typing():
-
- # Get avatar bytes
- image_bytes = await ctx.author.avatar_url.read()
- avatar = Image.open(BytesIO(image_bytes))
- avatar = avatar.convert("RGBA").resize((1024, 1024))
-
- avatar = self.crop_avatar(avatar)
-
- ring = Image.open(Path(f"bot/resources/pride/flags/{flag}.png")).resize((1024, 1024))
- ring = ring.convert("RGBA")
- ring = self.crop_ring(ring, pixels)
-
- avatar.alpha_composite(ring, (0, 0))
- bufferedio = BytesIO()
- avatar.save(bufferedio, format="PNG")
- bufferedio.seek(0)
-
- file = discord.File(bufferedio, filename="pride_avatar.png") # Creates file to be used in embed
- embed = discord.Embed(
- name="Your Lovely Pride Avatar",
- description=f"Here is your lovely avatar, surrounded by\n a beautiful {option} flag. Enjoy :D"
- )
- embed.set_image(url="attachment://pride_avatar.png")
- embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url)
-
- await ctx.send(file=file, embed=embed)
-
- @prideavatar.command()
- async def flags(self, ctx: commands.Context) -> None:
- """This lists the flags that can be used with the prideavatar command."""
- choices = sorted(set(OPTIONS.values()))
- options = "• " + "\n• ".join(choices)
- embed = discord.Embed(
- title="I have the following flags:",
- description=options,
- colour=Colours.soft_red
- )
-
- await ctx.send(embed=embed)
-
-
-def setup(bot: commands.Bot) -> None:
- """Cog load."""
- bot.add_cog(PrideAvatar(bot))
- log.info("PrideAvatar cog loaded")
diff --git a/bot/seasons/pride/pride_facts.py b/bot/seasons/pride/pride_facts.py
deleted file mode 100644
index 5c19dfd0..00000000
--- a/bot/seasons/pride/pride_facts.py
+++ /dev/null
@@ -1,106 +0,0 @@
-import asyncio
-import json
-import logging
-import random
-from datetime import datetime
-from pathlib import Path
-from typing import Union
-
-import dateutil.parser
-import discord
-from discord.ext import commands
-
-from bot.constants import Channels
-from bot.constants import Colours
-
-log = logging.getLogger(__name__)
-
-Sendable = Union[commands.Context, discord.TextChannel]
-
-
-class PrideFacts(commands.Cog):
- """Provides a new fact every day during the Pride season!"""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.facts = self.load_facts()
-
- @staticmethod
- def load_facts() -> dict:
- """Loads a dictionary of years mapping to lists of facts."""
- with open(Path("bot/resources/pride/facts.json"), "r", encoding="utf-8") as f:
- return json.load(f)
-
- async def send_pride_fact_daily(self) -> None:
- """Background task to post the daily pride fact every day."""
- channel = self.bot.get_channel(Channels.seasonalbot_commands)
- while True:
- await self.send_select_fact(channel, datetime.utcnow())
- await asyncio.sleep(24 * 60 * 60)
-
- async def send_random_fact(self, ctx: commands.Context) -> None:
- """Provides a fact from any previous day, or today."""
- now = datetime.utcnow()
- previous_years_facts = (self.facts[x] for x in self.facts.keys() if int(x) < now.year)
- current_year_facts = self.facts.get(str(now.year), [])[:now.day]
- previous_facts = current_year_facts + [x for y in previous_years_facts for x in y]
- try:
- await ctx.send(embed=self.make_embed(random.choice(previous_facts)))
- except IndexError:
- await ctx.send("No facts available")
-
- async def send_select_fact(self, target: Sendable, _date: Union[str, datetime]) -> None:
- """Provides the fact for the specified day, if the day is today, or is in the past."""
- now = datetime.utcnow()
- if isinstance(_date, str):
- try:
- date = dateutil.parser.parse(_date, dayfirst=False, yearfirst=False, fuzzy=True)
- except (ValueError, OverflowError) as err:
- await target.send(f"Error parsing date: {err}")
- return
- else:
- date = _date
- if date.year < now.year or (date.year == now.year and date.day <= now.day):
- try:
- await target.send(embed=self.make_embed(self.facts[str(date.year)][date.day - 1]))
- except KeyError:
- await target.send(f"The year {date.year} is not yet supported")
- return
- except IndexError:
- await target.send(f"Day {date.day} of {date.year} is not yet support")
- return
- else:
- await target.send("The fact for the selected day is not yet available.")
-
- @commands.command(name="pridefact", aliases=["pridefacts"])
- async def pridefact(self, ctx: commands.Context) -> None:
- """
- Sends a message with a pride fact of the day.
-
- If "random" is given as an argument, a random previous fact will be provided.
-
- If a date is given as an argument, and the date is in the past, the fact from that day
- will be provided.
- """
- message_body = ctx.message.content[len(ctx.invoked_with) + 2:]
- if message_body == "":
- await self.send_select_fact(ctx, datetime.utcnow())
- elif message_body.lower().startswith("rand"):
- await self.send_random_fact(ctx)
- else:
- await self.send_select_fact(ctx, message_body)
-
- def make_embed(self, fact: str) -> discord.Embed:
- """Makes a nice embed for the fact to be sent."""
- return discord.Embed(
- colour=Colours.pink,
- title="Pride Fact!",
- description=fact
- )
-
-
-def setup(bot: commands.Bot) -> None:
- """Cog loader for pride facts."""
- bot.loop.create_task(PrideFacts(bot).send_pride_fact_daily())
- bot.add_cog(PrideFacts(bot))
- log.info("Pride facts cog loaded!")
diff --git a/bot/seasons/season.py b/bot/seasons/season.py
deleted file mode 100644
index 763a08d2..00000000
--- a/bot/seasons/season.py
+++ /dev/null
@@ -1,560 +0,0 @@
-import asyncio
-import contextlib
-import datetime
-import importlib
-import inspect
-import logging
-import pkgutil
-from pathlib import Path
-from typing import List, Optional, Tuple, Type, Union
-
-import async_timeout
-import discord
-from discord.ext import commands
-
-from bot.bot import bot
-from bot.constants import Channels, Client, Roles
-from bot.decorators import with_role
-
-log = logging.getLogger(__name__)
-
-ICON_BASE_URL = "https://raw.githubusercontent.com/python-discord/branding/master"
-
-
-def get_seasons() -> List[str]:
- """Returns all the Season objects located in /bot/seasons/."""
- seasons = []
-
- for module in pkgutil.iter_modules([Path("bot/seasons")]):
- if module.ispkg:
- seasons.append(module.name)
- return seasons
-
-
-def get_season_class(season_name: str) -> Type["SeasonBase"]:
- """Gets the season class of the season module."""
- season_lib = importlib.import_module(f"bot.seasons.{season_name}")
- class_name = season_name.replace("_", " ").title().replace(" ", "")
- return getattr(season_lib, class_name)
-
-
-def get_season(season_name: str = None, date: datetime.datetime = None) -> "SeasonBase":
- """Returns a Season object based on either a string or a date."""
- # If either both or neither are set, raise an error.
- if not bool(season_name) ^ bool(date):
- raise UserWarning("This function requires either a season or a date in order to run.")
-
- seasons = get_seasons()
-
- # Use season override if season name not provided
- if not season_name and Client.season_override:
- log.debug(f"Season override found: {Client.season_override}")
- season_name = Client.season_override
-
- # If name provided grab the specified class or fallback to evergreen.
- if season_name:
- season_name = season_name.lower()
- if season_name not in seasons:
- season_name = "evergreen"
- season_class = get_season_class(season_name)
- return season_class()
-
- # If not, we have to figure out if the date matches any of the seasons.
- seasons.remove("evergreen")
- for season_name in seasons:
- season_class = get_season_class(season_name)
- # check if date matches before returning an instance
- if season_class.is_between_dates(date):
- return season_class()
- else:
- evergreen_class = get_season_class("evergreen")
- return evergreen_class()
-
-
-class SeasonBase:
- """Base class for Seasonal classes."""
-
- name: Optional[str] = "evergreen"
- bot_name: str = "SeasonalBot"
-
- start_date: Optional[str] = None
- end_date: Optional[str] = None
- should_announce: bool = False
-
- colour: Optional[int] = None
- icon: Tuple[str, ...] = ("/logos/logo_full/logo_full.png",)
- bot_icon: Optional[str] = None
-
- date_format: str = "%d/%m/%Y"
-
- index: int = 0
-
- @staticmethod
- def current_year() -> int:
- """Returns the current year."""
- return datetime.date.today().year
-
- @classmethod
- def start(cls) -> datetime.datetime:
- """
- Returns the start date using current year and start_date attribute.
-
- If no start_date was defined, returns the minimum datetime to ensure it's always below checked dates.
- """
- if not cls.start_date:
- return datetime.datetime.min
- return datetime.datetime.strptime(f"{cls.start_date}/{cls.current_year()}", cls.date_format)
-
- @classmethod
- def end(cls) -> datetime.datetime:
- """
- Returns the start date using current year and end_date attribute.
-
- If no end_date was defined, returns the minimum datetime to ensure it's always above checked dates.
- """
- if not cls.end_date:
- return datetime.datetime.max
- return datetime.datetime.strptime(f"{cls.end_date}/{cls.current_year()}", cls.date_format)
-
- @classmethod
- def is_between_dates(cls, date: datetime.datetime) -> bool:
- """Determines if the given date falls between the season's date range."""
- return cls.start() <= date <= cls.end()
-
- @property
- def name_clean(self) -> str:
- """Return the Season's name with underscores replaced by whitespace."""
- return self.name.replace("_", " ").title()
-
- @property
- def greeting(self) -> str:
- """
- Provides a default greeting based on the season name if one wasn't defined in the season class.
-
- It's recommended to define one in most cases by overwriting this as a normal attribute in the
- inheriting class.
- """
- return f"New Season, {self.name_clean}!"
-
- async def get_icon(self, avatar: bool = False, index: int = 0) -> Tuple[bytes, str]:
- """
- Retrieve the season's icon from the branding repository using the Season's icon attribute.
-
- This also returns the relative URL path for logging purposes
- If `avatar` is True, uses optional bot-only avatar icon if present.
- Returns the data for the given `index`, defaulting to the first item.
-
- The icon attribute must provide the url path, starting from the master branch base url,
- including the starting slash.
- e.g. `/logos/logo_seasonal/valentines/loved_up.png`
- """
- icon = self.icon[index]
- if avatar and self.bot_icon:
- icon = self.bot_icon
-
- full_url = ICON_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(), icon)
-
- async def apply_username(self, *, debug: bool = False) -> Union[bool, None]:
- """
- Applies the username for the current season.
-
- Only changes nickname if `bool` is False, otherwise only changes the nickname.
-
- Returns True if it successfully changed the username.
- Returns False if it failed to change the username, falling back to nick.
- Returns None if `debug` was True and username change wasn't attempted.
- """
- guild = bot.get_guild(Client.guild)
- result = None
-
- # Change only nickname if in debug mode due to ratelimits for user edits
- if debug:
- if guild.me.display_name != self.bot_name:
- log.debug(f"Changing nickname to {self.bot_name}")
- await guild.me.edit(nick=self.bot_name)
-
- else:
- if bot.user.name != self.bot_name:
- # Attempt to change user details
- log.debug(f"Changing username to {self.bot_name}")
- with contextlib.suppress(discord.HTTPException):
- await bot.user.edit(username=self.bot_name)
-
- # Fallback on nickname if failed due to ratelimit
- if bot.user.name != self.bot_name:
- log.warning(f"Username failed to change: Changing nickname to {self.bot_name}")
- await guild.me.edit(nick=self.bot_name)
- result = False
- else:
- result = True
-
- # Remove nickname if an old one exists
- if guild.me.nick and guild.me.nick != self.bot_name:
- log.debug(f"Clearing old nickname of {guild.me.nick}")
- await guild.me.edit(nick=None)
-
- return result
-
- async def apply_avatar(self) -> bool:
- """
- Applies the avatar for the current season.
-
- Returns True if successful.
- """
- # Track old avatar hash for later comparison
- old_avatar = bot.user.avatar
-
- # Attempt the change
- icon, name = await self.get_icon(avatar=True)
- log.debug(f"Changing avatar to {name}")
- 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 {name}")
- return True
-
- log.warning(f"Changing avatar failed: {name}")
- return False
-
- async def apply_server_icon(self) -> bool:
- """
- Applies the server icon for the current season.
-
- Returns True if was successful.
- """
- guild = bot.get_guild(Client.guild)
-
- # Track old icon hash for later comparison
- old_icon = guild.icon
-
- # Attempt the change
-
- icon, name = await self.get_icon(index=self.index)
-
- log.debug(f"Changing server icon to {name}")
-
- with contextlib.suppress(discord.HTTPException, asyncio.TimeoutError):
- async with async_timeout.timeout(5):
- await guild.edit(icon=icon, reason=f"Seasonbot Season Change: {self.name}")
-
- new_icon = bot.get_guild(Client.guild).icon
- if new_icon != old_icon:
- log.debug(f"Server icon changed to {name}")
- return True
-
- log.warning(f"Changing server icon failed: {name}")
- return False
-
- async def change_server_icon(self) -> bool:
- """
- Changes the server icon.
-
- This only has an effect when the Season's icon attribute is a list, in which it cycles through.
- Returns True if was successful.
- """
- if len(self.icon) == 1:
- return
-
- self.index += 1
- self.index %= len(self.icon)
-
- return await self.apply_server_icon()
-
- async def announce_season(self) -> None:
- """
- Announces a change in season in the announcement channel.
-
- Auto-announcement is configured by the `should_announce` `SeasonBase` attribute
- """
- # Short circuit if the season had disabled automatic announcements
- if not self.should_announce:
- log.debug(f"Season changed without announcement: {self.name}")
- return
-
- guild = bot.get_guild(Client.guild)
- channel = guild.get_channel(Channels.announcements)
- mention = f"<@&{Roles.announcements}>"
-
- # Build cog info output
- doc = inspect.getdoc(self)
- announce = "\n\n".join(l.replace("\n", " ") for l in doc.split("\n\n"))
-
- # No announcement message found
- if not doc:
- return
-
- embed = discord.Embed(description=f"{announce}\n\n", colour=self.colour or guild.me.colour)
- embed.set_author(name=self.greeting)
-
- if self.icon:
- embed.set_image(url=ICON_BASE_URL+self.icon[0])
-
- # Find any seasonal commands
- cogs = []
- for cog in bot.cogs.values():
- if "evergreen" in cog.__module__:
- continue
- cog_name = type(cog).__name__
- if cog_name != "SeasonManager":
- cogs.append(cog_name)
-
- if cogs:
- def cog_name(cog: commands.Cog) -> str:
- return type(cog).__name__
-
- cog_info = []
- for cog in sorted(cogs, key=cog_name):
- doc = inspect.getdoc(bot.get_cog(cog))
- if doc:
- cog_info.append(f"**{cog}**\n*{doc}*")
- else:
- cog_info.append(f"**{cog}**")
-
- cogs_text = "\n".join(cog_info)
- embed.add_field(name="New Command Categories", value=cogs_text)
- embed.set_footer(text="To see the new commands, use .help Category")
-
- await channel.send(mention, embed=embed)
-
- async def load(self) -> None:
- """
- Loads extensions, bot name and avatar, server icon and announces new season.
-
- If in debug mode, the avatar, server icon, and announcement will be skipped.
- """
- self.index = 0
- # Prepare all the seasonal cogs, and then the evergreen ones.
- extensions = []
- for ext_folder in {self.name, "evergreen"}:
- if ext_folder:
- log.info(f"Start loading extensions from seasons/{ext_folder}/")
- path = Path("bot/seasons") / ext_folder
- for ext_name in [i[1] for i in pkgutil.iter_modules([path])]:
- extensions.append(f"bot.seasons.{ext_folder}.{ext_name}")
-
- # Finally we can load all the cogs we've prepared.
- bot.load_extensions(extensions)
-
- # Apply seasonal elements after extensions successfully load
- username_changed = await self.apply_username(debug=Client.debug)
-
- # Avoid major changes and announcements if debug mode
- if not Client.debug:
- log.info("Applying avatar.")
- await self.apply_avatar()
- 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(commands.Cog):
- """A cog for managing seasons."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.season = get_season(date=datetime.datetime.utcnow())
- self.season_task = bot.loop.create_task(self.load_seasons())
-
- # Figure out number of seconds until a minute past midnight
- tomorrow = datetime.datetime.now() + datetime.timedelta(1)
- midnight = datetime.datetime(
- year=tomorrow.year,
- month=tomorrow.month,
- day=tomorrow.day,
- hour=0,
- minute=0,
- second=0
- )
- self.sleep_time = (midnight - datetime.datetime.now()).seconds + 60
-
- async def load_seasons(self) -> None:
- """Asynchronous timer loop to check for a new season every midnight."""
- await self.bot.wait_until_ready()
- await self.season.load()
- days_since_icon_change = 0
-
- while True:
- await asyncio.sleep(self.sleep_time) # Sleep until midnight
- self.sleep_time = 24 * 3600 # Next time, sleep for 24 hours
-
- days_since_icon_change += 1
- log.debug(f"Days since last icon change: {days_since_icon_change}")
-
- # If the season has changed, load it.
- new_season = get_season(date=datetime.datetime.utcnow())
- if new_season.name != self.season.name:
- self.season = new_season
- await self.season.load()
- days_since_icon_change = 0 # Start counting afresh for the new season
-
- # Otherwise we check whether it's time for an icon cycle within the current season
- else:
- if days_since_icon_change == Client.icon_cycle_frequency:
- await self.season.change_server_icon()
- days_since_icon_change = 0
- else:
- log.debug(f"Waiting {Client.icon_cycle_frequency - days_since_icon_change} more days to cycle icon")
-
- @with_role(Roles.moderator, Roles.admin, Roles.owner)
- @commands.command(name="season")
- async def change_season(self, ctx: commands.Context, new_season: str) -> None:
- """Changes the currently active season on the bot."""
- self.season = get_season(season_name=new_season)
- await self.season.load()
- await ctx.send(f"Season changed to {new_season}.")
-
- @with_role(Roles.moderator, Roles.admin, Roles.owner)
- @commands.command(name="seasons")
- async def show_seasons(self, ctx: commands.Context) -> None:
- """Shows the available seasons and their dates."""
- # Sort by start order, followed by lower duration
- def season_key(season_class: Type[SeasonBase]) -> Tuple[datetime.datetime, datetime.timedelta]:
- return season_class.start(), season_class.end() - datetime.datetime.max
-
- current_season = self.season.name
-
- forced_space = "\u200b "
-
- entries = []
- seasons = [get_season_class(s) for s in get_seasons()]
- for season in sorted(seasons, key=season_key):
- start = season.start_date
- end = season.end_date
- if start and not end:
- period = f"From {start}"
- elif end and not start:
- period = f"Until {end}"
- elif not end and not start:
- period = f"Always"
- else:
- period = f"{start} to {end}"
-
- # Bold period if current date matches season date range
- is_current = season.is_between_dates(datetime.datetime.utcnow())
- pdec = "**" if is_current else ""
-
- # Underline currently active season
- is_active = current_season == season.name
- sdec = "__" if is_active else ""
-
- entries.append(
- f"**{sdec}{season.__name__}:{sdec}**\n"
- f"{forced_space*3}{pdec}{period}{pdec}\n"
- )
-
- embed = discord.Embed(description="\n".join(entries), colour=ctx.guild.me.colour)
- embed.set_author(name="Seasons")
- await ctx.send(embed=embed)
-
- @with_role(Roles.moderator, Roles.admin, Roles.owner)
- @commands.group()
- async def refresh(self, ctx: commands.Context) -> None:
- """Refreshes certain seasonal elements without reloading seasons."""
- if not ctx.invoked_subcommand:
- await ctx.send_help(ctx.command)
-
- @refresh.command(name="avatar")
- async def refresh_avatar(self, ctx: commands.Context) -> None:
- """Re-applies the bot avatar for the currently loaded season."""
- # Attempt the change
- is_changed = await self.season.apply_avatar()
-
- if is_changed:
- colour = ctx.guild.me.colour
- title = "Avatar Refreshed"
- else:
- colour = discord.Colour.red()
- title = "Avatar Failed to Refresh"
-
- # Report back details
- season_name = type(self.season).__name__
- embed = discord.Embed(
- description=f"**Season:** {season_name}\n**Avatar:** {self.season.bot_icon or self.season.icon}",
- colour=colour
- )
- embed.set_author(name=title)
- embed.set_thumbnail(url=bot.user.avatar_url_as(format="png"))
- await ctx.send(embed=embed)
-
- @refresh.command(name="icon")
- async def refresh_server_icon(self, ctx: commands.Context) -> None:
- """Re-applies the server icon for the currently loaded season."""
- # Attempt the change
- is_changed = await self.season.apply_server_icon()
-
- if is_changed:
- colour = ctx.guild.me.colour
- title = "Server Icon Refreshed"
- else:
- colour = discord.Colour.red()
- title = "Server Icon Failed to Refresh"
-
- # Report back details
- season_name = type(self.season).__name__
- embed = discord.Embed(
- description=f"**Season:** {season_name}\n**Icon:** {self.season.icon}",
- colour=colour
- )
- embed.set_author(name=title)
- embed.set_thumbnail(url=bot.get_guild(Client.guild).icon_url_as(format="png"))
- await ctx.send(embed=embed)
-
- @refresh.command(name="username", aliases=("name",))
- async def refresh_username(self, ctx: commands.Context) -> None:
- """Re-applies the bot username for the currently loaded season."""
- old_username = str(bot.user)
- old_display_name = ctx.guild.me.display_name
-
- # Attempt the change
- is_changed = await self.season.apply_username()
-
- if is_changed:
- colour = ctx.guild.me.colour
- title = "Username Refreshed"
- changed_element = "Username"
- old_name = old_username
- new_name = str(bot.user)
- else:
- colour = discord.Colour.red()
-
- # If None, it's because it wasn't meant to change username
- if is_changed is None:
- title = "Nickname Refreshed"
- else:
- title = "Username Failed to Refresh"
- changed_element = "Nickname"
- old_name = old_display_name
- new_name = self.season.bot_name
-
- # Report back details
- season_name = type(self.season).__name__
- embed = discord.Embed(
- description=f"**Season:** {season_name}\n"
- f"**Old {changed_element}:** {old_name}\n"
- f"**New {changed_element}:** {new_name}",
- colour=colour
- )
- embed.set_author(name=title)
- await ctx.send(embed=embed)
-
- @with_role(Roles.moderator, Roles.admin, Roles.owner)
- @commands.command()
- async def announce(self, ctx: commands.Context) -> None:
- """Announces the currently loaded season."""
- await self.season.announce_season()
-
- def cog_unload(self) -> None:
- """Cancel season-related tasks on cog unload."""
- self.season_task.cancel()
diff --git a/bot/seasons/valentines/__init__.py b/bot/seasons/valentines/__init__.py
deleted file mode 100644
index 6e5d16f7..00000000
--- a/bot/seasons/valentines/__init__.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from bot.constants import Colours
-from bot.seasons import SeasonBase
-
-
-class Valentines(SeasonBase):
- """
- Love is in the air! We've got a new icon and set of commands for the season of love.
-
- Get yourself into the bot-commands channel and check out the new features!
- """
-
- name = "valentines"
- bot_name = "Tenderbot"
- greeting = "Get loved-up!"
-
- start_date = "01/02"
- end_date = "01/03"
-
- colour = Colours.pink
- icon = (
- "/logos/logo_seasonal/valentines/loved_up.png",
- )
diff --git a/bot/seasons/valentines/be_my_valentine.py b/bot/seasons/valentines/be_my_valentine.py
deleted file mode 100644
index ab8ea290..00000000
--- a/bot/seasons/valentines/be_my_valentine.py
+++ /dev/null
@@ -1,234 +0,0 @@
-import logging
-import random
-from json import load
-from pathlib import Path
-from typing import Optional, Tuple
-
-import discord
-from discord.ext import commands
-from discord.ext.commands.cooldowns import BucketType
-
-from bot.constants import Channels, 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: commands.Bot):
- self.bot = bot
- self.valentines = self.load_json()
-
- @staticmethod
- def load_json() -> dict:
- """Load Valentines messages from the static resources."""
- p = Path("bot/resources/valentines/bemyvalentine_valentines.json")
- with p.open() as json_data:
- valentines = load(json_data)
- return valentines
-
- @commands.group(name="lovefest", invoke_without_command=True)
- async def lovefest_role(self, ctx: commands.Context) -> None:
- """
- Subscribe or unsubscribe from the lovefest role.
-
- 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.send_help(ctx.command)
-
- @lovefest_role.command(name="sub")
- async def add_role(self, ctx: commands.Context) -> None:
- """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: commands.Context) -> None:
- """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: commands.Context, user: Optional[discord.Member] = None, *, valentine_type: str = None
- ) -> None:
- """
- Send a valentine to user, if specified, or to a random user with the 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(Channels.seasonalbot_commands)
- 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: commands.Context, user: Optional[discord.Member] = None, *, valentine_type: str = None
- ) -> None:
- """
- Send an anonymous Valentine via DM to to a user, if specified, or to a random with the 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: str) -> Tuple[str, str]:
- """Return the appropriate Valentine type & title based on the invoking user's input."""
- 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: discord.Member, members: discord.Member) -> None:
- """
- Picks a random member from the list provided in `members`.
-
- The invoking author is ignored.
- """
- if author in members:
- members.remove(author)
-
- return random.choice(members) if members else None
-
- @staticmethod
- def random_emoji() -> Tuple[str, str]:
- """Return two random emoji from the module-defined constants."""
- emoji_1 = random.choice(HEART_EMOJIS)
- emoji_2 = random.choice(HEART_EMOJIS)
- return emoji_1, emoji_2
-
- def random_valentine(self) -> Tuple[str, str]:
- """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) -> str:
- """Grabs a random poem."""
- valentine_poem = random.choice(self.valentines['valentine_poems'])
- return valentine_poem
-
- def valentine_compliment(self) -> str:
- """Grabs a random compliment."""
- valentine_compliment = random.choice(self.valentines['valentine_compliments'])
- return valentine_compliment
-
-
-def setup(bot: commands.Bot) -> None:
- """Be my Valentine Cog load."""
- bot.add_cog(BeMyValentine(bot))
- log.info("BeMyValentine cog loaded")
diff --git a/bot/seasons/valentines/lovecalculator.py b/bot/seasons/valentines/lovecalculator.py
deleted file mode 100644
index 03d3d7d5..00000000
--- a/bot/seasons/valentines/lovecalculator.py
+++ /dev/null
@@ -1,104 +0,0 @@
-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: commands.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: commands.Context, who: Union[Member, str], whom: Union[Member, str] = None) -> 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: Union[Member, str]) -> str:
- 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() 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: commands.Bot) -> None:
- """Love calculator Cog load."""
- 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
deleted file mode 100644
index ce1d7d5b..00000000
--- a/bot/seasons/valentines/movie_generator.py
+++ /dev/null
@@ -1,63 +0,0 @@
-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: commands.Bot):
- self.bot = bot
-
- @commands.command(name="romancemovie")
- async def romance_movie(self, ctx: commands.Context) -> None:
- """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: commands.Bot) -> None:
- """Romance movie Cog load."""
- bot.add_cog(RomanceMovieFinder(bot))
- log.info("RomanceMovieFinder cog loaded")
diff --git a/bot/seasons/valentines/myvalenstate.py b/bot/seasons/valentines/myvalenstate.py
deleted file mode 100644
index 0256c39a..00000000
--- a/bot/seasons/valentines/myvalenstate.py
+++ /dev/null
@@ -1,87 +0,0 @@
-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):
- """A Cog to find your most likely Valentine's vacation destination."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- def levenshtein(self, source: str, goal: str) -> int:
- """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: commands.Context, *, name: str = None) -> None:
- """Find the vacation spot(s) with the most matching characters to the invoking user."""
- 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: commands.Bot) -> None:
- """Valenstate Cog load."""
- bot.add_cog(MyValenstate(bot))
- log.info("MyValenstate cog loaded")
diff --git a/bot/seasons/valentines/pickuplines.py b/bot/seasons/valentines/pickuplines.py
deleted file mode 100644
index 8b2c9822..00000000
--- a/bot/seasons/valentines/pickuplines.py
+++ /dev/null
@@ -1,45 +0,0 @@
-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: commands.Bot):
- self.bot = bot
-
- @commands.command()
- async def pickupline(self, ctx: commands.Context) -> None:
- """
- 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: commands.Bot) -> None:
- """Pickup lines Cog load."""
- bot.add_cog(PickupLine(bot))
- log.info('PickupLine cog loaded')
diff --git a/bot/seasons/valentines/savethedate.py b/bot/seasons/valentines/savethedate.py
deleted file mode 100644
index e0bc3904..00000000
--- a/bot/seasons/valentines/savethedate.py
+++ /dev/null
@@ -1,42 +0,0 @@
-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 Valentine's date."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command()
- async def savethedate(self, ctx: commands.Context) -> None:
- """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: commands.Bot) -> None:
- """Save the date Cog Load."""
- 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
deleted file mode 100644
index c8d77e75..00000000
--- a/bot/seasons/valentines/valentine_zodiac.py
+++ /dev/null
@@ -1,58 +0,0 @@
-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: commands.Bot):
- self.bot = bot
- self.zodiacs = self.load_json()
-
- @staticmethod
- def load_json() -> dict:
- """Load zodiac compatibility from static JSON resource."""
- p = Path("bot/resources/valentines/zodiac_compatibility.json")
- with p.open() as json_data:
- zodiacs = load(json_data)
- return zodiacs
-
- @commands.command(name="partnerzodiac")
- async def counter_zodiac(self, ctx: commands.Context, zodiac_sign: str) -> None:
- """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: commands.Bot) -> None:
- """Valentine zodiac Cog load."""
- bot.add_cog(ValentineZodiac(bot))
- log.info("ValentineZodiac cog loaded")
diff --git a/bot/seasons/valentines/whoisvalentine.py b/bot/seasons/valentines/whoisvalentine.py
deleted file mode 100644
index b8586dca..00000000
--- a/bot/seasons/valentines/whoisvalentine.py
+++ /dev/null
@@ -1,53 +0,0 @@
-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):
- """A Cog for displaying facts about Saint Valentine."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(aliases=('whoisvalentine', 'saint_valentine'))
- async def who_is_valentine(self, ctx: commands.Context) -> None:
- """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: commands.Context) -> None:
- """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: commands.Bot) -> None:
- """Who is Valentine Cog load."""
- bot.add_cog(ValentineFacts(bot))
- log.info("ValentineFacts cog loaded")
diff --git a/bot/seasons/wildcard/__init__.py b/bot/seasons/wildcard/__init__.py
deleted file mode 100644
index 354e979d..00000000
--- a/bot/seasons/wildcard/__init__.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from bot.seasons import SeasonBase
-
-
-class Wildcard(SeasonBase):
- """
- For the month of August, the season is a Wildcard.
-
- This docstring will not be used for announcements.
- Instead, we'll do the announcement manually, since
- it will change every year.
-
- This class needs slight changes every year,
- such as the bot_name, bot_icon and icon.
-
- IMPORTANT: DO NOT ADD ANY FEATURES TO THIS FOLDER.
- ALL WILDCARD FEATURES SHOULD BE ADDED
- TO THE EVERGREEN FOLDER!
- """
-
- name = "wildcard"
- bot_name = "RetroBot"
-
- # Duration of season
- start_date = "01/08"
- end_date = "01/09"
-
- # Season logo
- bot_icon = "/logos/logo_seasonal/retro_gaming/logo_8bit_indexed_504.png"
- icon = (
- "/logos/logo_seasonal/retro_gaming_animated/logo_spin_plain/logo_spin_plain_504.gif",
- )