diff options
| author | 2021-02-08 14:05:25 +0300 | |
|---|---|---|
| committer | 2021-02-08 14:05:25 +0300 | |
| commit | b0169f28f8f6f27d80bdc81e4f186431391560e4 (patch) | |
| tree | b92236b8fba11020de69e9c13a570c14cfb1ed46 /bot | |
| 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
Diffstat (limited to 'bot')
| -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 | 
12 files changed, 3450 insertions, 122 deletions
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" +    ] +}  |