diff --git a/README.md b/README.md index 7efbfc6..7862fc7 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ chatchart - Generates a pie chart to display chat activity over the last 5000 me dungeon - New users with new accounts will be shuffled off to a locked channel on-join to help mitigate raiders. Please see the dungeon_readme.md file on this repo for more information. +joinleave - Alerts a set channel when an account under a certain age of days joins a server and leaves within the set time... any server the bot can see. This is a global cog with owner only commands. I wrote this for myself so I won't be adding user requests for the most part, and will be using it to combat the recent user scrapers found joining and leaving servers quickly. + inspirobot - Fetch "inspirational" messages from inspirobot.me with [p]inspireme. nolinks - A very blunt hammer to remove anything that looks like a link. Roles can be whitelisted and it can watch multiple channels. diff --git a/cah/__init__.py b/cah/__init__.py new file mode 100644 index 0000000..e0c6273 --- /dev/null +++ b/cah/__init__.py @@ -0,0 +1,8 @@ +from redbot.core import commands, data_manager +from .cah import CardsAgainstHumanity + + +def setup(bot: commands.Bot): + n = CardsAgainstHumanity(bot) + data_manager.load_bundled_data(n, __file__) + bot.add_cog(n) diff --git a/cah/cah.py b/cah/cah.py new file mode 100644 index 0000000..4d98e93 --- /dev/null +++ b/cah/cah.py @@ -0,0 +1,1836 @@ +import asyncio +import discord +import html +import json +import random +import time +from random import shuffle +from redbot.core import commands +from redbot.core.data_manager import bundled_data_path + + +BaseCog = getattr(commands, "Cog", object) + + +class CardsAgainstHumanity(BaseCog): + def __init__(self, bot): + self.bot = bot + self.games = [] + self.maxBots = 5 # Max number of bots that can be added to a game - don't count toward max players + self.maxPlayers = 10 # Max players for ranjom joins + self.maxDeadTime = 3600 # Allow an hour of dead time before killing a game + self.checkTime = 300 # 5 minutes between dead time checks + self.winAfter = 10 # 10 wins for the game + self.botWaitMin = 5 # Minimum number of seconds before the bot makes a decision (default 5) + self.botWaitMax = 30 # Max number of seconds before a bot makes a decision (default 30) + self.userTimeout = 500 # 5 minutes to timeout + self.utCheck = 30 # Check timeout every 30 seconds + self.utWarn = 60 # Warn the user if they have 60 seconds or less before being kicked + self.charset = "1234567890" + self.botName = "Rando Cardrissian" + self.minMembers = 3 + + self.bot.loop.create_task(self.checkDead()) + self.bot.loop.create_task(self.checkUserTimeout()) + + def cleanJson(self, json): + json = html.unescape(json) + # Clean out html formatting + json = json.replace("_", "[blank]") + json = json.replace("
", "\n") + json = json.replace("
", "\n") + json = json.replace("", "*") + json = json.replace("", "*") + return json + + def displayname(self, member: discord.Member): + # A helper function to return the member's display name + nick = name = None + try: + nick = member.nick + except AttributeError: + pass + try: + name = member.name + except AttributeError: + pass + if nick: + return nick + if name: + return name + return None + + def memberforname(self, name, server): + # Check nick first - then name + for member in server.members: + if member.nick: + if member.nick.lower() == name.lower(): + return member + for member in server.members: + if member.name.lower() == name.lower(): + return member + # No member yet - try ID + memID = "".join(list(filter(str.isdigit, name))) + newMem = memberForID(memID, server) + if newMem: + return newMem + return None + + def getreadabletimebetween(self, first, last): + # A helper function to make a readable string between two times + timeBetween = int(last - first) + weeks = int(timeBetween / 604800) + days = int((timeBetween - (weeks * 604800)) / 86400) + hours = int((timeBetween - (days * 86400 + weeks * 604800)) / 3600) + minutes = int((timeBetween - (hours * 3600 + days * 86400 + weeks * 604800)) / 60) + seconds = int(timeBetween - (minutes * 60 + hours * 3600 + days * 86400 + weeks * 604800)) + msg = "" + + if weeks > 0: + if weeks == 1: + msg = "{}{} week, ".format(msg, str(weeks)) + else: + msg = "{}{} weeks, ".format(msg, str(weeks)) + if days > 0: + if days == 1: + msg = "{}{} day, ".format(msg, str(days)) + else: + msg = "{}{} days, ".format(msg, str(days)) + if hours > 0: + if hours == 1: + msg = "{}{} hour, ".format(msg, str(hours)) + else: + msg = "{}{} hours, ".format(msg, str(hours)) + if minutes > 0: + if minutes == 1: + msg = "{}{} minute, ".format(msg, str(minutes)) + else: + msg = "{}{} minutes, ".format(msg, str(minutes)) + if seconds > 0: + if seconds == 1: + msg = "{}{} second, ".format(msg, str(seconds)) + else: + msg = "{}{} seconds, ".format(msg, str(seconds)) + + if not msg: + return "0 seconds" + else: + return msg[:-2] + + async def checkUserTimeout(self): + while True: + # Wait first - then check + await asyncio.sleep(self.utCheck) + for game in self.games: + if not game["Timeout"]: + continue + if len(game["Members"]) >= self.minMembers: + # Game is started + for member in game["Members"]: + if member["IsBot"]: + continue + if game["Judging"]: + if not member == game["Members"][game["Judge"]]: + # Not the judge - don't hold against the user + member["Time"] = int(time.time()) + continue + else: + # Not judging + if member == game["Members"][game["Judge"]]: + # The judge - don't hold that against them + member["Time"] = int(time.time()) + continue + currentTime = int(time.time()) + userTime = member["Time"] + downTime = currentTime - userTime + # Check if downTime results in a kick + if downTime >= self.userTimeout: + # You gettin kicked, son. + await self.removeMember(member["User"]) + self.checkGame(game) + continue + # Check if downTime is in warning time + if downTime >= (self.userTimeout - self.utWarn): + # Check if we're at warning phase + if self.userTimeout - downTime >= (self.utWarn - self.utCheck): + kickTime = self.userTimeout - downTime + if kickTime % self.utCheck: + # Kick time isn't exact time - round out to the next loop + kickTime = kickTime - (kickTime % self.utCheck) + self.utCheck + # Warning time! + timeString = self.getreadabletimebetween(0, kickTime) + msg = "**WARNING** - You will be kicked from the game if you do not make a move in *{}!*".format( + timeString + ) + await member["User"].send(msg) + else: + for member in game["Members"]: + # Reset timer + member["Time"] = int(time.time()) + + async def checkDead(self): + while True: + # Wait first - then check + await asyncio.sleep(self.checkTime) + for game in self.games: + gameTime = game["Time"] + currentTime = int(time.time()) + timeRemain = currentTime - gameTime + if timeRemain > self.maxDeadTime: + # Game is dead - quit it and alert members + for member in game["Members"]: + if member["IsBot"]: + # Clear pending tasks and set to None + if not member["Task"] == None: + task = member["Task"] + if not task.done(): + task.cancel() + member["Task"] = None + continue + msg = "Game id: *{}* has been closed due to inactivity.".format(game["ID"]) + await member["User"].send(msg) + + # Set running to false + game["Running"] = False + self.games.remove(game) + + async def checkPM(self, message): + # Checks if we're talking in PM, and if not - outputs an error + if isinstance(message.channel, discord.abc.PrivateChannel): + # PM + return True + else: + # Not in PM + await message.channel.send("Cards Against Humanity commands must be run in PM.") + return False + + def randomID(self, length=8): + # Create a random id that doesn't already exist + while True: + # Repeat until found + newID = "".join(random.choice(self.charset) for i in range(length)) + exists = False + for game in self.games: + if game["ID"] == newID: + exists = True + break + if not exists: + break + return newID + + def randomBotID(self, game, length=4): + # Returns a random id for a bot that doesn't already exist + while True: + # Repeat until found + newID = "".join(random.choice(self.charset) for i in range(length)) + exists = False + for member in game["Members"]: + if member["ID"] == newID: + exists = True + break + if not exists: + break + return newID + + async def userGame(self, user): + # Returns the game the user is currently in + if not len(str(user)) == 4: + if not type(user) is int: + # Assume it's a discord.Member/User + user = user.id + + for game in self.games: + for member in game["Members"]: + if member["ID"] == user: + # Found our user + return game + return None + + def gameForID(self, id): + # Returns the game with the passed id + for game in self.games: + if game["ID"] == id: + return game + return None + + async def removeMember(self, user, game=None): + if not len(str(user)) == 4: + if not type(user) is int: + # Assume it's a discord.Member/User + user = user.id + outcome = False + removed = None + if not game: + game = await self.userGame(user) + if game: + for member in game["Members"]: + if member["ID"] == user: + removed = member + outcome = True + judgeChanged = False + # Reset judging flag to retrigger actions + game["Judging"] = False + # Get current Judge - only if game has started + if len(game["Members"]) >= self.minMembers: + judge = game["Members"][game["Judge"]] + game["Members"].remove(member) + # Check if we're removing the current judge + if judge == member: + # Judge will change + judgeChanged = True + # Find out if our member was the last in line + if game["Judge"] >= len(game["Members"]): + game["Judge"] = 0 + # Reset judge var + judge = game["Members"][game["Judge"]] + else: + # Judge didn't change - so let's reset judge index + index = game["Members"].index(judge) + game["Judge"] = index + else: + judge = None + # Just remove the member + game["Members"].remove(member) + + if member["Creator"]: + # We're losing the game creator - pick a new one + for newCreator in game["Members"]: + if not newCreator["IsBot"]: + newCreator["Creator"] = True + await newCreator["User"].send( + "The creator of this game left. **YOU** are now the creator." + ) + break + + # Remove submissions + for sub in game["Submitted"]: + # Remove deleted member and new judge's submissions + if sub["By"] == member or sub["By"] == judge: + # Found it! + game["Submitted"].remove(sub) + break + if member["IsBot"]: + if not member["Task"] == None: + task = member["Task"] + if not task.done(): + task.cancel() + member["Task"] = None + else: + msg = "**You were removed from game id:** ***{}.***".format(game["ID"]) + await member["User"].send(msg) + # Removed, no need to finish the loop + break + if not outcome: + return outcome + # We removed someone - let's tell the world + for member in game["Members"]: + if member["IsBot"]: + continue + if removed["IsBot"]: + msg = "***{} ({})*** **left the game - reorganizing...**".format( + self.botName, removed["ID"] + ) + else: + msg = "***{}*** **left the game - reorganizing...**".format( + self.displayname(removed["User"]) + ) + # Check if the judge changed + if judgeChanged: + # Judge changed + newJudge = game["Members"][game["Judge"]] + if newJudge["IsBot"]: + msg += "\n\n***{} ({})*** **is now judging!**".format( + self.botName, newJudge["ID"] + ) + # Schedule judging task + else: + if newJudge == member: + msg += "\n\n***YOU*** **are now judging!**" + else: + msg += "\n\n***{}*** **is now judging!**".format( + self.displayname(newJudge["User"]) + ) + await member["User"].send(msg) + return game + + def checkGame(self, game): + for member in game["Members"]: + if not member["IsBot"]: + return True + # If we got here - only bots, or empty game + # Kill all bots' loops + for member in game["Members"]: + if member["IsBot"]: + # Clear pending tasks and set to None + if not member["Task"] == None: + task = member["Task"] + if not task.done(): + task.cancel() + member["Task"] = None + # Set running to false + game["Running"] = False + self.games.remove(game) + return False + + async def typing(self, game, typeTime=5): + # Allows us to show the bot typing + waitTime = random.randint(self.botWaitMin, self.botWaitMax) + preType = waitTime - typeTime + if preType > 0: + await asyncio.sleep(preType) + for member in game["Members"]: + if member["IsBot"]: + continue + await asyncio.sleep(0.1) + await asyncio.sleep(typeTime) + else: + for member in game["Members"]: + if member["IsBot"]: + continue + await asyncio.sleep(0.1) + await asyncio.sleep(waitTime) + + async def botPick(self, ctx, bot, game): + # Has the bot pick their card + blackNum = game["BlackCard"]["Pick"] + if blackNum == 1: + cardSpeak = "card" + else: + cardSpeak = "cards" + i = 0 + cards = [] + while i < blackNum: + randCard = random.randint(0, len(bot["Hand"]) - 1) + cards.append(bot["Hand"].pop(randCard)["Text"]) + i += 1 + + await self.typing(game) + + # Make sure we haven't laid any cards + if bot["Laid"] == False and game["Judging"] == False: + newSubmission = {"By": bot, "Cards": cards} + game["Submitted"].append(newSubmission) + # Shuffle cards + shuffle(game["Submitted"]) + bot["Laid"] = True + game["Time"] = currentTime = int(time.time()) + await self.checkSubmissions(ctx, game, bot) + + async def botPickWin(self, ctx, game): + totalUsers = len(game["Members"]) - 1 + submitted = len(game["Submitted"]) + if submitted >= totalUsers: + # Judge is a bot - and all cards are in! + await self.typing(game) + # Pick a winner + winner = random.randint(0, totalUsers - 1) + await self.winningCard(ctx, game, winner) + + async def checkSubmissions(self, ctx, game, user=None): + totalUsers = len(game["Members"]) - 1 + submitted = len(game["Submitted"]) + for member in game["Members"]: + msg = "" + # Is the game running? + if len(game["Members"]) < self.minMembers: + if member["IsBot"]: + # Clear pending tasks and set to None + if not member["Task"] == None: + task = member["Task"] + if not task.done(): + # Task isn't finished - we're on a new hand, cancel it + task.cancel() + member["Task"] = None + continue + # not enough members - send the embed + prefix = await self.bot.db.prefix() + stat_embed = discord.Embed(color=discord.Color.red()) + stat_embed.set_author( + name="Not enough players to continue! ({}/{})".format( + len(game["Members"]), self.minMembers + ) + ) + stat_embed.set_footer( + text="Have other users join with: {}joincah {}".format(prefix[0], game["ID"]) + ) + await member["User"].send(embed=stat_embed) + continue + if member["IsBot"] == True: + continue + # Check if we have a user + if user: + blackNum = game["BlackCard"]["Pick"] + if blackNum == 1: + card = "card" + else: + card = "cards" + if user["IsBot"]: + msg = "*{} ({})* submitted their {}! ".format(self.botName, user["ID"], card) + else: + if not member == user: + # Don't say this to the submitting user + msg = "*{}* submitted their {}! ".format( + self.displayname(user["User"]), card + ) + if submitted < totalUsers: + msg += "{}/{} cards submitted...".format(submitted, totalUsers) + if len(msg): + # We have something to say + await member["User"].send(msg) + + async def checkCards(self, ctx, game): + while True: + if not game["Running"]: + break + # wait for 1 second + await asyncio.sleep(1) + # Check for all cards + if len(game["Members"]) < self.minMembers: + # Not enough members + continue + # Enough members - let's check if we're judging + if game["Judging"]: + continue + # Enough members, and not judging - let's check cards + totalUsers = len(game["Members"]) - 1 + submitted = len(game["Submitted"]) + if submitted >= totalUsers: + game["Judging"] = True + # We have enough cards + for member in game["Members"]: + if member["IsBot"]: + continue + msg = "All cards have been submitted!" + # if + await member["User"].send(msg) + await self.showOptions(ctx, member["User"]) + + # Check if a bot is the judge + judge = game["Members"][game["Judge"]] + if not judge["IsBot"]: + continue + # task = self.bot.loop.create_task(self.botPickWin(ctx, game)) + task = asyncio.ensure_future(self.botPickWin(ctx, game)) + judge["Task"] = task + + async def winningCard(self, ctx, game, card): + # Let's pick our card and alert everyone + winner = game["Submitted"][card] + if winner["By"]["IsBot"]: + winnerName = "{} ({})".format(self.botName, winner["By"]["ID"]) + winner["By"]["Points"] += 1 + winner["By"]["Won"].append(game["BlackCard"]["Text"]) + else: + winnerName = self.displayname(winner["By"]["User"]) + for member in game["Members"]: + if member["IsBot"]: + continue + stat_embed = discord.Embed(color=discord.Color.gold()) + stat_embed.set_footer(text="Cards Against Humanity - id: {}".format(game["ID"])) + index = game["Members"].index(member) + if index == game["Judge"]: + stat_embed.set_author(name="You picked {}'s card!".format(winnerName)) + elif member == winner["By"]: + stat_embed.set_author(name="YOU WON!!") + member["Points"] += 1 + member["Won"].append(game["BlackCard"]["Text"]) + else: + stat_embed.set_author(name="{} won!".format(winnerName)) + if len(winner["Cards"]) == 1: + msg = "The **Winning** card was:\n\n{}".format( + "{}".format(" - ".join(winner["Cards"])) + ) + else: + msg = "The **Winning** cards were:\n\n{}".format( + "{}".format(" - ".join(winner["Cards"])) + ) + await member["User"].send(embed=stat_embed) + await member["User"].send(msg) + await asyncio.sleep(0.1) + + # await self.nextPlay(ctx, game) + + # Start the game loop + event = game["NextHand"] + self.bot.loop.call_soon_threadsafe(event.set) + game["Time"] = currentTime = int(time.time()) + + async def gameCheckLoop(self, ctx, game): + task = game["NextHand"] + while True: + if not game["Running"]: + break + # Clear the pending task + task.clear() + # Queue up the next hand + await self.nextPlay(ctx, game) + # Wait until our next clear + await task.wait() + + async def messagePlayers(self, ctx, message, game, judge=False): + # Messages all the users on in a game + for member in game["Members"]: + if member["IsBot"]: + continue + # Not bots + if member is game["Members"][game["Judge"]]: + # Is the judge + if judge: + await member["User"].send(message) + else: + # Not the judge + await member["User"].send(message) + + ################################################ + + async def showPlay(self, ctx, user): + # Creates an embed and displays the current game stats + stat_embed = discord.Embed(color=discord.Color.blue()) + game = await self.userGame(user) + if not game: + return + # Get the judge's name + if game["Members"][game["Judge"]]["User"] == user: + judge = "**YOU** are" + else: + if game["Members"][game["Judge"]]["IsBot"]: + # Bot + judge = "*{} ({})* is".format(self.botName, game["Members"][game["Judge"]]["ID"]) + else: + judge = "*{}* is".format(self.displayname(game["Members"][game["Judge"]]["User"])) + + # Get the Black Card + try: + blackCard = game["BlackCard"]["Text"] + blackNum = game["BlackCard"]["Pick"] + except Exception: + blackCard = "None." + blackNum = 0 + + msg = "{} the judge.\n\n".format(judge) + msg += "__Black Card:__\n\n**{}**\n\n".format(blackCard) + + totalUsers = len(game["Members"]) - 1 + submitted = len(game["Submitted"]) + if len(game["Members"]) >= self.minMembers: + if submitted < totalUsers: + msg += "{}/{} cards submitted...".format(submitted, totalUsers) + else: + msg += "All cards have been submitted!" + await self.showOptions(ctx, user) + return + prefix = await self.bot.db.prefix() + if not judge == "**YOU** are": + # Judge doesn't need to lay a card + if blackNum == 1: + # Singular + msg += "\n\nLay a card with `{}lay [card number]`".format(prefix[0]) + elif blackNum > 1: + # Plural + msg += "\n\nLay **{} cards** with `{}lay [card numbers separated by commas (1,2,3)]`".format( + blackNum, prefix[0] + ) + + stat_embed.set_author(name="Current Play") + stat_embed.set_footer(text="Cards Against Humanity - id: {}".format(game["ID"])) + await user.send(embed=stat_embed) + await user.send(msg) + + async def showHand(self, ctx, user): + # Shows the user's hand in an embed + stat_embed = discord.Embed(color=discord.Color.green()) + game = await self.userGame(user) + if not game: + return + i = 0 + msg = "" + points = "? points" + for member in game["Members"]: + if member["ID"] == user.id: + # Got our user + if member["Points"] == 1: + points = "1 point" + else: + points = "{} points".format(member["Points"]) + for card in member["Hand"]: + i += 1 + msg += "{}. {}\n".format(i, card["Text"]) + + try: + blackCard = "**{}**".format(game["BlackCard"]["Text"]) + except Exception: + blackCard = "**None.**" + stat_embed.set_author(name="Your Hand - {}".format(points)) + stat_embed.set_footer(text="Cards Against Humanity - id: {}".format(game["ID"])) + await user.send(embed=stat_embed) + await user.send(msg) + + async def showOptions(self, ctx, user): + # Shows the judgement options + stat_embed = discord.Embed(color=discord.Color.orange()) + game = await self.userGame(user) + if not game: + return + # Add title + stat_embed.set_author(name="JUDGEMENT TIME!!") + stat_embed.set_footer(text="Cards Against Humanity - id: {}".format(game["ID"])) + await user.send(embed=stat_embed) + + if game["Members"][game["Judge"]]["User"] == user: + judge = "**YOU** are" + else: + if game["Members"][game["Judge"]]["IsBot"]: + # Bot + judge = "*{} ({})* is".format(self.botName, game["Members"][game["Judge"]]["ID"]) + else: + judge = "*{}* is".format(self.displayname(game["Members"][game["Judge"]]["User"])) + blackCard = game["BlackCard"]["Text"] + + msg = "{} judging.\n\n".format(judge) + msg += "__Black Card:__\n\n**{}**\n\n".format(blackCard) + msg += "__Submitted White Cards:__\n\n" + + i = 0 + for sub in game["Submitted"]: + i += 1 + msg += "{}. {}\n".format(i, " - ".join(sub["Cards"])) + if judge == "**YOU** are": + prefix = await self.bot.db.prefix() + msg += "\nPick a winner with `{}pick [submission number]`.".format(prefix[0]) + await user.send(msg) + + async def drawCard(self, game): + with open(str(bundled_data_path(self)) + "/deck.json", 'r') as deck_file: + deck = json.load(deck_file) + # Draws a random unused card and shuffles the deck if needed + totalDiscard = len(game["Discard"]) + for member in game["Members"]: + totalDiscard += len(member["Hand"]) + if totalDiscard >= len(deck["whiteCards"]): + # Tell everyone the cards were shuffled + for member in game["Members"]: + if member["IsBot"]: + continue + user = member["User"] + await user.send("Shuffling white cards...") + # Shuffle the cards + self.shuffle(game) + while True: + # Random grab a unique card + index = random.randint(0, len(deck["whiteCards"]) - 1) + if not index in game["Discard"]: + game["Discard"].append(index) + text = deck["whiteCards"][index] + text = self.cleanJson(text) + card = {"Index": index, "Text": text} + return card + + def shuffle(self, game): + # Adds discards back into the deck + game["Discard"] = [] + for member in game["Members"]: + for card in member["Hand"]: + game["Discard"].append(card["Index"]) + + async def drawCards(self, user, cards=10): + if not len(str(user)) == 4: + if not type(user) is int: + # Assume it's a discord.Member/User + user = user.id + # fills the user's hand up to number of cards + game = await self.userGame(user) + for member in game["Members"]: + if member["ID"] == user: + # Found our user - let's draw cards + i = len(member["Hand"]) + while i < cards: + # Draw unique cards until we fill our hand + newCard = await self.drawCard(game) + member["Hand"].append(newCard) + i += 1 + + async def drawBCard(self, game): + with open(str(bundled_data_path(self)) + "/deck.json", 'r') as deck_file: + deck = json.load(deck_file) + # Draws a random black card + totalDiscard = len(game["BDiscard"]) + if totalDiscard >= len(deck["blackCards"]): + # Tell everyone the cards were shuffled + for member in game["Members"]: + if member["IsBot"]: + continue + user = member["User"] + await user.send("Shuffling black cards...") + # Shuffle the cards + game["BDiscard"] = [] + while True: + # Random grab a unique card + index = random.randint(0, len(deck["blackCards"]) - 1) + if not index in game["BDiscard"]: + game["BDiscard"].append(index) + text = deck["blackCards"][index]["text"] + text = self.cleanJson(text) + game["BlackCard"] = {"Text": text, "Pick": deck["blackCards"][index]["pick"]} + return game["BlackCard"] + + async def nextPlay(self, ctx, game): + # Advances the game + if len(game["Members"]) < self.minMembers: + prefix = await self.bot.db.prefix() + stat_embed = discord.Embed(color=discord.Color.red()) + stat_embed.set_author( + name="Not enough players to continue! ({}/{})".format( + len(game["Members"]), self.minMembers + ) + ) + stat_embed.set_footer( + text="Have other users join with: {}joincah {}".format(prefix[0], game["ID"]) + ) + for member in game["Members"]: + if member["IsBot"]: + continue + await member["User"].send(embed=stat_embed) + return + + # Find if we have a winner + winner = False + stat_embed = discord.Embed(color=discord.Color.lighter_grey()) + for member in game["Members"]: + if member["IsBot"]: + # Clear pending tasks and set to None + if not member["Task"] == None: + task = member["Task"] + if not task.done(): + # Task isn't finished - we're on a new hand, cancel it + task.cancel() + member["Task"] = None + if member["Points"] >= self.winAfter: + # We have a winner! + winner = True + if member["IsBot"]: + stat_embed.set_author( + name="{} ({}) is the WINNER!!".format(self.botName, member["ID"]) + ) + else: + stat_embed.set_author( + name="{} is the WINNER!!".format(self.displayname(member["User"])) + ) + stat_embed.set_footer(text="Congratulations!".format(game["ID"])) + break + if winner: + for member in game["Members"]: + if not member["IsBot"]: + await member["User"].send(embed=stat_embed) + # Reset all users + member["Hand"] = [] + member["Points"] = 0 + member["Won"] = [] + member["Laid"] = False + member["Refreshed"] = False + return + + game["Judging"] = False + # Clear submitted cards + game["Submitted"] = [] + # We have enough members + if game["Judge"] == -1: + # First game - randomize judge + game["Judge"] = random.randint(0, len(game["Members"]) - 1) + else: + game["Judge"] += 1 + # Reset the judge if out of bounds + if game["Judge"] >= len(game["Members"]): + game["Judge"] = 0 + + # Draw the next black card + bCard = await self.drawBCard(game) + + # Draw cards + for member in game["Members"]: + member["Laid"] = False + await self.drawCards(member["ID"]) + + # Show hands + for member in game["Members"]: + if member["IsBot"]: + continue + await self.showPlay(ctx, member["User"]) + index = game["Members"].index(member) + if not index == game["Judge"]: + await self.showHand(ctx, member["User"]) + await asyncio.sleep(0.1) + + # Have the bots lay their cards + for member in game["Members"]: + if not member["IsBot"]: + continue + if member["ID"] == game["Members"][game["Judge"]]["ID"]: + continue + # Not a human player, and not the judge + # task = self.bot.loop.create_task(self.botPick(ctx, member, game))\ + task = asyncio.ensure_future(self.botPick(ctx, member, game)) + member["Task"] = task + # await self.botPick(ctx, member, game) + + @commands.command() + async def game(self, ctx, *, message=None): + """Displays the game's current status.""" + if not await self.checkPM(ctx.message): + return + userGame = await self.userGame(ctx.message.author) + if not userGame: + prefix = await self.bot.db.prefix() + msg = "You're not in a game - you can create one with `{}newcah` or join one with `{}joincah`.".format( + prefix[0], prefix[0] + ) + await ctx.message.author.send(msg) + return + await self.showPlay(ctx, ctx.message.author) + + @commands.command() + async def chat(self, ctx, *, message=None): + """Broadcasts a message to the other players in your game.""" + if not await self.checkPM(ctx.message): + return + userGame = await self.userGame(ctx.message.author) + if not userGame: + prefix = await self.bot.db.prefix() + msg = "You're not in a game - you can create one with `{}newcah` or join one with `{}joincah`.".format( + prefix[0], prefix[0] + ) + await ctx.message.author.send(msg) + return + userGame["Time"] = int(time.time()) + if message == None: + msg = "Ooookay, you say *nothing...*" + await ctx.message.author.send(msg) + return + msg = "*{}* says: {}".format(ctx.message.author.name, message) + for member in userGame["Members"]: + if member["IsBot"]: + continue + # Tell them all!! + if not member["User"] == ctx.message.author: + # Don't tell yourself + await member["User"].send(msg) + else: + # Update member's time + member["Time"] = int(time.time()) + await ctx.message.author.send("Message sent!") + + @commands.command() + async def lay(self, ctx, *, card=None): + """Lays a card or cards from your hand. If multiple cards are needed, separate them by a comma (1,2,3).""" + if not await self.checkPM(ctx.message): + return + prefix = await self.bot.db.prefix() + userGame = await self.userGame(ctx.message.author) + if not userGame: + msg = "You're not in a game - you can create one with `{}newcah` or join one with `{}joincah`.".format( + prefix[0], prefix[0] + ) + await ctx.message.author.send(msg) + return + userGame["Time"] = int(time.time()) + for member in userGame["Members"]: + if member["User"] == ctx.message.author: + member["Time"] = int(time.time()) + user = member + index = userGame["Members"].index(member) + if index == userGame["Judge"]: + await ctx.message.author.send( + "You're the judge. You don't get to lay cards this round." + ) + return + for submit in userGame["Submitted"]: + if submit["By"]["User"] == ctx.message.author: + await ctx.message.author.send("You already made your submission this round.") + return + if card == None: + await ctx.message.author.send("You need you input *something.*") + return + card = card.strip() + card = card.replace(" ", "") + # Not the judge + if len(userGame["Members"]) < self.minMembers: + stat_embed = discord.Embed(color=discord.Color.red()) + stat_embed.set_author( + name="Not enough players to continue! ({}/{})".format( + len(userGame["Members"]), self.minMembers + ) + ) + stat_embed.set_footer( + text="Have other users join with: {}joincah {}".format(prefix[0], userGame["ID"]) + ) + await ctx.message.author.send(embed=stat_embed) + return + + numberCards = userGame["BlackCard"]["Pick"] + cards = [] + if numberCards > 1: + cardSpeak = "cards" + try: + card = card.split(",") + except Exception: + card = [] + if not len(card) == numberCards: + msg = "You need to lay **{} cards** (no duplicates) with `{}lay [card numbers separated by commas (1,2,3)]`".format( + numberCards, prefix[0] + ) + await ctx.message.author.send(msg) + await self.showHand(ctx, ctx.message.author) + return + # Got something + # Check for duplicates + if not len(card) == len(set(card)): + msg = "You need to lay **{} cards** (no duplicates) with `{}lay [card numbers separated by commas (1,2,3)]`".format( + numberCards, prefix[0] + ) + await ctx.message.author.send(msg) + await self.showHand(ctx, ctx.message.author) + return + # Works + for c in card: + try: + c = int(c) + except Exception: + msg = "You need to lay **{} cards** (no duplicates) with `{}lay [card numbers separated by commas (1,2,3)]`".format( + numberCards, prefix[0] + ) + await ctx.message.author.send(msg) + await self.showHand(ctx, ctx.message.author) + return + + if c < 1 or c > len(user["Hand"]): + msg = "Card numbers must be between 1 and {}.".format(len(user["Hand"])) + await ctx.message.author.send(msg) + await self.showHand(ctx, ctx.message.author) + return + cards.append(user["Hand"][c - 1]["Text"]) + # Remove from user's hand + card = sorted(card, key=lambda card: int(card), reverse=True) + for c in card: + user["Hand"].pop(int(c) - 1) + # Valid cards + + newSubmission = {"By": user, "Cards": cards} + else: + cardSpeak = "card" + try: + card = int(card) + except Exception: + msg = "You need to lay a valid card with `{}lay [card number]`".format(prefix[0]) + await ctx.message.author.send(msg) + await self.showHand(ctx, ctx.message.author) + return + if card < 1 or card > len(user["Hand"]): + msg = "Card numbers must be between 1 and {}.".format(len(user["Hand"])) + await ctx.message.author.send(msg) + await self.showHand(ctx, ctx.message.author) + return + # Valid card + newSubmission = {"By": user, "Cards": [user["Hand"].pop(card - 1)["Text"]]} + userGame["Submitted"].append(newSubmission) + + # Shuffle cards + shuffle(userGame["Submitted"]) + + user["Laid"] = True + await ctx.message.author.send("You submitted your {}!".format(cardSpeak)) + await self.checkSubmissions(ctx, userGame, user) + + @commands.command() + async def pick(self, ctx, *, card=None): + """As the judge - pick the winning card(s).""" + prefix = await self.bot.db.prefix() + if not await self.checkPM(ctx.message): + return + # Check if the user is already in game + userGame = await self.userGame(ctx.message.author) + if not userGame: + # Not in a game + msg = "You're not in a game - you can create one with `{}newcah` or join one with `{}joincah`.".format( + prefix[0], prefix[0] + ) + await ctx.message.author.send(msg) + return + userGame["Time"] = int(time.time()) + isJudge = False + for member in userGame["Members"]: + if member["User"] == ctx.message.author: + member["Time"] = int(time.time()) + user = member + index = userGame["Members"].index(member) + if index == userGame["Judge"]: + isJudge = True + if not isJudge: + msg = "You're not the judge - I guess you'll have to wait your turn.".format( + prefix[0], prefix[0] + ) + await ctx.message.author.send(msg) + return + # Am judge + totalUsers = len(userGame["Members"]) - 1 + submitted = len(userGame["Submitted"]) + if submitted < totalUsers: + if totalUsers - submitted == 1: + msg = "Still waiting on 1 card..." + else: + msg = "Still waiting on {} cards...".format(totalUsers - submitted) + await ctx.message.author.send(msg) + return + try: + card = int(card) - 1 + except Exception: + card = -1 + if card < 0 or card >= totalUsers: + msg = "Your pick must be between 1 and {}.".format(totalUsers) + await ctx.message.author.send(msg) + return + # Pick is good! + await self.winningCard(ctx, userGame, card) + + @commands.command() + async def hand(self, ctx): + """Shows your hand.""" + if not await self.checkPM(ctx.message): + return + # Check if the user is already in game + userGame = await self.userGame(ctx.message.author) + if not userGame: + # Not in a game + prefix = await self.bot.db.prefix() + msg = "You're not in a game - you can create one with `{}newcah` or join one with `{}joincah`.".format( + prefix[0], prefix[0] + ) + await ctx.message.author.send(msg) + return + await self.showHand(ctx, ctx.message.author) + userGame["Time"] = currentTime = int(time.time()) + + @commands.command() + async def newcah(self, ctx): + """Starts a new Cards Against Humanity game.""" + # if not await self.checkPM(ctx.message): + # return + # Check if the user is already in game + prefix = await self.bot.db.prefix() + userGame = await self.userGame(ctx.message.author) + if userGame: + # Already in a game + msg = "You're already in a game (id: *{}*)\nType `{}leavecah` to leave that game.".format( + userGame["ID"], prefix[0] + ) + await ctx.message.author.send(msg) + return + + # Not in a game - create a new one + gameID = self.randomID() + currentTime = int(time.time()) + newGame = { + "ID": gameID, + "Members": [], + "Discard": [], + "BDiscard": [], + "Judge": -1, + "Time": currentTime, + "BlackCard": None, + "Submitted": [], + "NextHand": asyncio.Event(), + "Judging": False, + "Timeout": True, + } + member = { + "ID": ctx.message.author.id, + "User": ctx.message.author, + "Points": 0, + "Won": [], + "Hand": [], + "Laid": False, + "Refreshed": False, + "IsBot": False, + "Creator": True, + "Task": None, + "Time": currentTime, + } + newGame["Members"].append(member) + newGame["Running"] = True + task = self.bot.loop.create_task(self.gameCheckLoop(ctx, newGame)) + task = self.bot.loop.create_task(self.checkCards(ctx, newGame)) + self.games.append(newGame) + # Tell the user they created a new game and list its ID + await ctx.message.channel.send("You created game id: *{}*".format(gameID)) + await self.drawCards(ctx.message.author) + # await self.showHand(ctx, ctx.message.author) + # await self.nextPlay(ctx, newGame) + + @commands.command() + async def leavecah(self, ctx): + """Leaves the current game you're in.""" + removeCheck = await self.removeMember(ctx.message.author) + if not removeCheck: + msg = "You are not in a game." + await ctx.message.channel.send(msg) + return + if self.checkGame(removeCheck): + # await self.nextPlay(ctx, removeCheck) + + """# Start the game loop + event = removeCheck['NextHand'] + self.bot.loop.call_soon_threadsafe(event.set)""" + # Player was removed - try to handle it calmly... + await self.checkSubmissions(ctx, removeCheck) + + @commands.command() + async def joincah(self, ctx, *, id=None): + """Join a Cards Against Humanity game. If no id or user is passed, joins a random game.""" + # if not await self.checkPM(ctx.message): + # return + # Check if the user is already in game + prefix = await self.bot.db.prefix() + userGame = await self.userGame(ctx.message.author) + isCreator = False + if userGame: + # Already in a game + msg = "You're already in a game (id: *{}*)\nType `{}leavecah` to leave that game.".format( + userGame["ID"], prefix[0] + ) + await ctx.message.channel.send(msg) + return + if len(self.games): + if id: + game = self.gameForID(id) + if game == None: + # That id doesn't exist - or is possibly a user + # If user, has to be joined from server chat + if not ctx.message.guild: + msg = "I couldn't find a game attached to that id. If you are trying to join a user - run the `{}joincah [user]` command in a channel on a server you share with that user.".format( + prefix[0] + ) + await ctx.message.channel.send(msg) + return + else: + # We have a server - let's try for a user + member = self.memberforname(id, ctx.message.guild) + if not member: + # Couldn't find user! + msg = "I couldn't find a game attached to that id. If you are trying to join a user - run the `{}joincah [user]` command in a channel on a server you share with that user.".format( + prefix[0] + ) + await ctx.message.channel.send(msg) + return + # Have a user - check if they're in a game + game = await self.userGame(member) + if not game: + # That user is NOT in a game! + msg = "That user doesn't appear to be playing." + await ctx.message.channel.send(msg) + return + + else: + game = random.choice(self.games) + else: + # No games - create a new one + gameID = self.randomID() + currentTime = int(time.time()) + game = { + "ID": gameID, + "Members": [], + "Discard": [], + "BDiscard": [], + "Judge": -1, + "Time": currentTime, + "BlackCard": None, + "Submitted": [], + "NextHand": asyncio.Event(), + "Judging": False, + "Timeout": True, + } + game["Running"] = True + task = self.bot.loop.create_task(self.gameCheckLoop(ctx, game)) + task = self.bot.loop.create_task(self.checkCards(ctx, game)) + self.games.append(game) + # Tell the user they created a new game and list its ID + await ctx.message.channel.send("**You created game id:** ***{}***".format(gameID)) + isCreator = True + + # Tell everyone else you joined + for member in game["Members"]: + if member["IsBot"]: + continue + await member["User"].send( + "***{}*** **joined the game!**".format(self.displayname(ctx.message.author)) + ) + + # We got a user! + currentTime = int(time.time()) + member = { + "ID": ctx.message.author.id, + "User": ctx.message.author, + "Points": 0, + "Won": [], + "Hand": [], + "Laid": False, + "Refreshed": False, + "IsBot": False, + "Creator": isCreator, + "Task": None, + "Time": currentTime, + } + game["Members"].append(member) + await self.drawCards(ctx.message.author) + if len(game["Members"]) == 1: + # Just created the game + await self.drawCards(ctx.message.author) + else: + msg = "**You've joined game id:** ***{}!***\n\nThere are *{} users* in this game.".format( + game["ID"], len(game["Members"]) + ) + await ctx.message.channel.send(msg) + + # Check if adding put us at minimum members + if len(game["Members"]) - 1 < self.minMembers: + # It was - *actually* start a game + event = game["NextHand"] + self.bot.loop.call_soon_threadsafe(event.set) + else: + # It was not - just incorporate new players + await self.checkSubmissions(ctx, game) + # Reset judging flag to retrigger actions + game["Judging"] = False + # Show the user the current card and their hand + await self.showPlay(ctx, member["User"]) + await self.showHand(ctx, member["User"]) + event = game["NextHand"] + + game["Time"] = int(time.time()) + + @commands.command() + async def joinbot(self, ctx): + """Adds a bot to the game. Can only be done by the player who created the game.""" + if not await self.checkPM(ctx.message): + return + # Check if the user is already in game + prefix = await self.bot.db.prefix() + userGame = await self.userGame(ctx.message.author) + if not userGame: + # Not in a game + msg = "You're not in a game - you can create one with `{}newcah` or join one with `{}joincah`.".format( + prefix[0], prefix[0] + ) + await ctx.message.author.send(msg) + return + botCount = 0 + for member in userGame["Members"]: + if member["IsBot"]: + botCount += 1 + continue + if member["User"] == ctx.message.author: + if not member["Creator"]: + # You didn't make this game + msg = "Only the player that created the game can add bots." + await ctx.message.author.send(msg) + return + member["Time"] = int(time.time()) + # We are the creator - let's check the number of bots + if botCount >= self.maxBots: + # Too many bots! + msg = "You already have enough bots (max is {}).".format(self.maxBots) + await ctx.message.author.send(msg) + return + # We can get another bot! + botID = self.randomBotID(userGame) + lobot = { + "ID": botID, + "User": None, + "Points": 0, + "Won": [], + "Hand": [], + "Laid": False, + "Refreshed": False, + "IsBot": True, + "Creator": False, + "Task": None, + } + userGame["Members"].append(lobot) + await self.drawCards(lobot["ID"]) + msg = "***{} ({})*** **joined the game!**".format(self.botName, botID) + for member in userGame["Members"]: + if member["IsBot"]: + continue + await member["User"].send(msg) + # await self.nextPlay(ctx, userGame) + + # Check if adding put us at minimum members + if len(userGame["Members"]) - 1 < self.minMembers: + # It was - *actually* start a game + event = userGame["NextHand"] + self.bot.loop.call_soon_threadsafe(event.set) + else: + # It was not - just incorporate new players + await self.checkSubmissions(ctx, userGame) + # Reset judging flag to retrigger actions + userGame["Judging"] = False + # Schedule stuff + task = asyncio.ensure_future(self.botPick(ctx, lobot, userGame)) + lobot["Task"] = task + + @commands.command() + async def joinbots(self, ctx, number=None): + """Adds bots to the game. Can only be done by the player who created the game.""" + if not await self.checkPM(ctx.message): + return + prefix = await self.bot.db.prefix() + # Check if the user is already in game + userGame = await self.userGame(ctx.message.author) + if not userGame: + # Not in a game + msg = "You're not in a game - you can create one with `{}newcah` or join one with `{}joincah`.".format( + prefix[0], prefix[0] + ) + await ctx.message.author.send(msg) + return + botCount = 0 + for member in userGame["Members"]: + if member["IsBot"]: + botCount += 1 + continue + if member["User"] == ctx.message.author: + if not member["Creator"]: + # You didn't make this game + msg = "Only the player that created the game can add bots." + await ctx.message.author.send(msg) + return + member["Time"] = int(time.time()) + if number == None: + # No number specified - let's add the max number of bots + number = self.maxBots - botCount + + try: + number = int(number) + except Exception: + msg = "Number of bots to add must be an integer." + await ctx.message.author.send(msg) + return + + # We are the creator - let's check the number of bots + if botCount >= self.maxBots: + # Too many bots! + msg = "You already have enough bots (max is {}).".format(self.maxBots) + await ctx.message.author.send(msg) + return + + if number > (self.maxBots - botCount): + number = self.maxBots - botCount + + if number == 1: + msg = "**Adding {} bot:**\n\n".format(number) + else: + msg = "**Adding {} bots:**\n\n".format(number) + + newBots = [] + for i in range(0, number): + # We can get another bot! + botID = self.randomBotID(userGame) + lobot = { + "ID": botID, + "User": None, + "Points": 0, + "Won": [], + "Hand": [], + "Laid": False, + "Refreshed": False, + "IsBot": True, + "Creator": False, + "Task": None, + } + userGame["Members"].append(lobot) + newBots.append(lobot) + await self.drawCards(lobot["ID"]) + msg += "***{} ({})*** **joined the game!**\n".format(self.botName, botID) + # await self.nextPlay(ctx, userGame) + + for member in userGame["Members"]: + if member["IsBot"]: + continue + await member["User"].send(msg) + + # Check if adding put us at minimum members + if len(userGame["Members"]) - number < self.minMembers: + # It was - *actually* start a game + event = userGame["NextHand"] + self.bot.loop.call_soon_threadsafe(event.set) + else: + # It was not - just incorporate new players + await self.checkSubmissions(ctx, userGame) + # Reset judging flag to retrigger actions + game["Judging"] = False + for bot in newBots: + # Schedule stuff + task = asyncio.ensure_future(self.botPick(ctx, bot, userGame)) + bot["Task"] = task + + @commands.command() + async def removebot(self, ctx, *, id=None): + """Removes a bot from the game. Can only be done by the player who created the game.""" + if not await self.checkPM(ctx.message): + return + # Check if the user is already in game + prefix = await self.bot.db.prefix() + userGame = await self.userGame(ctx.message.author) + if not userGame: + # Not in a game + msg = "You're not in a game - you can create one with `{}newcah` or join one with `{}joincah`.".format( + prefix[0], prefix[0] + ) + await ctx.message.author.send(msg) + return + botCount = 0 + for member in userGame["Members"]: + if member["IsBot"]: + botCount += 1 + continue + if member["User"] == ctx.message.author: + if not member["Creator"]: + # You didn't make this game + msg = "Only the player that created the game can remove bots." + await ctx.message.author.send(msg) + return + member["Time"] = int(time.time()) + # We are the creator - let's check the number of bots + if id == None: + # Just remove the first bot we find + for member in userGame["Members"]: + if member["IsBot"]: + await self.removeMember(member["ID"]) + """# Start the game loop + event = userGame['NextHand'] + self.bot.loop.call_soon_threadsafe(event.set)""" + # Bot was removed - try to handle it calmly... + await self.checkSubmissions(ctx, userGame) + return + msg = "No bots to remove!" + await ctx.message.author.send(msg) + return + else: + # Remove a bot by id + if not await self.removeMember(id): + # not found + msg = "I couldn't locate that bot on this game. If you're trying to remove a player, try the `{}removeplayer [name]` command.".format( + prefix[0] + ) + await ctx.message.author.send(msg) + return + # await self.nextPlay(ctx, userGame) + + """# Start the game loop + event = userGame['NextHand'] + self.bot.loop.call_soon_threadsafe(event.set)""" + # Bot was removed - let's try to handle it calmly... + await self.checkSubmissions(ctx, userGame) + + @commands.command() + async def cahgames(self, ctx): + """Displays up to 10 CAH games in progress.""" + shuffledGames = list(self.games) + random.shuffle(shuffledGames) + if not len(shuffledGames): + await ctx.message.channel.send("No games being played currently.") + return + + max = 10 + if len(shuffledGames) < 10: + max = len(shuffledGames) + msg = "__Current CAH Games__:\n\n" + + for i in range(0, max): + playerCount = 0 + botCount = 0 + gameID = shuffledGames[i]["ID"] + for j in shuffledGames[i]["Members"]: + if j["IsBot"]: + botCount += 1 + else: + playerCount += 1 + botText = "{} bot".format(botCount) + if not botCount == 1: + botText += "s" + playerText = "{} player".format(playerCount) + if not playerCount == 1: + playerText += "s" + + msg += "{}. {} - {} | {}\n".format(i + 1, gameID, playerText, botText) + + await ctx.message.channel.send(msg) + + @commands.command() + async def score(self, ctx): + """Display the score of the current game.""" + if not await self.checkPM(ctx.message): + return + # Check if the user is already in game + userGame = await self.userGame(ctx.message.author) + if not userGame: + # Not in a game + prefix = await self.bot.db.prefix() + msg = "You're not in a game - you can create one with `{}newcah` or join one with `{}joincah`.".format( + prefix[0], prefix[0] + ) + await ctx.message.author.send(msg) + return + stat_embed = discord.Embed(color=discord.Color.purple()) + stat_embed.set_author(name="Current Score") + stat_embed.set_footer(text="Cards Against Humanity - id: {}".format(userGame["ID"])) + await ctx.message.author.send(embed=stat_embed) + users = sorted(userGame["Members"], key=lambda card: int(card["Points"]), reverse=True) + msg = "" + i = 0 + if len(users) > 10: + msg += "__10 of {} Players:__\n\n".format(len(users)) + else: + msg += "__Players:__\n\n" + for user in users: + i += 1 + if i > 10: + break + if user["Points"] == 1: + if user["User"]: + # Person + msg += "{}. *{}* - 1 point\n".format(i, self.displayname(user["User"])) + else: + # Bot + msg += "{}. *{} ({})* - 1 point\n".format(i, self.botName, user["ID"]) + else: + if user["User"]: + # Person + msg += "{}. *{}* - {} points\n".format( + i, self.displayname(user["User"]), user["Points"] + ) + else: + # Bot + msg += "{}. *{} ({})* - {} points\n".format( + i, self.botName, user["ID"], user["Points"] + ) + await ctx.message.author.send(msg) + + @commands.command() + async def laid(self, ctx): + """Shows who laid their cards and who hasn't.""" + if not await self.checkPM(ctx.message): + return + # Check if the user is already in game + userGame = await self.userGame(ctx.message.author) + if not userGame: + # Not in a game + prefix = await self.bot.db.prefix() + msg = "You're not in a game - you can create one with `{}newcah` or join one with `{}joincah`.".format( + prefix[0], prefix[0] + ) + await ctx.message.author.send(msg) + return + stat_embed = discord.Embed(color=discord.Color.purple()) + stat_embed.set_author(name="Card Check") + stat_embed.set_footer(text="Cards Against Humanity - id: {}".format(userGame["ID"])) + await ctx.message.author.send(embed=stat_embed) + users = sorted(userGame["Members"], key=lambda card: int(card["Laid"])) + msg = "" + i = 0 + if len(users) > 10: + msg += "__10 of {} Players:__\n\n".format(len(users)) + else: + msg += "__Players:__\n\n" + for user in users: + if len(userGame["Members"]) >= self.minMembers: + if user == userGame["Members"][userGame["Judge"]]: + continue + i += 1 + if i > 10: + break + + if user["Laid"]: + if user["User"]: + # Person + msg += "{}. *{}* - Cards are in.\n".format(i, self.displayname(user["User"])) + else: + # Bot + msg += "{}. *{} ({})* - Cards are in.\n".format(i, self.botName, user["ID"]) + else: + if user["User"]: + # Person + msg += "{}. *{}* - Waiting for cards...\n".format( + i, self.displayname(user["User"]) + ) + else: + # Bot + msg += "{}. *{} ({})* - Waiting for cards...\n".format( + i, self.botName, user["ID"] + ) + await ctx.message.author.send(msg) + + @commands.command() + async def removeplayer(self, ctx, *, name=None): + """Removes a player from the game. Can only be done by the player who created the game.""" + if not await self.checkPM(ctx.message): + return + # Check if the user is already in game + userGame = await self.userGame(ctx.message.author) + if not userGame: + # Not in a game + prefix = await self.bot.db.prefix() + msg = "You're not in a game - you can create one with `{}newcah` or join one with `{}joincah`.".format( + prefix[0], prefix[0] + ) + await ctx.message.author.send(msg) + return + botCount = 0 + for member in userGame["Members"]: + if member["IsBot"]: + botCount += 1 + continue + if member["User"] == ctx.message.author: + if not member["Creator"]: + # You didn't make this game + msg = "Only the player that created the game can remove players." + await ctx.message.author.send(msg) + return + member["Time"] = int(time.time()) + # We are the creator - let's check the number of bots + if name == None: + # Nobody named! + msg = "Okay, I removed... no one from the game..." + await ctx.message.author.send(msg) + return + + # Let's get the person either by name, or by id + nameID = "".join(list(filter(str.isdigit, name))) + for member in userGame["Members"]: + toRemove = False + if member["IsBot"]: + continue + if name.lower() == self.displayname(member["User"]).lower(): + # Got em! + toRemove = True + elif nameID == member["ID"]: + # Got em! + toRemove = True + if toRemove: + await self.removeMember(member["ID"]) + break + # await self.nextPlay(ctx, userGame) + + if toRemove: + """# Start the game loop + event = userGame['NextHand'] + self.bot.loop.call_soon_threadsafe(event.set)""" + # Player was removed - try to handle it calmly... + await self.checkSubmissions(ctx, userGame) + else: + prefix = await self.bot.db.prefix() + msg = "I couldn't locate that player on this game. If you're trying to remove a bot, try the `{}removebot [id]` command.".format( + prefix[0] + ) + await ctx.message.author.send(msg) + return + + @commands.command() + async def flushhand(self, ctx): + """Flushes the cards in your hand - can only be done once per game.""" + if not await self.checkPM(ctx.message): + return + # Check if the user is already in game + userGame = await self.userGame(ctx.message.author) + if not userGame: + # Not in a game + prefix = await self.bot.db.prefix() + msg = "You're not in a game - you can create one with `{}newcah` or join one with `{}joincah`.".format( + prefix[0], prefix[0] + ) + await ctx.message.author.send(msg) + return + if userGame["Judge"] == -1: + msg = "The game hasn't started yet. Probably not worth it to flush your hand before you get it..." + await ctx.message.author.send(msg) + return + for member in userGame["Members"]: + if member["IsBot"]: + continue + if member["User"] == ctx.message.author: + member["Time"] = int(time.time()) + # Found us! + if member["Refreshed"]: + # Already flushed their hand + msg = "You have already flushed your hand this game." + await ctx.message.author.send(msg) + return + else: + member["Hand"] = [] + await self.drawCards(member["ID"]) + member["Refreshed"] = True + msg = "Flushing your hand!" + await ctx.message.author.send(msg) + await self.showHand(ctx, ctx.message.author) + return + + @commands.command() + async def idlekick(self, ctx, *, setting=None): + """Sets whether or not to kick members if idle for 5 minutes or more. Can only be done by the player who created the game.""" + if not await self.checkPM(ctx.message): + return + # Check if the user is already in game + userGame = await self.userGame(ctx.message.author) + if not userGame: + # Not in a game + prefix = await self.bot.db.prefix() + msg = "You're not in a game - you can create one with `{}newcah` or join one with `{}joincah`.".format( + prefix[0], prefix[0] + ) + await ctx.message.author.send(msg) + return + botCount = 0 + for member in userGame["Members"]: + if member["IsBot"]: + botCount += 1 + continue + if member["User"] == ctx.message.author: + if not member["Creator"]: + # You didn't make this game + msg = "Only the player that created the game can remove bots." + await ctx.message.author.send(msg) + return + # We are the creator - let's check the number of bots + if setting == None: + # Output idle kick status + if userGame["Timeout"]: + await ctx.message.channel.send("Idle kick is enabled.") + else: + await ctx.message.channel.send("Idle kick is disabled.") + return + elif setting.lower() == "yes" or setting.lower() == "on" or setting.lower() == "true": + setting = True + elif setting.lower() == "no" or setting.lower() == "off" or setting.lower() == "false": + setting = False + else: + setting = None + + if setting == True: + if userGame["Timeout"] == True: + msg = "Idle kick remains enabled." + else: + msg = "Idle kick now enabled." + for member in userGame["Members"]: + member["Time"] = int(time.time()) + else: + if userGame["Timeout"] == False: + msg = "Idle kick remains disabled." + else: + msg = "Idle kick now disabled." + userGame["Timeout"] = setting + + await ctx.message.channel.send(msg) + + @commands.command() + async def cahcredits(self, ctx): + """Code credits.""" + await ctx.send( + "```\nThis cog is made possible by CorpBot.\nPlease visit https://github.com/corpnewt/CorpBot.py for more information.\n```" + ) diff --git a/cah/data/deck.json b/cah/data/deck.json new file mode 100644 index 0000000..208bf32 --- /dev/null +++ b/cah/data/deck.json @@ -0,0 +1,2434 @@ +{ + "blackCards":[ + { + "text":"Why can't I sleep at night?", + "pick":1 + }, + { + "text":"I got 99 problems but _ ain't one.", + "pick":1 + }, + { + "text":"What's a girl's best friend?", + "pick":1 + }, + { + "text":"What's that smell?", + "pick":1 + }, + { + "text":"This is the way the world ends \/ This is the way the world ends \/ Not with a bang but with _.", + "pick":1 + }, + { + "text":"What is Batman's guilty pleasure?", + "pick":1 + }, + { + "text":"TSA guidelines now prohibit _ on airplanes.", + "pick":1 + }, + { + "text":"What ended my last relationship?", + "pick":1 + }, + { + "text":"MTV's new reality show features eight washed-up celebrities living with _.", + "pick":1 + }, + { + "text":"I drink to forget _.", + "pick":1 + }, + { + "text":"I'm sorry, Professor, but I couldn't complete my homework because of _.", + "pick":1 + }, + { + "text":"Alternative medicine is now embracing the curative powers of _.", + "pick":1 + }, + { + "text":"What's that sound?", + "pick":1 + }, + { + "text":"What's the next Happy Meal toy?", + "pick":1 + }, + { + "text":"It's a pity that kids these days are all getting involved with _.", + "pick":1 + }, + { + "text":"In the new Disney Channel Original Movie, Hannah Montana struggles with _ for the first time.", + "pick":1 + }, + { + "text":"_. That's how I want to die.", + "pick":1 + }, + { + "text":"What does Dick Cheney prefer?", + "pick":1 + }, + { + "text":"What's the most emo?", + "pick":1 + }, + { + "text":"Instead of coal, Santa now gives the bad children _.", + "pick":1 + }, + { + "text":"Next from J.K. Rowling: Harry Potter and the Chamber of _.", + "pick":1 + }, + { + "text":"A romantic, candlelit dinner would be incomplete without _.", + "pick":1 + }, + { + "text":"White people like _.", + "pick":1 + }, + { + "text":"_. Betcha can't have just one!", + "pick":1 + }, + { + "text":"War! What is it good for?", + "pick":1 + }, + { + "text":"BILLY MAYS HERE FOR _.", + "pick":1 + }, + { + "text":"_. High five, bro.", + "pick":1 + }, + { + "text":"During sex, I like to think about _.", + "pick":1 + }, + { + "text":"What did I bring back from Mexico?", + "pick":1 + }, + { + "text":"What are my parents hiding from me?", + "pick":1 + }, + { + "text":"What will always get you laid?", + "pick":1 + }, + { + "text":"What would grandma find disturbing, yet oddly charming?", + "pick":1 + }, + { + "text":"What did the U.S. airdrop to the children of Afghanistan?", + "pick":1 + }, + { + "text":"What helps Obama unwind?", + "pick":1 + }, + { + "text":"What's there a ton of in heaven?", + "pick":1 + }, + { + "text":"Major League Baseball has banned _ for giving players an unfair advantage.", + "pick":1 + }, + { + "text":"When I am a billionaire, I shall erect a 50-foot statue to commemorate _.", + "pick":1 + }, + { + "text":"What's the new fad diet?", + "pick":1 + }, + { + "text":"When I am the President of the United States, I will create the Department of _.", + "pick":1 + }, + { + "text":"_. It's a trap!", + "pick":1 + }, + { + "text":"How am I maintaining my relationship status?", + "pick":1 + }, + { + "text":"What will I bring back in time to convince people that I am a powerful wizard?", + "pick":1 + }, + { + "text":"While the United States raced the Soviet Union to the moon, the Mexican government funneled millions of pesos into research on _.", + "pick":1 + }, + { + "text":"Coming to Broadway this season, _: The Musical.", + "pick":1 + }, + { + "text":"What's my secret power?", + "pick":1 + }, + { + "text":"What gives me uncontrollable gas?", + "pick":1 + }, + { + "text":"But before I kill you, Mr. Bond, I must show you _.", + "pick":1 + }, + { + "text":"What never fails to liven up the party?", + "pick":1 + }, + { + "text":"What am I giving up for Lent?", + "pick":1 + }, + { + "text":"What do old people smell like? ", + "pick":1 + }, + { + "text":"The class field trip was completely ruined by _.", + "pick":1 + }, + { + "text":"When Pharaoh remained unmoved, Moses called down a plague of _.", + "pick":1 + }, + { + "text":"I do not know with which weapons World War III will be fought, but World War IV will be fought with _.", + "pick":1 + }, + { + "text":"What's Teach for America using to inspire inner city students to succeed?", + "pick":1 + }, + { + "text":"In Michael Jackson's final moments, he thought about _.", + "pick":1 + }, + { + "text":"Why do I hurt all over?", + "pick":1 + }, + { + "text":"Studies show that lab rats navigate mazes 50% faster after being exposed to _.", + "pick":1 + }, + { + "text":"Why am I sticky?", + "pick":1 + }, + { + "text":"What's my anti-drug?", + "pick":1 + }, + { + "text":"And the Academy Award for _ goes to _.", + "pick":2 + }, + { + "text":"For my next trick, I will pull _ out of _.", + "pick":2 + }, + { + "text":"_: Good to the last drop.", + "pick":1 + }, + { + "text":"What did Vin Diesel eat for dinner?", + "pick":1 + }, + { + "text":"_: kid-tested, mother-approved.", + "pick":1 + }, + { + "text":"What gets better with age?", + "pick":1 + }, + { + "text":"I never truly understood _ until I encountered _.", + "pick":2 + }, + { + "text":"Rumor has it that Vladimir Putin's favorite delicacy is _ stuffed with _.", + "pick":2 + }, + { + "text":"Lifetime presents _, the story of _.", + "pick":2 + }, + { + "text":"Make a haiku.", + "pick":3 + }, + { + "text":"In M. Night Shyamalan's new movie, Bruce Willis discovers that _ had really been _ all along.", + "pick":2 + }, + { + "text":"_ is a slippery slope that leads to _.", + "pick":2 + }, + { + "text":"In a world ravaged by _, our only solace is _.", + "pick":2 + }, + { + "text":"That's right, I killed _. How, you ask? _.", + "pick":2 + }, + { + "text":"When I was tripping on acid, _ turned into _.", + "pick":2 + }, + { + "text":"_ + _ = _.", + "pick":3 + }, + { + "text":"What's the next superhero\/sidekick duo?", + "pick":2 + }, + { + "text":"Dear Abby, I'm having some trouble with _ and would like your advice.", + "pick":1 + }, + { + "text":"After the earthquake, Sean Penn brought _ to the people of Haiti.", + "pick":1 + }, + { + "text":"In L.A. County Jail, word is you can trade 200 cigarettes for _.", + "pick":1 + }, + { + "text":"Maybe she's born with it. Maybe it's _.", + "pick":1 + }, + { + "text":"Life for American Indians was forever changed when the White Man introduced them to _.", + "pick":1 + }, + { + "text":"Next on ESPN2, the World Series of _.", + "pick":1 + }, + { + "text":"Step 1: _. Step 2: _. Step 3: Profit.", + "pick":2 + }, + { + "text":"Here is the church... Here is the steeple... Open the doors... And there is _.", + "pick":1 + }, + { + "text":"How did I lose my virginity?", + "pick":1 + }, + { + "text":"During his childhood, Salvador Dali produced hundreds of paintings of _.", + "pick":1 + }, + { + "text":"In 1,000 years, when paper money is a distant memory, how will we pay for goods and services?", + "pick":1 + }, + { + "text":"What don't you want to find in your Kung Pao chicken?", + "pick":1 + }, + { + "text":"The Smithsonian Museum of Natural History has just opened an exhibit on _.", + "pick":1 + }, + { + "text":"Daddy, why is Mommy crying?", + "pick":1 + }, + { + "text":"An international tribunal has found _ guilty of _.", + "pick":2 + }, + { + "text":"And I would have gotten away with it, too, if it hadn't been for _!", + "pick":1 + }, + { + "text":"Dear Sir or Madam, We regret to inform you that the Office of _ has denied your request for _.", + "pick":2 + }, + { + "text":"He who controls _ controls the world.", + "pick":1 + }, + { + "text":"I learned the hard way that you can't cheer up a grieving friend with _.", + "pick":1 + }, + { + "text":"In a pinch, _ can be a suitable substitute for _.", + "pick":2 + }, + { + "text":"In his new self-produced album, Kanye West raps over the sounds of _.", + "pick":1 + }, + { + "text":"In its new tourism campaign, Detroit proudly proclaims that it has finally eliminated _.", + "pick":1 + }, + { + "text":"In Rome, there are whisperings that the Vatican has a secret room devoted to _.", + "pick":1 + }, + { + "text":"In the distant future, historians will agree that _ marked the beginning of America's decline.", + "pick":1 + }, + { + "text":"Michael Bay's new three-hour action epic pits _ against _.", + "pick":2 + }, + { + "text":"My plan for world domination begins with _.", + "pick":1 + }, + { + "text":"Next season on Man vs, Wild, Bear Grylls must survive the depths of the Amazon with only _ and his wits.", + "pick":1 + }, + { + "text":"Science will never explain _.", + "pick":1 + }, + { + "text":"Science will never explain the origin of _.", + "pick":1 + }, + { + "text":"The CIA now interrogates enemy agents by repeatedly subjecting them to _.", + "pick":1 + }, + { + "text":"The secret to a lasting marriage is communication, communication, and _.", + "pick":1 + }, + { + "text":"The socialist governments of Scandinavia have declared that access to _ is a basic human right.", + "pick":1 + }, + { + "text":"This season on Man vs. Wild, Bear Grylls must survive in the depths of the", + "pick":1 + }, + { + "text":"Amazon with only _ and his wits.", + "pick":1 + }, + { + "text":"What brought the orgy to a grinding halt?", + "pick":1 + }, + { + "text":"What has been making life difficult at the nudist colony?", + "pick":1 + }, + { + "text":"What's the gift that keeps on giving?", + "pick":1 + }, + { + "text":"When all else fails, I can always masturbate to _.", + "pick":1 + }, + { + "text":"When I pooped, what came out of my butt?", + "pick":1 + }, + { + "text":"_ would be woefully incomplete without _.", + "pick":2 + }, + { + "text":"After months of debate, the Occupy Wall Street General Assembly could only agree on \"More _!\"", + "pick":1 + }, + { + "text":"Before _, all we had was _.", + "pick":2 + }, + { + "text":"Before I run for president, I must destroy all evidence of my involvement with _.", + "pick":1 + }, + { + "text":"Charades was ruined for me forever when my mom had to act out _.", + "pick":1 + }, + { + "text":"During his midlife crisis, my dad got really into _.", + "pick":1 + }, + { + "text":"Everyone down on the ground! We don't want to hurt anyone. We're just here for _.", + "pick":1 + }, + { + "text":"I spent my whole life working toward _, only to have it ruined by _.", + "pick":2 + }, + { + "text":"I went from _ to _, all thanks to _.", + "pick":3 + }, + { + "text":"If God didn't want us to enjoy _, he wouldn't have given us _.", + "pick":2 + }, + { + "text":"In his newest and most difficult stunt, David Blaine must escape from _.", + "pick":1 + }, + { + "text":"Little Miss Muffet Sat on a tuffet, Eating her curds and _.", + "pick":1 + }, + { + "text":"Members of New York's social elite are paying thousands of dollars just to experience _.", + "pick":1 + }, + { + "text":"My country, 'tis of thee, sweet land of _.", + "pick":1 + }, + { + "text":"My mom freaked out when she looked at my browser history and found _.com\/_.", + "pick":2 + }, + { + "text":"My new favorite porn star is Joey \"_\" McGee.", + "pick":1 + }, + { + "text":"Next time on Dr. Phil: How to talk to your child about _.", + "pick":1 + }, + { + "text":"Only two things in life are certain: death and _.", + "pick":1 + }, + { + "text":"The Five Stages of Grief: denial, anger, bargaining, _, acceptance.", + "pick":1 + }, + { + "text":"The healing process began when I joined a support group for victims of _.", + "pick":1 + }, + { + "text":"The votes are in, and the new high school mascot is _.", + "pick":1 + }, + { + "text":"This is your captain speaking. Fasten your seatbelts and prepare for _.", + "pick":1 + }, + { + "text":"This month's Cosmo: \"Spice up your sex life by bringing _ into the bedroom.\"", + "pick":1 + }, + { + "text":"Tonight on 20\/20: What you don't know about _ could kill you.", + "pick":1 + }, + { + "text":"You haven't truly lived until you've experienced _ and _ at the same time.", + "pick":2 + }, + { + "text":"A remarkable new study has shown that chimps have evolved their own primitive version of _.", + "pick":1 + }, + { + "text":"What's harshing my mellow, man?", + "pick":1 + }, + { + "text":"Your persistence is admirable, my dear Prince. But you cannot win my heart with _ alone.", + "pick":1 + }, + { + "text":"In the seventh circle of Hell, sinners must endure _ for all eternity.", + "pick":1 + }, + { + "text":"A successful job interview begins with a firm handshake and ends with _.", + "pick":1 + }, + { + "text":"Lovin' you is easy 'cause you're _.", + "pick":1 + }, + { + "text":"My life is ruled by a vicious cycle of _ and _.", + "pick":2 + }, + { + "text":"The blind date was going horribly until we discovered our shared interest in _.", + "pick":1 + }, + { + "text":"_. Awesome in theory, kind of a mess in practice.", + "pick":1 + }, + { + "text":"I'm not like the rest of you. I'm too rich and busy for _.", + "pick":1 + }, + { + "text":"_: Hours of fun. Easy to use. Perfect for _!", + "pick":2 + }, + { + "text":"What left this stain on my couch?", + "pick":1 + }, + { + "text":"Call the law offices of Goldstein & Goldstein, because no one should have to tolerate _ in the workplace.", + "pick":1 + }, + { + "text":"When you get right down to it, _ is just _.", + "pick":2 + }, + { + "text":"Turns out that _-Man was neither the hero we needed nor wanted.", + "pick":1 + }, + { + "text":"As part of his daily regimen, Anderson Cooper sets aside 15 minutes for _.", + "pick":1 + }, + { + "text":"Money can't buy me love, but it can buy me _.", + "pick":1 + }, + { + "text":"With enough time and pressure, _ will turn into _.", + "pick":2 + }, + { + "text":"And what did you bring for show and tell?", + "pick":1 + }, + { + "text":"During high school I never really fit in until I found _ club.", + "pick":1 + }, + { + "text":"Hey baby, come back to my place and I'll show you _.", + "pick":1 + }, + { + "text":"After months of practice with _, I think I'm finally ready for _.", + "pick":2 + }, + { + "text":"To prepare for his upcoming role, Daniel Day-Lewis immersed himself in the world of _.", + "pick":1 + }, + { + "text":"Finally! A service that delivers _ right to your door.", + "pick":1 + }, + { + "text":"My gym teacher got fired for adding _ to the obstacle course.", + "pick":1 + }, + { + "text":"Having problems with _? Try _!", + "pick":2 + }, + { + "text":"As part of his contract, Prince won't perform without _ in his dressing room.", + "pick":1 + }, + { + "text":"Listen, son. If you want to get involved with _, I won't stop you. Just steer clear of _.", + "pick":2 + }, + { + "text":"What's fun until it gets weird?", + "pick":1 + }, + { + "text":"In the beginning, there was _. And the Lord said, \"Let there be _.\"", + "pick":2 + }, + { + "text":"Wes Anderson's new film tells the story of a precocious child coming to terms with _.", + "pick":1 + }, + { + "text":"_ will never be the same after _.", + "pick":2 + }, + { + "text":"I'm sorry, sir, but we don't allow _ at the country club.", + "pick":1 + }, + { + "text":"How am I compensating for my tiny penis?", + "pick":1 + }, + { + "text":"You've seen the bearded lady! You've seen the ring of fire! Now, ladies and gentlemen, feast your eyes upon _!", + "pick":1 + }, + { + "text":"We never did find _, but along the way we sure learned a lot about _.", + "pick":2 + }, + { + "text":"She's up all night for good fun. I'm up all night for _.", + "pick":1 + }, + { + "text":"_ may pass, but _ will last forever.", + "pick":2 + }, + { + "text":"Dear Leader Kim Jong-un, our village praises your", + "pick":1 + }, + { + "text":"infinite wisdom with a humble offering of _.", + "pick":1 + }, + { + "text":"Man, this is bullshit. Fuck _.", + "pick":1 + }, + { + "text":"You guys, I saw this crazy movie last night. It opens on _, and then there's some stuff about _, and then it ends with _.", + "pick":3 + }, + { + "text":"In return for my soul, the Devil promised me _, but all I got was _.", + "pick":2 + }, + { + "text":"The Japanese have developed a smaller, more efficient version of _.", + "pick":1 + }, + { + "text":"Alright, bros. Our frat house is condemned, and all the hot slampieces are over at Gamma Phi. That time has come to commence Operation _.", + "pick":1 + }, + { + "text":"This is the prime of my life. I'm young, hot, and full of _.", + "pick":1 + }, + { + "text":"I'm pretty sure I'm high right now, because I'm absolutely mesmerized by _.", + "pick":1 + }, + { + "text":"It lurks in the night. It hungers for flesh. This summer, no one is safe from _.", + "pick":1 + }, + { + "text":"If you can't handle _, you'd better stay away from _.", + "pick":2 + }, + { + "text":"Forget everything you know about _, because now we've supercharged it with _!", + "pick":2 + }, + { + "text":"Honey, I have a new role-play I want to try tonight!", + "pick":1 + }, + { + "text":"You can be _, and I'll be _.", + "pick":2 + }, + { + "text":"This year's hottest album is \"_\" by _.", + "pick":2 + }, + { + "text":"Every step towards _ gets me a little bit closer to _.", + "pick":2 + }, + { + "text":"Oprah's book of the month is \"_ For _: A Story of Hope.\"", + "pick":2 + }, + { + "text":"Do not fuck with me! I am literally _ right now.", + "pick":1 + }, + { + "text":"2 AM in the city that never sleeps. The door swings open and she walks in, legs up to here. Something in her eyes tells me she's looking for _.", + "pick":1 + }, + { + "text":"As king, how will I keep the peasants in line?", + "pick":1 + }, + { + "text":"Adventure. Romance. _. From Paramount Pictures, \"_.\"", + "pick":2 + }, + { + "text":"I am become _, destroyer of _!", + "pick":2 + }, + { + "text":"And today's soup is Cream of _.", + "pick":1 + }, + { + "text":"Armani suit: $1,000. Dinner for two at that swanky restaurant: $300. The look on her face when you surprise her with _: priceless.", + "pick":1 + }, + { + "text":"Do the Dew with our most extreme flavor yet! Get ready for Mountain Dew _!", + "pick":1 + }, + { + "text":"Do you lack energy? Does it sometimes feel like the whole world is _ ? Zoloft.", + "pick":1 + }, + { + "text":"Don't forget! Beginning this week, Casual Friday will officially become \"_ Friday.\"", + "pick":1 + }, + { + "text":"Get ready for the movie of the summer! One cop plays by the book. The other's only interested in one thing: _.", + "pick":1 + }, + { + "text":"Having the worst day EVER. #_", + "pick":1 + }, + { + "text":"Heed my voice, mortals! I am the god of _ , and I will not tolerate _!", + "pick":2 + }, + { + "text":"Help me doctor, I've got _ in my butt!", + "pick":1 + }, + { + "text":"Here at the Academy for Gifted Children, we all students to explore _ at their own pace.", + "pick":1 + }, + { + "text":"Hi MTV! My name is Kendra, I live in Malibu, I'm into _, and I love to have a good time.", + "pick":1 + }, + { + "text":"Hi, this is Jim from accounting. We noticed a $1,200 charge labeled \"_.\" Can you explain?", + "pick":1 + }, + { + "text":"I don't mean to brag, but they call me the Micheal Jordan of _.", + "pick":1 + }, + { + "text":"In his farewell address, George Washington famously warned Americans about the dangers of _.", + "pick":1 + }, + { + "text":"In his new action comedy, Jackie Chan must fend off ninjas while also dealing with _.", + "pick":1 + }, + { + "text":"Life's pretty tough in the fast lane. That's why I never leave the house without _.", + "pick":1 + }, + { + "text":"Now in bookstores: \"The Audacity of _\" by Barack Obama.", + "pick":1 + }, + { + "text":"Patient presents with _ . Likely a result of _ .", + "pick":2 + }, + { + "text":"Well if _ is good enough for _, it's good enough for me.", + "pick":2 + }, + { + "text":"Well what do you have to say for yourself, Casey? This is the third time you've been sent to the principal's office for _.", + "pick":1 + }, + { + "text":"What killed my boner?", + "pick":1 + }, + { + "text":"What's making things awkward in the sauna?", + "pick":1 + }, + { + "text":"WHOOO! God damn I love _!", + "pick":1 + }, + { + "text":"Why am I broke?", + "pick":1 + }, + { + "text":"Yo' mama's so fat she _!", + "pick":1 + }, + { + "text":"I work my ass off all day for this family, and this what I come home to? _!?", + "pick":1 + }, + { + "text":"I have a strict policy. First date, dinner. Second date, kiss. Third date, _.", + "pick":1 + }, + { + "text":"When I was a kid we used to play Cowboys and _.", + "pick":1 + }, + { + "text":"This is America. If you don't work hard, you don't succeed. I don't care if you're black, white, purple, or _.", + "pick":1 + }, + { + "text":"You Won't Believe These 15 Hilarious _ Bloopers!", + "pick":1 + }, + { + "text":"James is a lonely boy. But when he discovers a secret door in his attic, he meets a magical new friend: _.", + "pick":1 + }, + { + "text":"Don't worry, kid. It gets better. I've been living with _ for 20 years.", + "pick":1 + }, + { + "text":"My grandfather worked his way up from nothing.", + "pick":1 + }, + { + "text":"When he came to this country, all he had was the shoes on his feet and _.", + "pick":1 + }, + { + "text":"Behind every powerful man is _.", + "pick":1 + }, + { + "text":"You are not alone. Millions of Americans struggle with _ every day.", + "pick":1 + }, + { + "text":"Come to Dubai, where you can relax in our world-famous spas, experience the nightlife, or simply enjoy _ by the poolside.", + "pick":1 + }, + { + "text":"'This is madness!' 'No. THIS IS _!'", + "pick":1 + }, + { + "text":"Listen, Gary, I like you. But if you want that corner office, you're going to have to show me _.", + "pick":1 + }, + { + "text":"I went to the desert and ate of the peyote cactus.", + "pick":1 + }, + { + "text":"Turns out my spirit animal is _.", + "pick":1 + }, + { + "text":"And would you like those buffalo wings mild, hot, or _?", + "pick":1 + }, + { + "text":"The six things I could never do without: oxygen, facebook, chocolate, netflix, friends, and _ LOL!", + "pick":1 + }, + { + "text":"Why won't you make love to me anymore? Is it _?", + "pick":1 + }, + { + "text":"Puberty is a time of change. You might notice hair growing in new places. You might develop an interest in _. This is normal.", + "pick":1 + }, + { + "text":"I'm sorry, Mrs. Chen, but there was nothing we could do. At 4:15 this morning, your son succumbed to _.", + "pick":1 + }, + { + "text":"I'm Miss Tennessee, and if I could make the world better by changing one thing, I would get rid of _.", + "pick":1 + }, + { + "text":"Tonight, we will have sex. And afterwards, if you'd like, a little bit of _.", + "pick":1 + }, + { + "text":"Everybody join hands and close your eyes. Do you sense that? That's the presence of _ in this room.", + "pick":1 + }, + { + "text":"To become a true Yanomamo warrior, you must prove that you can withstand _ without crying out.", + "pick":1 + }, + { + "text":"Y'all ready to get this thing started? I'm Nick Cannon, and this is America's Got _.", + "pick":1 + }, + { + "text":"If you had to describe me, the Card Czar, using only one of the cards in your hand, which one would it be?", + "pick":1 + }, + { + "text":"I may not be much to look at, but I fuck like _.", + "pick":1 + }, + { + "text":"Errbody in the club _.", + "pick":1 + }, + { + "text":"The top Google auto-complete results for \"Barack Obama\": Barack Obama Height. Barack Obama net worth. Barack Obama _.", + "pick":1 + }, + { + "text":"Barack Obama Height.", + "pick":1 + }, + { + "text":"Barack Obama net worth.", + "pick":1 + }, + { + "text":"Barack Obama ___.", + "pick":1 + }, + { + "text":"Son, take it from someone who's been around the block a few times. Nothin' puts her in the mood like _.", + "pick":1 + }, + { + "text":"Art isn't just a painting in a stuffy museum. Art is alive. Art is _.", + "pick":1 + }, + { + "text":"You won't believe what's in my pussy. It's _.", + "pick":1 + }, + { + "text":"What's the most problematic?", + "pick":1 + }, + { + "text":"Feeling so grateful! #amazing #mylife #_.", + "pick":1 + }, + { + "text":"What's a total waste of Hillary Clinton's time?", + "pick":1 + }, + { + "text":"What's the gayest?", + "pick":1 + }, + { + "text":"_ be all like _.", + "pick":2 + }, + { + "text":"Girls just wanna have _.", + "pick":1 + }, + { + "text":"One more thing. Watch out for Big Mike. They say he killed a man with _.", + "pick":1 + }, + { + "text":"Well, shit. My eyes ain't so good, but I'll eat my own boot if that ain't _!", + "pick":1 + }, + { + "text":"If at first you don't succeed, try _.", + "pick":1 + }, + { + "text":"What will end racism once and for all?", + "pick":1 + }, + { + "text":"I'll take the BBQ bacon burger with friend egg and fuck it how about _.", + "pick":1 + }, + { + "text":"Well if _ is a crime, then lock me up!", + "pick":1 + }, + { + "text":"We do not shake with our left hands in this country. That is the hand we use for _.", + "pick":1 + }, + { + "text":"You know who else liked _? Hitler.", + "pick":1 + }, + { + "text":"Poor Brandon, still living in his parent's basement. I heard he never got over _.", + "pick":1 + }, + { + "text":"What totally destroyed my asshole?", + "pick":1 + }, + { + "text":"I don't believe in God. I believe in _.", + "pick":1 + }, + { + "text":"Then the princess kissed the frog, and all of a sudden the frog was _!", + "pick":1 + }, + { + "text":"I got rhythm, I've got music, I've got _. Who could ask for anything more?", + "pick":1 + }, + { + "text":"No, no, no, no, no, no, NO! I will NOT let _ ruin this wedding.", + "pick":1 + }, + { + "text":"Coming to Red Lobster this month, _.", + "pick":1 + }, + { + "text":"Ooo, daddy like _.", + "pick":1 + }, + { + "text":"Best you go back where you came from, now. We don't take too kindly to _ in these parts.", + "pick":1 + }, + { + "text":"Summer lovin', had me a blast. _, happened so fast.", + "pick":1 + }, + { + "text":"LSD + _ = really bad time.", + "pick":1 + }, + { + "text":"What are all those whales singing about?", + "pick":1 + }, + { + "text":"As Teddy Roosevelt said, the four manly virtues are honor, temperance, industry, and _.", + "pick":1 + }, + { + "text":"I tell you, it was a non-stop fuckfest. When it was over, my asshole looked like _.", + "pick":1 + }, + { + "text":"What turned me into a Republican?", + "pick":1 + }, + { + "text":"I'm sorry, sir, but your insurance plan doesn't cover injuries caused by _.", + "pick":1 + }, + { + "text":"Run, run, as fast as you can! You can't catch me, I'm _!", + "pick":1 + }, + { + "text":"There is no God. It's just _ and then you die.", + "pick":1 + }, + { + "text":"She's just one of the guys, you know? She likes beer, and football, and _.", + "pick":1 + }, + { + "text":"This Friday at the Liquid Lunge, it's _ Night! Ladies drink free.", + "pick":1 + }, + { + "text":"What sucks balls?", + "pick":1 + }, + { + "text":"I've had a horrible vision, father. I saw mountains crumbling, stars falling from the sky. I saw _.", + "pick":1 + }, + { + "text":"Oh no! Siri, how do I fix _?", + "pick":1 + }, + { + "text":"Dance like there's nobody watching, love like you'll never be hurt, and live like you're _.", + "pick":1 + }, + { + "text":"Mom's to-do list: Buy Groceries Clean up ___. Soccer Practice.", + "pick":1 + }, + { + "text":"Why am I laughing and crying and taking off my clothes?", + "pick":1 + }, + { + "text":"She's a lady in the streets, _ in the sheets.", + "pick":1 + }, + { + "text":"What's about to take dance floor to the next level?", + "pick":1 + }, + { + "text":"CNN breaking news! Scientists discover _.", + "pick":1 + }, + { + "text":"Most Americans would not vote for a candidate who is openly _.", + "pick":1 + }, + { + "text":"Congratulations! You have been selected for our summer internship program. While we are unable to offer a salary, we can offer you _.", + "pick":1 + }, + { + "text":"In the 1950s, psychologists prescribed _ as a cure for homosexually.", + "pick":1 + }, + { + "text":"As reparations for slavery, all African Americans will receive _.", + "pick":1 + }, + { + "text":"With a one-time gift of just $10, you can save this child from _.", + "pick":1 + }, + { + "text":"Google Calendar alert: _ in 10 minutes.", + "pick":1 + } + ], + "whiteCards":[ + "Coat hanger abortions.", + "Man meat.", + "Autocannibalism.", + "Vigorous jazz hands.", + "Flightless birds.", + "Pictures of boobs.", + "Doing the right thing.", + "The violation of our most basic human rights.", + "Viagra.", + "Self-loathing.", + "Spectacular abs.", + "A balanced breakfast.", + "Roofies.", + "Concealing a boner.", + "Amputees.", + "The Big Bang.", + "Former President George W. Bush.", + "The Rev. Dr. Martin Luther King, Jr.", + "Smegma.", + "Being marginalized.", + "Cuddling.", + "Laying an egg.", + "The Pope.", + "Aaron Burr.", + "Genital piercings.", + "Fingering.", + "A bleached asshole.", + "Horse meat.", + "Fear itself.", + "Science.", + "Elderly Japanese men.", + "Stranger danger.", + "The terrorists.", + "Praying the gay away.", + "Same-sex ice dancing.", + "Ethnic cleansing.", + "Cheating in the Special Olympics.", + "German dungeon porn.", + "Bingeing and purging.", + "Making a pouty face.", + "William Shatner.", + "Heteronormativity.", + "Nickelback.", + "Tom Cruise.", + "The profoundly handicapped.", + "The placenta.", + "Chainsaws for hands.", + "Arnold Schwarzenegger.", + "An icepick lobotomy.", + "Goblins.", + "Object permanence.", + "Dying.", + "Foreskin.", + "A falcon with a cap on its head.", + "Hormone injections.", + "Dying of dysentery.", + "Sexy pillow fights.", + "The invisible hand.", + "A really cool hat.", + "Sean Penn.", + "Heartwarming orphans.", + "The clitoris.", + "The Three-Fifths compromise.", + "A sad handjob.", + "Men.", + "Historically black colleges.", + "A micropenis.", + "Raptor attacks.", + "Agriculture.", + "Vikings.", + "Pretending to care.", + "The Underground Railroad.", + "My humps.", + "Being a dick to children.", + "Geese.", + "Bling.", + "Sniffing glue.", + "The South.", + "An Oedipus complex.", + "Eating all of the cookies before the AIDS bake-sale.", + "Sexting.", + "YOU MUST CONSTRUCT ADDITIONAL PYLONS.", + "Mutually-assured destruction.", + "Sunshine and rainbows.", + "Count Chocula.", + "Sharing needles.", + "Being rich.", + "Skeletor.", + "A sausage festival.", + "Michael Jackson.", + "Emotions.", + "Farting and walking away.", + "The Chinese gymnastics team.", + "Necrophilia.", + "Spontaneous human combustion.", + "Yeast.", + "Leaving an awkward voicemail.", + "Dick Cheney.", + "White people.", + "Penis envy.", + "Teaching a robot to love.", + "Sperm whales.", + "Scrubbing under the folds.", + "Panda sex.", + "Whipping it out.", + "Catapults.", + "Masturbation.", + "Natural selection.", + "Opposable thumbs.", + "A sassy black woman.", + "AIDS.", + "The KKK.", + "Figgy pudding.", + "Seppuku.", + "Gandhi.", + "Preteens.", + "Toni Morrison's vagina.", + "Five-Dollar Footlongs.", + "Land mines.", + "A sea of troubles.", + "A zesty breakfast burrito.", + "Christopher Walken.", + "Friction.", + "Balls.", + "Dental dams.", + "A can of whoop-ass.", + "A tiny horse.", + "Waiting 'til marriage.", + "Authentic Mexican cuisine.", + "Genghis Khan.", + "Old-people smell.", + "Feeding Rosie O'Donnell.", + "Pixelated bukkake.", + "Friends with benefits.", + "The token minority.", + "The Tempur-Pedic Swedish Sleep System.", + "A thermonuclear detonation.", + "Take-backsies.", + "The Rapture.", + "A cooler full of organs.", + "Sweet, sweet vengeance.", + "RoboCop.", + "Keanu Reeves.", + "Drinking alone.", + "Giving 110%.", + "Flesh-eating bacteria.", + "The American Dream.", + "Taking off your shirt.", + "Me time.", + "A murder most foul.", + "The inevitable heat death of the universe.", + "The folly of man.", + "That thing that electrocutes your abs.", + "Cards Against Humanity.", + "Fiery poops.", + "Poor people.", + "Edible underpants.", + "Britney Spears at 55.", + "All-you-can-eat shrimp for $4.99.", + "Pooping back and forth. Forever.", + "Fancy Feast.", + "Jewish fraternities.", + "Being a motherfucking sorcerer.", + "Pulling out.", + "Picking up girls at the abortion clinic.", + "The homosexual agenda.", + "The Holy Bible.", + "Passive-agression.", + "Ronald Reagan.", + "Vehicular manslaughter.", + "Nipple blades.", + "Assless chaps.", + "Full frontal nudity.", + "Hulk Hogan.", + "Daddy issues.", + "The hardworking Mexican.", + "Natalie Portman.", + "Waking up half-naked in a Denny's parking lot.", + "God.", + "Sean Connery.", + "Saxophone solos.", + "Gloryholes.", + "The World of Warcraft.", + "Homeless people.", + "Scalping.", + "Darth Vader.", + "Eating the last known bison.", + "Guys who don't call.", + "Hot Pockets.", + "A time travel paradox.", + "The milk man.", + "Testicular torsion.", + "Dropping a chandelier on your enemies and riding the rope up.", + "World peace.", + "A salty surprise.", + "Poorly-timed Holocaust jokes.", + "Smallpox blankets.", + "Licking things to claim them as your own.", + "The heart of a child.", + "Robert Downey, Jr.", + "Lockjaw.", + "Eugenics.", + "A good sniff.", + "Friendly fire.", + "The taint; the grundle; the fleshy fun-bridge.", + "Wearing underwear inside-out to avoid doing laundry.", + "Hurricane Katrina.", + "Free samples.", + "Jerking off into a pool of children's tears.", + "A foul mouth.", + "The glass ceiling.", + "Republicans.", + "Explosions.", + "Michelle Obama's arms.", + "Getting really high.", + "Attitude.", + "Sarah Palin.", + "The Ubermensch.", + "Altar boys.", + "My soul.", + "My sex life.", + "Pedophiles.", + "72 virgins.", + "Pabst Blue Ribbon.", + "Domino's Oreo Dessert Pizza.", + "A snapping turtle biting the tip of your penis.", + "The Blood of Christ.", + "Half-assed foreplay.", + "My collection of high-tech sex toys.", + "A middle-aged man on roller skates.", + "Bitches.", + "Bill Nye the Science Guy.", + "Italians.", + "A windmill full of corpses.", + "Adderall.", + "Crippling debt.", + "A stray pube.", + "Prancing.", + "Passing a kidney stone.", + "A brain tumor.", + "Leprosy.", + "Puppies!", + "Bees?", + "Frolicking.", + "Repression.", + "Road head.", + "A bag of magic beans.", + "An asymmetric boob job.", + "Dead parents.", + "Public ridicule.", + "A mating display.", + "A mime having a stroke.", + "Stephen Hawking talking dirty.", + "African children.", + "Mouth herpes.", + "Overcompensation.", + "Riding off into the sunset.", + "Being on fire.", + "Tangled Slinkys.", + "Civilian casualties.", + "Auschwitz.", + "My genitals.", + "Not reciprocating oral sex.", + "Lactation.", + "Being fabulous.", + "Shaquille O'Neal's acting career.", + "My relationship status.", + "Asians who aren't good at math.", + "Alcoholism.", + "Incest.", + "Grave robbing.", + "Hope.", + "8 oz. of sweet Mexican black-tar heroin.", + "Kids with ass cancer.", + "Winking at old people.", + "The Jews.", + "Justin Bieber.", + "Doin' it in the butt.", + "A lifetime of sadness.", + "The Hamburglar.", + "Swooping.", + "Classist undertones.", + "New Age music.", + "Not giving a shit about the Third World.", + "The Kool-Aid Man.", + "A hot mess.", + "Tentacle porn.", + "Lumberjack fantasies.", + "The gays.", + "Scientology.", + "Estrogen.", + "GoGurt.", + "Judge Judy.", + "Dick fingers.", + "Racism.", + "Surprise sex!", + "Police brutality.", + "Passable transvestites.", + "The Virginia Tech Massacre.", + "When you fart and a little bit comes out.", + "Oompa-Loompas.", + "A fetus.", + "Obesity.", + "Tasteful sideboob.", + "Hot people.", + "BATMAN!!!", + "Black people.", + "A gassy antelope.", + "Sexual tension.", + "Third base.", + "Racially-biased SAT questions.", + "Porn stars.", + "A Super Soaker full of cat pee.", + "Muhammed (Praise Be Unto Him).", + "Puberty.", + "A disappointing birthday party.", + "An erection that lasts longer than four hours.", + "White privilege.", + "Getting so angry that you pop a boner.", + "Wifely duties.", + "Two midgets shitting into a bucket.", + "Queefing.", + "Wiping her butt.", + "Golden showers.", + "Barack Obama.", + "Nazis.", + "A robust mongoloid.", + "An M. Night Shyamalan plot twist.", + "Getting drunk on mouthwash.", + "Lunchables.", + "Women in yogurt commercials.", + "John Wilkes Booth.", + "Powerful thighs.", + "Mr. Clean, right behind you.", + "Multiple stab wounds.", + "Cybernetic enhancements.", + "Serfdom.", + "Kanye West.", + "Women's suffrage.", + "Children on leashes.", + "Harry Potter erotica.", + "The Dance of the Sugar Plum Fairy.", + "Lance Armstrong's missing testicle.", + "Parting the Red Sea.", + "The Amish.", + "Dead babies.", + "Child beauty pageants.", + "AXE Body Spray.", + "Centaurs.", + "Copping a feel.", + "Grandma.", + "Famine.", + "The Trail of Tears.", + "The miracle of childbirth.", + "Finger painting.", + "A monkey smoking a cigar.", + "The Make-A-Wish Foundation.", + "Anal beads.", + "The Force.", + "Kamikaze pilots.", + "Dry heaving.", + "Active listening.", + "Ghosts.", + "The Hustle.", + "Peeing a little bit.", + "Another goddamn vampire movie.", + "Shapeshifters.", + "The Care Bear Stare.", + "Hot cheese.", + "A mopey zoo lion.", + "A defective condom.", + "Teenage pregnancy.", + "A Bop It.", + "Expecting a burp and vomiting on the floor.", + "Horrifying laser hair removal accidents.", + "Boogers.", + "Unfathomable stupidity.", + "Breaking out into song and dance.", + "Soup that is too hot.", + "Morgan Freeman's voice.", + "Getting naked and watching Nickelodeon.", + "MechaHitler.", + "Flying sex snakes.", + "The true meaning of Christmas.", + "My inner demons.", + "Pac-Man uncontrollably guzzling cum.", + "My vagina.", + "A homoerotic volleyball montage.", + "Actually taking candy from a baby.", + "Crystal meth.", + "Exactly what you'd expect.", + "Natural male enhancement.", + "Passive-aggressive Post-it notes.", + "Inappropriate yodeling.", + "Lady Gaga.", + "The Little Engine That Could.", + "Vigilante justice.", + "A death ray.", + "Poor life choices.", + "A gentle caress of the inner thigh.", + "Embryonic stem cells.", + "Nicolas Cage.", + "Firing a rifle into the air while balls deep in a squealing hog.", + "Switching to Geico.", + "The chronic.", + "Erectile dysfunction.", + "Home video of Oprah sobbing into a Lean Cuisine.", + "A bucket of fish heads.", + "50,000 volts straight to the nipples.", + "Being fat and stupid.", + "Hospice care.", + "A pyramid of severed heads.", + "Getting married, having a few kids, buying some stuff, retiring to Florida, and dying.", + "A subscription to Men's Fitness.", + "Crucifixion.", + "A micropig wearing a tiny raincoat and booties.", + "Some god-damn peace and quiet.", + "Used panties.", + "A tribe of warrior women.", + "The penny whistle solo from \"My Heart Will Go On.\"", + "An oversized lollipop.", + "Helplessly giggling at the mention of Hutus and Tutsis.", + "Not wearing pants.", + "Consensual sex.", + "Her Majesty, Queen Elizabeth II.", + "Funky fresh rhymes.", + "The art of seduction.", + "The Devil himself.", + "Advice from a wise, old black man.", + "Destroying the evidence.", + "The light of a billion suns.", + "Wet dreams.", + "Synergistic management solutions.", + "Growing a pair.", + "Silence.", + "An M16 assault rifle.", + "Poopy diapers.", + "A live studio audience.", + "The Great Depression.", + "A spastic nerd.", + "Rush Limbaugh's soft, shitty body.", + "Tickling Sean Hannity, even after he tells you to stop.", + "Stalin.", + "Brown people.", + "Rehab.", + "Capturing Newt Gingrich and forcing him to dance in a monkey suit.", + "Battlefield amputations.", + "An uppercut.", + "Shiny objects.", + "An ugly face.", + "Menstrual rage.", + "A bitch slap.", + "One trillion dollars.", + "Chunks of dead prostitute.", + "The entire Mormon Tabernacle Choir.", + "The female orgasm.", + "Extremely tight pants.", + "The Boy Scouts of America.", + "Stormtroopers.", + "Throwing a virgin into a volcano.", + "24-hour media coverage", + "A beached whale.", + "A big black dick.", + "A bloody pacifier.", + "A crappy little hand.", + "A fat bald man from the internet.", + "A low standard of living.", + "A nuanced critique.", + "A panty raid.", + "A passionate Latino lover.", + "A plunger to the face.", + "A rival dojo.", + "A smiling black man, a latina businesswoman, a cool asian, and some whites.", + "A web of lies.", + "A woman scorned.", + "An atomic wedgie.", + "An Etsy steampunk strap-on.", + "An evil man in evil clothes.", + "AndrÈ the Giant's enormous, leathery scrotum.", + "Apologizing.", + "Appreciative snapping.", + "Ashton Kutcher.", + "Beating your wives.", + "Being a busy adult with many important things to do.", + "Being a dinosaur.", + "Blaxploitation.", + "Bosnian chicken farmers.", + "Breaking nip slip news.", + "Carnies.", + "Clams.", + "Clenched butt cheeks.", + "Coughing into a vagina.", + "Cutting.", + "Dancing with a broom.", + "Deflowering a princess.", + "Deflowering the princess.", + "Dorito breath.", + "Eating an albino.", + "Enormous Scandinavian women.", + "Fabricating statistics.", + "Finding a skeleton.", + "Gandalf.", + "Genetically engineered super-soldiers.", + "George Clooney's musk.", + "Getting abducted by Peter Pan.", + "Getting in her pants, politely.", + "Gladiatorial combat.", + "Good grammar.", + "having a penis", + "Hipsters.", + "Historical revisionism.", + "Insatiable bloodlust.", + "Jafar.", + "Jean-Claude Van Damme in slow motion.", + "Jean-Claude Van Damme.", + "Just the tip.", + "Leveling up.", + "Literally eating shit.", + "Mad hacky-sack skills.", + "Making the penises kiss.", + "Media coverage.", + "Medieval Times Dinner & Tournament.", + "Mom.", + "Moral ambiguity.", + "My machete.", + "Neil Patrick Harris.", + "NOOOOOOOOO!!!", + "Nubile slave boys.", + "Ominous background music.", + "One thousand Slim Jims.", + "Overpowering your father.", + "Panty raids.", + "Pistol-whipping a hostage.", + "Quiche.", + "Quivering jowls.", + "Revenge fucking.", + "Ripping into a man's chest and pulling out his still-beating heart.", + "Ryan Gosling riding in on a white horse.", + "Salvia.", + "Sanding off a man's nose.", + "Santa Claus.", + "Savagely beating a mascot", + "Scrotum tickling.", + "Sexual humiliation.", + "Sexy Siamese twins.", + "Shaft.", + "Slow motion.", + "Space muffins.", + "Statistically validated stereotypes.", + "Stockholm syndrome", + "Sudden Poop Explosion Disease.", + "Suicidal thoughts.", + "Syphilitic insanity", + "The boners of the elderly.", + "The economy.", + "The Fanta girls.", + "The four arms of Vishnu.", + "The gulags.", + "The harsh light of day.", + "The hiccups.", + "The ooze", + "The shambling corpse of Larry King.", + "This guy!", + "Tripping balls.", + "Walking in on Dad peeing into Mom's mouth.", + "Words, words, words.", + "Zeus's sexual appetites.", + "A 55-gallon drum of lube.", + "A bigger, blacker dick.", + "A Burmese tiger pit.", + "A dollop of sour cream.", + "A fortuitous turnip harvest.", + "A magic hippie love cloud.", + "A man in yoga pants with a ponytail and feather earrings.", + "A piÒata full of scorpions", + "A sad fat dragon with no friends.", + "A slightly shittier parallel universe.", + "A sofa that says \"I have style, but I like to be comfortable.\"", + "A soulful rendition of \"Ol' Man River.\"", + "A squadron of moles wearing aviator goggles.", + "A sweaty, panting leather daddy.", + "A sweet spaceship.", + "All of this blood.", + "An army of skeletons.", + "An ether-soaked rag.", + "An unhinged ferris wheel rolling toward the sea.", + "Another shot of morphine.", + "Basic human decency.", + "Beefin' over turf.", + "Being awesome at sex.", + "Boris the Soviet Love Hammer.", + "Bullshit.", + "Catastrophic urethral trauma.", + "Crushing Mr. Peanut's brittle body.", + "Daddy's belt.", + "Death by Steven Seagal.", + "Dennis the Menace.", + "Dining with cardboard cutouts of the cast of \"Friends.\"", + "Double penetration.", + "Existing.", + "Fetal alcohol syndrome.", + "Finding Waldo.", + "Fuck Mountain.", + "Getting hilariously gang-banged by the Blue Man Group.", + "Grandpa's ashes.", + "Graphic violence, adult language, and some sexual content.", + "Hillary Clinton's death stare.", + "Intimacy problems.", + "Jeff Goldblum.", + "Living in a trashcan.", + "Loki, the trickster god.", + "Making a friend.", + "Maximal insertion.", + "Me.", + "Mild autism.", + "Mooing.", + "My first kill.", + "Nunchuck moves.", + "Oncoming traffic.", + "One Ring to rule them all.", + "Power", + "Pretty Pretty Princess Board Game.", + "Pumping out a baby every nine months.", + "Rising from the grave.", + "Scrotal frostbite.", + "Some really fucked-up shit.", + "Special musical guest, Cher.", + "Spring break!", + "Subduing a grizzly bear and making her your wife.", + "Survivor's guilt.", + "Swiftly achieving orgasm.", + "Taking a man's eyes and balls out and putting his eyes where his balls go and then his balls in the eye holes.", + "The black Power Ranger", + "The corporations.", + "The day the birds attacked.", + "The Google.", + "The grey nutrient broth that sustains Mitt Romney.", + "The human body.", + "The mere concept of Applebee's.", + "The mixing of the races.", + "The new Radiohead album.", + "Tiny nipples.", + "Tongue.", + "Upgrading homeless people to mobile hotspots.", + "Weapons-grade plutonium.", + "Wearing an octopus for a hat.", + "Whining like a little bitch.", + "Whipping a disobedient slave.", + "That ass.", + "Nothing.", + "Shutting the fuck up.", + "The primal, ball-slapping sex your parents are having right now.", + "A cat video so cute that your eyes roll back and your spine slides out of your anus.", + "Cock.", + "A cop who is also a dog.", + "Dying alone and in pain.", + "Gay aliens.", + "The way white people is.", + "Reverse cowgirl.", + "The Quesadilla Explosion Salad from Chili's.", + "Actually getting shot, for real.", + "Not having sex.", + "Vietnam flashbacks.", + "Running naked through a mall, pissing and shitting everywhere.", + "Warm, velvety muppet sex.", + "Self-flagellation.", + "The systematic destruction of an entire people and their way of life.", + "Samuel L. Jackson.", + "A boo-boo.", + "Going around punching people.", + "The entire Internet.", + "Some kind of bird-man.", + "Chugging a lava lamp.", + "Having sex on top of a pizza.", + "Indescribable loneliness.", + "An ass disaster.", + "All my friends dying.", + "Putting an entire peanut butter and jelly sandwich into the VCR.", + "Spending lots of money.", + "Some douche with an acoustic guitar.", + "Flying robots that kill people.", + "A greased-up Matthew McConaughey.", + "An unstoppable wave of fire ants.", + "Not contributing to society in any meaningful way.", + "An all-midget production of Shakespeare's Richard III.", + "Screaming like a maniac.", + "The moist, demanding chasm of his mouth.", + "Filling every orifice with butterscotch pudding.", + "Unlimited soup, salad, and breadsticks.", + "Crying into the pages of Sylvia Plath.", + "Velcro.", + "A PowerPoint presentation.", + "A surprising amount of hair.", + "Eating Tom Selleck's mustache to gain his powers.", + "Roland the Farter, flatulist to the king.", + "A pile of squirming bodies.", + "Buying the right pants to be, cool.", + "Blood farts.", + "Three months in the hole.", + "A botched circumcision.", + "The Land of Chocolate.", + "Slapping a racist old lady.", + "A lamprey swimming up the toilet and latching onto your taint.", + "Jumping out at people.", + "A black male in his early 20s, last seen wearing a hoodie.", + "Mufasa's death scene.", + "Bill Clinton, naked on a bearskin rug with a saxophone.", + "Demonic possession.", + "The Harlem Globetrotters.", + "Vomiting mid-blowjob.", + "My manservant, Claude.", + "Having shotguns for legs.", + "Letting everyone down.", + "A spontaneous conga line.", + "A vagina that leads to another dimension.", + "Disco fever.", + "Getting your dick stuck in a Chinese finger trap with another dick.", + "Fisting.", + "The thin veneer of situational causality that underlies porn.", + "Girls that always be textin'.", + "Blowing some dudes in an alley.", + "Drinking ten 5-hour ENERGYs to get fifty continuous hours of energy.", + "Sneezing, farting, and coming at the same time.", + "A bunch of idiots playing a card game instead of interacting like normal humans.", + "A sex goblin with a carnival penis.", + "Lots and lots of abortions.", + "Injecting speed into one arm and horse tranquilizer into the other.", + "Sharks with legs.", + "A sex comet from Neptune that plunges the Earth into eternal sexiness.", + "How awesome I am.", + "Smoking crack, for instance.", + "A dance move that's just sex.", + "A hopeless amount of spiders.", + "Drinking responsibly.", + "Angelheaded hipsters burning for the ancient heavenly connection to the starry dynamo in the machinery of the night.", + "Bouncing up and down.", + "A shiny rock that proves I love you.", + "Crazy opium eyes.", + "Moderate-to-severe joint pain.", + "Actual mutants with medical conditions and no superpowers.", + "The complex geopolitical quagmire that is the Middle East.", + "Finally finishing off the Indians.", + "Neil Diamond's Greatest Hits.", + "No clothes on, penis in vagina.", + "Whispering all sexy.", + "A horse with no legs.", + "Depression.", + "Almost giving money to a homeless person.", + "Interspecies marriage.", + "Blackula.", + "What Jesus would do.", + "A manhole.", + "My dad's dumb fucking face.", + "A Ugandan warlord.", + "My worthless son.", + "A Native American who solves crimes by going into the spirit world.", + "A kiss on the lips.", + "A fart.", + "The peaceful and nonthreatening rise of China.", + "Snorting coke off a clown's boner.", + "Three consecutive seconds of happiness.", + "Falling into the toilet.", + "Ass to mouth.", + "Some sort of Asian.", + "The size of my penis.", + "The safe word.", + "Party Mexicans.", + "Ambiguous sarcasm.", + "Jizz.", + "An interracial handshake.", + "Incredible Facts About the Anus.", + "The secret formula for ultimate female satisfaction.", + "Sugar madness.", + "Calculating every mannerism so as not to suggest homosexuality.", + "Fucking a corpse back to life.", + "All the single ladies.", + "Whatever a McRib is made of.", + "Africa.", + "The euphoric rush of strangling a drifter.", + "Khakis.", + "A gender identity that can only be conveyed through slam poetry.", + "Stuffing a child's face with Fun Dip until he starts having fun.", + "A for-real lizard that spits blood from its eyes.", + "The tiniest shred of evidence that God is real.", + "Prince Ali, fabulous he, Ali Ababwa.", + "Dem titties.", + "Exploding pigeons.", + "My sex dungeon.", + "Child Protective Services.", + "Doo-doo.", + "Sports.", + "Unquestioning obedience.", + "Grammar nazis who are also regular Nazis.", + "40 acres and a mule.", + "A crazy little thing called love.", + "A disappointing salad.", + "A face full of horse cum.", + "A giant powdery manbaby.", + "A mouthful of potato salad.", + "A one-way ticket to Gary, Indiana.", + "A powered exoskeleton.", + "A reason not to commit suicide.", + "A team of lawyers.", + "A whole new kind of porn.", + "A zero-risk way to make $2,000 from home.", + "AIDS monkeys.", + "All these decorative pillows.", + "An unforgettable quinceanera.", + "An uninterrupted history of imperialism and exploitation.", + "Anal fissures like you wouldn't believe.", + "Ancient Athenian boy-fucking.", + "Backwards knees.", + "Being nine years old.", + "Being paralyzed from the neck down.", + "Being worshiped as the one true God.", + "Blackface.", + "Blowjobs for everyone.", + "Boring Vaginal sex.", + "Butt stuff.", + "Changing a person's mind with logic and facts.", + "Child support payments.", + "Cutting off a flamingo's legs with garden shears.", + "Daddy's credit card.", + "Deez nuts.", + "Denzel.", + "Doing the right stuff to her nipples.", + "Ejaculating live bees and the bees are angry.", + "Ennui.", + "Figuring out how to have sex with a dolphin.", + "Free ice cream, yo.", + "Genghis Khan's DNA.", + "Getting caught by the police and going to jail.", + "Getting drive-by shot.", + "Getting eaten alive by Guy Fieri.", + "Giant sperm from outer space.", + "Going to a high school reunion on ketamine.", + "Having been dead a while.", + "Mom's new boyfriend.", + "My boyfriends stupid penis.", + "My dead son's baseball glove.", + "My first period.", + "Not believing in giraffes.", + "Oil!", + "Out-of-this-world bazongas.", + "P.F. Chang himself.", + "Russian super-tuberculosis.", + "Seeing my village burned and my family slaughtered before my eyes.", + "Seeing things from Hitler's perspective.", + "September 11th, 2001.", + "Slowly easing down onto a cucumber.", + "Social justice warriors with flamethrowers of compassion.", + "Some shit-hot guitar licks.", + "The Abercrombie & Fitch lifestyle.", + "The basic suffering that pervades all of existence.", + "The eight gay warlocks who dictate the rules of fashion.", + "The ghost of Marlon Brando.", + "The inability to form meaningful relationships.", + "The passage of time.", + "The swim team, all at once.", + "The tiger that killed my father.", + "The unbelievable world of mushrooms.", + "The black half of Barack Obama.", + "The white half of Barack Obama.", + "Too much cocaine.", + "Unrelenting genital punishment.", + "Vegetarian options.", + "Wearing glasses and sounding smart.", + "Western standards of beauty.", + "A bass drop so huge it tears the starry vault asunder to reveal the face of God.", + "Growing up chained to a radiator in perpetual darkness.", + "Shitting all over the floor like a bad, bad girl.", + "A buttload of candy.", + "Sucking all the milk out of a yak.", + "Bullets.", + "A man who is so cool that he rides on a motorcycle.", + "Sudden penis loss.", + "Getting all offended.", + "Crying and shitting and eating spaghetti.", + "One unforgettable night of passion.", + "Being popular and good at sports.", + "Filling a man's anus with concrete.", + "Two whales fucking the shit out of each other.", + "Cool, relatable cancer teens.", + "The amount of gay I am.", + "A possible Muslim.", + "Unsheathing my massive horse cock.", + "A bowl of gourds.", + "The male gaze.", + "The power of the Dark Side.", + "Ripping a dog in half.", + "A constant need for validation.", + "Meaningless sex.", + "Such a big boy.", + "Throwing stones at a man until he dies.", + "Cancer.", + "Like a million alligators.", + "Eating together like a god damn family for once.", + "Cute boys.", + "Pussy.", + "Being a terrible mother.", + "Never having sex again.", + "A pizza guy who fucked up.", + "A whole lotta woman.", + "The all-new Nissan Pathfinder with 0.9% APR financing!", + "A peyote-fueled vision quest.", + "Kale.", + "Breastfeeding a ten year old.", + "Crippling social anxiety.", + "Immortality cream.", + "Texas.", + "Teaching a girl how to handjob the penis.", + "A turd.", + "Shapes and colors.", + "Whatever you wish, mother.", + "The haunting stare of an Iraqi child.", + "Robots who just want to party.", + "A self-microwaving burrito.", + "Forgetting grandma's first name.", + "Our new Buffalo Chicken Dippers!", + "Treasures beyond your wildest dreams.", + "Getting shot out of a cannon.", + "The sweet song of sword against sword and the braying of mighty war beasts.", + "Walking into a glass door.", + "The color \"puce.\"", + "Every ounce of charisma left in Mick Jagger's tired body.", + "The eighth graders.", + "Setting my balls on fire and cartwheeling to Ohio.", + "The dentist.", + "Gwyneth Paltrow's opinions.", + "Turning the rivers red with the blood of infidels.", + "Rabies.", + "Important news about Taylor Swift.", + "Ejaculating inside another man's wife.", + "Owls, the perfect predator.", + "Being John Malkovich.", + "Bathing in moonsblood and dancing around the ancient oak.", + "An oppressed people with a vibrant culture.", + "An overwhelming variety of cheeses.", + "Reading the entire End-User License Agreement.", + "Morpheus.", + "Peeing into a girl's butt to make a baby.", + "Generally having no idea what's going on.", + "No longer finding any Cards Against Humanity card funny.", + "Content.", + "Fucking me good and taking me to Red Lobster.", + "Self-identifying as a DJ.", + "Getting high with mom.", + "Beyonce.", + "Gazpacho.", + "Discovering that what I really want in life is to kill people and have sex with their corpses.", + "A man with the head of a goat and the body of a goat.", + "How good lead paint taste.", + "Dropping dead in a Sbarro's bathroom and not being found for 72 hours.", + "Eating too many Cinnabons and then vomiting and then eating the vomit.", + "Some of that good dick.", + "Two shitty kids and a garbage husband.", + "Pooping in the potty.", + "Bad emotions I don't want.", + "Mixing M&Ms and Skittles like some kind of psychopath.", + "Fucking my therapist.", + "The best, deepest quotes from The Dark Knight.", + "Meatloaf, the man.", + "Meatloaf, the food.", + "My huge penis and substantial fortune.", + "Hot lettuce.", + "It being too late to stop having sex with a horse.", + "Becoming the President of the United States.", + "Microaggressions.", + "Getting the Dorito crumbs out of my purse.", + "The sweet, forbidden meat of the monkey.", + "Consensual, nonreproductive incest.", + "Grunting for ten minutes and then peeing sand.", + "Prematurely ejaculating like a total loser.", + "Jazz.", + "Straight blazin' 24/7.", + "Having sex with a beautiful person.", + "Going around pulling people's tampons out.", + "Reaching an age where barbecue chips are better than sex.", + "Daddy going away forever.", + "Three hours of nonstop penetration.", + "Holding the proper political beliefs of my time to attract a mate.", + "Scissoring, if that's a thing.", + "Creamy slices of real, California avocado.", + "ISIS.", + "A weird guy who says weird stuff and weirds me out.", + "Rubbing my bush all over your bald head.", + "Farting all over my face with your tight little asshole.", + "Quinoa.", + "How sad it will be when Morgan Freeman dies.", + "A cheerfulness that belies a deep-seated self-loathing.", + "Farting a huge shit out of my pussy.", + "Defeating a gorilla in single combat.", + "A big ol' plate of fettuccine alfredo.", + "Brunch.", + "Anal.", + "A women's perspective.", + "A long business meeting with no obvious purpose.", + "Thinking about what eating even is.", + "Doing a somersault and barfing.", + "Trees.", + "Hating Jews.", + "Whooping your ass at Mario Kart.", + "A massive collection of child pornography.", + "Systems and policies designed to preserve centuries-old power structures.", + "Having an awesome time drinking and driving.", + "Muchin' puss.", + "Moon people.", + "Picking up a glass of water and taking a sip and being the president.", + "Critical thinking.", + "Showing all the boys my pussy.", + "Homework.", + "China.", + "Putting more black people in jail.", + "The ol' penis-in-the-popcorn surprise.", + "One of them big-city Jew lawyers.", + "Informing you that I am a registered sex offender.", + "Rolling so hard.", + "Who really did 9\/11.", + "Being turned into sausages.", + "Eating ass.", + "A dolphin that learns to talk and becomes the Dead of Harvard Law School.", + "Gay thoughts.", + "My dog dying.", + "Dominating a man by peeing on his eldest son.", + "Dis bitch.", + "A strong horse and enough rations for thirty days.", + "Feminism.", + "A cold and indifferent universe.", + "An incurable homosexual.", + "The amount of baby carrots I can fit up my ass.", + "Huge big balls full of jizz.", + "Gregor, my largest son.", + "Esmeralda, my most beautiful daughter.", + "Trevor, the world's greatest boyfriend.", + "Jason, the teen mayor.", + "That bitch, Stacy.", + "Gayle from HR.", + "Gary.", + "Just now finding out about the Armenian Genocide.", + "Opening your mouth to talk and a big penis fops out.", + "Twisting my cock and balls into a balloon poodle.", + "The wind.", + "A gun that shoots cobras.", + "Out-of-control teenage blowjob parties.", + "A black friend.", + "The body of a 46-year-old man.", + "Art.", + "Water.", + "Doritos and a Fruit Roll-Up.", + "Sucking each other's penises for hours on end.", + "10,000 shrieking teenage girls.", + "Whomsoever let the dogs out.", + "The chicken from Popeyes.", + "Assassinating the president.", + "Having sex with a man and then eating his head.", + "A burrito that's just sour cream.", + "An arrangement wherein I give a person money they have sex with me.", + "Facilitating dialogue and deconstructing binaries.", + "Taking the form of a falcon.", + "Watching you die.", + "An X-Man whose power is that he has sex with dogs and children.", + "Loud, scary thunder.", + "Every man's ultimate fantasy: a perfectly cylindrical vagina.", + "Tiny, rancid girl farts.", + "Math.", + "Founding a major world religion.", + "Plowing that ass like a New England corn farmer.", + "Period poops.", + "The feeling of going to McDonald's as a 6-year-old.", + "Misogyny.", + "Tables.", + "Feeling the emotion of anger.", + "One of those \"blow jobs\" I've been hearing so much about.", + "A creepy child singing a nursery rhyme.", + "Blossoming into a beautiful young woman.", + "The secret to truly resilient hair.", + "Rock-hard tits and a huge vagina.", + "The lived experience of African Americans.", + "Mental illness.", + "Getting eaten out by a dog.", + "10 football players with erections barreling towards you at full speed.", + "Starting a shitty podcast.", + "Overthrowing the democratically-elected government of Chile.", + "Guns.", + "A hug.", + "Getting aborted.", + "Crazy anal orgasms.", + "Getting this party started!", + "Being sexually attracted to children.", + "Antidepressants.", + "Getting trapped in a conversation about Ayn Rand.", + "Swearing praise upon the Sultan's hideous daughters.", + "Turning 32.", + "Sudden and unwanted slam poetry.", + "A negative body image that is totally justified.", + "Exploring each other's buttholes.", + "You.", + "Quacking like a duck in lieu of a cogent argument.", + "Catching a live salmon in your mouth.", + "Eating people.", + "Our baby.", + "Breastfeeding in public like a radiant earth goddess.", + "Big, smart money boys tap-tapping on their keyboards.", + "Finding a nice elevator to poop in.", + "The mysterious fog rolling into town.", + "Two beautiful pig sisters.", + "Condoleezza Rice.", + "The full force of the American military.", + "A woman's right to choose.", + "A terrified fat child who won't come out of the bushes.", + "A medium horchata.", + "How great my ass looks in these jeans.", + "Raising three kids on minimum wage.", + "The bond between a woman and her horse.", + "Slamming a dunk.", + "Chris Hemsworth.", + "Ejaculating at the apex of a cartwheel.", + "Child labor.", + "An older man.", + "Crushing the patriarchy.", + "Denying the Holocaust.", + "Falling into a pit of waffles.", + "Objectifying women.", + "Everything.", + "A creature made of penises that must constantly arouse itself to survive.", + "Getting blasted in the face by a t-shirt cannon.", + "Onions.", + "Dumpster juice.", + "Forty-five minutes of finger blasting.", + "Film roles for actresses over 40.", + "Having sex with your mom.", + "Having a vagina.", + "Regurgitating a half-digested sparrow.", + "The hottest MILF in Dallas.", + "Some real spicy shrimps.", + "The Rwandan Genocide.", + "The LGBT community.", + "Twenty bucks.", + "The full blown marginalization of ugly people.", + "A finger up the butt.", + "A big, beautiful mouth packed to the brim with sparkling teeth.", + "Getting laid like all the time.", + "Happy daddies with happy sandals.", + "Libertarians.", + "Late-stage dementia.", + "How strange it is to be anything at all.", + "Pooping in a leotard and hoping no one notices.", + "Restoring Germany to its former glory.", + "Participating.", + "Going to bed at a reasonable hour.", + "Smashing my balls at the moment of climax.", + "Making out and stuff.", + "Menopause.", + "The government.", + "The graceful path of an autumn leaf as it falls to its earthen cradle.", + "Tender chunks of all-white-meat chicken.", + "Twenty cheerleaders laughing at your tiny penis.", + "Mommy and daddy fighting all the time.", + "Playing my asshole like a trumpet.", + "Getting naked too soon.", + "A slowly encroaching circle of wolves.", + "A man in a suit with perfect hair who tells you beautiful lies.", + "Seizing control of the means of production.", + "Comprehensive immigration reform.", + "Awesome pictures of planets and stuff.", + "Aborting the shit out of a fetus.", + "A genetic predisposition for alcoholism.", + "The flaming wreckage of the International Space Station.", + "Waking up inside of a tornado.", + "When the big truck goes \"Toot! Toot!\"", + "The fear and hatred in men's hearts.", + "Getting killed and dragged up a tree by a leopard.", + "Eternal screaming madness.", + "My brother's hot friends.", + "Salsa Night at Dave's Cantina.", + "The clown that followed me home from the grocery store.", + "Watching a hot person eat.", + "Pretending to be one of the guys but actually being the spider god.", + "Getting pegged.", + "An empowered woman.", + "An old dog full of tumors.", + "A duffel bag full of lizards.", + "All these people I've killed.", + "Working so hard to have muscles and then having them.", + "Political correctness.", + "A tiny fireman who puts out tiny fires.", + "Albert Einstein but if he had a huge muscles and a rhinoceros cock.", + "Chipotle." + ] +} diff --git a/chatchart/chatchart.py b/chatchart/chatchart.py index d22a617..1238a3c 100644 --- a/chatchart/chatchart.py +++ b/chatchart/chatchart.py @@ -119,6 +119,10 @@ class Chatchart(BaseCog): msg_data["users"][whole_name]["msgcount"] = 1 msg_data["total count"] += 1 + if msg_data['users'] == {}: + await em.delete() + return await ctx.message.channel.send(f'Only bots have sent messages in {channel.mention}') + for usr in msg_data["users"]: pd = float(msg_data["users"][usr]["msgcount"]) / float(msg_data["total count"]) msg_data["users"][usr]["percent"] = round(pd * 100, 1) diff --git a/dungeon/dungeon.py b/dungeon/dungeon.py index f499cb9..3c48054 100644 --- a/dungeon/dungeon.py +++ b/dungeon/dungeon.py @@ -75,7 +75,7 @@ class Dungeon(BaseCog): @commands.group(autohelp=True) @commands.guild_only() - @checks.admin_or_permissions(manage_server=True) + @checks.admin_or_permissions(manage_guild=True) async def dungeon(self, ctx): """Main dungeon commands.""" pass @@ -380,7 +380,7 @@ class Dungeon(BaseCog): await member.send(auto_ban_msg) except discord.Forbidden: if announce_channel: - return await channel_object.send( + await channel_object.send( f"I couldn't DM {member} ({member.id}) to let them know they've been banned, they've blocked me." ) else: @@ -460,6 +460,8 @@ class Dungeon(BaseCog): await member.add_roles(user_role_obj, reason="Adding member role to new user.") except discord.Forbidden: pass + except AttributeError: + pass @staticmethod def _dynamic_time(time): diff --git a/joinleave/__init__.py b/joinleave/__init__.py new file mode 100644 index 0000000..6d961dd --- /dev/null +++ b/joinleave/__init__.py @@ -0,0 +1,5 @@ +from .joinleave import JoinLeave + + +def setup(bot): + bot.add_cog(JoinLeave(bot)) diff --git a/joinleave/joinleave.py b/joinleave/joinleave.py new file mode 100644 index 0000000..b227f66 --- /dev/null +++ b/joinleave/joinleave.py @@ -0,0 +1,256 @@ +import asyncio +import datetime +import discord +import re +from redbot.core import Config, commands, checks +from redbot.core.utils.chat_formatting import bold + +BaseCog = getattr(commands, "Cog", object) + + +class JoinLeave(BaseCog): + """Report users that join and leave quickly, with new accounts.""" + def __init__(self, bot): + self.bot = bot + self.config = Config.get_conf(self, 2713731002, force_registration=True) + + default_global = { + "announce_channel": None, + "join_days": 7, + "toggle": False, + "cooldown": 120, + "pingrole": None, + "pingserver": None, + } + + default_user = {"last_join": "2018-01-01 00:00:00.000001", "new": True} + + self.config.register_user(**default_user) + self.config.register_global(**default_global) + + @commands.group() + @commands.guild_only() + @checks.is_owner() + async def joinleave(self, ctx): + """Main joinleave commands.""" + pass + + @joinleave.command() + async def channel(self, ctx, channel: discord.TextChannel): + """Sets the announcement channel.""" + await self.config.announce_channel.set(channel.id) + announce_channel_id = await self.config.announce_channel() + await ctx.send( + f"User announcement channel set to: {self.bot.get_channel(announce_channel_id).mention}." + ) + + @joinleave.command() + async def cooldown(self, ctx, cooldown_time: int = 0): + """Set the time window in seconds for a valid join/leave flag. + + Leave time blank to reset to default (2m).""" + cooldown = await self.config.cooldown() + if not cooldown_time: + await self.config.cooldown.set(120) + await ctx.send("Join/leave time window reset to 2m.") + else: + await self.config.cooldown.set(cooldown_time) + await ctx.send( + f"Join/leave time window set to {self._dynamic_time(int(cooldown_time))}." + ) + + @joinleave.command() + async def days(self, ctx, days: int): + """Set how old an account needs to be a trusted user.""" + await self.config.join_days.set(days) + await ctx.send(f"Users must have accounts older than {days} day(s) to be ignored.") + + @joinleave.command() + async def role(self, ctx, *, role_name: discord.Role = None): + """Set the role to ping on a first sighting. Leave blank to turn off.""" + if not role_name: + await self.config.pingrole.set(None) + await self.config.pingserver.set(None) + return await ctx.send( + "Role has been removed. No pinging will occur on a first sighting." + ) + + await self.config.pingserver.set(ctx.guild.id) + await self.config.pingrole.set(role_name.id) + role_obj = discord.utils.get(ctx.guild.roles, id=await self.config.pingrole()) + await ctx.send(f"Pingable role set to: {role_obj.name}.") + + @joinleave.command() + async def settings(self, ctx): + """Show the current settings.""" + data = await self.config.all() + + try: + achannel = self.bot.get_channel(data["announce_channel"]).name + except AttributeError: + achannel = None + joinleave_enabled = data["toggle"] + join_days = data["join_days"] + cooldown = data["cooldown"] + pingrole_id = data["pingrole"] + ping_guild = self.bot.get_guild(data["pingserver"]) + if not pingrole_id: + pingrole = "None" + else: + try: + pingrole_obj = discord.utils.get(ping_guild.roles, id=pingrole_id) + pingrole = pingrole_obj.name + except AttributeError: + pingrole = "None" + + msg = ( + "```ini\n---JoinLeave Settings--- \n" + f"Announce Channel: [{achannel}]\n" + f"Join/Leave Span: [{self._dynamic_time(int(cooldown))}]\n" + f"Day Threshold: [{str(join_days)}]\n" + f"Ping role: [{pingrole}]\n" + f"JoinLeave Active: [{joinleave_enabled}]\n```" + ) + + embed = discord.Embed(colour=ctx.guild.me.top_role.colour, description=msg) + return await ctx.send(embed=embed) + + @joinleave.command() + async def toggle(self, ctx): + """Toggle the joinleave on or off. This is global.""" + joinleave_enabled = await self.config.toggle() + announce_channel = await self.config.announce_channel() + if not announce_channel: + await self.config.announce_channel.set(ctx.message.channel.id) + await self.config.toggle.set(not joinleave_enabled) + await ctx.send(f"JoinLeave enabled: {not joinleave_enabled}.") + if not announce_channel: + await ctx.send(f"JoinLeave report channel set to: {ctx.message.channel.mention}.") + + async def on_member_join(self, member): + global_data = await self.config.all() + if not global_data["toggle"]: + return + + join_date = datetime.datetime.strptime(str(member.created_at), "%Y-%m-%d %H:%M:%S.%f") + now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + since_join = now - join_date + + if since_join.days < global_data["join_days"]: + await self.config.user(member).last_join.set(str(now)) + + async def on_member_remove(self, member): + user_data = await self.config.user(member).all() + global_data = await self.config.all() + if not global_data["toggle"]: + return + + channel_obj = self.bot.get_channel(global_data["announce_channel"]) + + if not channel_obj: + print("joinleave.py: toggled on but no announce channel") + return + + last_time = datetime.datetime.strptime(str(user_data["last_join"]), "%Y-%m-%d %H:%M:%S.%f") + + join_date = datetime.datetime.strptime(str(member.created_at), "%Y-%m-%d %H:%M:%S.%f") + now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + since_join = now - join_date + + if int((now - last_time).total_seconds()) < global_data["cooldown"]: + await channel_obj.send(f"**{member.id}**") + msg = f"User: {member} ({member.id})\nServer: {member.guild.name} ({member.guild.id})\nAccount is {self._dynamic_time(int(since_join.total_seconds()))} old" + if user_data["new"]: + await self.config.user(member).new.set(False) + if not global_data["pingrole"]: + return await channel_obj.send(f"\N{WARNING SIGN} First sighting\n{msg}") + else: + ping_guild = self.bot.get_guild(await self.config.pingserver()) + role_obj = discord.utils.get(ping_guild.roles, id=global_data["pingrole"]) + try: + await role_obj.edit(mentionable=True) + await channel_obj.send( + f"{role_obj.mention}\n\N{WARNING SIGN} First sighting\n{msg}" + ) + return await role_obj.edit(mentionable=False) + except AttributeError: + return await channel_obj.send( + f"I can't find the role that's set to ping (is it on another server?)\n\N{WARNING SIGN} First sighting\n{msg}" + ) + except discord.errors.Forbidden: + return await channel_obj.send( + f"I tried to ping for this alert but I don't have permissions to manage roles!\n\N{WARNING SIGN} First sighting\n{msg}" + ) + else: + await channel_obj.send(msg) + + async def on_message(self, message): + global_data = await self.config.all() + if not global_data["toggle"]: + return + channel_obj = self.bot.get_channel(global_data["announce_channel"]) + if not channel_obj: + return + if message.author.bot: + return + date_join = datetime.datetime.strptime(str(message.author.created_at), "%Y-%m-%d %H:%M:%S.%f") + date_now = datetime.datetime.now(datetime.timezone.utc) + date_now = date_now.replace(tzinfo=None) + since_join = date_now - date_join + if since_join.days < global_data["join_days"]: + sentence = message.content.split() + for word in sentence: + if self._match_url(word): + await channel_obj.send(bold(f"{message.author.id}")) + msg = "**Message posted in** {} ({})\n".format(message.guild.name, message.guild.id) + msg += "**Message posted in** {} ({})\n".format(message.channel.mention, message.channel.id) + msg += "**Message sent by**: {} ({})\n".format(message.author.name, message.author.id) + msg += "**Message content**:\n{}".format(message.content) + + if not global_data["pingrole"]: + return await channel_obj.send(f"\N{WARNING SIGN} \N{LINK SYMBOL} New user link post\n{msg}") + else: + ping_guild = self.bot.get_guild(await self.config.pingserver()) + try: + role_obj = discord.utils.get(ping_guild.roles, id=global_data["pingrole"]) + await role_obj.edit(mentionable=True) + await channel_obj.send( + f"{role_obj.mention}\n\N{WARNING SIGN} \N{LINK SYMBOL} New user link post\n{msg}" + ) + return await role_obj.edit(mentionable=False) + except AttributeError: + return await channel_obj.send( + f"I can't find the role that's set to ping (is it on another server?)\n\N{WARNING SIGN} \N{LINK SYMBOL} New user link post\n{msg}" + ) + except discord.errors.Forbidden: + return await channel_obj.send( + f"I tried to ping for this alert but I don't have permissions to manage roles!\n\N{WARNING SIGN} \N{LINK SYMBOL} New user link post\n{msg}" + ) + + @staticmethod + def _dynamic_time(time): + m, s = divmod(time, 60) + h, m = divmod(m, 60) + d, h = divmod(h, 24) + + if d > 0: + msg = "{0}d {1}h" + elif d == 0 and h > 0: + msg = "{1}h {2}m" + elif d == 0 and h == 0 and m > 0: + msg = "{2}m {3}s" + elif d == 0 and h == 0 and m == 0 and s > 0: + msg = "{3}s" + else: + msg = "" + return msg.format(d, h, m, s) + + @staticmethod + def _match_url(url): + regex = re.compile( + "(([\w]+:)?//)?(([\d\w]|%[a-fA-f\d]{2,2})+(:([\d\w]|%[a-fA-f\d]{2,2})+)?@)?([\d\w][-\d\w]{0,253}[\d\w]\.)+[\w]{2,63}(:[\d]+)?(/([-+_~.\d\w]|%[a-fA-f\d]{2,2})*)*(\?(&?([-+_~.\d\w]|%[a-fA-f\d]{2,2})=?)*)?(#([-+_~.\d\w]|%[a-fA-f\d]{2,2})*)?" + ) + if regex.match(url): + return True + else: + return False diff --git a/partycrash/info.json b/partycrash/info.json index 282d6ba..9527a60 100644 --- a/partycrash/info.json +++ b/partycrash/info.json @@ -5,11 +5,11 @@ "description": "Posts invites to servers, if the bot is allowed to view them. Does not generate new invites.", "install_msg": "Thanks for installing, have fun.", "permissions" : [ - "manage_server" + "manage_guild" ], "short": "Post server invites.", "tags": [ "invite" ], "type": "COG" -} \ No newline at end of file +} diff --git a/timezone/timezone.py b/timezone/timezone.py index 1b8ffd6..8a60426 100644 --- a/timezone/timezone.py +++ b/timezone/timezone.py @@ -110,7 +110,7 @@ class Timezone(BaseCog): ) @time.command() - @checks.admin_or_permissions(manage_server=True) + @checks.admin_or_permissions(manage_guild=True) async def set(self, ctx, user: discord.Member, *, tz): """Allows the mods to edit timezones.""" author = ctx.message.author diff --git a/tools/tools.py b/tools/tools.py index 1fc8d6c..580673c 100644 --- a/tools/tools.py +++ b/tools/tools.py @@ -11,9 +11,9 @@ from redbot.core.utils.menus import menu, DEFAULT_CONTROLS from tabulate import tabulate from contextlib import suppress as sps - BaseCog = getattr(commands, "Cog", object) + class Tools(BaseCog): """Mod and Admin tools.""" @@ -170,7 +170,7 @@ class Tools(BaseCog): @commands.guild_only() @commands.command() - @checks.mod_or_permissions(manage_server=True) + @checks.mod_or_permissions(manage_guild=True) async def banlist(self, ctx): """Displays the server's banlist.""" try: @@ -271,7 +271,7 @@ class Tools(BaseCog): @commands.guild_only() @commands.command() - @checks.mod_or_permissions(manage_server=True) + @checks.mod_or_permissions(manage_guild=True) async def inrole(self, ctx, *, rolename): """Check members in the role specified.""" guild = ctx.guild @@ -359,9 +359,52 @@ class Tools(BaseCog): ) await awaiter.edit(embed=embed) + @commands.command(name='listguilds', aliases=['listservers', 'guildlist', 'serverlist']) + @checks.mod_or_permissions() + async def listguilds(self, ctx): + """ + List the guilds|servers the bot is in + """ + asciidoc = lambda m: "```asciidoc\n{}\n```".format(m) + guilds = sorted(self.bot.guilds, key=lambda g: -g.member_count) + header = ("```\n" + "The bot is in the following {} server{}\n" + "```").format(len(guilds), 's' if len(guilds) > 1 else '') + + max_zpadding = max([len(str(g.member_count)) for g in guilds]) + form = "{gid} :: {mems:0{zpadding}} :: {name}" + all_forms = [form.format(gid=g.id, mems=g.member_count, name=g.name, zpadding=max_zpadding) for g in guilds] + final = '\n'.join(all_forms) + + await ctx.send(header) + for page in cf.pagify(final, delims=['\n'], shorten_by=16): + await ctx.send(asciidoc(page)) + + @commands.guild_only() + @checks.mod_or_permissions(manage_channels=True) + @commands.command(name='listchannel', aliases=['channellist']) + async def listchannel(self, ctx): + """ + List the channels of the current server + """ + asciidoc = lambda m: "```asciidoc\n{}\n```".format(m) + channels = ctx.guild.channels + top_channels, category_channels = self.sort_channels(ctx.guild.channels) + + topChannels_formed = '\n'.join(self.channels_format(top_channels)) + categories_formed = '\n\n'.join([self.category_format(tup) for tup in category_channels]) + + await ctx.send(f"{ctx.guild.name} has {len(channels)} channel{'s' if len(channels) > 1 else ''}.") + + for page in cf.pagify(topChannels_formed, delims=['\n'], shorten_by=16): + await ctx.send(asciidoc(page)) + + for page in cf.pagify(categories_formed, delims=['\n\n'], shorten_by=16): + await ctx.send(asciidoc(page)) + @commands.guild_only() @commands.command() - @checks.mod_or_permissions(manage_server=True) + @checks.mod_or_permissions(manage_guild=True) async def newusers(self, ctx, count: int = 5, fm: str = "py"): """Lists the newest 5 members.""" guild = ctx.guild @@ -410,7 +453,7 @@ class Tools(BaseCog): @commands.guild_only() @commands.command() - @checks.mod_or_permissions(manage_server=True) + @checks.mod_or_permissions(manage_guild=True) async def perms(self, ctx, user: discord.Member = None): """Fetch a specific user's permissions.""" if user is None: @@ -641,16 +684,20 @@ class Tools(BaseCog): data += "[ID]: {}\n".format(user.id) data += "[Status]: {}\n".format(user.status) data += "[Servers]: {} shared\n".format(seen) - if user.activity is None: + act = user.activity + if act is None: pass - elif user.activity.url is None: - if user.activity.type == discord.ActivityType.playing: - data += "[Playing]: {}\n".format(cf.escape(str(user.activity.name))) - elif user.activity.type == discord.ActivityType.listening: - data += "[Listening]: {}\n".format(cf.escape(str(user.activity.name))) - elif user.activity.type == discord.ActivityType.listening: - data += "[Watching]: {}\n".format(cf.escape(str(user.activity.name))) + elif act.type == discord.ActivityType.playing: + data += "[Playing]: {}\n".format(cf.escape(str(act.name))) + elif act.type == discord.ActivityType.listening: + if isinstance(act, discord.Spotify): + _form = act.title + else: + _form = act.name + data += "[Listening]: {}\n".format(cf.escape(_form)) + elif act.type == discord.ActivityType.listening: + data += "[Watching]: {}\n".format(cf.escape(str(user.activity.name))) else: data += "[Streaming]: [{}]({})\n".format( cf.escape(str(user.activity.name)), cf.escape(user.activity.url) @@ -742,3 +789,48 @@ class Tools(BaseCog): roles = guild.roles role = discord.utils.find(lambda r: r.name.lower() == str(rolename).lower(), roles) return role + + def sort_channels(self, channels): + temp = dict() + + channels = sorted(channels, key=lambda c: c.position) + + for c in channels[:]: + if isinstance(c, discord.CategoryChannel): + channels.pop(channels.index(c)) + temp[c] = list() + + for c in channels[:]: + if c.category: + channels.pop(channels.index(c)) + temp[c.category].append(c) + + category_channels = sorted([(cat, sorted(chans, key=lambda c: c.position)) for cat, chans in temp.items()], key=lambda t: t[0].position) + return channels, category_channels + + def channels_format(self, channels: list): + if channels == []: + return [] + + channel_form = "{name} :: {ctype} :: {cid}" + + def type_name(channel): + return channel.__class__.__name__[:-7] + + name_justify = max([len(c.name[:24]) for c in channels]) + type_justify = max([len(type_name(c)) for c in channels]) + + return [channel_form.format(name=c.name[:24].ljust(name_justify), ctype=type_name(c).ljust(type_justify), cid=c.id) for c in channels] + + + def category_format(self, cat_chan_tuple: tuple): + + cat = cat_chan_tuple[0] + chs = cat_chan_tuple[1] + + chfs = self.channels_format(chs) + if chfs != []: + ch_forms = ['\t' + f for f in chfs] + return '\n'.join([f'{cat.name} :: {cat.id}'] + ch_forms) + else: + return '\n'.join([f'{cat.name} :: {cat.id}'] + ['\tNo Channels']) diff --git a/trickortreat/trickortreat.py b/trickortreat/trickortreat.py index 61d148d..aac7878 100644 --- a/trickortreat/trickortreat.py +++ b/trickortreat/trickortreat.py @@ -248,7 +248,7 @@ class TrickOrTreat(BaseCog): @commands.guild_only() @commands.cooldown(1, 600, discord.ext.commands.BucketType.user) @commands.command() - async def pick(self, ctx): + async def pickup(self, ctx): """Pick up some candy, if there is any.""" candies = await self.config.user(ctx.author).candies() to_pick = await self.config.guild(ctx.guild).pick() diff --git a/wolfram/wolfram.py b/wolfram/wolfram.py index 926ddf6..a7f2b49 100644 --- a/wolfram/wolfram.py +++ b/wolfram/wolfram.py @@ -1,16 +1,19 @@ -import os import aiohttp +import os from redbot.core import Config, commands, checks +from redbot.core.utils.chat_formatting import box import xml.etree.ElementTree as ET BaseCog = getattr(commands, "Cog", object) + class Wolfram(BaseCog): - """Ask Wolfram Alpha a question.""" + """Ask Wolfram Alpha any question.""" def __init__(self, bot): self.bot = bot + self.session = aiohttp.ClientSession() default_global = {"WOLFRAM_API_KEY": None} @@ -18,21 +21,18 @@ class Wolfram(BaseCog): self.config.register_guild(**default_global) @commands.command(name="wolfram", aliases=["ask"]) - async def _wolfram(self, ctx, *arguments: str): - """ - Ask Wolfram Alpha any question. - """ + async def _wolfram(self, ctx, *question: str): + """Ask Wolfram Alpha any question.""" + api_key = await self.config.WOLFRAM_API_KEY() + if api_key: url = "http://api.wolframalpha.com/v2/query?" - query = " ".join(arguments) + query = " ".join(question) payload = {"input": query, "appid": api_key} headers = {"user-agent": "Red-cog/2.0.0"} - conn = aiohttp.TCPConnector(verify_ssl=False) - session = aiohttp.ClientSession(connector=conn) - async with session.get(url, params=payload, headers=headers) as r: + async with self.session.get(url, params=payload, headers=headers) as r: result = await r.text() - session.close() root = ET.fromstring(result) a = [] for pt in root.findall(".//plaintext"): @@ -43,17 +43,17 @@ class Wolfram(BaseCog): else: message = "\n".join(a[0:3]) else: - message = ( - "No API key set for Wolfram Alpha. Get one at http://products.wolframalpha.com/api/" - ) - await ctx.send("```{0}```".format(message)) + message = "No API key set for Wolfram Alpha. Get one at http://products.wolframalpha.com/api/" + await ctx.send(box(message)) - @commands.command(name="setwolframapi", aliases=["setwolfram"]) @checks.is_owner() + @commands.command(name="setwolframapi", aliases=["setwolfram"]) async def _setwolframapi(self, ctx, key: str): - """ - Set the api-key. - """ + """Set the api-key.""" + if key: await self.config.WOLFRAM_API_KEY.set(key) await ctx.send("Key set.") + + def __unload(self): + self.bot.loop.create_task(self.session.close())