aboutsummaryrefslogtreecommitdiffstats
path: root/bot/cogs
diff options
context:
space:
mode:
authorGravatar Leon Sandøy <[email protected]>2018-11-20 13:06:20 +0100
committerGravatar Leon Sandøy <[email protected]>2018-11-20 13:06:20 +0100
commit1bd808f4c57ccf3b918d9624eabf3524175f6369 (patch)
treef30cf13f575ac82a9732e655a78aa348120c5334 /bot/cogs
parentMerge branch 'DerpDays-master' (diff)
parentMerge pull request #67 from python-discord/dfitzpatrick-vote-monster-refactor (diff)
Merge branch 'master' of github.com:python-discord/seasonalbot
Diffstat (limited to 'bot/cogs')
-rw-r--r--bot/cogs/candy_collection.py228
-rw-r--r--bot/cogs/hacktober/halloween_facts.py2
-rw-r--r--bot/cogs/hacktober/halloweenify.py2
-rw-r--r--bot/cogs/hacktober/monstersurvey.py192
-rw-r--r--bot/cogs/resources/candy_collection.json8
5 files changed, 430 insertions, 2 deletions
diff --git a/bot/cogs/candy_collection.py b/bot/cogs/candy_collection.py
new file mode 100644
index 00000000..59eadd93
--- /dev/null
+++ b/bot/cogs/candy_collection.py
@@ -0,0 +1,228 @@
+import discord
+from discord.ext import commands
+import random
+import json
+import functools
+import os
+
+json_location = os.path.join(os.getcwd(), 'resources', 'candy_collection.json')
+
+HACKTOBER_CHANNEL_ID = 498804484324196362
+
+# chance is 1 in x range, so 1 in 20 range would give 5% chance (for add candy)
+ADD_CANDY_REACTION_CHANCE = 20 # 5%
+ADD_CANDY_EXISTING_REACTION_CHANCE = 10 # 10%
+ADD_SKULL_REACTION_CHANCE = 50 # 2%
+ADD_SKULL_EXISTING_REACTION_CHANCE = 20 # 5%
+
+
+class CandyCollection:
+ def __init__(self, bot):
+ self.bot = bot
+ with open(json_location) as candy:
+ self.candy_json = json.load(candy)
+ self.msg_reacted = self.candy_json['msg_reacted']
+ self.get_candyinfo = dict()
+ for userinfo in self.candy_json['records']:
+ userid = userinfo['userid']
+ self.get_candyinfo[userid] = userinfo
+
+ async def on_message(self, message):
+ """
+ Randomly adds candy or skull to certain messages
+ """
+
+ # make sure its a human message
+ if message.author.bot:
+ return
+ # ensure it's hacktober channel
+ if message.channel.id != HACKTOBER_CHANNEL_ID:
+ return
+
+ # do random check for skull first as it has the lower chance
+ if random.randint(1, ADD_SKULL_REACTION_CHANCE) == 1:
+ d = {"reaction": '\N{SKULL}', "msg_id": message.id, "won": False}
+ self.msg_reacted.append(d)
+ return await message.add_reaction('\N{SKULL}')
+ # check for the candy chance next
+ if random.randint(1, ADD_CANDY_REACTION_CHANCE) == 1:
+ d = {"reaction": '\N{CANDY}', "msg_id": message.id, "won": False}
+ self.msg_reacted.append(d)
+ return await message.add_reaction('\N{CANDY}')
+
+ async def on_reaction_add(self, reaction, user):
+ """
+ Add/remove candies from a person if the reaction satisfies criteria
+ """
+
+ message = reaction.message
+ # check to ensure the reactor is human
+ if user.bot:
+ return
+ # check to ensure it is in correct channel
+ if message.channel.id != HACKTOBER_CHANNEL_ID:
+ return
+
+ # if its not a candy or skull, and it is one of 10 most recent messages,
+ # proceed to add a skull/candy with higher chance
+ if str(reaction.emoji) not in ('\N{SKULL}', '\N{CANDY}'):
+ if message.id in await self.ten_recent_msg():
+ await self.reacted_msg_chance(message)
+ return
+
+ for react in self.msg_reacted:
+ # check to see if the message id of a message we added a
+ # reaction to is in json file, and if nobody has won/claimed it yet
+ if react['msg_id'] == message.id and react['won'] is False:
+ react['user_reacted'] = user.id
+ react['won'] = True
+ try:
+ # if they have record/candies in json already it will do this
+ user_records = self.get_candyinfo[user.id]
+ if str(reaction.emoji) == '\N{CANDY}':
+ user_records['record'] += 1
+ if str(reaction.emoji) == '\N{SKULL}':
+ if user_records['record'] <= 3:
+ user_records['record'] = 0
+ lost = 'all of your'
+ else:
+ lost = random.randint(1, 3)
+ user_records['record'] -= lost
+ await self.send_spook_msg(message.author, message.channel, lost)
+
+ except KeyError:
+ # otherwise it will raise KeyError so we need to add them to file
+ if str(reaction.emoji) == '\N{CANDY}':
+ print('ok')
+ d = {"userid": user.id, "record": 1}
+ self.candy_json['records'].append(d)
+ await self.remove_reactions(reaction)
+
+ async def reacted_msg_chance(self, message):
+ """
+ Randomly add a skull or candy to a message if there is a reaction there already
+ (higher probability)
+ """
+
+ if random.randint(1, ADD_SKULL_EXISTING_REACTION_CHANCE) == 1:
+ d = {"reaction": '\N{SKULL}', "msg_id": message.id, "won": False}
+ self.msg_reacted.append(d)
+ return await message.add_reaction('\N{SKULL}')
+
+ if random.randint(1, ADD_CANDY_EXISTING_REACTION_CHANCE) == 1:
+ d = {"reaction": '\N{CANDY}', "msg_id": message.id, "won": False}
+ self.msg_reacted.append(d)
+ return await message.add_reaction('\N{CANDY}')
+
+ async def ten_recent_msg(self):
+ """Get the last 10 messages sent in the channel"""
+ ten_recent = []
+ recent_msg = max(message.id for message
+ in self.bot._connection._messages
+ if message.channel.id == self.HACKTOBER_CHANNEL_ID)
+
+ channel = await self.hacktober_channel()
+ ten_recent.append(recent_msg.id)
+
+ for i in range(9):
+ o = discord.Object(id=recent_msg.id + i)
+ msg = await channel.history(limit=1, before=o).next()
+ ten_recent.append(msg.id)
+
+ return ten_recent
+
+ async def get_message(self, msg_id):
+ """
+ Get the message from it's ID.
+ """
+
+ try:
+ o = discord.Object(id=msg_id + 1)
+ # Use history rather than get_message due to
+ # poor ratelimit (50/1s vs 1/1s)
+ msg = await self.hacktober_channel.history(limit=1, before=o).next()
+
+ if msg.id != msg_id:
+ return None
+
+ return msg
+
+ except Exception:
+ return None
+
+ async def hacktober_channel(self):
+ """
+ Get #hacktoberbot channel from it's id
+ """
+ return self.bot.get_channel(id=HACKTOBER_CHANNEL_ID)
+
+ async def remove_reactions(self, reaction):
+ """
+ Remove all candy/skull reactions
+ """
+
+ try:
+ async for user in reaction.users():
+ await reaction.message.remove_reaction(reaction.emoji, user)
+
+ except discord.HTTPException:
+ pass
+
+ async def send_spook_msg(self, author, channel, candies):
+ """
+ Send a spooky message
+ """
+ e = discord.Embed(colour=author.colour)
+ e.set_author(name="Ghosts and Ghouls and Jack o' lanterns at night; "
+ f"I took {candies} candies and quickly took flight.")
+ await channel.send(embed=e)
+
+ def save_to_json(self):
+ """
+ Save json to the file.
+ """
+ with open(json_location, 'w') as outfile:
+ json.dump(self.candy_json, outfile)
+
+ @commands.command()
+ async def candy(self, ctx):
+ """
+ Get the candy leaderboard and save to json when this is called
+ """
+
+ # use run_in_executor to prevent blocking
+ thing = functools.partial(self.save_to_json)
+ await self.bot.loop.run_in_executor(None, thing)
+
+ emoji = (
+ '\N{FIRST PLACE MEDAL}',
+ '\N{SECOND PLACE MEDAL}',
+ '\N{THIRD PLACE MEDAL}',
+ '\N{SPORTS MEDAL}',
+ '\N{SPORTS MEDAL}'
+ )
+
+ top_sorted = sorted(self.candy_json['records'], key=lambda k: k.get('record', 0), reverse=True)
+ top_five = top_sorted[:5]
+
+ usersid = []
+ records = []
+ for record in top_five:
+ usersid.append(record['userid'])
+ records.append(record['record'])
+
+ value = '\n'.join(f'{emoji[index]} <@{usersid[index]}>: {records[index]}'
+ for index in range(0, len(usersid))) or 'No Candies'
+
+ e = discord.Embed(colour=discord.Colour.blurple())
+ e.add_field(name="Top Candy Records", value=value, inline=False)
+ e.add_field(name='\u200b',
+ value=f"Candies will randomly appear on messages sent. "
+ f"\nHit the candy when it appears as fast as possible to get the candy! "
+ f"\nBut beware the ghosts...",
+ inline=False)
+ await ctx.send(embed=e)
+
+
+def setup(bot):
+ bot.add_cog(CandyCollection(bot))
diff --git a/bot/cogs/hacktober/halloween_facts.py b/bot/cogs/hacktober/halloween_facts.py
index e97c80d2..304d5d94 100644
--- a/bot/cogs/hacktober/halloween_facts.py
+++ b/bot/cogs/hacktober/halloween_facts.py
@@ -26,7 +26,7 @@ class HalloweenFacts:
def __init__(self, bot):
self.bot = bot
- with open(Path("./bot/resources", "halloween_facts.json"), "r") as file:
+ with open(Path("bot", "resources", "halloween", "halloween_facts.json"), "r") as file:
self.halloween_facts = json.load(file)
self.channel = None
self.last_fact = None
diff --git a/bot/cogs/hacktober/halloweenify.py b/bot/cogs/hacktober/halloweenify.py
index a5fe45ef..9b93ac99 100644
--- a/bot/cogs/hacktober/halloweenify.py
+++ b/bot/cogs/hacktober/halloweenify.py
@@ -21,7 +21,7 @@ class Halloweenify:
"""
Change your nickname into a much spookier one!
"""
- with open(Path('./bot/resources', 'halloweenify.json'), 'r') as f:
+ with open(Path('bot', 'resources', 'halloween', 'halloweenify.json'), 'r') as f:
data = load(f)
# Choose a random character from our list we loaded above and set apart the nickname and image url.
diff --git a/bot/cogs/hacktober/monstersurvey.py b/bot/cogs/hacktober/monstersurvey.py
new file mode 100644
index 00000000..9f33e31b
--- /dev/null
+++ b/bot/cogs/hacktober/monstersurvey.py
@@ -0,0 +1,192 @@
+import json
+import logging
+import os
+from typing import Optional, Union
+
+from discord import Embed
+from discord.ext import commands
+from discord.ext.commands import Bot, Context
+
+log = logging.getLogger(__name__)
+
+EMOJIS = {
+ 'SUCCESS': u'\u2705',
+ 'ERROR': u'\u274C'
+}
+
+
+class MonsterSurvey:
+ """
+ Vote for your favorite monster!
+ This command allows users to vote for their favorite listed monster.
+ Users may change their vote, but only their current vote will be counted.
+ """
+
+ def __init__(self, bot: Bot):
+ """Initializes values for the bot to use within the voting commands."""
+ self.bot = bot
+ self.registry_location = os.path.join(os.getcwd(), 'bot', 'resources', 'halloween', 'monstersurvey.json')
+ with open(self.registry_location, 'r') as jason:
+ self.voter_registry = json.load(jason)
+
+ def json_write(self):
+ log.info("Saved Monster Survey Results")
+ with open(self.registry_location, 'w') as jason:
+ json.dump(self.voter_registry, jason, indent=2)
+
+ def cast_vote(self, id: int, monster: str):
+ """
+
+ :param id: The id of the person voting
+ :param monster: the string key of the json that represents a monster
+ :return: None
+ """
+ vr = self.voter_registry
+ for m in vr.keys():
+ if id not in vr[m]['votes'] and m == monster:
+ vr[m]['votes'].append(id)
+ else:
+ if id in vr[m]['votes'] and m != monster:
+ vr[m]['votes'].remove(id)
+
+ def get_name_by_leaderboard_index(self, n):
+ n = n - 1
+ vr = self.voter_registry
+ top = sorted(vr, key=lambda k: len(vr[k]['votes']), reverse=True)
+ name = top[n] if n >= 0 else None
+ return name
+
+ @commands.group(
+ name='monster',
+ aliases=['ms']
+ )
+ async def monster_group(self, ctx: Context):
+ """
+ The base voting command. If nothing is called, then it will return an embed.
+ """
+
+ if ctx.invoked_subcommand is None:
+ default_embed = Embed(
+ title='Monster Voting',
+ color=0xFF6800,
+ description='Vote for your favorite monster!'
+ )
+ default_embed.add_field(
+ name='.monster show monster_name(optional)',
+ value='Show a specific monster. If none is listed, it will give you an error with valid choices.',
+ inline=False)
+ default_embed.add_field(
+ name='.monster vote monster_name',
+ value='Vote for a specific monster. You get one vote, but can change it at any time.',
+ inline=False
+ )
+ default_embed.add_field(
+ name='.monster leaderboard',
+ value='Which monster has the most votes? This command will tell you.',
+ inline=False
+ )
+ default_embed.set_footer(text=f"Monsters choices are: {', '.join(self.voter_registry.keys())}")
+ return await ctx.send(embed=default_embed)
+
+ @monster_group.command(
+ name='vote'
+ )
+ async def monster_vote(self, ctx: Context, name = None):
+ """Casts a vote for a particular monster, or displays a list of monsters that can be voted for
+ if one is not given."""
+ if name is None:
+ await ctx.invoke(self.monster_leaderboard)
+ return
+ vote_embed = Embed(
+ name='Monster Voting',
+ color=0xFF6800
+ )
+ if isinstance(name, int):
+ name = self.get_name_by_leaderboard_index(name)
+ else:
+ name = name.lower()
+ m = self.voter_registry.get(name)
+ if m is None:
+
+ vote_embed.description = f'You cannot vote for {name} because it\'s not in the running.'
+ vote_embed.add_field(
+ name='Use `.monster show {monster_name}` for more information on a specific monster',
+ value='or use `.monster vote {monster}` to cast your vote for said monster.',
+ inline=False
+ )
+ vote_embed.add_field(
+ name='You may vote for or show the following monsters:',
+ value=f"{', '.join(self.voter_registry.keys())}"
+ )
+ return await ctx.send(embed=vote_embed)
+ self.cast_vote(ctx.author.id, name)
+ vote_embed.add_field(
+ name='Vote successful!',
+ value=f'You have successfully voted for {m["full_name"]}!',
+ inline=False
+ )
+ vote_embed.set_thumbnail(url=m['image'])
+ vote_embed.set_footer(text="Please note that any previous votes have been removed.")
+ self.json_write()
+ return await ctx.send(embed=vote_embed)
+
+ @monster_group.command(
+ name='show'
+ )
+ async def monster_show(self, ctx: Context, name = None):
+ """
+ Shows the named monster. If one is not named, it sends the default voting embed instead.
+ :param ctx:
+ :param name:
+ :return:
+ """
+ if name is None:
+ await ctx.invoke(self.monster_leaderboard)
+ return
+ if isinstance(name, int):
+ m = self.voter_registry.get(self.get_name_by_leaderboard_index(name))
+ else:
+ name = name.lower()
+ m = self.voter_registry.get(name)
+ if not m:
+ await ctx.send('That monster does not exist.')
+ return await ctx.invoke(self.monster_vote)
+ embed = Embed(title=m['full_name'], color=0xFF6800)
+ embed.add_field(name='Summary', value=m['summary'])
+ embed.set_image(url=m['image'])
+ embed.set_footer(text=f'To vote for this monster, type .monster vote {name}')
+ return await ctx.send(embed=embed)
+
+ @monster_group.command(
+ name='leaderboard',
+ aliases=['lb']
+ )
+ async def monster_leaderboard(self, ctx: Context):
+ """
+ Shows the current standings.
+ :param ctx:
+ :return:
+ """
+ vr = self.voter_registry
+ top = sorted(vr, key=lambda k: len(vr[k]['votes']), reverse=True)
+
+ embed = Embed(title="Monster Survey Leader Board", color=0xFF6800)
+ total_votes = sum(len(m['votes']) for m in self.voter_registry.values())
+ for rank, m in enumerate(top):
+ votes = len(vr[m]['votes'])
+ percentage = ((votes / total_votes) * 100) if total_votes > 0 else 0
+ embed.add_field(name=f"{rank+1}. {vr[m]['full_name']}",
+ value=f"{votes} votes. {percentage:.1f}% of total votes.\n"
+ f"Vote for this monster by typing "
+ f"'.monster vote {m}'\n"
+ f"Get more information on this monster by typing "
+ f"'.monster show {m}'",
+ inline=False)
+
+ embed.set_footer(text="You can also vote by their rank number. '.monster vote {number}' ")
+ await ctx.send(embed=embed)
+
+
+def setup(bot):
+ bot.add_cog(MonsterSurvey(bot))
+ log.debug("MonsterSurvey COG Loaded")
diff --git a/bot/cogs/resources/candy_collection.json b/bot/cogs/resources/candy_collection.json
new file mode 100644
index 00000000..6313dd10
--- /dev/null
+++ b/bot/cogs/resources/candy_collection.json
@@ -0,0 +1,8 @@
+{
+ "msg_reacted": [
+
+ ],
+ "records": [
+
+ ]
+}