diff options
author | 2021-02-08 14:05:25 +0300 | |
---|---|---|
committer | 2021-02-08 14:05:25 +0300 | |
commit | b0169f28f8f6f27d80bdc81e4f186431391560e4 (patch) | |
tree | b92236b8fba11020de69e9c13a570c14cfb1ed46 | |
parent | Default redis host in docker-compose (diff) | |
parent | Merge pull request #580 from Shivansh-007/fix/cht.sh (diff) |
Merge branch 'master' into docker-compse-env-file
-rwxr-xr-x | README.md | 5 | ||||
-rw-r--r-- | bot/constants.py | 79 | ||||
-rw-r--r-- | bot/exts/evergreen/battleship.py | 2 | ||||
-rw-r--r-- | bot/exts/evergreen/cheatsheet.py | 113 | ||||
-rw-r--r-- | bot/exts/evergreen/game.py | 83 | ||||
-rw-r--r-- | bot/exts/evergreen/issues.py | 160 | ||||
-rw-r--r-- | bot/exts/evergreen/status_cats.py | 33 | ||||
-rw-r--r-- | bot/exts/evergreen/status_codes.py | 71 | ||||
-rw-r--r-- | bot/exts/evergreen/tic_tac_toe.py | 323 | ||||
-rw-r--r-- | bot/exts/evergreen/xkcd.py | 89 | ||||
-rw-r--r-- | bot/exts/halloween/spookynamerate.py | 401 | ||||
-rw-r--r-- | bot/resources/evergreen/trivia_quiz.json | 12 | ||||
-rw-r--r-- | bot/resources/halloween/spookynamerate_names.json | 2206 |
13 files changed, 3454 insertions, 123 deletions
@@ -1,8 +1,9 @@ # Sir Lancebot +[![Discord][5]][6] [![Lint Badge][1]][2] [![Build Badge][3]][4] -[](https://discord.gg/2B963hn) +[](LICENSE)  @@ -25,3 +26,5 @@ See [Sir Lancebot's Wiki](https://pythondiscord.com/pages/contributing/sir-lance [2]:https://github.com/python-discord/sir-lancebot/actions?query=workflow%3ALint+branch%3Amaster [3]:https://github.com/python-discord/sir-lancebot/workflows/Build/badge.svg?branch=master [4]:https://github.com/python-discord/sir-lancebot/actions?query=workflow%3ABuild+branch%3Amaster +[5]: https://raw.githubusercontent.com/python-discord/branding/master/logos/badge/badge_github.svg +[6]: https://discord.gg/python diff --git a/bot/constants.py b/bot/constants.py index f6da272e..bb538487 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -9,6 +9,7 @@ __all__ = ( "AdventOfCode", "Branding", "Channels", + "Categories", "Client", "Colours", "Emojis", @@ -100,20 +101,17 @@ class Channels(NamedTuple): big_brother_logs = 468507907357409333 bot = 267659945086812160 checkpoint_test = 422077681434099723 + organisation = 551789653284356126 devalerts = 460181980097675264 devlog = int(environ.get("CHANNEL_DEVLOG", 622895325144940554)) dev_contrib = 635950537262759947 dev_branding = 753252897059373066 - help_0 = 303906576991780866 - help_1 = 303906556754395136 - help_2 = 303906514266226689 - help_3 = 439702951246692352 - help_4 = 451312046647148554 - help_5 = 454941769734422538 helpers = 385474242440986624 message_log = 467752170159079424 mod_alerts = 473092532147060736 modlog = 282638479504965634 + mod_meta = 775412552795947058 + mod_tools = 775413915391098921 off_topic_0 = 291284109232308226 off_topic_1 = 463035241142026251 off_topic_2 = 463035268514185226 @@ -124,24 +122,15 @@ class Channels(NamedTuple): verification = 352442727016693763 python_discussion = 267624335836053506 hacktoberfest_2020 = 760857070781071431 - voice_chat = 412357430186344448 - - # Core Dev Sprint channels - sprint_announcements = 755958119963557958 - sprint_information = 753338352136224798 - sprint_organisers = 753340132639375420 - sprint_general = 753340631538991305 - sprint_social1_cheese_shop = 758779754789863514 - sprint_social2_pet_shop = 758780951978573824 - sprint_escape_room = 761031075942105109 - sprint_stdlib = 758553316732698634 - sprint_asyncio = 762904152438472714 - sprint_typing = 762904690341838888 - sprint_discussion_capi = 758553358587527218 - sprint_discussion_triage = 758553458365300746 - sprint_discussion_design = 758553492662255616 - sprint_discussion_mentor = 758553536623280159 - sprint_documentation = 761038271127093278 + voice_chat_0 = 412357430186344448 + voice_chat_1 = 799647045886541885 + + +class Categories(NamedTuple): + help_in_use = 696958401460043776 + development = 411199786025484308 + devprojects = 787641585624940544 + media = 799054581991997460 class Client(NamedTuple): @@ -190,6 +179,23 @@ class Emojis: pull_request_closed = "<:PRClosed:629695470519713818>" merge = "<:PRMerged:629695470570176522>" + # TicTacToe Emojis + number_emojis = { + 1: "\u0031\ufe0f\u20e3", + 2: "\u0032\ufe0f\u20e3", + 3: "\u0033\ufe0f\u20e3", + 4: "\u0034\ufe0f\u20e3", + 5: "\u0035\ufe0f\u20e3", + 6: "\u0036\ufe0f\u20e3", + 7: "\u0037\ufe0f\u20e3", + 8: "\u0038\ufe0f\u20e3", + 9: "\u0039\ufe0f\u20e3" + } + confirmation = "\u2705" + decline = "\u274c" + x = "\U0001f1fd" + o = "\U0001f1f4" + status_online = "<:status_online:470326272351010816>" status_idle = "<:status_idle:470326266625785866>" status_dnd = "<:status_dnd:470326272082313216>" @@ -237,7 +243,6 @@ class Roles(NamedTuple): announcements = 463658397560995840 champion = 430492892331769857 contributor = 295488872404484098 - developer = 352427296948486144 devops = 409416496733880320 jammer = 423054537079783434 moderator = 267629731250176001 @@ -248,6 +253,7 @@ class Roles(NamedTuple): rockstars = 458226413825294336 core_developers = 587606783669829632 events_lead = 778361735739998228 + everyone_role = 267624335836053506 class Tokens(NamedTuple): @@ -257,7 +263,8 @@ class Tokens(NamedTuple): youtube = environ.get("YOUTUBE_API_KEY") tmdb = environ.get("TMDB_API_KEY") nasa = environ.get("NASA_API_KEY") - igdb = environ.get("IGDB_API_KEY") + igdb_client_id = environ.get("IGDB_CLIENT_ID") + igdb_client_secret = environ.get("IGDB_CLIENT_SECRET") github = environ.get("GITHUB_TOKEN") @@ -294,24 +301,8 @@ WHITELISTED_CHANNELS = ( Channels.off_topic_0, Channels.off_topic_1, Channels.off_topic_2, - Channels.voice_chat, - - # Core Dev Sprint Channels - Channels.sprint_announcements, - Channels.sprint_information, - Channels.sprint_organisers, - Channels.sprint_general, - Channels.sprint_social1_cheese_shop, - Channels.sprint_social2_pet_shop, - Channels.sprint_escape_room, - Channels.sprint_stdlib, - Channels.sprint_asyncio, - Channels.sprint_typing, - Channels.sprint_discussion_capi, - Channels.sprint_discussion_triage, - Channels.sprint_discussion_design, - Channels.sprint_discussion_mentor, - Channels.sprint_documentation, + Channels.voice_chat_0, + Channels.voice_chat_1, ) GIT_SHA = environ.get("GIT_SHA", "foobar") diff --git a/bot/exts/evergreen/battleship.py b/bot/exts/evergreen/battleship.py index 9bc374e6..fa3fb35c 100644 --- a/bot/exts/evergreen/battleship.py +++ b/bot/exts/evergreen/battleship.py @@ -140,7 +140,7 @@ class Game: @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") + index = ord(square[0].upper()) - ord("A") number = int(square[1:]) return grid[number-1][index] # -1 since lists are indexed from 0 diff --git a/bot/exts/evergreen/cheatsheet.py b/bot/exts/evergreen/cheatsheet.py new file mode 100644 index 00000000..a64ddd69 --- /dev/null +++ b/bot/exts/evergreen/cheatsheet.py @@ -0,0 +1,113 @@ +import random +import re +import typing as t +from urllib.parse import quote_plus + +from discord import Embed +from discord.ext import commands +from discord.ext.commands import BucketType, Context + +from bot import constants +from bot.constants import Categories, Channels, Colours, ERROR_REPLIES, Roles, WHITELISTED_CHANNELS +from bot.utils.decorators import with_role + +ERROR_MESSAGE = f""" +Unknown cheat sheet. Please try to reformulate your query. + +**Examples**: +```md +{constants.Client.prefix}cht read json +{constants.Client.prefix}cht hello world +{constants.Client.prefix}cht lambda +``` +If the problem persists send a message in <#{Channels.dev_contrib}> +""" + +URL = 'https://cheat.sh/python/{search}' +ESCAPE_TT = str.maketrans({"`": "\\`"}) +ANSI_RE = re.compile(r"\x1b\[.*?m") +# We need to pass headers as curl otherwise it would default to aiohttp which would return raw html. +HEADERS = {'User-Agent': 'curl/7.68.0'} + + +class CheatSheet(commands.Cog): + """Commands that sends a result of a cht.sh search in code blocks.""" + + def __init__(self, bot: commands.Bot): + self.bot = bot + + @staticmethod + def fmt_error_embed() -> Embed: + """ + Format the Error Embed. + + If the cht.sh search returned 404, overwrite it to send a custom error embed. + link -> https://github.com/chubin/cheat.sh/issues/198 + """ + embed = Embed( + title=random.choice(ERROR_REPLIES), + description=ERROR_MESSAGE, + colour=Colours.soft_red + ) + return embed + + def result_fmt(self, url: str, body_text: str) -> t.Tuple[bool, t.Union[str, Embed]]: + """Format Result.""" + if body_text.startswith("# 404 NOT FOUND"): + embed = self.fmt_error_embed() + return True, embed + + body_space = min(1986 - len(url), 1000) + + if len(body_text) > body_space: + description = (f"**Result Of cht.sh**\n" + f"```python\n{body_text[:body_space]}\n" + f"... (truncated - too many lines)```\n" + f"Full results: {url} ") + else: + description = (f"**Result Of cht.sh**\n" + f"```python\n{body_text}```\n" + f"{url}") + return False, description + + @commands.command( + name="cheat", + aliases=("cht.sh", "cheatsheet", "cheat-sheet", "cht"), + ) + @commands.cooldown(1, 10, BucketType.user) + @with_role(Roles.everyone_role) + async def cheat_sheet(self, ctx: Context, *search_terms: str) -> None: + """ + Search cheat.sh. + + Gets a post from https://cheat.sh/python/ by default. + Usage: + --> .cht read json + """ + if not ( + ctx.channel.category.id == Categories.help_in_use + or ctx.channel.id in WHITELISTED_CHANNELS + ): + return + + async with ctx.typing(): + search_string = quote_plus(" ".join(search_terms)) + + async with self.bot.http_session.get( + URL.format(search=search_string), headers=HEADERS + ) as response: + result = ANSI_RE.sub("", await response.text()).translate(ESCAPE_TT) + + is_embed, description = self.result_fmt( + URL.format(search=search_string), + result + ) + if is_embed: + await ctx.send(embed=description) + else: + await ctx.send(content=description) + + +def setup(bot: commands.Bot) -> None: + """Load the CheatSheet cog.""" + bot.add_cog(CheatSheet(bot)) diff --git a/bot/exts/evergreen/game.py b/bot/exts/evergreen/game.py index d0fd7a40..d37be0e2 100644 --- a/bot/exts/evergreen/game.py +++ b/bot/exts/evergreen/game.py @@ -2,7 +2,8 @@ import difflib import logging import random import re -from datetime import datetime as dt +from asyncio import sleep +from datetime import datetime as dt, timedelta from enum import IntEnum from typing import Any, Dict, List, Optional, Tuple @@ -17,10 +18,25 @@ from bot.utils.decorators import with_role from bot.utils.pagination import ImagePaginator, LinePaginator # Base URL of IGDB API -BASE_URL = "https://api-v3.igdb.com" +BASE_URL = "https://api.igdb.com/v4" -HEADERS = { - "user-key": Tokens.igdb, +CLIENT_ID = Tokens.igdb_client_id +CLIENT_SECRET = Tokens.igdb_client_secret + +# The number of seconds before expiry that we attempt to re-fetch a new access token +ACCESS_TOKEN_RENEWAL_WINDOW = 60*60*24*2 + +# URL to request API access token +OAUTH_URL = "https://id.twitch.tv/oauth2/token" + +OAUTH_PARAMS = { + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "grant_type": "client_credentials" +} + +BASE_HEADERS = { + "Client-ID": CLIENT_ID, "Accept": "application/json" } @@ -135,8 +151,47 @@ class Games(Cog): self.http_session: ClientSession = bot.http_session self.genres: Dict[str, int] = {} - - self.refresh_genres_task.start() + self.headers = BASE_HEADERS + + self.bot.loop.create_task(self.renew_access_token()) + + async def renew_access_token(self) -> None: + """Refeshes V4 access token a number of seconds before expiry. See `ACCESS_TOKEN_RENEWAL_WINDOW`.""" + while True: + async with self.http_session.post(OAUTH_URL, params=OAUTH_PARAMS) as resp: + result = await resp.json() + if resp.status != 200: + # If there is a valid access token continue to use that, + # otherwise unload cog. + if "Authorization" in self.headers: + time_delta = timedelta(seconds=ACCESS_TOKEN_RENEWAL_WINDOW) + logger.error( + "Failed to renew IGDB access token. " + f"Current token will last for {time_delta} " + f"OAuth response message: {result['message']}" + ) + else: + logger.warning( + "Invalid OAuth credentials. Unloading Games cog. " + f"OAuth response message: {result['message']}" + ) + self.bot.remove_cog('Games') + + return + + self.headers["Authorization"] = f"Bearer {result['access_token']}" + + # Attempt to renew before the token expires + next_renewal = result["expires_in"] - ACCESS_TOKEN_RENEWAL_WINDOW + + time_delta = timedelta(seconds=next_renewal) + logger.info(f"Successfully renewed access token. Refreshing again in {time_delta}") + + # This will be true the first time this loop runs. + # Since we now have an access token, its safe to start this task. + if self.genres == {}: + self.refresh_genres_task.start() + await sleep(next_renewal) @tasks.loop(hours=24.0) async def refresh_genres_task(self) -> None: @@ -156,9 +211,8 @@ class Games(Cog): 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: + async with self.http_session.post(f"{BASE_URL}/genres", data=body, headers=self.headers) as resp: result = await resp.json() - genres = {genre["name"].capitalize(): genre["id"] for genre in result} # Replace complex names with names from ALIASES @@ -306,7 +360,7 @@ class Games(Cog): 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: + async with self.http_session.post(url=f"{BASE_URL}/games", data=body, headers=self.headers) as resp: return await resp.json() async def create_page(self, data: Dict[str, Any]) -> Tuple[str, str]: @@ -348,7 +402,7 @@ class Games(Cog): # 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: + async with self.http_session.post(url=f"{BASE_URL}/games", data=body, headers=self.headers) as resp: data = await resp.json() # Loop over games, format them to good format, make line and append this to total lines @@ -377,7 +431,7 @@ class Games(Cog): "offset": offset }) - async with self.http_session.get(url=f"{BASE_URL}/companies", data=body, headers=HEADERS) as resp: + async with self.http_session.post(url=f"{BASE_URL}/companies", data=body, headers=self.headers) as resp: return await resp.json() async def create_company_page(self, data: Dict[str, Any]) -> Tuple[str, str]: @@ -418,7 +472,10 @@ class Games(Cog): def setup(bot: Bot) -> 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.") + if not Tokens.igdb_client_id: + logger.warning("No IGDB client ID. Not loading Games cog.") + return + if not Tokens.igdb_client_secret: + logger.warning("No IGDB client secret. Not loading Games cog.") return bot.add_cog(Games(bot)) diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index e419a6f5..73ebe547 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -1,11 +1,13 @@ import logging import random +import re +import typing as t +from enum import Enum import discord -from discord.ext import commands +from discord.ext import commands, tasks -from bot.constants import Channels, Colours, ERROR_REPLIES, Emojis, Tokens, WHITELISTED_CHANNELS -from bot.utils.decorators import override_in_channel +from bot.constants import Categories, Channels, Colours, ERROR_REPLIES, Emojis, Tokens, WHITELISTED_CHANNELS log = logging.getLogger(__name__) @@ -15,55 +17,86 @@ BAD_RESPONSE = { } MAX_REQUESTS = 10 - REQUEST_HEADERS = dict() + +REPOS_API = "https://api.github.com/orgs/{org}/repos" if GITHUB_TOKEN := Tokens.github: REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}" +WHITELISTED_CATEGORIES = ( + Categories.devprojects, Categories.media, Categories.development +) +WHITELISTED_CHANNELS_ON_MESSAGE = (Channels.organisation, Channels.mod_meta, Channels.mod_tools) + +CODE_BLOCK_RE = re.compile( + r"^`([^`\n]+)`" # Inline codeblock + r"|```(.+?)```", # Multiline codeblock + re.DOTALL | re.MULTILINE +) + + +class FetchIssueErrors(Enum): + """Errors returned in fetch issues.""" + + value_error = "Numbers not found." + max_requests = "Max requests hit." + 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 + (Channels.dev_contrib, Channels.dev_branding)) - async def issue( - self, - ctx: commands.Context, - numbers: commands.Greedy[int], - repository: str = "sir-lancebot", - user: str = "python-discord" - ) -> None: - """Command to retrieve issue(s) from a GitHub repository.""" + self.repos = [] + self.get_pydis_repos.start() + + @tasks.loop(minutes=30) + async def get_pydis_repos(self) -> None: + """Get all python-discord repositories on github.""" + async with self.bot.http_session.get(REPOS_API.format(org="python-discord")) as resp: + if resp.status == 200: + data = await resp.json() + for repo in data: + self.repos.append(repo["full_name"].split("/")[1]) + self.repo_regex = "|".join(self.repos) + else: + log.debug(f"Failed to get latest Pydis repositories. Status code {resp.status}") + + @staticmethod + def check_in_block(message: discord.Message, repo_issue: str) -> bool: + """Check whether the <repo>#<issue> is in codeblocks.""" + block = re.findall(CODE_BLOCK_RE, message.content) + + if not block: + return False + elif "#".join(repo_issue.split(" ")) in "".join([*block[0]]): + return True + return False + + async def fetch_issues( + self, + numbers: set, + repository: str, + user: str + ) -> t.Union[FetchIssueErrors, str, list]: + """Retrieve issue(s) from a GitHub repository.""" links = [] - numbers = set(numbers) # Convert from list to set to remove duplicates, if any - if not numbers: - await ctx.invoke(self.bot.get_command('help'), 'issue') - return + return FetchIssueErrors.value_error if len(numbers) > MAX_REQUESTS: - embed = discord.Embed( - title=random.choice(ERROR_REPLIES), - color=Colours.soft_red, - description=f"Too many issues/PRs! (maximum of {MAX_REQUESTS})" - ) - await ctx.send(embed=embed) - return + return FetchIssueErrors.max_requests for number in numbers: url = f"https://api.github.com/repos/{user}/{repository}/issues/{number}" merge_url = f"https://api.github.com/repos/{user}/{repository}/pulls/{number}/merge" - log.trace(f"Querying GH issues API: {url}") async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as r: json_data = await r.json() if r.status in BAD_RESPONSE: log.warning(f"Received response {r.status} from: {url}") - return await ctx.send(f"[{str(r.status)}] #{number} {BAD_RESPONSE.get(r.status)}") + return f"[{str(r.status)}] #{number} {BAD_RESPONSE.get(r.status)}" # The initial API request is made to the issues API endpoint, which will return information # if the issue or PR is present. However, the scope of information returned for PRs differs @@ -92,15 +125,80 @@ class Issues(commands.Cog): issue_url = json_data.get("html_url") links.append([icon_url, f"[{repository}] #{number} {json_data.get('title')}", issue_url]) - # Issue/PR format: emoji to show if open/closed/merged, number and the title as a singular link. - description_list = ["{0} [{1}]({2})".format(*link) for link in links] + return links + + @staticmethod + def get_embed(result: list, user: str = "python-discord", repository: str = "") -> discord.Embed: + """Get Response Embed.""" + description_list = ["{0} [{1}]({2})".format(*link) for link in result] resp = discord.Embed( colour=Colours.bright_green, description='\n'.join(description_list) ) resp.set_author(name="GitHub", url=f"https://github.com/{user}/{repository}") - await ctx.send(embed=resp) + return resp + + @commands.command(aliases=("pr",)) + async def issue( + self, + ctx: commands.Context, + numbers: commands.Greedy[int], + repository: str = "sir-lancebot", + user: str = "python-discord" + ) -> None: + """Command to retrieve issue(s) from a GitHub repository.""" + if not( + ctx.channel.category.id in WHITELISTED_CATEGORIES + or ctx.channel.id in WHITELISTED_CHANNELS + ): + return + + result = await self.fetch_issues(set(numbers), repository, user) + + if result == FetchIssueErrors.value_error: + await ctx.invoke(self.bot.get_command('help'), 'issue') + + elif result == FetchIssueErrors.max_requests: + embed = discord.Embed( + title=random.choice(ERROR_REPLIES), + color=Colours.soft_red, + description=f"Too many issues/PRs! (maximum of {MAX_REQUESTS})" + ) + await ctx.send(embed=embed) + + elif isinstance(result, list): + # Issue/PR format: emoji to show if open/closed/merged, number and the title as a singular link. + resp = self.get_embed(result, user, repository) + await ctx.send(embed=resp) + + elif isinstance(result, str): + await ctx.send(result) + + @commands.Cog.listener() + async def on_message(self, message: discord.Message) -> None: + """Command to retrieve issue(s) from a GitHub repository using automatic linking if matching <repo>#<issue>.""" + if not( + message.channel.category.id in WHITELISTED_CATEGORIES + or message.channel.id in WHITELISTED_CHANNELS_ON_MESSAGE + ): + return + + message_repo_issue_map = re.findall(fr"({self.repo_regex})#(\d+)", message.content) + links = [] + + if message_repo_issue_map: + for repo_issue in message_repo_issue_map: + if not self.check_in_block(message, " ".join(repo_issue)): + result = await self.fetch_issues({repo_issue[1]}, repo_issue[0], "python-discord") + if isinstance(result, list): + links.extend(result) + + if not links: + return + + resp = self.get_embed(links, "python-discord") + await message.channel.send(embed=resp) def setup(bot: commands.Bot) -> None: diff --git a/bot/exts/evergreen/status_cats.py b/bot/exts/evergreen/status_cats.py deleted file mode 100644 index 586b8378..00000000 --- a/bot/exts/evergreen/status_cats.py +++ /dev/null @@ -1,33 +0,0 @@ -from http import HTTPStatus - -import discord -from discord.ext import commands - - -class StatusCats(commands.Cog): - """Commands that give HTTP statuses described and visualized by cats.""" - - def __init__(self, bot: commands.Bot): - self.bot = bot - - @commands.command(aliases=['statuscat']) - async def http_cat(self, ctx: commands.Context, code: int) -> None: - """Sends an embed with an image of a cat, potraying the status code.""" - embed = discord.Embed(title=f'**Status: {code}**') - - try: - HTTPStatus(code) - - except ValueError: - embed.set_footer(text='Inputted status code does not exist.') - - else: - embed.set_image(url=f'https://http.cat/{code}.jpg') - - finally: - await ctx.send(embed=embed) - - -def setup(bot: commands.Bot) -> None: - """Load the StatusCats cog.""" - bot.add_cog(StatusCats(bot)) diff --git a/bot/exts/evergreen/status_codes.py b/bot/exts/evergreen/status_codes.py new file mode 100644 index 00000000..874c87eb --- /dev/null +++ b/bot/exts/evergreen/status_codes.py @@ -0,0 +1,71 @@ +from http import HTTPStatus + +import discord +from discord.ext import commands + +HTTP_DOG_URL = "https://httpstatusdogs.com/img/{code}.jpg" +HTTP_CAT_URL = "https://http.cat/{code}.jpg" + + +class HTTPStatusCodes(commands.Cog): + """Commands that give HTTP statuses described and visualized by cats and dogs.""" + + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.group(name="http_status", aliases=("status", "httpstatus")) + async def http_status_group(self, ctx: commands.Context) -> None: + """Group containing dog and cat http status code commands.""" + if not ctx.invoked_subcommand: + await ctx.send_help(ctx.command) + + @http_status_group.command(name='cat') + async def http_cat(self, ctx: commands.Context, code: int) -> None: + """Sends an embed with an image of a cat, portraying the status code.""" + embed = discord.Embed(title=f'**Status: {code}**') + url = HTTP_CAT_URL.format(code=code) + + try: + HTTPStatus(code) + async with self.bot.http_session.get(url, allow_redirects=False) as response: + if response.status != 404: + embed.set_image(url=url) + else: + raise NotImplementedError + + except ValueError: + embed.set_footer(text='Inputted status code does not exist.') + + except NotImplementedError: + embed.set_footer(text='Inputted status code is not implemented by http.cat yet.') + + finally: + await ctx.send(embed=embed) + + @http_status_group.command(name='dog') + async def http_dog(self, ctx: commands.Context, code: int) -> None: + """Sends an embed with an image of a dog, portraying the status code.""" + embed = discord.Embed(title=f'**Status: {code}**') + url = HTTP_DOG_URL.format(code=code) + + try: + HTTPStatus(code) + async with self.bot.http_session.get(url, allow_redirects=False) as response: + if response.status != 302: + embed.set_image(url=url) + else: + raise NotImplementedError + + except ValueError: + embed.set_footer(text='Inputted status code does not exist.') + + except NotImplementedError: + embed.set_footer(text='Inputted status code is not implemented by httpstatusdogs.com yet.') + + finally: + await ctx.send(embed=embed) + + +def setup(bot: commands.Bot) -> None: + """Load the HTTPStatusCodes cog.""" + bot.add_cog(HTTPStatusCodes(bot)) diff --git a/bot/exts/evergreen/tic_tac_toe.py b/bot/exts/evergreen/tic_tac_toe.py new file mode 100644 index 00000000..e1190502 --- /dev/null +++ b/bot/exts/evergreen/tic_tac_toe.py @@ -0,0 +1,323 @@ +import asyncio +import random +import typing as t + +import discord +from discord.ext.commands import Cog, Context, check, group, guild_only + +from bot.bot import Bot +from bot.constants import Emojis +from bot.utils.pagination import LinePaginator + +CONFIRMATION_MESSAGE = ( + "{opponent}, {requester} wants to play Tic-Tac-Toe against you. React to this message with " + f"{Emojis.confirmation} to accept or with {Emojis.decline} to decline." +) + + +def check_win(board: t.Dict[int, str]) -> bool: + """Check from board, is any player won game.""" + return any( + ( + # Horizontal + board[1] == board[2] == board[3], + board[4] == board[5] == board[6], + board[7] == board[8] == board[9], + # Vertical + board[1] == board[4] == board[7], + board[2] == board[5] == board[8], + board[3] == board[6] == board[9], + # Diagonal + board[1] == board[5] == board[9], + board[3] == board[5] == board[7], + ) + ) + + +class Player: + """Class that contains information about player and functions that interact with player.""" + + def __init__(self, user: discord.User, ctx: Context, symbol: str): + self.user = user + self.ctx = ctx + self.symbol = symbol + + async def get_move(self, board: t.Dict[int, str], msg: discord.Message) -> t.Tuple[bool, t.Optional[int]]: + """ + Get move from user. + + Return is timeout reached and position of field what user will fill when timeout don't reach. + """ + def check_for_move(r: discord.Reaction, u: discord.User) -> bool: + """Check does user who reacted is user who we want, message is board and emoji is in board values.""" + return ( + u.id == self.user.id + and msg.id == r.message.id + and r.emoji in board.values() + and r.emoji in Emojis.number_emojis.values() + ) + + try: + react, _ = await self.ctx.bot.wait_for('reaction_add', timeout=30.0, check=check_for_move) + except asyncio.TimeoutError: + return True, None + else: + return False, list(Emojis.number_emojis.keys())[list(Emojis.number_emojis.values()).index(react.emoji)] + + def __str__(self) -> str: + """Return mention of user.""" + return self.user.mention + + +class AI: + """Tic Tac Toe AI class for against computer gaming.""" + + def __init__(self, symbol: str): + self.symbol = symbol + + async def get_move(self, board: t.Dict[int, str], _: discord.Message) -> t.Tuple[bool, int]: + """Get move from AI. AI use Minimax strategy.""" + possible_moves = [i for i, emoji in board.items() if emoji in list(Emojis.number_emojis.values())] + + for symbol in (Emojis.o, Emojis.x): + for move in possible_moves: + board_copy = board.copy() + board_copy[move] = symbol + if check_win(board_copy): + return False, move + + open_corners = [i for i in possible_moves if i in (1, 3, 7, 9)] + if len(open_corners) > 0: + return False, random.choice(open_corners) + + if 5 in possible_moves: + return False, 5 + + open_edges = [i for i in possible_moves if i in (2, 4, 6, 8)] + return False, random.choice(open_edges) + + def __str__(self) -> str: + """Return `AI` as user name.""" + return "AI" + + +class Game: + """Class that contains information and functions about Tic Tac Toe game.""" + + def __init__(self, players: t.List[t.Union[Player, AI]], ctx: Context): + self.players = players + self.ctx = ctx + self.board = { + 1: Emojis.number_emojis[1], + 2: Emojis.number_emojis[2], + 3: Emojis.number_emojis[3], + 4: Emojis.number_emojis[4], + 5: Emojis.number_emojis[5], + 6: Emojis.number_emojis[6], + 7: Emojis.number_emojis[7], + 8: Emojis.number_emojis[8], + 9: Emojis.number_emojis[9] + } + + self.current = self.players[0] + self.next = self.players[1] + + self.winner: t.Optional[t.Union[Player, AI]] = None + self.loser: t.Optional[t.Union[Player, AI]] = None + self.over = False + self.canceled = False + self.draw = False + + async def get_confirmation(self) -> t.Tuple[bool, t.Optional[str]]: + """ + Ask does user want to play TicTacToe against requester. First player is always requester. + + This return tuple that have: + - first element boolean (is game accepted?) + - (optional, only when first element is False, otherwise None) reason for declining. + """ + confirm_message = await self.ctx.send( + CONFIRMATION_MESSAGE.format( + opponent=self.players[1].user.mention, + requester=self.players[0].user.mention + ) + ) + await confirm_message.add_reaction(Emojis.confirmation) + await confirm_message.add_reaction(Emojis.decline) + + def confirm_check(reaction: discord.Reaction, user: discord.User) -> bool: + """Check is user who reacted from who this was requested, message is confirmation and emoji is valid.""" + return ( + reaction.emoji in (Emojis.confirmation, Emojis.decline) + and reaction.message.id == confirm_message.id + and user == self.players[1].user + ) + + try: + reaction, user = await self.ctx.bot.wait_for( + "reaction_add", + timeout=60.0, + check=confirm_check + ) + except asyncio.TimeoutError: + self.over = True + self.canceled = True + await confirm_message.delete() + return False, "Running out of time... Cancelled game." + + await confirm_message.delete() + if reaction.emoji == Emojis.confirmation: + return True, None + else: + self.over = True + self.canceled = True + return False, "User declined" + + async def add_reactions(self, msg: discord.Message) -> None: + """Add number emojis to message.""" + for nr in Emojis.number_emojis.values(): + await msg.add_reaction(nr) + + def format_board(self) -> str: + """Get formatted tic-tac-toe board for message.""" + board = list(self.board.values()) + return "\n".join( + (f"{board[line]} {board[line + 1]} {board[line + 2]}" for line in range(0, len(board), 3)) + ) + + async def play(self) -> None: + """Start and handle game.""" + await self.ctx.send("It's time for the game! Let's begin.") + board = await self.ctx.send( + embed=discord.Embed(description=self.format_board()) + ) + await self.add_reactions(board) + + for _ in range(9): + if isinstance(self.current, Player): + announce = await self.ctx.send( + f"{self.current.user.mention}, it's your turn! " + "React with an emoji to take your go." + ) + timeout, pos = await self.current.get_move(self.board, board) + if isinstance(self.current, Player): + await announce.delete() + if timeout: + await self.ctx.send(f"{self.current.user.mention} ran out of time. Canceling game.") + self.over = True + self.canceled = True + return + self.board[pos] = self.current.symbol + await board.edit( + embed=discord.Embed(description=self.format_board()) + ) + await board.clear_reaction(Emojis.number_emojis[pos]) + if check_win(self.board): + self.winner = self.current + self.loser = self.next + await self.ctx.send( + f":tada: {self.current} won this game! :tada:" + ) + await board.clear_reactions() + break + self.current, self.next = self.next, self.current + if not self.winner: + self.draw = True + await self.ctx.send("It's a DRAW!") + self.over = True + + +def is_channel_free() -> t.Callable: + """Check is channel where command will be invoked free.""" + async def predicate(ctx: Context) -> bool: + return all(game.channel != ctx.channel for game in ctx.cog.games if not game.over) + return check(predicate) + + +def is_requester_free() -> t.Callable: + """Check is requester not already in any game.""" + async def predicate(ctx: Context) -> bool: + return all( + ctx.author not in (player.user for player in game.players) for game in ctx.cog.games if not game.over + ) + return check(predicate) + + +class TicTacToe(Cog): + """TicTacToe cog contains tic-tac-toe game commands.""" + + def __init__(self, bot: Bot): + self.bot = bot + self.games: t.List[Game] = [] + + @guild_only() + @is_channel_free() + @is_requester_free() + @group(name="tictactoe", aliases=("ttt",), invoke_without_command=True) + async def tic_tac_toe(self, ctx: Context, opponent: t.Optional[discord.User]) -> None: + """Tic Tac Toe game. Play against friends or AI. Use reactions to add your mark to field.""" + if opponent == ctx.author: + await ctx.send("You can't play against yourself.") + return + if opponent is not None and not all( + opponent not in (player.user for player in g.players) for g in ctx.cog.games if not g.over + ): + await ctx.send("Opponent is already in game.") + return + if opponent is None: + game = Game( + [Player(ctx.author, ctx, Emojis.x), AI(Emojis.o)], + ctx + ) + else: + game = Game( + [Player(ctx.author, ctx, Emojis.x), Player(opponent, ctx, Emojis.o)], + ctx + ) + self.games.append(game) + if opponent is not None: + confirmed, msg = await game.get_confirmation() + + if not confirmed: + if msg: + await ctx.send(msg) + return + await game.play() + + @tic_tac_toe.group(name="history", aliases=("log",), invoke_without_command=True) + async def tic_tac_toe_logs(self, ctx: Context) -> None: + """Show most recent tic-tac-toe games.""" + if len(self.games) < 1: + await ctx.send("No recent games.") + return + log_games = [] + for i, game in enumerate(self.games): + if game.over and not game.canceled: + if game.draw: + log_games.append( + f"**#{i+1}**: {game.players[0]} vs {game.players[1]} (draw)" + ) + else: + log_games.append( + f"**#{i+1}**: {game.winner} :trophy: vs {game.loser}" + ) + await LinePaginator.paginate( + log_games, + ctx, + discord.Embed(title="Most recent Tic Tac Toe games") + ) + + @tic_tac_toe_logs.command(name="show", aliases=("s",)) + async def show_tic_tac_toe_board(self, ctx: Context, game_id: int) -> None: + """View game board by ID (ID is possible to get by `.tictactoe history`).""" + if len(self.games) < game_id: + await ctx.send("Game don't exist.") + return + game = self.games[game_id - 1] + await ctx.send(f"{game.winner} :trophy: vs {game.loser}") + await ctx.send(game.format_board()) + + +def setup(bot: Bot) -> None: + """Load TicTacToe Cog.""" + bot.add_cog(TicTacToe(bot)) diff --git a/bot/exts/evergreen/xkcd.py b/bot/exts/evergreen/xkcd.py new file mode 100644 index 00000000..d3224bfe --- /dev/null +++ b/bot/exts/evergreen/xkcd.py @@ -0,0 +1,89 @@ +import logging +import re +from random import randint +from typing import Dict, Optional, Union + +from discord import Embed +from discord.ext import tasks +from discord.ext.commands import Cog, Context, command + +from bot.bot import Bot +from bot.constants import Colours + +log = logging.getLogger(__name__) + +COMIC_FORMAT = re.compile(r"latest|[0-9]+") +BASE_URL = "https://xkcd.com" + + +class XKCD(Cog): + """Retrieving XKCD comics.""" + + def __init__(self, bot: Bot) -> None: + self.bot = bot + self.latest_comic_info: Dict[str, Union[str, int]] = {} + self.get_latest_comic_info.start() + + def cog_unload(self) -> None: + """Cancels refreshing of the task for refreshing the most recent comic info.""" + self.get_latest_comic_info.cancel() + + @tasks.loop(minutes=30) + async def get_latest_comic_info(self) -> None: + """Refreshes latest comic's information ever 30 minutes. Also used for finding a random comic.""" + async with self.bot.http_session.get(f"{BASE_URL}/info.0.json") as resp: + if resp.status == 200: + self.latest_comic_info = await resp.json() + else: + log.debug(f"Failed to get latest XKCD comic information. Status code {resp.status}") + + @command(name="xkcd") + async def fetch_xkcd_comics(self, ctx: Context, comic: Optional[str]) -> None: + """ + Getting an xkcd comic's information along with the image. + + To get a random comic, don't type any number as an argument. To get the latest, type 'latest'. + """ + embed = Embed(title=f"XKCD comic '{comic}'") + + embed.colour = Colours.soft_red + + if comic and (comic := re.match(COMIC_FORMAT, comic)) is None: + embed.description = "Comic parameter should either be an integer or 'latest'." + await ctx.send(embed=embed) + return + + comic = randint(1, self.latest_comic_info['num']) if comic is None else comic.group(0) + + if comic == "latest": + info = self.latest_comic_info + else: + async with self.bot.http_session.get(f"{BASE_URL}/{comic}/info.0.json") as resp: + if resp.status == 200: + info = await resp.json() + else: + embed.title = f"XKCD comic #{comic}" + embed.description = f"{resp.status}: Could not retrieve xkcd comic #{comic}." + log.debug(f"Retrieving xkcd comic #{comic} failed with status code {resp.status}.") + await ctx.send(embed=embed) + return + + embed.title = f"XKCD comic #{info['num']}" + + if info["img"][-3:] in ("jpg", "png", "gif"): + embed.set_image(url=info["img"]) + date = f"{info['year']}/{info['month']}/{info['day']}" + embed.set_footer(text=f"{date} - #{info['num']}, \'{info['safe_title']}\'") + embed.colour = Colours.soft_green + else: + embed.description = ( + "The selected comic is interactive, and cannot be displayed within an embed.\n" + f"Comic can be viewed [here](https://xkcd.com/{info['num']})." + ) + + await ctx.send(embed=embed) + + +def setup(bot: Bot) -> None: + """Loading the XKCD cog.""" + bot.add_cog(XKCD(bot)) diff --git a/bot/exts/halloween/spookynamerate.py b/bot/exts/halloween/spookynamerate.py new file mode 100644 index 00000000..e2950343 --- /dev/null +++ b/bot/exts/halloween/spookynamerate.py @@ -0,0 +1,401 @@ +import asyncio +import json +import random +from collections import defaultdict +from datetime import datetime, timedelta +from logging import getLogger +from os import getenv +from pathlib import Path +from typing import Dict, Union + +from async_rediscache import RedisCache +from discord import Embed, Reaction, TextChannel, User +from discord.colour import Colour +from discord.ext import tasks +from discord.ext.commands import Bot, Cog, Context, group + +from bot.constants import Channels, Client, Colours, Month +from bot.utils.decorators import InMonthCheckFailure + +logger = getLogger(__name__) + +EMOJIS_VAL = { + "\N{Jack-O-Lantern}": 1, + "\N{Ghost}": 2, + "\N{Skull and Crossbones}": 3, + "\N{Zombie}": 4, + "\N{Face Screaming In Fear}": 5, +} +ADDED_MESSAGES = [ + "Let's see if you win?", + ":jack_o_lantern: SPOOKY :jack_o_lantern:", + "If you got it, haunt it.", + "TIME TO GET YOUR SPOOKY ON! :skull:", +] +PING = "<@{id}>" + +EMOJI_MESSAGE = "\n".join([f"- {emoji} {val}" for emoji, val in EMOJIS_VAL.items()]) +HELP_MESSAGE_DICT = { + "title": "Spooky Name Rate", + "description": f"Help for the `{Client.prefix}spookynamerate` command", + "color": Colours.soft_orange, + "fields": [ + { + "name": "How to play", + "value": ( + "Everyday, the bot will post a random name, which you will need to spookify using your creativity.\n" + "You can rate each message according to how scary it is.\n" + "At the end of the day, the author of the message with most reactions will be the winner of the day.\n" + f"On a scale of 1 to {len(EMOJIS_VAL)}, the reactions order:\n" + f"{EMOJI_MESSAGE}" + ), + "inline": False, + }, + { + "name": "How do I add my spookified name?", + "value": f"Simply type `{Client.prefix}spookynamerate add my name`", + "inline": False, + }, + { + "name": "How do I *delete* my spookified name?", + "value": f"Simply type `{Client.prefix}spookynamerate delete`", + "inline": False, + }, + ], +} + + +class SpookyNameRate(Cog): + """ + A game that asks the user to spookify or halloweenify a name that is given everyday. + + It sends a random name everyday. The user needs to try and spookify it to his best ability and + send that name back using the `spookynamerate add entry` command + """ + + # This cache stores the message id of each added word along with a dictionary which contains the name the author + # added, the author's id, and the author's score (which is 0 by default) + messages = RedisCache() + + # The data cache stores small information such as the current name that is going on and whether it is the first time + # the bot is running + data = RedisCache() + debug = getenv('SPOOKYNAMERATE_DEBUG', False) # Enable if you do not want to limit the commands to October or if + # you do not want to wait till 12 UTC. Note: if debug is enabled and you run `.cogs reload spookynamerate`, it + # will automatically start the scoring and announcing the result (without waiting for 12, so do not expect it to.). + # Also, it won't wait for the two hours (when the poll closes). + + def __init__(self, bot: Bot) -> None: + self.bot = bot + + names_data = self.load_json( + Path("bot", "resources", "halloween", "spookynamerate_names.json") + ) + self.first_names = names_data["first_names"] + self.last_names = names_data["last_names"] + # the names are from https://www.mockaroo.com/ + + self.name = None + + self.bot.loop.create_task(self.load_vars()) + + self.first_time = None + self.poll = False + self.announce_name.start() + self.checking_messages = asyncio.Lock() + # Define an asyncio.Lock() to make sure the dictionary isn't changed + # when checking the messages for duplicate emojis' + + async def load_vars(self) -> None: + """Loads the variables that couldn't be loaded in __init__.""" + self.first_time = await self.data.get("first_time", True) + self.name = await self.data.get("name") + + @group(name="spookynamerate", invoke_without_command=True) + async def spooky_name_rate(self, ctx: Context) -> None: + """Get help on the Spooky Name Rate game.""" + await ctx.send(embed=Embed.from_dict(HELP_MESSAGE_DICT)) + + @spooky_name_rate.command(name="list", aliases=["all", "entries"]) + async def list_entries(self, ctx: Context) -> None: + """Send all the entries up till now in a single embed.""" + await ctx.send(embed=await self.get_responses_list(final=False)) + + @spooky_name_rate.command(name="name") + async def tell_name(self, ctx: Context) -> None: + """Tell the current random name.""" + if not self.poll: + await ctx.send(f"The name is **{self.name}**") + return + + await ctx.send( + f"The name ~~is~~ was **{self.name}**. The poll has already started, so you cannot " + "add an entry." + ) + + @spooky_name_rate.command(name="add", aliases=["register"]) + async def add_name(self, ctx: Context, *, name: str) -> None: + """Use this command to add/register your spookified name.""" + if self.poll: + logger.info(f"{ctx.message.author} tried to add a name, but the poll had already started.") + await ctx.send("Sorry, the poll has started! You can try and participate in the next round though!") + return + + message = ctx.message + + for data in (json.loads(user_data) for _, user_data in await self.messages.items()): + if data["author"] == message.author.id: + await ctx.send( + "But you have already added an entry! Type " + f"`{self.bot.command_prefix}spookynamerate " + "delete` to delete it, and then you can add it again" + ) + return + + elif data["name"] == name: + await ctx.send("TOO LATE. Someone has already added this name.") + return + + msg = await (await self.get_channel()).send(f"{message.author.mention} added the name {name!r}!") + + await self.messages.set( + msg.id, + json.dumps( + { + "name": name, + "author": message.author.id, + "score": 0, + } + ), + ) + + for emoji in EMOJIS_VAL: + await msg.add_reaction(emoji) + + logger.info(f"{message.author} added the name {name!r}") + + @spooky_name_rate.command(name="delete") + async def delete_name(self, ctx: Context) -> None: + """Delete the user's name.""" + if self.poll: + await ctx.send("You can't delete your name since the poll has already started!") + return + for message_id, data in await self.messages.items(): + data = json.loads(data) + + if ctx.author.id == data["author"]: + await self.messages.delete(message_id) + await ctx.send(f'Name deleted successfully ({data["name"]!r})!') + return + + await ctx.send( + f"But you don't have an entry... :eyes: Type `{self.bot.command_prefix}spookynamerate add your entry`" + ) + + @Cog.listener() + async def on_reaction_add(self, reaction: Reaction, user: User) -> None: + """Ensures that each user adds maximum one reaction.""" + if user.bot or not await self.messages.contains(reaction.message.id): + return + + async with self.checking_messages: # Acquire the lock so that the dictionary isn't reset while iterating. + if reaction.emoji in EMOJIS_VAL: + # create a custom counter + reaction_counter = defaultdict(int) + for msg_reaction in reaction.message.reactions: + async for reaction_user in msg_reaction.users(): + if reaction_user == self.bot.user: + continue + reaction_counter[reaction_user] += 1 + + if reaction_counter[user] > 1: + await user.send( + "Sorry, you have already added a reaction, " + "please remove your reaction and try again." + ) + await reaction.remove(user) + return + + @tasks.loop(hours=24.0) + async def announce_name(self) -> None: + """Announces the name needed to spookify every 24 hours and the winner of the previous game.""" + if not self.in_allowed_month(): + return + + channel = await self.get_channel() + + if self.first_time: + await channel.send( + "Okkey... Welcome to the **Spooky Name Rate Game**! It's a relatively simple game.\n" + f"Everyday, a random name will be sent in <#{Channels.community_bot_commands}> " + "and you need to try and spookify it!\nRegister your name using " + f"`{self.bot.command_prefix}spookynamerate add spookified name`" + ) + + await self.data.set("first_time", False) + self.first_time = False + + else: + if await self.messages.items(): + await channel.send(embed=await self.get_responses_list(final=True)) + self.poll = True + if not SpookyNameRate.debug: + await asyncio.sleep(2 * 60 * 60) # sleep for two hours + + logger.info("Calculating score") + for message_id, data in await self.messages.items(): + data = json.loads(data) + + msg = await channel.fetch_message(message_id) + score = 0 + for reaction in msg.reactions: + reaction_value = EMOJIS_VAL.get(reaction.emoji, 0) # get the value of the emoji else 0 + score += reaction_value * (reaction.count - 1) # multiply by the num of reactions + # subtract one, since one reaction was done by the bot + + logger.debug(f"{self.bot.get_user(data['author'])} got a score of {score}") + data["score"] = score + await self.messages.set(message_id, json.dumps(data)) + + # Sort the winner messages + winner_messages = sorted( + ((msg_id, json.loads(usr_data)) for msg_id, usr_data in await self.messages.items()), + key=lambda x: x[1]["score"], + reverse=True, + ) + + winners = [] + for i, winner in enumerate(winner_messages): + winners.append(winner) + if len(winner_messages) > i + 1: + if winner_messages[i + 1][1]["score"] != winner[1]["score"]: + break + elif len(winner_messages) == (i + 1) + 1: # The next element is the last element + if winner_messages[i + 1][1]["score"] != winner[1]["score"]: + break + + # one iteration is complete + await channel.send("Today's Spooky Name Rate Game ends now, and the winner(s) is(are)...") + + async with channel.typing(): + await asyncio.sleep(1) # give the drum roll feel + + if not winners: # There are no winners (no participants) + await channel.send("Hmm... Looks like no one participated! :cry:") + return + + score = winners[0][1]["score"] + congratulations = "to all" if len(winners) > 1 else PING.format(id=winners[0][1]["author"]) + names = ", ".join(f'{win[1]["name"]} ({PING.format(id=win[1]["author"])})' for win in winners) + + # display winners, their names and scores + await channel.send( + f"Congratulations {congratulations}!\n" + f"You have a score of {score}!\n" + f"Your name{ 's were' if len(winners) > 1 else 'was'}:\n{names}" + ) + + # Send random party emojis + party = (random.choice([":partying_face:", ":tada:"]) for _ in range(random.randint(1, 10))) + await channel.send(" ".join(party)) + + async with self.checking_messages: # Acquire the lock to delete the messages + await self.messages.clear() # reset the messages + + # send the next name + self.name = f"{random.choice(self.first_names)} {random.choice(self.last_names)}" + await self.data.set("name", self.name) + + await channel.send( + "Let's move on to the next name!\nAnd the next name is...\n" + f"**{self.name}**!\nTry to spookify that... :smirk:" + ) + + self.poll = False # accepting responses + + @announce_name.before_loop + async def wait_till_scheduled_time(self) -> None: + """Waits till the next day's 12PM if crossed it, otherwise waits till the same day's 12PM.""" + if SpookyNameRate.debug: + return + + now = datetime.utcnow() + if now.hour < 12: + twelve_pm = now.replace(hour=12, minute=0, second=0, microsecond=0) + time_left = twelve_pm - now + await asyncio.sleep(time_left.seconds) + return + + tomorrow_12pm = now + timedelta(days=1) + tomorrow_12pm = tomorrow_12pm.replace(hour=12, minute=0, second=0, microsecond=0) + await asyncio.sleep((tomorrow_12pm - now).seconds) + + async def get_responses_list(self, final: bool = False) -> Embed: + """Returns an embed containing the responses of the people.""" + channel = await self.get_channel() + + embed = Embed(color=Colour.red()) + + if await self.messages.items(): + if final: + embed.title = "Spooky Name Rate is about to end!" + embed.description = ( + "This Spooky Name Rate round is about to end in 2 hours! You can review " + "the entries below! Have you rated other's names?" + ) + else: + embed.title = "All the spookified names!" + embed.description = "See a list of all the entries entered by everyone!" + else: + embed.title = "No one has added an entry yet..." + + for message_id, data in await self.messages.items(): + data = json.loads(data) + + embed.add_field( + name=(self.bot.get_user(data["author"]) or await self.bot.fetch_user(data["author"])).name, + value=f"[{(data)['name']}](https://discord.com/channels/{Client.guild}/{channel.id}/{message_id})", + ) + + return embed + + async def get_channel(self) -> Union[TextChannel, None]: + """Gets the sir-lancebot-channel after waiting until ready.""" + await self.bot.wait_until_ready() + channel = self.bot.get_channel( + Channels.community_bot_commands + ) or await self.bot.fetch_channel(Channels.community_bot_commands) + if not channel: + logger.warning("Bot is unable to get the #seasonalbot-commands channel. Please check the channel ID.") + return channel + + @staticmethod + def load_json(file: Path) -> Dict[str, str]: + """Loads a JSON file and returns its contents.""" + with file.open("r", encoding="utf-8") as f: + return json.load(f) + + @staticmethod + def in_allowed_month() -> bool: + """Returns whether running in the limited month.""" + if SpookyNameRate.debug: + return True + + if not Client.month_override: + return datetime.utcnow().month == Month.OCTOBER + return Client.month_override == Month.OCTOBER + + def cog_check(self, ctx: Context) -> bool: + """A command to check whether the command is being called in October.""" + if not self.in_allowed_month(): + raise InMonthCheckFailure("You can only use these commands in October!") + return True + + def cog_unload(self) -> None: + """Stops the announce_name task.""" + self.announce_name.cancel() + + +def setup(bot: Bot) -> None: + """Loads the SpookyNameRate Cog.""" + bot.add_cog(SpookyNameRate(bot)) diff --git a/bot/resources/evergreen/trivia_quiz.json b/bot/resources/evergreen/trivia_quiz.json index 8f0a4114..faa3bc3b 100644 --- a/bot/resources/evergreen/trivia_quiz.json +++ b/bot/resources/evergreen/trivia_quiz.json @@ -247,6 +247,18 @@ "question": "What is the capital of Iraq?", "answer": "Baghdad", "info": "Baghdad is the capital of Iraq. It has a population of 7 million people." + }, + { + "id": 136, + "question": "The United Nations headquarters is located at which city?", + "answer": "New York", + "info": "The United Nations is headquartered in New York City in a complex designed by a board of architects led by Wallace Harrison and built by the architectural firm Harrison & Abramovitz. The complex has served as the official headquarters of the United Nations since its completion in 1951." + }, + { + "id": 137, + "question": "At what year did Christopher Columbus discover America?", + "answer": "1492", + "info": "The explorer Christopher Columbus made four trips across the Atlantic Ocean from Spain: in 1492, 1493, 1498 and 1502. He was determined to find a direct water route west from Europe to Asia, but he never did. Instead, he stumbled upon the Americas" } ] } diff --git a/bot/resources/halloween/spookynamerate_names.json b/bot/resources/halloween/spookynamerate_names.json new file mode 100644 index 00000000..7657880b --- /dev/null +++ b/bot/resources/halloween/spookynamerate_names.json @@ -0,0 +1,2206 @@ +{ + "first_names": [ + "Eberhard", + "Gladys", + "Joshua", + "Misty", + "Bondy", + "Constantine", + "Juliette", + "Dalis", + "Nap", + "Sandy", + "Inglebert", + "Sasha", + "Julietta", + "Christoforo", + "Del", + "Zelma", + "Vladimir", + "Wayland", + "Enos", + "Siobhan", + "Farrand", + "Ailee", + "Horatia", + "Gloriana", + "Britney", + "Shel", + "Lindsey", + "Francis", + "Elsa", + "Fred", + "Upton", + "Lothaire", + "Cara", + "Margarete", + "Wolfgang", + "Charin", + "Loydie", + "Aurelea", + "Sibel", + "Glenden", + "Julian", + "Roby", + "Gerri", + "Sandie", + "Twila", + "Shaylyn", + "Clyde", + "Dina", + "Chase", + "Caron", + "Carlin", + "Aida", + "Rhonda", + "Rebekkah", + "Charmian", + "Lindy", + "Obadiah", + "Willy", + "Matti", + "Melodie", + "Ira", + "Wilfrid", + "Berton", + "Denver", + "Clarette", + "Nicolas", + "Tawnya", + "Cynthy", + "Arman", + "Sherwood", + "Flemming", + "Berri", + "Beret", + "Aili", + "Hannie", + "Eadie", + "Tannie", + "Gilda", + "Walton", + "Nolly", + "Tonya", + "Meaghan", + "Timmi", + "Faina", + "Sarge", + "Britteny", + "Farlay", + "Carola", + "Skippy", + "Corrina", + "Hans", + "Courtnay", + "Taffy", + "Averill", + "Martie", + "Tobye", + "Broderic", + "Gardner", + "Lucky", + "Beverie", + "Ignaz", + "Siana", + "Marybelle", + "Leif", + "Baily", + "Pyotr", + "Myrtle", + "Darb", + "Gar", + "Vinni", + "Samson", + "Kinny", + "Briant", + "Verney", + "Del", + "Marion", + "Beniamino", + "Nona", + "Fay", + "Noreen", + "Maurizio", + "Nial", + "Mirabella", + "Melisa", + "Anatol", + "Halette", + "Johnathon", + "Antonietta", + "Germana", + "Towny", + "Shayne", + "Court", + "Merrile", + "Staffard", + "Odele", + "Gustav", + "Moyna", + "Warden", + "Craggie", + "Hurleigh", + "Hartley", + "Rustie", + "Raven", + "Farra", + "Leonidas", + "Jorrie", + "Maximilian", + "Augustin", + "Cordelia", + "Christoffer", + "Lana", + "Vittorio", + "Janith", + "Margaret", + "Bethanne", + "Brooke", + "Payton", + "Poul", + "Diahann", + "Andy", + "Garek", + "Isa", + "Dunn", + "Anny", + "Hillary", + "Andres", + "Winn", + "Gare", + "Ameline", + "Audre", + "Rodrigo", + "Anabal", + "Reuben", + "Cecil", + "Alexandro", + "Corny", + "Erek", + "William", + "Rudyard", + "Muffin", + "Allin", + "Emmit", + "Heindrick", + "Myrna", + "Kriste", + "Perry", + "Annmarie", + "Jasun", + "Esdras", + "Jobyna", + "Marian", + "Theodore", + "Dionisio", + "Efren", + "Clarita", + "Leilah", + "Modestia", + "Clem", + "Jemmy", + "Karol", + "Minni", + "Damien", + "Tessy", + "Roanne", + "Daniele", + "Camel", + "Charlot", + "Daron", + "Cherey", + "Ashil", + "Joel", + "Michell", + "Sukey", + "Micheil", + "Chev", + "Winny", + "Natale", + "Kendra", + "Bell", + "Darice", + "Beilul", + "Leonore", + "Abba", + "Warden", + "Bryna", + "Sammy", + "Brantley", + "Goldi", + "Meridith", + "Eleanor", + "Brear", + "Kristina", + "Muriel", + "Serge", + "Iver", + "Jonis", + "Ada", + "Marleen", + "Pavlov", + "Kellia", + "Abdel", + "Waylin", + "Ignazio", + "Tana", + "Kiley", + "Lynna", + "Peyton", + "Linoel", + "Patrice", + "Loria", + "Linda", + "Edna", + "Viki", + "Kelcy", + "Chelsae", + "Olga", + "Trace", + "Ethel", + "Giorgio", + "Geralda", + "Rosaline", + "Caralie", + "Duke", + "Sig", + "Seana", + "Boris", + "Jeanie", + "Stacee", + "Giffie", + "Myrta", + "Prescott", + "Roger", + "Ame", + "Lelia", + "Marthena", + "Mord", + "Tommi", + "Artemus", + "Wynn", + "Rodi", + "Denna", + "Joleen", + "Iris", + "Pascale", + "Cody", + "Kienan", + "Darline", + "Lanna", + "Chandra", + "Michel", + "Nanete", + "Rosana", + "Ondrea", + "Linette", + "Syd", + "Rhianon", + "Christiano", + "Moyna", + "Darbee", + "Chadd", + "Roselia", + "Niki", + "Flint", + "Natala", + "Merrie", + "Noelyn", + "Arvin", + "Vin", + "Khalil", + "Nance", + "Seward", + "Dagmar", + "Shanta", + "Noland", + "Vance", + "Kyla", + "Locke", + "Abagail", + "Guthrey", + "Thalia", + "Devlen", + "Parrnell", + "Leonard", + "Amber", + "Dell", + "Lolita", + "Revkah", + "Ronna", + "Ninnetta", + "Jobey", + "Larisa", + "Wendel", + "Sonnnie", + "Saul", + "Lem", + "Wang", + "Borg", + "Korie", + "Rosanna", + "Barnaby", + "Channa", + "Gordan", + "Wang", + "Dasi", + "Laurianne", + "Jo ann", + "Bond", + "Kean", + "Harwell", + "Abbey", + "Carlo", + "Hamil", + "Ameline", + "Tristam", + "Donn", + "Earle", + "Lanie", + "Maximilianus", + "Frieda", + "Noella", + "Orsa", + "Timmi", + "Linea", + "Claudina", + "Langsdon", + "Murdock", + "Cello", + "Lek", + "Viviyan", + "Candra", + "Erena", + "Shirline", + "Mariann", + "Keelby", + "Jacquelin", + "Clerissa", + "Davis", + "Ara", + "My", + "Andris", + "Drugi", + "Lynn", + "Andonis", + "Jamie", + "Cherise", + "Lonni", + "Reamonn", + "Cathee", + "Clarence", + "Joletta", + "Tanny", + "Gasparo", + "Heddie", + "Cullin", + "Sander", + "Emmalee", + "Gwendolin", + "Hayley", + "Mandie", + "Cassondra", + "Celestyna", + "Fanny", + "Alica", + "Vivyan", + "Kippy", + "Leandra", + "Jerry", + "Elspeth", + "Lexine", + "Tobie", + "Allin", + "Ambros", + "Ash", + "Conroy", + "Melonie", + "Aylmer", + "Maximo", + "Connie", + "Torre", + "Tammie", + "Corabella", + "Beau", + "Nancee", + "Ailbert", + "Florrie", + "Trevar", + "Tiffani", + "Dre", + "Eward", + "Hallie", + "Stesha", + "Ralina", + "Vinni", + "Bastien", + "Galvan", + "Romain", + "Yasmin", + "Theodoric", + "Maxy", + "Lesly", + "Gerald", + "Erskine", + "Joice", + "Theadora", + "Sheeree", + "Danit", + "Burr", + "Morten", + "Godfree", + "Lacey", + "Sandye", + "Louisa", + "Annora", + "Rochester", + "Saundra", + "Deeann", + "Aloisia", + "Oralle", + "Ree", + "Kaile", + "Rogerio", + "Graeme", + "Garald", + "Hulda", + "Deny", + "Bessy", + "Zarah", + "Melisande", + "Taffy", + "Jed", + "Bar", + "Jacki", + "Avictor", + "Damiano", + "Yasmeen", + "Geralda", + "Kermie", + "Verge", + "Cyril", + "Klara", + "Anna", + "Abey", + "Mariellen", + "Mirabel", + "Charmain", + "Carleton", + "Biddie", + "Junina", + "Cass", + "Jdavie", + "Laird", + "Olenka", + "Dion", + "Hedy", + "Haley", + "Stacy", + "Alis", + "Morena", + "Damita", + "Wynn", + "Kellia", + "Midge", + "Gerri", + "Symon", + "Markus", + "Brenn", + "Rancell", + "Marlon", + "Dulciana", + "Lemmy", + "Neale", + "Vladamir", + "Alasteir", + "Gilberta", + "Seumas", + "Ronda", + "Myrvyn", + "Gabey", + "Goldia", + "Lothaire", + "Averil", + "Marlo", + "Nanice", + "Bernadette", + "Nehemiah", + "Ivar", + "Natala", + "Dorthy", + "Melva", + "Alisha", + "Ruthann", + "Ray", + "Ariel", + "Gib", + "Pippo", + "Miner", + "Ardith", + "Letisha", + "Granger", + "Sue", + "Toby", + "Tallou", + "Stephi", + "Hunter", + "Terrell", + "Pail", + "Moise", + "Rosetta", + "Ira", + "Denyse", + "Jackie", + "Fons", + "Goldy", + "Rani", + "Bendick", + "Valentijn", + "Annabell", + "Ardith", + "Lesly", + "Almire", + "Emmalyn", + "Mechelle", + "Anna", + "Duff", + "Louise", + "Vivian", + "Farand", + "Sophi", + "Thedric", + "Vivien", + "Jere", + "Kassie", + "Andy", + "Helli", + "Ros", + "Babara", + "Othella", + "Shelton", + "Hector", + "Charmian", + "Rosamond", + "Maison", + "Magda", + "Gustave", + "Latisha", + "Erik", + "Gavin", + "Bobette", + "Masha", + "Collie", + "Kippie", + "Jillayne", + "Fairfax", + "Ulrika", + "Juliann", + "Joly", + "Aldus", + "Clarie", + "Aluin", + "Claudetta", + "Noella", + "Nichols", + "Rutger", + "Niall", + "Hunter", + "Hyacinthia", + "Eva", + "Humphrey", + "Randi", + "Leontyne", + "Bordy", + "Orin", + "Tobey", + "Aldis", + "Vernon", + "Griz", + "Dynah", + "Ann-marie", + "Inglebert", + "Gifford", + "Emeline", + "Shem", + "Sigvard", + "Mayne", + "Rhodia", + "Seward", + "Valencia", + "Babara", + "Cirstoforo", + "Nye", + "Merissa", + "Lucinda", + "Wynn", + "Vassili", + "Cletus", + "Felisha", + "Laural", + "William", + "Emmalynne", + "Angy", + "Charles", + "Jemmy", + "Edward", + "Millicent", + "Homer", + "Allie", + "Brandyn", + "Dannye", + "Hector", + "Fawne", + "Frayda", + "Issiah", + "Deana", + "Bearnard", + "Ken", + "Sinclare", + "Mallorie", + "Noby", + "Deonne", + "Brig", + "Ruy", + "Vivia", + "Nyssa", + "Ame", + "Carmen", + "Solly", + "Carolee", + "Felice", + "Claiborne", + "Layney", + "Raina", + "Tami", + "Dosi", + "Barth", + "Julita", + "Gardiner", + "Stesha", + "Geneva", + "Saudra", + "Ella", + "Welbie", + "Marya", + "Happy", + "Brandise", + "Jewell", + "Joana", + "Eddy", + "Buck", + "Leslie", + "Yolanda", + "Murdoch", + "Muffin", + "Myrna", + "Susi", + "Berthe", + "Debra", + "Kyla", + "Bron", + "Thurston", + "Case", + "Shelli", + "Danika", + "Charissa", + "Wylie", + "Corine", + "Caitrin", + "Atalanta", + "Vevay", + "Thekla", + "Inez", + "Pris", + "Zsazsa", + "Ardenia", + "Ole", + "Kelcy", + "Earl", + "Pierson", + "Opalina", + "Leta", + "Keefer", + "Conrado", + "Chen", + "Alys", + "Floyd", + "Kai", + "Warden", + "Peyton", + "Debora", + "Walton", + "Fionna", + "Kendra", + "Michail", + "Christa", + "Theodor", + "Avivah", + "Patric", + "Quinton", + "Fey", + "Lewiss", + "Loren", + "Nedi", + "Fergus", + "Jeanie", + "Liuka", + "Ashley", + "Ellsworth", + "Winslow", + "Land", + "Rooney", + "Kati", + "Joelie", + "Garner", + "Clarice", + "Clair", + "Heddi", + "Ivan", + "Enrichetta", + "Umberto", + "Alys", + "Marcellina", + "Elnore", + "Wilburt", + "Ami", + "Meridith", + "Devlin", + "Cicely", + "Nathanael", + "Rafi", + "Arluene", + "Erasmus", + "Tasia", + "Seumas", + "George", + "Fredrika", + "Jayne", + "Linus", + "Mathilde", + "Klarrisa", + "Willy", + "Rad", + "Rae", + "Wilfred", + "Amberly", + "Paulo", + "Robbi", + "Gladys", + "Mirilla", + "Danica", + "Montgomery", + "Bellina", + "Neill", + "Roddie", + "Sebastiano", + "Adrianne", + "Gilli", + "Rhodia", + "Orbadiah", + "Levy", + "Griswold", + "Millicent", + "Carry", + "Alexander", + "Carole", + "Othilie", + "Enrica", + "Corissa", + "Meaghan", + "Margret", + "Sheff", + "Walton", + "Tremain", + "Bear", + "Maximilian", + "Theodora", + "Fredric", + "Baudoin", + "Rees", + "Roldan", + "Mayor", + "Angelica", + "Clemente", + "Florencia", + "Lancelot", + "Valencia", + "Caddric", + "Frieda", + "Jarvis", + "Shamus", + "Kalindi", + "Allen", + "Maureen", + "Ax", + "Barbra", + "Craggy", + "Howie", + "Orson", + "Cammy", + "Sullivan", + "Marleen", + "Jarrad", + "Lucy", + "Catha", + "Guillemette", + "Birdie", + "Forrest", + "Luce", + "Myriam", + "Serge", + "Kali", + "Ruperto", + "Trisha", + "Shaylynn", + "Janella", + "Franciskus", + "Melinde", + "Effie", + "Letti", + "Roderic", + "Jandy", + "Michaelina", + "Mohammed", + "Dolorita", + "Elbertine", + "Esma", + "Emmett", + "Lucila", + "Joyann", + "Mufi", + "Karlotta", + "Vannie", + "Daphna", + "Blondie", + "Madelene", + "Tomkin", + "Kassie", + "Flynn", + "Zebadiah", + "Lauritz", + "Brian", + "Leah", + "Amalita", + "Corissa", + "Onfre", + "Shantee", + "Deena", + "Marena", + "Alejoa", + "Fania", + "Catha", + "Cherlyn", + "Gerrilee", + "Brook", + "Yardley", + "Karry", + "Dennis", + "Ingra", + "Damian", + "Alexandros", + "Romola", + "Grantley", + "Antons", + "Randal", + "Lorilee", + "Brier", + "Tyrone", + "Jennica", + "Deidre", + "Arlin", + "Marline", + "Lyell", + "Lorelei", + "Marius", + "Willy", + "Teddy", + "Grantham", + "Yelena", + "Jaimie", + "Brewer", + "Tess", + "Othelia", + "Bondy", + "Rebecka", + "Laurice", + "Jasen", + "Betty", + "Alverta", + "Pepita", + "Kandace", + "Loni", + "Doreen", + "Ketty", + "Ree", + "Danni", + "Zorah", + "Shayla", + "Ivy", + "Darin", + "Karie", + "Brittaney", + "Viole", + "Harlene", + "Jasun", + "Aime", + "Rickie", + "Heath", + "Andris", + "Vaughn", + "Giorgi", + "Maddalena", + "Shirley", + "Cherie", + "Zacharia", + "Darcey", + "Barbee", + "Ernest", + "Sher", + "Faustina", + "Nari", + "Gusella", + "Reginald", + "Zack", + "Michele", + "Gene", + "Lindy", + "Mirilla", + "Tudor", + "Tyler", + "Bernadina", + "Magdalen", + "Nollie", + "Coreen", + "Hoebart", + "Virginie", + "Waylin", + "Hank", + "Valenka", + "Sabine", + "Jesus", + "Annabell", + "Jesselyn", + "Marysa", + "Corbett", + "Carena", + "Bert", + "Tanhya", + "Alphonse", + "Johnette", + "Vince", + "Cordell", + "Ramonda", + "Trev", + "Glenna", + "Loy", + "Arni", + "Tedd", + "Tristam", + "Zelma", + "Emmeline", + "Ellswerth", + "Janeta", + "Hughie", + "Tarun", + "Enid", + "Rafe", + "Hal", + "Melissa", + "Layan", + "Sia", + "Horace", + "Derry", + "Kelsi", + "Zacharia", + "Tillie", + "Dillon", + "Maxwell", + "Shanai", + "Charlize", + "Usama", + "Nabeela", + "Emily-Jane", + "Martyn", + "Tre", + "Ioan", + "Elysia", + "Mikaeel", + "Danny", + "Ciaron", + "Ace", + "Amy-Louise", + "Gabrielle", + "Robbie", + "Thea", + "Gloria", + "Jana", + "Cole", + "Eamon", + "Samiyah", + "Ellie-Mai", + "Lawson", + "Gia", + "Merryn", + "Andre", + "Ansh", + "Kavita", + "Alasdair", + "Aamina", + "Donna", + "Dario", + "Sahra", + "Brittany", + "Shakeel", + "Taylor", + "Ellenor", + "Kacy", + "Gene", + "Hetty", + "Fletcher", + "Donte", + "Krisha", + "Everett", + "Leila", + "Aairah", + "Zander", + "Sakina", + "Sanaya", + "Nelly", + "Manon", + "Antonio", + "Aimie", + "Kyran", + "Daria", + "Tilly-Mae", + "Lisa", + "Ammaarah", + "Adina", + "Kaan", + "Torin", + "Sadie", + "Mia-Rose", + "Aadam", + "Phyllis", + "Jace", + "Fraser", + "Tamanna", + "Dahlia", + "Cristian", + "Maira", + "Lana", + "Lily-Mai", + "Barney", + "Beatrice", + "Tabitha", + "Anis", + "Heidi", + "Ahyan", + "Usaamah", + "Jolene", + "Melisa", + "Magdalena", + "Hina" + ], + "last_names": [ + "Silveston", + "Manson", + "Hoodlass", + "Auden", + "Speakman", + "Seavers", + "Sodeau", + "Gouth", + "Pickersail", + "Ferschke", + "Buzzing", + "Kinnar", + "Pemberton", + "Firebrace", + "Kornilyev", + "Linsley", + "Petyanin", + "McCobb", + "Disdel", + "Eskrick", + "Pringuer", + "Clavering", + "Sims", + "Lippitt", + "Springall", + "Spiteri", + "Dwyr", + "Tomas", + "Cleminson", + "Crowder", + "Juster", + "Leven", + "Doucette", + "Schimoni", + "Readwing", + "Karet", + "Reef", + "Welden", + "Bemand", + "Schulze", + "Bartul", + "Collihole", + "Thain", + "Bernhardt", + "Tolputt", + "Hedges", + "Lowne", + "Kobu", + "Cabrera", + "Gavozzi", + "Ghilardini", + "Leamon", + "Gadsden", + "Gregg", + "Tew", + "Bangle", + "Youster", + "Vince", + "Cristea", + "Ablott", + "Lightowlers", + "Kittredge", + "Armour", + "Bukowski", + "Knowlton", + "Juett", + "Santorini", + "Ends", + "Hawkings", + "Janowicz", + "Harry", + "Bougourd", + "Gillow", + "Whalebelly", + "Conneau", + "Mellows", + "Stolting", + "Stickells", + "Maryet", + "Echallie", + "Edgecombe", + "Orchart", + "Mowles", + "McGibbon", + "Titchen", + "Madgewick", + "Fairburne", + "Colgan", + "Chaudhry", + "Taks", + "Lorinez", + "Eixenberger", + "Burel", + "Chapleo", + "Margram", + "Purse", + "MacKay", + "Oxlade", + "Prahm", + "Wellbank", + "Blackborow", + "Woodbridge", + "Sodory", + "Vedmore", + "Beeckx", + "Newcomb", + "Ridel", + "Desporte", + "Jobling", + "Winear", + "Korneichuk", + "Aucott", + "Wawer", + "Aicheson", + "Hawkslee", + "Wynes", + "St. Quentin", + "McQuorkel", + "Hendrick", + "Rudsdale", + "Winsor", + "Thunders", + "Stonbridge", + "Perrie", + "D'Alessandro", + "Banasevich", + "Mc Elory", + "Cobbledick", + "Wreakes", + "Carnie", + "Pallister", + "Yeates", + "Hoovart", + "Doogood", + "Churn", + "Gillon", + "Nibley", + "Dusting", + "Melledy", + "O'Noland", + "Crosfeld", + "Pairpoint", + "Longson", + "Rodden", + "Foyston", + "Le Teve", + "Brumen", + "Pudsey", + "Klimentov", + "Agent", + "Seabert", + "Cramp", + "Bitcheno", + "Embery", + "Etheredge", + "Sheardown", + "McKune", + "Vearncomb", + "Lavington", + "Rylands", + "Derges", + "Olivetti", + "Matasov", + "Thrower", + "Jobin", + "Ramsell", + "Rude", + "Tregale", + "Bradforth", + "McQuarter", + "Walburn", + "Poad", + "Filtness", + "Carneck", + "Pavis", + "Pinchen", + "Polye", + "Abry", + "Radloff", + "McDugal", + "Loughton", + "Revitt", + "Baniard", + "Kovalski", + "Mapother", + "Hendrikse", + "Rickardsson", + "Featherbie", + "Harlow", + "Kruschov", + "McCrillis", + "Barabich", + "Peaker", + "Skamell", + "Gorges", + "Chance", + "Bresner", + "Profit", + "Swinfon", + "Goldson", + "Nunson", + "Tarling", + "Ruperti", + "Grimsell", + "Davey", + "Deetlof", + "Gave", + "Fawltey", + "Tyre", + "Whaymand", + "Trudgian", + "McAndrew", + "Aleksankov", + "Dimbleby", + "Beseke", + "Cleverley", + "Aberhart", + "Courtin", + "MacKellen", + "Johannesson", + "Churm", + "Laverock", + "Astbury", + "Canto", + "Nelles", + "Dormand", + "Blucher", + "Youngs", + "Dalrymple", + "M'Chirrie", + "Jansens", + "Golthorpp", + "Ibberson", + "Andriveau", + "Paulton", + "Parrington", + "Shergill", + "Bickerton", + "Hugonneau", + "Cornelissen", + "Spincks", + "Malkinson", + "Kettow", + "Wasiel", + "Skeat", + "Maynard", + "Goutcher", + "Cratchley", + "Loving", + "Averies", + "Cahillane", + "Alvarado", + "Truggian", + "Bravington", + "McGonigle", + "Crocombe", + "Slorance", + "Dukes", + "Nairns", + "Condict", + "Got", + "Flowerdew", + "Deboy", + "Death", + "Patroni", + "Colgrave", + "Polley", + "Spraging", + "Orteaux", + "Daskiewicz", + "Dunsmore", + "Forrington", + "De Gogay", + "Swires", + "Grimmert", + "Castells", + "Scraggs", + "Chase", + "Dixsee", + "Brennans", + "Gookes", + "MacQueen", + "Galbreth", + "Buttwell", + "Annear", + "Sutherley", + "Portis", + "Pashen", + "Blackbourn", + "Sedgemond", + "Huegett", + "Emms", + "Leifer", + "Paschek", + "Bynold", + "Mahony", + "Izacenko", + "Hadland", + "Sallows", + "Hamper", + "Godlee", + "Rablin", + "Emms", + "Zealy", + "Russi", + "Crassweller", + "Shotbolt", + "Van Der Weedenburg", + "MacGille", + "Carillo", + "Guerin", + "Cuolahan", + "Metzel", + "Martinovsky", + "Stoggles", + "Brameld", + "Coupland", + "Kaaskooper", + "Sallows", + "Rizzotto", + "Dike", + "O'Lochan", + "Spragg", + "Lavarack", + "MacNess", + "Swetenham", + "Dillet", + "Coffey", + "Meikle", + "Loynes", + "Josum", + "Adkin", + "Tompsett", + "Maclaine", + "Fippe", + "Bispo", + "Whittek", + "Rylett", + "Iveagh", + "Elgar", + "Casswell", + "Tilt", + "Macklin", + "Lillee", + "Hamshere", + "Coite", + "Dollard", + "Tiesman", + "Coltart", + "Stothert", + "Crosswaite", + "Padgett", + "Gleadle", + "Meedendorpe", + "Alexsandrovich", + "Williamson", + "Futty", + "Antwis", + "Romanski", + "Dionisetti", + "Dimitriev", + "Swalowe", + "Dewing", + "O'Driscoll", + "Jeandel", + "Summerly", + "Shoute", + "Trelevan", + "Matkin", + "Headey", + "Rosson", + "Dunn", + "Gunner", + "Stapells", + "Fratczak", + "McGillivray", + "Edis", + "Treuge", + "Haskayne", + "Perell", + "O'Fairy", + "Slisby", + "Axcell", + "Mattingley", + "Tumilty", + "Kibble", + "Lambert", + "Hassall", + "Simpkin", + "Nitti", + "Stiegar", + "Pavitt", + "Kerby", + "Ruzic", + "Westwick", + "Tonbye", + "Bocken", + "Kinforth", + "Wren", + "Attow", + "McComish", + "McNickle", + "Wildman", + "O'Corhane", + "Jewar", + "Caveau", + "Woodrooffe", + "Batson", + "Stayt", + "A'field", + "Domesday", + "Taberer", + "Gigg", + "Stanmore", + "Hanton", + "Roskell", + "Brasener", + "Stanbro", + "Cordy", + "O'Bradane", + "Hansberry", + "Erdes", + "Wagon", + "Jimmes", + "Ruffles", + "Wigginton", + "Haste", + "Rymill", + "Tomsett", + "Ambrosoli", + "Reidshaw", + "Nurcombe", + "Costigan", + "Berwick", + "Hinchon", + "Blissitt", + "Golston", + "Goullee", + "Hudspeth", + "Traher", + "Salandino", + "Fatscher", + "Davidov", + "Baukham", + "Mallan", + "Kilmurray", + "Dmych", + "Mair", + "Felmingham", + "Kedward", + "Leechman", + "Frank", + "Tremoulet", + "Manley", + "Newcom", + "Brandone", + "Cliffe", + "Shorte", + "Baalham", + "Fairhead", + "Sheal", + "Effnert", + "MacCaughey", + "Rizzolo", + "Linthead", + "Greenhouse", + "Clayson", + "Franca", + "Lambell", + "Egdal", + "Pringell", + "Penni", + "Train", + "Langfitt", + "Dady", + "Rannigan", + "Ledwidge", + "Summerton", + "D'Hooghe", + "Ary", + "Gooderick", + "Scarsbrooke", + "Janouch", + "Pond", + "Menichini", + "Crinidge", + "Sneesbie", + "Harflete", + "Ubsdell", + "Littleover", + "Vanne", + "Fassbender", + "Zellner", + "Gorce", + "McKeighan", + "Claffey", + "MacGarvey", + "Norwich", + "Antosch", + "Loughton", + "McCuthais", + "Arnaudi", + "Broz", + "Stert", + "McMechan", + "Texton", + "Bees", + "Couser", + "Easseby", + "McCorry", + "Fetterplace", + "Crankshaw", + "Spancock", + "Neasam", + "Bruckental", + "Badgers", + "Rodda", + "Bossingham", + "Crump", + "Jurgensen", + "Noyes", + "Scarman", + "Bakey", + "Swindin", + "Tolworthie", + "Vynehall", + "Shallcrass", + "Bazoge", + "Jonczyk", + "Eatherton", + "Finlason", + "Hembery", + "Lassetter", + "Soule", + "Baldocci", + "Thurman", + "Poppy", + "Eveque", + "Summerlad", + "Eberle", + "Pettecrew", + "Hitzmann", + "Allonby", + "Bodimeade", + "Catteroll", + "Wooldridge", + "Baines", + "Halloway", + "Doghartie", + "Bracher", + "Kynnd", + "Metherell", + "Routham", + "Fielder", + "Ashleigh", + "Aked", + "Kolakowski", + "Picardo", + "Murdy", + "Feacham", + "Lewin", + "Braben", + "Salaman", + "Letterick", + "Bovaird", + "Moriarty", + "Bertot", + "Cowan", + "Dionisi", + "Maybey", + "Joskowicz", + "Shoutt", + "Bernli", + "Dikles", + "Corringham", + "Shaw", + "Donovin", + "Merigeau", + "Pinckney", + "Queripel", + "Sampson", + "Benfell", + "Cansdell", + "Tasseler", + "Amthor", + "Nancekivell", + "Stock", + "Boltwood", + "Goreisr", + "Le Grand", + "Terrans", + "Knapp", + "Roseman", + "Gunstone", + "Hissie", + "Orto", + "Bell", + "Colam", + "Drust", + "Roseblade", + "Sulman", + "Jennaway", + "Joust", + "Curthoys", + "Cajkler", + "MacIllrick", + "Print", + "Coulthard", + "Lemmon", + "Bush", + "McMurrugh", + "Toping", + "Brute", + "Fryman", + "Bosomworth", + "Lawson", + "Lauder", + "Heinssen", + "Bittlestone", + "Brinson", + "Hambling", + "Vassman", + "Brookbank", + "Bolstridge", + "Leslie", + "Berndsen", + "Aindrais", + "Mogra", + "Wilson", + "Josefs", + "Norgan", + "Wong", + "le Keux", + "Hastwall", + "Bunson", + "Van", + "Waghorne", + "Ojeda", + "Boole", + "Winters", + "Gurge", + "Gallemore", + "Perulli", + "Dight", + "Di Filippo", + "Winsley", + "Chalcraft", + "Human", + "Laetham", + "Lennie", + "McSorley", + "Toolan", + "Brammar", + "Cadogan", + "Molloy", + "Shoveller", + "Vignaux", + "Hannaway", + "Sykora", + "Brealey", + "Harness", + "Profit", + "Goldsbury", + "Brands", + "Godmar", + "Binden", + "Kondratenya", + "Warsap", + "Rumble", + "Maudson", + "Demer", + "Laxtonne", + "Kmietsch", + "Colten", + "Raysdale", + "Gadd", + "Blanche", + "Viant", + "Daskiewicz", + "Macura", + "Crouch", + "Janicijevic", + "Oade", + "Fancourt", + "Dimitriev", + "Earnshaw", + "Wing", + "Fountain", + "Fearey", + "Nottram", + "Bescoby", + "Jeandeau", + "Mapowder", + "Iacobo", + "Rabjohns", + "Dean", + "Whiterod", + "Mathiasen", + "Josephson", + "Boc", + "Olivet", + "Yeardley", + "Labuschagne", + "Curmi", + "Rogger", + "Tesoe", + "Mellhuish", + "Malan", + "McArt", + "Ing", + "Renowden", + "Mellsop", + "Critchlow", + "Seedhouse", + "Tiffin", + "Chirm", + "Oldknow", + "Wolffers", + "Dainter", + "Bundy", + "Copplestone", + "Moses", + "Weedon", + "Borzone", + "Craigg", + "Pyrah", + "Shoorbrooke", + "Jeandeau", + "Halgarth", + "Bamlett", + "Greally", + "Abrahamovitz", + "Oger", + "Mandrake", + "Craigg", + "Stenning", + "Tommei", + "Mapother", + "Cree", + "Clandillon", + "Thorlby", + "Careswell", + "Woolnough", + "McMeekin", + "Woodman", + "Mougin", + "Burchill", + "Pegg", + "Morin", + "Eskriett", + "Gelderd", + "Latham", + "Siney", + "Freen", + "Walrond", + "Bell", + "Twigley", + "D'Souza", + "Anton", + "Doyle", + "Pieters", + "Rosenvasser", + "Mackneis", + "Brisse", + "Boffin", + "Rushe", + "Cozens", + "Bensusan", + "Plampin", + "Gauford", + "Lecky", + "Belton", + "Fleming", + "Gent", + "Bunclark", + "Climar", + "Milner", + "Karolovsky", + "Claesens", + "Oleksiak", + "Barkway", + "Glenister", + "Steynor", + "Hecks", + "Rollo", + "Elcoux", + "Altham", + "Veschambes", + "Livingstone", + "Miroy", + "Edy", + "Bendle", + "Widdall", + "Onions", + "Devita", + "McOwan", + "Ahearne", + "Wisniowski", + "Pask", + "Ciccottini", + "Parlatt", + "Gindghill", + "Marquess", + "Claworth", + "Veel", + "Fairbairn", + "Galletley", + "Glew", + "Gillice", + "Liddyard", + "Babin", + "Ryson", + "Kyteley", + "Toms", + "Downton", + "Mougel", + "Inglefield", + "Gaskins", + "Bradie", + "Stanbury", + "McMenamy", + "Cranstone", + "Thody", + "Iacovozzo", + "Theobalds", + "Perrins", + "Dyott", + "Hupe", + "Gelling", + "Eadington", + "Crumbie", + "Stainsby", + "Kolakowski", + "Norwich", + "Ehrat", + "Basnett", + "Marden", + "Godby", + "Kubacki", + "Wiles", + "Littrick", + "Chuck", + "Negus", + "Aisthorpe", + "Danelut", + "Helversen", + "McCombe", + "Dallender", + "Offner", + "Leser", + "Savin", + "Belcham", + "Pockett", + "Selway", + "Santostefano.", + "Telford", + "Presser", + "Haken", + "Wybourne", + "Reolfo", + "Mineghelli", + "Beverage", + "Grimsdike", + "Drogan", + "Bynert", + "Boothman", + "Postle", + "Baskwell", + "Branno", + "Hechlin", + "Geake", + "Morstatt", + "Towne", + "Phillott", + "Doumerc", + "Ladewig", + "Sexty", + "Sleigh", + "Simonaitis", + "Han", + "Crommett", + "Blowes", + "Floyde", + "Delgardo", + "Brounsell", + "Klimowski", + "Jaffray", + "Kingzeth", + "Pithie", + "Eriksson", + "Gudgin", + "Hamal", + "Hooks", + "Rosle", + "Braysher", + "O'Curneen", + "Millett", + "Woofinden", + "Lillistone", + "Broxis", + "Mochar", + "Drewell", + "Hedgeman", + "Wharf", + "Lambden", + "Lambol", + "Slowcock", + "Cicchillo", + "Trineman", + "Sinyard", + "Brandone", + "Masding", + "Britnell", + "Quinlan", + "Arnopp", + "Jeratt", + "Bantick", + "Craigs", + "Pantling", + "Klais", + "Pickvance", + "Goodwill", + "McGavin", + "Esslemont", + "Bakewell", + "Downer", + "Scallan", + "Ronchka", + "Scholcroft", + "Van Der Walt", + "Armfield", + "Chalker", + "Chinge", + "Yakubov", + "Folkerd", + "Manon", + "Gookey", + "Connold", + "Dusey", + "Muselli", + "Skala", + "Dibbin", + "Kreber", + "De Blasi", + "Drei", + "Argo", + "Maudson", + "Stanlick", + "Steinham", + "Dallewater", + "Litchmore", + "Mathie", + "Gook", + "Forrestor", + "Ferreira", + "Budd", + "Joskowitz", + "Whetnall", + "Beany", + "Keymar", + "Merrin", + "Waldera", + "O'Gleasane", + "Duiged", + "Cumo", + "Giddings", + "Craker", + "Olenov", + "Whayman", + "Raoux", + "Delete", + "McDell", + "Gauntlett", + "Gomby", + "Rottgers", + "Spraggon", + "Orth", + "Shortan", + "Lineen", + "Monkhouse", + "Di Domenico", + "Brinsden", + "MacCallister", + "Sieghard", + "Pheasant", + "Cloney", + "Igglesden", + "Checklin", + "Grosier", + "Garnett", + "Vasnetsov", + "Chsteney", + "Manifield", + "Coutts", + "Bagshawe", + "Pryn", + "Dunstall", + "Rowlings", + "Whines", + "Bish", + "Solomon", + "Mackay", + "Daugherty", + "Gutierrez", + "Goff", + "Villanueva", + "Heath", + "Serrano", + "Munro", + "Levine", + "Barrett", + "Bateman", + "Colon", + "Alford", + "Whitehouse", + "Mendoza", + "Keith", + "Orr", + "Shepherd", + "North", + "Steele", + "Morales", + "Shea", + "Olsen", + "Wormald", + "Torres", + "Haines", + "Kerr", + "Reeves", + "Bates", + "Potts", + "Foreman", + "Herrera", + "Mccoy", + "Fulton", + "Charles", + "Clay", + "Estes", + "Mata", + "Childs", + "Kendall", + "Wallace", + "Thorpe", + "Oconnell", + "Waters", + "Roth", + "Barker", + "Fritz", + "Singleton", + "Sharpe", + "Little", + "Oliver", + "Ayala", + "Khan", + "Braun", + "Dean", + "Stout", + "Adamson", + "Tate", + "Juarez", + "Pickett", + "Burke", + "Gordon", + "Mackenzie", + "Bloggs", + "Read", + "Britton", + "Jefferson", + "Lutz", + "Chen", + "Wagstaff", + "Coates", + "Gilliam", + "Mullins", + "Ryan", + "Moon", + "Thompson", + "Abbott", + "Cotton", + "Barajas", + "Chan", + "Bostock", + "Spencer", + "Sparrow", + "Robinson", + "Morrison", + "Aguirre", + "Clayton", + "Hope", + "Swanson", + "Ochoa", + "Ruiz", + "Truong", + "Gibbons", + "Daniel", + "Zimmerman", + "Flynn", + "Keeling", + "Greenaway", + "Edwards" + ] +} |