1
0
Fork 0
mirror of synced 2024-06-03 03:04:33 +12:00

Mass update: Added comments to most of the project. A couple small tweaks made as well

This commit is contained in:
Phxntxm 2016-08-15 22:30:52 -05:00
parent d334c5584d
commit f26e04b1a4
13 changed files with 521 additions and 194 deletions

View file

@ -24,6 +24,8 @@ class Core:
async def calendar(self, month: str=None, year: int=None): async def calendar(self, month: str=None, year: int=None):
"""Provides a printout of the current month's calendar """Provides a printout of the current month's calendar
Provide month and year to print the calendar of that year and month""" Provide month and year to print the calendar of that year and month"""
# calendar takes in a number for the month, not the words, so we need this dictionary to transform the word to the number
months = { months = {
"january": 1, "january": 1,
"february": 2, "february": 2,
@ -38,6 +40,7 @@ class Core:
"november": 11, "november": 11,
"december": 12 "december": 12
} }
# In month was not passed, use the current month
if month is None: if month is None:
month = datetime.date.today().month month = datetime.date.today().month
else: else:
@ -45,8 +48,10 @@ class Core:
if month is None: if month is None:
await self.bot.say("Please provide a valid Month!") await self.bot.say("Please provide a valid Month!")
return return
# If year was not passed, use the current year
if year is None: if year is None:
year = datetime.datetime.today().year year = datetime.datetime.today().year
# Here we create the actual "text" calendar that we are printing
cal = calendar.TextCalendar().formatmonth(year, month) cal = calendar.TextCalendar().formatmonth(year, month)
await self.bot.say("```\n{}```".format(cal)) await self.bot.say("```\n{}```".format(cal))
@ -54,16 +59,20 @@ class Core:
@checks.custom_perms(send_messages=True) @checks.custom_perms(send_messages=True)
async def info(self): async def info(self):
"""This command can be used to print out some of my information""" """This command can be used to print out some of my information"""
# fmt is a dictionary so we can set the key to it's output, then print both
# The only real use of doing it this way is easier editing if the info in this command is changed
fmt = {} fmt = {}
all_members = list(self.bot.get_all_members()) all_members = list(self.bot.get_all_members())
fmt['Official Bot Server'] = "https://discord.gg/f6uzJEj"
# We can pretty safely assume that the author is going to be in at least one channel with the bot
# So find the author based on that list
authors = [] authors = []
for author_id in config.owner_ids: for author_id in config.owner_ids:
authors.append(discord.utils.get(all_members, id=author_id).name) authors.append(discord.utils.get(all_members, id=author_id).name)
fmt['Official Bot Server'] = config.dev_server
fmt['Author'] = ", ".join(authors) fmt['Author'] = ", ".join(authors)
fmt['Uptime'] = (pendulum.utcnow() - self.bot.uptime).in_words() fmt['Uptime'] = (pendulum.utcnow() - self.bot.uptime).in_words()
fmt['Total Servers'] = len(self.bot.servers) fmt['Total Servers'] = len(self.bot.servers)
@ -93,14 +102,16 @@ class Core:
perms.embed_links = True perms.embed_links = True
perms.read_message_history = True perms.read_message_history = True
perms.attach_files = True perms.attach_files = True
app_info = await self.bot.application_info()
await self.bot.say("Use this URL to add me to a server that you'd like!\n{}" await self.bot.say("Use this URL to add me to a server that you'd like!\n{}"
.format(discord.utils.oauth_url('183748889814237186', perms))) .format(discord.utils.oauth_url(app_info.id, perms)))
@commands.command(pass_context=True) @commands.command(pass_context=True)
@checks.custom_perms(send_messages=True) @checks.custom_perms(send_messages=True)
async def doggo(self, ctx): async def doggo(self, ctx):
"""Use this to print a random doggo image. """Use this to print a random doggo image.
Doggo is love, doggo is life.""" Doggo is love, doggo is life."""
# Find a random image based on how many we currently have
f = glob.glob('images/doggo*')[random.SystemRandom().randint(0, len(glob.glob('images/doggo*')) - 1)] f = glob.glob('images/doggo*')[random.SystemRandom().randint(0, len(glob.glob('images/doggo*')) - 1)]
with open(f, 'rb') as f: with open(f, 'rb') as f:
await self.bot.upload(f) await self.bot.upload(f)
@ -110,6 +121,7 @@ class Core:
async def snek(self, ctx): async def snek(self, ctx):
"""Use this to print a random snek image. """Use this to print a random snek image.
Sneks are o3o""" Sneks are o3o"""
# Find a random image based on how many we currently have
f = glob.glob('images/snek*')[random.SystemRandom().randint(0, len(glob.glob('images/snek*')) - 1)] f = glob.glob('images/snek*')[random.SystemRandom().randint(0, len(glob.glob('images/snek*')) - 1)]
with open(f, 'rb') as f: with open(f, 'rb') as f:
await self.bot.upload(f) await self.bot.upload(f)
@ -118,6 +130,7 @@ class Core:
@checks.custom_perms(send_messages=True) @checks.custom_perms(send_messages=True)
async def joke(self): async def joke(self):
"""Prints a random riddle""" """Prints a random riddle"""
# Use the fortune riddles command because it's funny, I promise
fortune_command = "/usr/bin/fortune riddles" fortune_command = "/usr/bin/fortune riddles"
fortune = subprocess.check_output(fortune_command.split()).decode("utf-8") fortune = subprocess.check_output(fortune_command.split()).decode("utf-8")
await self.bot.say(fortune) await self.bot.say(fortune)
@ -127,15 +140,19 @@ class Core:
async def roll(self, ctx, notation: str="d6"): async def roll(self, ctx, notation: str="d6"):
"""Rolls a die based on the notation given """Rolls a die based on the notation given
Format should be #d#""" Format should be #d#"""
# Use regex to get the notation based on what was provided
try: try:
# We do not want to try to convert the dice, because we want d# to be a valid notation
dice = re.search("(\d*)d(\d*)", notation).group(1) dice = re.search("(\d*)d(\d*)", notation).group(1)
num = int(re.search("(\d*)d(\d*)", notation).group(2)) num = int(re.search("(\d*)d(\d*)", notation).group(2))
# Check if something like ed3 was provided, or something else entirely was provided
except (AttributeError, ValueError): except (AttributeError, ValueError):
await self.bot.say("Please provide the die notation in #d#!") await self.bot.say("Please provide the die notation in #d#!")
return return
# Dice will be None if d# was provided, assume this means 1d# # Dice will be None if d# was provided, assume this means 1d#
dice = dice or 1 dice = dice or 1
# Since we did not try to convert to int before, do it now after we have it set
dice = int(dice) dice = int(dice)
if dice > 10: if dice > 10:
await self.bot.say("I'm not rolling more than 10 dice, I have tiny hands") await self.bot.say("I'm not rolling more than 10 dice, I have tiny hands")

View file

@ -12,14 +12,19 @@ class Game:
def __init__(self, word, creator): def __init__(self, word, creator):
self.word = word self.word = word
self.creator = creator self.creator = creator
# This converts everything but spaces to a blank
# TODO: Only convert [a-zA-Z0-9]
self.blanks = "".join(" " if letter == " " else "_" for letter in word) self.blanks = "".join(" " if letter == " " else "_" for letter in word)
self.failed_letters = [] self.failed_letters = []
self.guessed_letters = [] self.guessed_letters = []
self.fails = 0 self.fails = 0
def guess_letter(self, letter): def guess_letter(self, letter):
# No matter what, add this to guessed letters so we only have to do one check if a letter was already guessed
self.guessed_letters.append(letter) self.guessed_letters.append(letter)
if letter in self.word: if letter in self.word:
# Replace every occurence of the guessed letter, with the correct letter
# Use the one in the word instead of letter, due to capitalization
self.blanks = "".join(word_letter if letter.lower() == word_letter.lower() else self.blanks[i] for i, word_letter in enumerate(self.word)) self.blanks = "".join(word_letter if letter.lower() == word_letter.lower() else self.blanks[i] for i, word_letter in enumerate(self.word))
return True return True
else: else:
@ -43,6 +48,8 @@ class Game:
def __str__(self): def __str__(self):
# Here's our fancy formatting for the hangman picture
# Each position in the hangman picture is either a space, or part of the man, based on how many fails there are
man = " ——\n" man = " ——\n"
man += " | |\n" man += " | |\n"
man += " {} |\n".format("o" if self.fails > 0 else " ") man += " {} |\n".format("o" if self.fails > 0 else " ")
@ -52,6 +59,7 @@ class Game:
man += " |\n" man += " |\n"
man += " ———————\n" man += " ———————\n"
fmt = "```\n{}```".format(man) fmt = "```\n{}```".format(man)
# Then just add the guesses and the blanks to the string
fmt += "```\nGuesses: {}\nWord: {}```".format(", ".join(self.failed_letters), " ".join(self.blanks)) fmt += "```\nGuesses: {}\nWord: {}```".format(", ".join(self.failed_letters), " ".join(self.blanks))
return fmt return fmt
@ -61,6 +69,7 @@ class Hangman:
self.games = {} self.games = {}
def create(self, word, ctx): def create(self, word, ctx):
# Create a new game, then save it as the server's game
game = Game(word, ctx.message.author) game = Game(word, ctx.message.author)
self.games[ctx.message.server.id] = game self.games[ctx.message.server.id] = game
return game return game
@ -78,10 +87,16 @@ class Hangman:
if ctx.message.author == game.creator: if ctx.message.author == game.creator:
await self.bot.say("You can't guess at your own hangman game! :S") await self.bot.say("You can't guess at your own hangman game! :S")
return return
# Check if we are guessing a letter or a phrase. Only one letter can be guessed at a time
# So if anything more than one was provided, we're guessing at the phrase
# We're creating a fmt variable, so that we can add a message for if a guess was correct or not
# And also add a message for a loss/win
if len(guess) == 1: if len(guess) == 1:
if guess in game.guessed_letters: if guess in game.guessed_letters:
await self.bot.say("That letter has already been guessed!") await self.bot.say("That letter has already been guessed!")
# Return here as we don't want to count this as a failure
return return
if game.guess_letter(guess): if game.guess_letter(guess):
fmt = "That's correct!" fmt = "That's correct!"
@ -111,16 +126,20 @@ class Hangman:
Due to the fact that I might not be able to delete a message, I will PM you and ask for the phrase you want. Due to the fact that I might not be able to delete a message, I will PM you and ask for the phrase you want.
The phrase needs to be under 30 characters""" The phrase needs to be under 30 characters"""
# Only have one hangman game per server, since anyone in a server (except the creator) can guess towards the current game
if self.games.get(ctx.message.server.id) != None: if self.games.get(ctx.message.server.id) != None:
await self.bot.say("Sorry but only one Hangman game can be running per server!") await self.bot.say("Sorry but only one Hangman game can be running per server!")
return return
# Make sure the phrase is less than 30 characters
check = lambda m: len(m.content) < 30 check = lambda m: len(m.content) < 30
# Doing this so that while we wait for the phrase, another one cannot be started. # Doing this so that while we wait for the phrase, another one cannot be started.
self.games[ctx.message.server.id] = "placeholder" self.games[ctx.message.server.id] = "placeholder"
# We want to send this message instead of just PM'ing the creator, as some people have PM's turned off/ don't pay attention to them
await self.bot.say("I have just PM'd you {}, please respond there with the phrase you want to start a new hangman game with".format(ctx.message.author.display_name)) await self.bot.say("I have just PM'd you {}, please respond there with the phrase you want to start a new hangman game with".format(ctx.message.author.display_name))
# The only reason we save this variable, is so that we can retrieve the PrivateChannel for it, for use in our wait_for_message command
_msg = await self.bot.whisper("Please respond with the phrase you would like to use for your new hangman game\n" _msg = await self.bot.whisper("Please respond with the phrase you would like to use for your new hangman game\n"
"Please note that it must be under 30 characters long") "Please note that it must be under 30 characters long")
msg = await self.bot.wait_for_message(timeout=60.0, channel=_msg.channel, check=check) msg = await self.bot.wait_for_message(timeout=60.0, channel=_msg.channel, check=check)
@ -131,6 +150,7 @@ class Hangman:
return return
else: else:
game = self.create(msg.content, ctx) game = self.create(msg.content, ctx)
# Let them know the game has started, then print the current game so that the blanks are shown
await self.bot.say("Alright, a hangman game has just started, you can start guessing now!\n{}".format(str(game))) await self.bot.say("Alright, a hangman game has just started, you can start guessing now!\n{}".format(str(game)))
def setup(bot): def setup(bot):

View file

@ -7,36 +7,47 @@ import random
def battling_off(player_id): def battling_off(player_id):
battling = config.get_content('battling') battling = config.get_content('battling') or {}
# Create a new dictionary, exactly the way the last one was setup, but don't include any that have the player's ID provided
battling = {p1: p2 for p1, p2 in battling.items() if not p2 == player_id and not p1 == player_id} battling = {p1: p2 for p1, p2 in battling.items() if not p2 == player_id and not p1 == player_id}
config.save_content('battling', battling) config.save_content('battling', battling)
def user_battling(ctx): def user_battling(ctx, player2=None):
battling = config.get_content('battling') battling = config.get_content('battling')
# If no one is battling, obviously the user is not battling
if battling is None: if battling is None:
return False return False
# Check if the author is battling
if ctx.message.author.id in battling.values() or ctx.message.author.id in battling.keys(): if ctx.message.author.id in battling.values() or ctx.message.author.id in battling.keys():
return True return True
if str(ctx.command) == 'battle': # Check if the player2 was provided, if they are check if they're in the list
return ctx.message.mentions[0].id in battling.values() or ctx.message.mentions[0].id in battling.keys() if player2 and (player2.id in battling.values() or player2.id in battling.keys()):
return True
# If neither are found, no one is battling
return False return False
def update_battle_records(winner, loser): def update_battle_records(winner, loser):
# We're using the Harkness scale to rate
# http://opnetchessclub.wikidot.com/harkness-rating-system
battles = config.get_content('battle_records') battles = config.get_content('battle_records')
if battles is None: if battles is None:
battles = {winner.id: "1-0", loser.id: "0-1"} battles = {winner.id: "1-0", loser.id: "0-1"}
# Start ratings at 1000 if they have no rating
winner_stats = battles.get(winner.id) or {} winner_stats = battles.get(winner.id) or {}
winner_rating = winner_stats.get('rating') or 1000 winner_rating = winner_stats.get('rating') or 1000
loser_stats = battles.get(loser.id) or {} loser_stats = battles.get(loser.id) or {}
loser_rating = loser_stats.get('rating') or 1000 loser_rating = loser_stats.get('rating') or 1000
# The scale is based off of increments of 25, increasing the change by 1 for each increment
# That is all this loop does, increment the "change" for every increment of 25
# The change caps off at 300 however, so break once we are over that limit
difference = abs(winner_rating - loser_rating) difference = abs(winner_rating - loser_rating)
rating_change = 0 rating_change = 0
count = 25 count = 25
@ -45,21 +56,24 @@ def update_battle_records(winner, loser):
break break
rating_change += 1 rating_change += 1
count += 25 count += 25
# 16 is the base change, increased or decreased based on whoever has the higher current rating
if winner_rating > loser_rating: if winner_rating > loser_rating:
winner_rating += 16 - rating_change winner_rating += 16 - rating_change
loser_rating -= 16 - rating_change loser_rating -= 16 - rating_change
else: else:
winner_rating += 16 + rating_change winner_rating += 16 + rating_change
loser_rating -= 16 + rating_change loser_rating -= 16 + rating_change
# Just increase wins/losses for each person, making sure it's at least 0
winner_wins = winner_stats.get('wins') or 0 winner_wins = winner_stats.get('wins') or 0
winner_losses = winner_stats.get('losses') or 0 winner_losses = winner_stats.get('losses') or 0
loser_wins = loser_stats.get('wins') or 0 loser_wins = loser_stats.get('wins') or 0
loser_losses = loser_stats.get('losses') or 0 loser_losses = loser_stats.get('losses') or 0
winner_wins += 1 winner_wins += 1
loser_losses += 1 loser_losses += 1
# Now save the new wins, losses, and ratings
winner_stats = {'wins': winner_wins, 'losses': winner_losses, 'rating': winner_rating} winner_stats = {'wins': winner_wins, 'losses': winner_losses, 'rating': winner_rating}
loser_stats = {'wins': loser_wins, 'losses': loser_losses, 'rating': loser_rating} loser_stats = {'wins': loser_wins, 'losses': loser_losses, 'rating': loser_rating}
battles[winner.id] = winner_stats battles[winner.id] = winner_stats
@ -79,27 +93,23 @@ class Interaction:
@checks.custom_perms(send_messages=True) @checks.custom_perms(send_messages=True)
async def battle(self, ctx, player2: discord.Member): async def battle(self, ctx, player2: discord.Member):
"""Challenges the mentioned user to a battle""" """Challenges the mentioned user to a battle"""
if len(ctx.message.mentions) == 0:
await self.bot.say("You must mention someone in the room " + ctx.message.author.mention + "!")
return
if len(ctx.message.mentions) > 1:
await self.bot.say("You cannot battle more than one person at once!")
return
if ctx.message.author.id == player2.id: if ctx.message.author.id == player2.id:
await self.bot.say("Why would you want to battle yourself? Suicide is not the answer") await self.bot.say("Why would you want to battle yourself? Suicide is not the answer")
return return
if self.bot.user.id == player2.id: if self.bot.user.id == player2.id:
await self.bot.say("I always win, don't even try it.") await self.bot.say("I always win, don't even try it.")
return return
if user_battling(ctx): if user_battling(ctx, player2):
await self.bot.say("You or the person you are trying to battle is already in a battle!") await self.bot.say("You or the person you are trying to battle is already in a battle!")
return return
# Add the author and player provided in a new battle
battling = config.get_content('battling') or {} battling = config.get_content('battling') or {}
battling[ctx.message.author.id] = ctx.message.mentions[0].id battling[ctx.message.author.id] = ctx.message.mentions[0].id
config.save_content('battling', battling) config.save_content('battling', battling)
fmt = "{0.mention} has challenged you to a battle {1.mention}\n!accept or !decline" fmt = "{0.mention} has challenged you to a battle {1.mention}\n!accept or !decline"
# Add a call to turn off battling, if the battle is not accepted/declined in 3 minutes
config.loop.call_later(180, battling_off, ctx.message.author.id) config.loop.call_later(180, battling_off, ctx.message.author.id)
await self.bot.say(fmt.format(ctx.message.author, player2)) await self.bot.say(fmt.format(ctx.message.author, player2))
await self.bot.delete_message(ctx.message) await self.bot.delete_message(ctx.message)
@ -108,10 +118,13 @@ class Interaction:
@checks.custom_perms(send_messages=True) @checks.custom_perms(send_messages=True)
async def accept(self, ctx): async def accept(self, ctx):
"""Accepts the battle challenge""" """Accepts the battle challenge"""
# Ensure that the author is actually in a battle, otherwise they can't accept one
if not user_battling(ctx): if not user_battling(ctx):
await self.bot.say("You are not currently in a battle!") await self.bot.say("You are not currently in a battle!")
return return
# This is an extra check to make sure that the author is the one being BATTLED
# And not the one that started the battle
battling = config.get_content('battling') or {} battling = config.get_content('battling') or {}
p1 = [p1_id for p1_id, p2_id in battling.items() if p2_id == ctx.message.author.id] p1 = [p1_id for p1_id, p2_id in battling.items() if p2_id == ctx.message.author.id]
if len(p1) == 0: if len(p1) == 0:
@ -120,10 +133,14 @@ class Interaction:
battleP1 = discord.utils.find(lambda m: m.id == p1[0], ctx.message.server.members) battleP1 = discord.utils.find(lambda m: m.id == p1[0], ctx.message.server.members)
battleP2 = ctx.message.author battleP2 = ctx.message.author
# Get a random win message from our list
fmt = config.battleWins[random.SystemRandom().randint(0, len(config.battleWins) - 1)] fmt = config.battleWins[random.SystemRandom().randint(0, len(config.battleWins) - 1)]
# Due to our previous check, the ID should only be in the dictionary once, in the current battle we're checking
battling_off(ctx.message.author.id) battling_off(ctx.message.author.id)
# Randomize the order of who is printed/sent to the update system
# All we need to do is change what order the challengers are printed/added as a paramater
if random.SystemRandom().randint(0, 1): if random.SystemRandom().randint(0, 1):
await self.bot.say(fmt.format(battleP1.mention, battleP2.mention)) await self.bot.say(fmt.format(battleP1.mention, battleP2.mention))
update_battle_records(battleP1, battleP2) update_battle_records(battleP1, battleP2)
@ -140,15 +157,20 @@ class Interaction:
if not user_battling(ctx): if not user_battling(ctx):
await self.bot.say("You are not currently in a battle!") await self.bot.say("You are not currently in a battle!")
return return
# This is an extra check to make sure that the author is the one being BATTLED
# And not the one that started the battle
battling = config.get_content('battling') or {} battling = config.get_content('battling') or {}
p1 = [p1_id for p1_id, p2_id in battling.items() if p2_id == ctx.message.author.id] p1 = [p1_id for p1_id, p2_id in battling.items() if p2_id == ctx.message.author.id]
if len(p1) == 0: if len(p1) == 0:
await self.bot.say("You are not currently being challenged to a battle!") await self.bot.say("You are not currently being challenged to a battle!")
return return
battleP1 = discord.utils.find(lambda m: m.id == p1[0], ctx.message.server.members) battleP1 = discord.utils.find(lambda m: m.id == p1[0], ctx.message.server.members)
battleP2 = ctx.message.author battleP2 = ctx.message.author
# There's no need to update the stats for the members if they declined the battle
battling_off(ctx.message.author.id) battling_off(ctx.message.author.id)
await self.bot.say("{0} has chickened out! What a loser~".format(battleP2.mention, battleP1.mention)) await self.bot.say("{0} has chickened out! What a loser~".format(battleP2.mention, battleP1.mention))
await self.bot.delete_message(ctx.message) await self.bot.delete_message(ctx.message)
@ -158,13 +180,6 @@ class Interaction:
@checks.custom_perms(send_messages=True) @checks.custom_perms(send_messages=True)
async def boop(self, ctx, boopee: discord.Member): async def boop(self, ctx, boopee: discord.Member):
"""Boops the mentioned person""" """Boops the mentioned person"""
booper = ctx.message.author
if len(ctx.message.mentions) == 0:
await self.bot.say("You must mention someone in the room " + ctx.message.author.mention + "!")
return
if len(ctx.message.mentions) > 1:
await self.bot.say("You cannot boop more than one person at once!")
return
if boopee.id == booper.id: if boopee.id == booper.id:
await self.bot.say("You can't boop yourself! Silly...") await self.bot.say("You can't boop yourself! Silly...")
return return
@ -173,14 +188,20 @@ class Interaction:
return return
boops = config.get_content('boops') or {} boops = config.get_content('boops') or {}
# This is only used to print the amount of times they've booped someone, set to 1 for the first time someone was booped
amount = 1 amount = 1
# Get all the booped stats for the author
booper_boops = boops.get(ctx.message.author.id) booper_boops = boops.get(ctx.message.author.id)
# If the author does not exist in the dictionary, then he has never booped someone
# Create a new dictionary with the amount
if booper_boops is None: if booper_boops is None:
boops[ctx.message.author.id] = {boopee.id: 1} boops[ctx.message.author.id] = {boopee.id: 1}
# If the booper has never booped the member provided, still add that user to the dictionary with the amount of 1 to start it off
elif booper_boops.get(boopee.id) is None: elif booper_boops.get(boopee.id) is None:
booper_boops[boopee.id] = 1 booper_boops[boopee.id] = 1
boops[ctx.message.author.id] = booper_boops boops[ctx.message.author.id] = booper_boops
# Otherwise increment how many times they've booped that user
else: else:
amount = booper_boops.get(boopee.id) + 1 amount = booper_boops.get(boopee.id) + 1
booper_boops[boopee.id] = amount booper_boops[boopee.id] = amount

View file

@ -12,6 +12,7 @@ class Links:
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
# Only default headers for all requests we should use sets the User-Agent
self.headers = {"User-Agent": "Bonfire/1.0.0"} self.headers = {"User-Agent": "Bonfire/1.0.0"}
self.session = aiohttp.ClientSession() self.session = aiohttp.ClientSession()
@ -19,17 +20,23 @@ class Links:
@checks.custom_perms(send_messages=True) @checks.custom_perms(send_messages=True)
async def wiki(self, *, query: str): async def wiki(self, *, query: str):
"""Pulls the top match for a specific term, and returns the definition""" """Pulls the top match for a specific term, and returns the definition"""
# All we need to do is search for the term provided, so the action list and format never need to change
base_url = "https://en.wikipedia.org/w/api.php?action=query&list=search&format=json&srsearch=" base_url = "https://en.wikipedia.org/w/api.php?action=query&list=search&format=json&srsearch="
async with self.session.get("{}/{}".format(base_url, query), headers=self.headers) as r: async with self.session.get("{}/{}".format(base_url, query), headers=self.headers) as r:
data = await r.json() data = await r.json()
if len(data['query']['search']) == 0: if len(data['query']['search']) == 0:
await self.bot.say("I could not find any results with that term, I tried my best :c") await self.bot.say("I could not find any results with that term, I tried my best :c")
return return
# Wiki articles' URLs are in the format https://en.wikipedia.org/wiki/[Titlehere]
# Replace spaces with %20
url = "https://en.wikipedia.org/wiki/{}".format(data['query']['search'][0]['title'].replace(' ', '%20')) url = "https://en.wikipedia.org/wiki/{}".format(data['query']['search'][0]['title'].replace(' ', '%20'))
snippet = data['query']['search'][0]['snippet'] snippet = data['query']['search'][0]['snippet']
# The next part replaces some of the HTML formatting that's provided
# These are the only ones I've encountered so far through testing, there may be more though
snippet = re.sub('<span class=\\"searchmatch\\">','', snippet) snippet = re.sub('<span class=\\"searchmatch\\">','', snippet)
snippet = re.sub('</span>','',snippet) snippet = re.sub('</span>','',snippet)
snippet = re.sub('&quot;','"',snippet) snippet = re.sub('&quot;','"',snippet)
await self.bot.say("Here is the best match I found with the query `{}`:\nURL: {}\nSnippet: \n```\n{}```".format(query, url, snippet)) await self.bot.say("Here is the best match I found with the query `{}`:\nURL: {}\nSnippet: \n```\n{}```".format(query, url, snippet))
@commands.command() @commands.command()
@ -38,12 +45,14 @@ class Links:
"""Pulls the top urbandictionary.com definition for a term""" """Pulls the top urbandictionary.com definition for a term"""
url = "http://api.urbandictionary.com/v0/define?term={}".format('+'.join(msg)) url = "http://api.urbandictionary.com/v0/define?term={}".format('+'.join(msg))
async with self.session.get(url, headers=self.headers) as r: async with self.session.get(url, headers=self.headers) as r:
response = await r.text() data = await r.json()
data = json.loads(response)
# Urban dictionary has some long definitions, some might not be able to be sent
try: try:
# List is the list of definitions found, if it's empty then nothing was found
if len(data['list']) == 0: if len(data['list']) == 0:
await self.bot.say("No result with that term!") await self.bot.say("No result with that term!")
# If the list is not empty, use the first result and print it's defintion
else: else:
await self.bot.say(data['list'][0]['definition']) await self.bot.say(data['list'][0]['definition'])
except discord.HTTPException: except discord.HTTPException:
@ -57,32 +66,40 @@ class Links:
# This sets the url as url?q=search+terms # This sets the url as url?q=search+terms
url = 'https://derpibooru.org/search.json?q={}'.format('+'.join(search)) url = 'https://derpibooru.org/search.json?q={}'.format('+'.join(search))
nsfw_channels = config.get_content("nsfw_channels") or {} nsfw_channels = config.get_content("nsfw_channels") or {}
# If this is a nsfw channel, we just need to tack on 'explicit' to the terms
# Also use the custom filter that I have setup, that blocks some certain tags
# If the channel is not nsfw, we don't need to do anything, as the default filter blocks explicit
if ctx.message.channel.id in nsfw_channels: if ctx.message.channel.id in nsfw_channels:
url += ",+explicit&filter_id=95938" url += ",+explicit&filter_id=95938"
# Get the response from derpibooru and parse the 'search' result from it # Get the response from derpibooru and parse the 'search' result from it
async with self.session.get(url, headers=self.headers) as r: async with self.session.get(url, headers=self.headers) as r:
response = await r.text() data = await r.json()
data = json.loads(response)
try: try:
results = data['search'] results = data['search']
except KeyError: except KeyError:
await self.bot.say("No results with that search term, {0}!".format(ctx.message.author.mention)) await self.bot.say("No results with that search term, {0}!".format(ctx.message.author.mention))
return return
# Get the link if it exists, if not return saying no results found # Find a random image based on the first page of results.
# Currently derpibooru provides no way to change how many results can be shown on one page
# Nor anyway to see how many pages are returned by a certain query
# Due to the fact that a query may only return one page, we cannot try to check more than one as it might fail
# So this is the best that we can do at the moment
if len(results) > 0: if len(results) > 0:
index = random.SystemRandom().randint(0, len(results) - 1) index = random.SystemRandom().randint(0, len(results) - 1)
imageLink = 'http://{}'.format(results[index].get('representations').get('full')[2:].strip()) image_link = 'http://{}'.format(results[index].get('representations').get('full')[2:].strip())
else: else:
await self.bot.say("No results with that search term, {0}!".format(ctx.message.author.mention)) await self.bot.say("No results with that search term, {0}!".format(ctx.message.author.mention))
return return
else: else:
# If no search term was provided, search for a random image # If no search term was provided, search for a random image
async with self.session.get('https://derpibooru.org/images/random') as r: async with self.session.get('https://derpibooru.org/images/random') as r:
imageLink = r.url # .url will be the URl we end up at, not the one requested.
await self.bot.say(imageLink) # https://derpibooru.org/images/random redirects to a random image, so this is exactly what we want
image_link = r.url
await self.bot.say(image_link)
@commands.command(pass_context=True) @commands.command(pass_context=True)
@ -91,21 +108,27 @@ class Links:
"""Searches for a random image from e621.net """Searches for a random image from e621.net
Format for the search terms need to be 'search term 1, search term 2, etc.' Format for the search terms need to be 'search term 1, search term 2, etc.'
If the channel the command is ran in, is registered as a nsfw channel, this image will be explicit""" If the channel the command is ran in, is registered as a nsfw channel, this image will be explicit"""
# This changes the formatting for queries, so we don't have to use e621's stupid formatting when using the command
tags = tags.replace(' ', '_') tags = tags.replace(' ', '_')
tags = tags.replace(',_', '%20') tags = tags.replace(',_', '%20')
url = 'https://e621.net/post/index.json?limit=320&tags={}'.format(tags) url = 'https://e621.net/post/index.json?limit=320&tags={}'.format(tags)
# e621 provides a way to change how many images can be shown on one request
# This gives more of a chance of random results, however it causes the lookup to take longer than most
# Due to this, send a message saying we're looking up the information first
await self.bot.say("Looking up an image with those tags....") await self.bot.say("Looking up an image with those tags....")
nsfw_channels = config.get_content("nsfw_channels") or {} nsfw_channels = config.get_content("nsfw_channels") or {}
# e621 by default does not filter explicit content, so tack on safe/explicit based on if this channel is nsfw or not
if ctx.message.channel.id in nsfw_channels: if ctx.message.channel.id in nsfw_channels:
url += "%20rating:explicit" url += "%20rating:explicit"
else: else:
url += "%20rating:safe" url += "%20rating:safe"
async with self.session.get(url, headers=self.headers) as r: async with self.session.get(url, headers=self.headers) as r:
response = await r.text() data = await r.json()
data = json.loads(response) # Check if there were any results, if there are find a random image based on the length of results
if len(data) == 0: if len(data) == 0:
await self.bot.say("No results with that image {}".format(ctx.message.author.mention)) await self.bot.say("No results with that image {}".format(ctx.message.author.mention))
return return

View file

@ -19,6 +19,7 @@ class Mod:
"""This command is used to set a channel as the server's 'notifications' channel """This command is used to set a channel as the server's 'notifications' channel
Any notifications (like someone going live on Twitch, or Picarto) will go to that channel""" Any notifications (like someone going live on Twitch, or Picarto) will go to that channel"""
server_alerts = config.get_content('server_alerts') or {} server_alerts = config.get_content('server_alerts') or {}
# This will update/add the channel if an entry for this server exists or not
server_alerts[ctx.message.server.id] = channel.id server_alerts[ctx.message.server.id] = channel.id
await self.bot.say("I have just changed this server's 'notifications' channel" await self.bot.say("I have just changed this server's 'notifications' channel"
"\nAll notifications will now go to `{}`".format(channel)) "\nAll notifications will now go to `{}`".format(channel))
@ -29,6 +30,8 @@ class Mod:
"""This command can be used to set whether or not you want user notificaitons to show """This command can be used to set whether or not you want user notificaitons to show
This will save what channel you run this command in, that will be the channel used to send the notification to This will save what channel you run this command in, that will be the channel used to send the notification to
Provide on, yes, or true to set it on; otherwise it will be turned off""" Provide on, yes, or true to set it on; otherwise it will be turned off"""
# Join/Leave notifications can be kept separate from normal alerts, so we base this channel on it's own and not from alerts
# When mod logging becomes available, that will be kept to it's own channel if wanted as well
on_off = ctx.message.channel.id if re.search("(on|yes|true)", on_off.lower()) else None on_off = ctx.message.channel.id if re.search("(on|yes|true)", on_off.lower()) else None
notifications = config.get_content('user_notifications') or {} notifications = config.get_content('user_notifications') or {}
notifications[ctx.message.server.id] = on_off notifications[ctx.message.server.id] = on_off
@ -39,6 +42,7 @@ class Mod:
@commands.group(pass_context=True, no_pm=True) @commands.group(pass_context=True, no_pm=True)
async def nsfw(self, ctx): async def nsfw(self, ctx):
"""Handles adding or removing a channel as a nsfw channel""" """Handles adding or removing a channel as a nsfw channel"""
# This command isn't meant to do anything, so just send an error if an invalid subcommand is passed
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await self.bot.say('Invalid subcommand passed: {0.subcommand_passed}'.format(ctx)) await self.bot.say('Invalid subcommand passed: {0.subcommand_passed}'.format(ctx))
@ -50,6 +54,7 @@ class Mod:
if ctx.message.channel.id in nsfw_channels: if ctx.message.channel.id in nsfw_channels:
await self.bot.say("This channel is already registered as 'nsfw'!") await self.bot.say("This channel is already registered as 'nsfw'!")
else: else:
# Append instead of setting to a certain channel, so that multiple channels can be nsfw
nsfw_channels.append(ctx.message.channel.id) nsfw_channels.append(ctx.message.channel.id)
config.save_content('nsfw_channels', nsfw_channels) config.save_content('nsfw_channels', nsfw_channels)
await self.bot.say("This channel has just been registered as 'nsfw'! Have fun you naughties ;)") await self.bot.say("This channel has just been registered as 'nsfw'! Have fun you naughties ;)")
@ -93,6 +98,9 @@ class Mod:
if perms_value is None: if perms_value is None:
await self.bot.say("That command has no custom permissions setup on it!") await self.bot.say("That command has no custom permissions setup on it!")
else: else:
# Permissions are saved as bit values, so create an object based on that value
# Then check which permission is true, that is our required permission
# There's no need to check for errors here, as we ensure a permission is valid when adding it
permissions = discord.Permissions(perms_value) permissions = discord.Permissions(perms_value)
needed_perm = [perm[0] for perm in permissions if perm[1]][0] needed_perm = [perm[0] for perm in permissions if perm[1]][0]
await self.bot.say("You need to have the permission `{}` " await self.bot.say("You need to have the permission `{}` "
@ -104,18 +112,24 @@ class Mod:
"""Sets up custom permissions on the provided command """Sets up custom permissions on the provided command
Format must be 'perms add <command> <permission>' Format must be 'perms add <command> <permission>'
If you want to open the command to everyone, provide 'none' as the permission""" If you want to open the command to everyone, provide 'none' as the permission"""
# Since subcommands exist, base the last word in the list as the permission, and the rest of it as the command
command = " ".join(msg[0:len(msg) - 1]) command = " ".join(msg[0:len(msg) - 1])
permissions = msg[len(msg) - 1] permissions = msg[len(msg) - 1]
# If a user can run the command, they have to have send_messages permissions; so use this as the base # If a user can run a command, they have to have send_messages permissions; so use this as the base
if permissions.lower() == "none": if permissions.lower() == "none":
permissions = "send_messages" permissions = "send_messages"
# Convert the string to an int value of the permissions obj, based on the required permission # Convert the string to an int value of the permissions object, based on the required permission
perm_obj = discord.Permissions.none() perm_obj = discord.Permissions.none()
setattr(perm_obj, permissions, True) setattr(perm_obj, permissions, True)
perm_value = perm_obj.value perm_value = perm_obj.value
# This next loop ensures the command given is valid. We need to loop through commands, as self.bot.commands only includes parent commands
# So we are splitting the command in parts, looping through the commands and getting the subcommand based on the next part
# If we try to access commands of a command that isn't a group, we'll hit an AttributeError, meaning an invalid command was given
# If we loop through and don't find anything, cmd will still be None, and we'll report an invalid was given as well
cmd = None cmd = None
for part in msg[0:len(msg) - 1]: for part in msg[0:len(msg) - 1]:
try: try:
@ -124,13 +138,17 @@ class Mod:
else: else:
cmd = cmd.commands.get(part) cmd = cmd.commands.get(part)
except AttributeError: except AttributeError:
cmd = None
break break
if cmd is None: if cmd is None:
await self.bot.say( await self.bot.say(
"That command does not exist! You can't have custom permissions on a non-existant command....") "That command does not exist! You can't have custom permissions on a non-existant command....")
return return
# Two cases I use should never have custom permissions setup on them, is_owner for obvious reasons
# The other case is if I'm using the default has_permissions case, which means I do not want to check custom permissions at all
# Currently the second case is only on adding and removing permissions, to avoid abuse on these
for check in cmd.checks: for check in cmd.checks:
if "is_owner" == check.__name__ or re.search("has_permissions", str(check)) is not None: if "is_owner" == check.__name__ or re.search("has_permissions", str(check)) is not None:
await self.bot.say("This command cannot have custom permissions setup!") await self.bot.say("This command cannot have custom permissions setup!")
@ -144,6 +162,7 @@ class Mod:
custom_perms = config.get_content('custom_permissions') or {} custom_perms = config.get_content('custom_permissions') or {}
server_perms = custom_perms.get(ctx.message.server.id) or {} server_perms = custom_perms.get(ctx.message.server.id) or {}
# Save the qualified name, so that we don't get screwed up by aliases
server_perms[cmd.qualified_name] = perm_value server_perms[cmd.qualified_name] = perm_value
custom_perms[ctx.message.server.id] = server_perms custom_perms[ctx.message.server.id] = server_perms
@ -154,17 +173,35 @@ class Mod:
@perms.command(name="remove", aliases=["delete"], pass_context=True, no_pm=True) @perms.command(name="remove", aliases=["delete"], pass_context=True, no_pm=True)
@commands.has_permissions(manage_server=True) @commands.has_permissions(manage_server=True)
async def remove_perms(self, ctx, *command: str): async def remove_perms(self, ctx, *command: str):
"""Removes the custom permissions setup on the command specified""" """Removes the custom permissions setup on the command specified"""
cmd = " ".join(command)
custom_perms = config.get_content('custom_permissions') or {} custom_perms = config.get_content('custom_permissions') or {}
server_perms = custom_perms.get(ctx.message.server.id) or {} server_perms = custom_perms.get(ctx.message.server.id) or {}
if server_perms is None: if server_perms is None:
await self.bot.say("There are no custom permissions setup on this server yet!") await self.bot.say("There are no custom permissions setup on this server yet!")
return return
command_perms = server_perms.get(cmd)
if command_perms is None: cmd = None
await self.bot.say("You do not have custom permissions setup on this command yet!") # This is the same loop as the add command, we need this to get the command object so we can get the qualified_name
for part in msg[0:len(msg) - 1]:
try:
if cmd is None:
cmd = self.bot.commands.get(part)
else:
cmd = cmd.commands.get(part)
except AttributeError:
cmd = None
break
if cmd is None:
await self.bot.say(
"That command does not exist! You can't have custom permissions on a non-existant command....")
return return
command_perms = server_perms.get(cmd.qualified_name)
if command_perms is None:
await self.bot.say("You do not have custom permissions setup on this command!")
return
del custom_perms[ctx.message.server.id][cmd] del custom_perms[ctx.message.server.id][cmd]
config.save_content('custom_permissions', custom_perms) config.save_content('custom_permissions', custom_perms)
await self.bot.say("I have just removed the custom permissions for {}!".format(cmd)) await self.bot.say("I have just removed the custom permissions for {}!".format(cmd))
@ -184,6 +221,7 @@ class Mod:
if server_rules is None or len(server_rules) == 0: if server_rules is None or len(server_rules) == 0:
await self.bot.say("This server currently has no rules on it! I see you like to live dangerously...") await self.bot.say("This server currently has no rules on it! I see you like to live dangerously...")
return return
# Enumerate the list, so that we can print the number and the rule for each rule
fmt = "\n".join("{}) {}".format(num + 1, rule) for num, rule in enumerate(server_rules)) fmt = "\n".join("{}) {}".format(num + 1, rule) for num, rule in enumerate(server_rules))
await self.bot.say('```\n{}```'.format(fmt)) await self.bot.say('```\n{}```'.format(fmt))
@ -191,6 +229,7 @@ class Mod:
@checks.custom_perms(manage_server=True) @checks.custom_perms(manage_server=True)
async def rules_add(self, ctx, *, rule: str): async def rules_add(self, ctx, *, rule: str):
"""Adds a rule to this server's rules""" """Adds a rule to this server's rules"""
# Nothing fancy here, just get the rules, append the rule, and save it
rules = config.get_content('rules') or {} rules = config.get_content('rules') or {}
server_rules = rules.get(ctx.message.server.id) or [] server_rules = rules.get(ctx.message.server.id) or []
server_rules.append(rule) server_rules.append(rule)
@ -210,12 +249,16 @@ class Mod:
await self.bot.say( await self.bot.say(
"This server currently has no rules on it! Can't remove something that doesn't exist bro") "This server currently has no rules on it! Can't remove something that doesn't exist bro")
return return
# Get the list of rules so that we can print it if no number was provided
# Since this is a list and not a dictionary, order is preserved, and we just need the number of the rule
list_rules = "\n".join("{}) {}".format(num + 1, rule) for num, rule in enumerate(server_rules)) list_rules = "\n".join("{}) {}".format(num + 1, rule) for num, rule in enumerate(server_rules))
if rule is None: if rule is None:
await self.bot.say("Your rules are:\n```\n{}```Please provide the rule number" await self.bot.say("Your rules are:\n```\n{}```Please provide the rule number"
"you would like to remove (just the number)".format(list_rules)) "you would like to remove (just the number)".format(list_rules))
# All we need for the check is to ensure that the content is just a digit, that is all we need
msg = await self.bot.wait_for_message(timeout=60.0, author=ctx.message.author, channel=ctx.message.channel, msg = await self.bot.wait_for_message(timeout=60.0, author=ctx.message.author, channel=ctx.message.channel,
check=lambda m: m.content.isdigit()) check=lambda m: m.content.isdigit())
if msg is None: if msg is None:
@ -224,7 +267,8 @@ class Mod:
del server_rules[int(msg.content) - 1] del server_rules[int(msg.content) - 1]
rules[ctx.message.server.id] = server_rules rules[ctx.message.server.id] = server_rules
config.save_content('rules', rules) config.save_content('rules', rules)
# This check is just to ensure a number was provided within the list's range
try: try:
del server_rules[rule - 1] del server_rules[rule - 1]
rules[ctx.message.server.id] = server_rules rules[ctx.message.server.id] = server_rules

View file

@ -7,6 +7,10 @@ import aiohttp
import json import json
base_url = "https://api.owapi.net/api/v2/u/" base_url = "https://api.owapi.net/api/v2/u/"
# This is a list of the possible things that we may want to retrieve from the stats
# The API returns something if it exists, and leaves it out of the data returned entirely if it does not
# For example if you have not win with a character, wins will not exist in the list
# This sets an easy way to use list comprehension later, to print all possible things we want, if it exists
check_g_stats = ["eliminations", "deaths", 'kpd', 'wins', 'losses', 'time_played', check_g_stats = ["eliminations", "deaths", 'kpd', 'wins', 'losses', 'time_played',
'cards', 'damage_done', 'healing_done', 'multikills'] 'cards', 'damage_done', 'healing_done', 'multikills']
check_o_stats = ['wins', 'losses'] check_o_stats = ['wins', 'losses']
@ -17,6 +21,8 @@ class Overwatch:
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.headers = {"User-Agent": "Bonfire/1.0.0"}
self.session = aiohttp.ClientSession()
@commands.group(no_pm=True) @commands.group(no_pm=True)
async def ow(self): async def ow(self):
@ -40,41 +46,46 @@ class Overwatch:
if bt is None: if bt is None:
await self.bot.say("I do not have this user's battletag saved!") await self.bot.say("I do not have this user's battletag saved!")
return return
# This API sometimes takes a while to look up information, so send a message saying we're processing
await self.bot.say("Searching profile information....") await self.bot.say("Searching profile information....")
if hero == "": if hero == "":
with aiohttp.ClientSession(headers={"User-Agent": "Bonfire/1.0.0"}) as s: # If no hero was provided, we just want the base stats for a player
async with s.get(base_url + "{}/stats/general".format(bt)) as r: async with self.session.get(base_url + "{}/stats/general".format(bt), headers=self.headers) as r:
result = await r.text() data = await r.json()
data = json.loads(result) # Here is our list comprehension to get what kind of data we want.
fmt = "\n".join("{}: {}".format(i, r) for i, r in data['game_stats'].items() if i in check_g_stats) fmt = "\n".join("{}: {}".format(i, r) for i, r in data['game_stats'].items() if i in check_g_stats)
fmt += "\n" fmt += "\n"
fmt += "\n".join("{}: {}".format(i, r) for i, r in data['overall_stats'].items() if i in check_o_stats) fmt += "\n".join("{}: {}".format(i, r) for i, r in data['overall_stats'].items() if i in check_o_stats)
# title and replace are used to format things nicely, while not having to have information for every piece of data
await self.bot.say( await self.bot.say(
"Overwatch stats for {}: ```py\n{}```".format(user.name, fmt.title().replace("_", " "))) "Overwatch stats for {}: ```py\n{}```".format(user.name, fmt.title().replace("_", " ")))
else: else:
# If there was a hero provided, search for a user's data on that hero
url = base_url + "{}/heroes/{}".format(bt, hero.lower().replace('-', '')) url = base_url + "{}/heroes/{}".format(bt, hero.lower().replace('-', ''))
with aiohttp.ClientSession(headers={"User-Agent": "Bonfire/1.0.0"}) as s: async with self.session.get(url, headers=self.headers) as r:
async with s.get(url) as r: data = await r.json()
result = await r.text() msg = data.get('msg')
msg = json.loads(result).get('msg') # Check if a user has not used the hero provided before
if msg == 'hero data not found': if msg == 'hero data not found':
fmt = "{} has not used the hero {} before!".format(user.name, hero.title()) fmt = "{} has not used the hero {} before!".format(user.name, hero.title())
await self.bot.say(fmt) await self.bot.say(fmt)
return return
elif msg == 'bad hero name': # Check if a hero that doesn't exist was provided
fmt = "{} is not an actual hero!".format(hero.title()) elif msg == 'bad hero name':
await self.bot.say(fmt) fmt = "{} is not an actual hero!".format(hero.title())
return await self.bot.say(fmt)
result = await r.text() return
data = json.loads(result)
# Same list comprehension as before
fmt = "\n".join("{}: {}".format(i, r) for i, r in data['general_stats'].items() if i in check_g_stats) fmt = "\n".join("{}: {}".format(i, r) for i, r in data['general_stats'].items() if i in check_g_stats)
# Someone was complaining there was no KDR provided, so I made one myself and added that to the list
if data['general_stats'].get('eliminations') and data['general_stats'].get('deaths'): if data['general_stats'].get('eliminations') and data['general_stats'].get('deaths'):
fmt += "\nKill Death Ratio: {0:.2f}".format(data['general_stats'].get('eliminations')/data['general_stats'].get('deaths')) fmt += "\nKill Death Ratio: {0:.2f}".format(data['general_stats'].get('eliminations')/data['general_stats'].get('deaths'))
fmt += "\n" fmt += "\n"
fmt += "\n".join("{}: {}".format(i, r) for i, r in data['hero_stats'].items()) fmt += "\n".join("{}: {}".format(i, r) for i, r in data['hero_stats'].items())
# Same formatting as above
await self.bot.say("Overwatch stats for {} using the hero {}: ```py\n{}``` " await self.bot.say("Overwatch stats for {} using the hero {}: ```py\n{}``` "
.format(user.name, hero.title(), fmt.title().replace("_", " "))) .format(user.name, hero.title(), fmt.title().replace("_", " ")))
@ -82,17 +93,23 @@ class Overwatch:
@checks.custom_perms(send_messages=True) @checks.custom_perms(send_messages=True)
async def add(self, ctx, bt: str): async def add(self, ctx, bt: str):
"""Saves your battletag for looking up information""" """Saves your battletag for looking up information"""
# Battletags are normally provided like name#id
# However the API needs this to be a -, so repliace # with - if it exists
bt = bt.replace("#", "-") bt = bt.replace("#", "-")
# This API sometimes takes a while to look up information, so send a message saying we're processing
await self.bot.say("Looking up your profile information....") await self.bot.say("Looking up your profile information....")
url = base_url + "{}/stats/general".format(bt) url = base_url + "{}/stats/general".format(bt)
with aiohttp.ClientSession(headers={"User-Agent": "Bonfire/1.0.0"}) as s: # All we're doing here is ensuring that the status is 200 when looking up someone's general information
async with s.get(url) as r: # If it's not, let them know exactly how to format their tag
if not r.status == 200: async with self.session.get(url, headers=self.headers) as r:
await self.bot.say("Profile does not exist! Battletags are picky, " if not r.status == 200:
"format needs to be `user#xxxx`. Capitalization matters") await self.bot.say("Profile does not exist! Battletags are picky, "
return "format needs to be `user#xxxx`. Capitalization matters")
return
# Now just save the battletag
ow = config.get_content('overwatch') or {} ow = config.get_content('overwatch') or {}
ow[ctx.message.author.id] = bt ow[ctx.message.author.id] = bt
await self.bot.say("I have just saved your battletag {}".format(ctx.message.author.mention)) await self.bot.say("I have just saved your battletag {}".format(ctx.message.author.mention))

View file

@ -24,18 +24,18 @@ class Owner:
async def saferestart(self, ctx): async def saferestart(self, ctx):
"""This commands is used to check if there is anything playing in any servers at the moment """This commands is used to check if there is anything playing in any servers at the moment
If there is, I'll tell you not to restart, if not I'll just go ahead and restart""" If there is, I'll tell you not to restart, if not I'll just go ahead and restart"""
voice_states = self.bot.get_cog('Music').voice_states # I do not want to restart the bot if someone is playing music
for server_id, state in voice_states.items(): # This gets all the exiting VoiceStates that are playing music right now
if state.is_playing: # If we are, say which server it
server = self.bot.get_server(server_id) servers_playing_music = [server_id for server_id, state in self.bot.get_cog('Music').voice_states.items() if state.is_playing()]
await self.bot.say("Sorry, it's not safe to restart. I am currently playing a song on the {} server".format(server.name)) await self.bot.say("Sorry, it's not safe to restart. I am currently playing a song on {} servers".format(len(servers_playing_music)))
return
ctx.invoke(self.bot.commands.get('restart')) ctx.invoke(self.bot.commands.get('restart'))
@commands.command(pass_context=True) @commands.command(pass_context=True)
@commands.check(checks.is_owner) @commands.check(checks.is_owner)
async def restart(self, ctx): async def restart(self, ctx):
"""Forces the bot to restart""" """Forces the bot to restart"""
# This command is left in so that we can invoke it from saferestart, or we need a restart no matter what
config.save_content('restart_server', ctx.message.channel.id) config.save_content('restart_server', ctx.message.channel.id)
await self.bot.say("Restarting; see you in the next life {0}!".format(ctx.message.author.mention)) await self.bot.say("Restarting; see you in the next life {0}!".format(ctx.message.author.mention))
python = sys.executable python = sys.executable
@ -45,42 +45,56 @@ class Owner:
@commands.check(checks.is_owner) @commands.check(checks.is_owner)
async def adddoggo(self, url: str): async def adddoggo(self, url: str):
"""Saves a URL as an image to add for the doggo command""" """Saves a URL as an image to add for the doggo command"""
local_path = 'images/doggo{}.jpg'.format(len(glob.glob('doggo*'))) # Save the local path based on how many images there currently are
local_path = 'images/doggo{}.jpg'.format(len(glob.glob('images/doggo*')))
# "read" the image and save as bytes
with aiohttp.ClientSession() as s: with aiohttp.ClientSession() as s:
async with s.get(url) as r: async with s.get(url) as r:
val = await r.read() val = await r.read()
with open(local_path, "wb") as f: with open(local_path, "wb") as f:
f.write(val) f.write(val)
await self.bot.say( await self.bot.say(
"Just saved a new doggo image! I now have {} doggo images!".format(len(glob.glob('doggo*')))) "Just saved a new doggo image! I now have {} doggo images!".format(len(glob.glob('images/doggo*'))))
@commands.command() @commands.command()
@commands.check(checks.is_owner) @commands.check(checks.is_owner)
async def addsnek(self, url: str): async def addsnek(self, url: str):
"""Saves a URL as an image to add for the snek command""" """Saves a URL as an image to add for the snek command"""
local_path = 'images/snek{}.jpg'.format(len(glob.glob('snek*'))) # Save the local path based on how many images there currently are
local_path = 'images/snek{}.jpg'.format(len(glob.glob('images/snek*')))
# "read" the image and save as bytes
with aiohttp.ClientSession() as s: with aiohttp.ClientSession() as s:
async with s.get(url) as r: async with s.get(url) as r:
val = await r.read() val = await r.read()
with open(local_path, "wb") as f: with open(local_path, "wb") as f:
f.write(val) f.write(val)
await self.bot.say( await self.bot.say(
"Just saved a new snek image! I now have {} snek images!".format(len(glob.glob('snek*')))) "Just saved a new snek image! I now have {} snek images!".format(len(glob.glob('images/snek*'))))
@commands.command(pass_context=True) @commands.command(pass_context=True)
@commands.check(checks.is_owner) @commands.check(checks.is_owner)
async def debug(self, ctx): async def debug(self, ctx):
"""Executes code""" """Executes code"""
# Eval and exec have different useful purposes, so use both
try: try:
# `Get all content in this format`
match_single = getter.findall(ctx.message.content) match_single = getter.findall(ctx.message.content)
# ```\nGet all content in this format```
match_multi = multi.findall(ctx.message.content) match_multi = multi.findall(ctx.message.content)
if not match_multi:
if match_single:
result = eval(match_single[0]) result = eval(match_single[0])
# In case the result needs to be awaited, handle that
if inspect.isawaitable(result): if inspect.isawaitable(result):
result = await result result = await result
await self.bot.say("```\n{0}```".format(result)) await self.bot.say("```\n{0}```".format(result))
elif match_multi: elif match_multi:
# Internal method to send the message to the channel, of whatever is passed
def r(v): def r(v):
self.bot.loop.create_task(self.bot.say("```\n{}```".format(v))) self.bot.loop.create_task(self.bot.say("```\n{}```".format(v)))
exec(match_multi[0]) exec(match_multi[0])
@ -114,20 +128,22 @@ class Owner:
@commands.command() @commands.command()
@commands.check(checks.is_owner) @commands.check(checks.is_owner)
async def status(self, *stat: str): async def status(self, *, status: str):
"""Changes the bot's 'playing' status""" """Changes the bot's 'playing' status"""
newStatus = ' '.join(stat) await self.bot.change_status(discord.Game(name=status, type=0))
game = discord.Game(name=newStatus, type=0)
await self.bot.change_status(game)
await self.bot.say("Just changed my status to '{0}'!".format(newStatus)) await self.bot.say("Just changed my status to '{0}'!".format(newStatus))
@commands.command() @commands.command()
@commands.check(checks.is_owner) @commands.check(checks.is_owner)
async def load(self, *, module: str): async def load(self, *, module: str):
"""Loads a module""" """Loads a module"""
# Do this because I'm too lazy to type cogs.module
module = module.lower() module = module.lower()
if not module.startswith("cogs"): if not module.startswith("cogs"):
module = "cogs.{}".format(module) module = "cogs.{}".format(module)
# This try catch will catch errors such as syntax errors in the module we are loading
try: try:
self.bot.load_extension(module) self.bot.load_extension(module)
await self.bot.say("I have just loaded the {} module".format(module)) await self.bot.say("I have just loaded the {} module".format(module))
@ -139,24 +155,27 @@ class Owner:
@commands.check(checks.is_owner) @commands.check(checks.is_owner)
async def unload(self, *, module: str): async def unload(self, *, module: str):
"""Unloads a module""" """Unloads a module"""
# Do this because I'm too lazy to type cogs.module
module = module.lower() module = module.lower()
if not module.startswith("cogs"): if not module.startswith("cogs"):
module = "cogs.{}".format(module) module = "cogs.{}".format(module)
try:
self.bot.unload_extension(module) self.bot.unload_extension(module)
await self.bot.say("I have just unloaded the {} module".format(module)) await self.bot.say("I have just unloaded the {} module".format(module))
except Exception as error:
fmt = 'An error occurred while processing this request: ```py\n{}: {}\n```'
await self.bot.say(fmt.format(type(error).__name__, error))
@commands.command() @commands.command()
@commands.check(checks.is_owner) @commands.check(checks.is_owner)
async def reload(self, *, module: str): async def reload(self, *, module: str):
"""Reloads a module""" """Reloads a module"""
# Do this because I'm too lazy to type cogs.module
module = module.lower() module = module.lower()
if not module.startswith("cogs"): if not module.startswith("cogs"):
module = "cogs.{}".format(module) module = "cogs.{}".format(module)
self.bot.unload_extension(module) self.bot.unload_extension(module)
# This try block will catch errors such as syntax errors in the module we are loading
try: try:
self.bot.load_extension(module) self.bot.load_extension(module)
await self.bot.say("I have just reloaded the {} module".format(module)) await self.bot.say("I have just reloaded the {} module".format(module))

View file

@ -16,54 +16,73 @@ key = '03e26294-b793-11e5-9a41-005056984bd4'
async def online_users(): async def online_users():
try: try:
# Someone from picarto contacted me and told me their database queries are odd
# It is more efficent on their end to make a query for all online users, and base checks off that
# In place of requesting for /channel and checking if that is online currently, for each channel
# This method is in place to just return all online_users
url = '{}/online/all?key={}'.format(base_url, key) url = '{}/online/all?key={}'.format(base_url, key)
with aiohttp.ClientSession(headers={"User-Agent": "Bonfire/1.0.0"}) as s: with aiohttp.ClientSession(headers={"User-Agent": "Bonfire/1.0.0"}) as s:
async with s.get(url) as r: async with s.get(url) as r:
response = await r.text() return await r.json()
return json.loads(response)
except: except:
return {} return {}
def check_online(online_channels, channel): def check_online(online_channels, channel):
# online_channels is the dictionary of all users online currently, and channel is the name we are checking against that
# This creates a list of all users that match this channel name (should only ever be 1) and returns True as long as it is more than 0
matches = [stream for stream in online_channels if stream['channel_name'].lower() == channel.lower()] matches = [stream for stream in online_channels if stream['channel_name'].lower() == channel.lower()]
return len(matches) > 0 return len(matches) > 0
class Picarto: class Picarto:
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.headers = {"User-Agent": "Bonfire/1.0.0"}
self.session = aiohttp.ClientSession()
async def check_channels(self): async def check_channels(self):
await self.bot.wait_until_ready() await self.bot.wait_until_ready()
# This is a loop that runs every 30 seconds, checking if anyone has gone online
while not self.bot.is_closed: while not self.bot.is_closed:
picarto = config.get_content('picarto') or {} picarto = config.get_content('picarto') or {}
# Get all online users before looping, so that only one request is needed
online_users_list = await online_users() online_users_list = await online_users()
for m_id, r in picarto.items(): for m_id, r in picarto.items():
url = r['picarto_url'] url = r['picarto_url']
# This is whether they are detected as live in the saved file
live = r['live'] live = r['live']
notify = r['notifications_on'] notify = r['notifications_on']
user = re.search("(?<=picarto.tv/)(.*)", url).group(1) user = re.search("(?<=picarto.tv/)(.*)", url).group(1)
# This is whether or not they are actually online
online = check_online(online_users_list, user) online = check_online(online_users_list, user)
# If they're set to notify, not live in the config file, but online currently, means they went online since the last check
if not live and notify and online: if not live and notify and online:
for server_id in r['servers'].items(): for server_id in r['servers']:
# Get the channel to send the message to, based on the saved alert's channel
server = self.bot.get_server(server_id) server = self.bot.get_server(server_id)
server_alerts = config.get_content('server_alerts') or {} server_alerts = config.get_content('server_alerts') or {}
channel_id = server_alerts.get(server_id) or server_id channel_id = server_alerts.get(server_id) or server_id
channel = self.bot.get_channel(channel_id) channel = self.bot.get_channel(channel_id)
member = discord.utils.find(lambda m: m.id == m_id, server.members) # Get the member that has just gone live
member = discord.utils.get(server.members, id=m_id)
# Set them as live in the configuration file, and send a message saying they have just gone live
picarto[m_id]['live'] = 1 picarto[m_id]['live'] = 1
fmt = "{} has just gone live! View their stream at {}".format(member.display_name, url) fmt = "{} has just gone live! View their stream at {}".format(member.display_name, url)
config.save_content('picarto', picarto) config.save_content('picarto', picarto)
await self.bot.send_message(channel, fmt) await self.bot.send_message(channel, fmt)
# If they're live in the configuration file, but not online currently, means they went offline since the last check
elif live and not online: elif live and not online:
for server_id, channel_id in r['servers'].items(): for server_id in r['servers']:
# Get the channel to send the message to, based on the saved alert's channel
server = self.bot.get_server(server_id) server = self.bot.get_server(server_id)
server_alerts = config.get_content('server_alerts') or {} server_alerts = config.get_content('server_alerts') or {}
channel_id = server_alerts.get(server_id) or server_id channel_id = server_alerts.get(server_id) or server_id
channel = self.bot.get_channel(channel_id) channel = self.bot.get_channel(channel_id)
member = discord.utils.find(lambda m: m.id == m_id, server.members) # Get the member that has just gone live
member = discord.utils.get(server.members, id=m_id)
# Set them as offline in the confugration, then send a message letting the channel know they've just gone offline
picarto[m_id]['live'] = 0 picarto[m_id]['live'] = 0
fmt = "{} has just gone offline! Catch them next time they stream at {}".format( fmt = "{} has just gone offline! Catch them next time they stream at {}".format(
member.display_name, member.display_name,
@ -76,25 +95,29 @@ class Picarto:
@checks.custom_perms(send_messages=True) @checks.custom_perms(send_messages=True)
async def picarto(self, ctx, member: discord.Member = None): async def picarto(self, ctx, member: discord.Member = None):
"""This command can be used to view Picarto stats about a certain member""" """This command can be used to view Picarto stats about a certain member"""
# If member is not given, base information on the author
member = member or ctx.message.author member = member or ctx.message.author
picarto_urls = config.get_content('picarto') or {} picarto_urls = config.get_content('picarto') or {}
member_url = picarto_urls.get(member.id) try:
if not member_url: member_url = picarto_urls.get(member.id)['picarto_url']
except:
await self.bot.say("That user does not have a picarto url setup!") await self.bot.say("That user does not have a picarto url setup!")
return return
member_url = member_url['picarto_url']
# Use regex to get the actual username so that we can make a request to the API
stream = re.search("(?<=picarto.tv/)(.*)", member_url).group(1) stream = re.search("(?<=picarto.tv/)(.*)", member_url).group(1)
url = '{}/channel/{}?key={}'.format(base_url, stream, key) url = '{}/channel/{}?key={}'.format(base_url, stream, key)
with aiohttp.ClientSession(headers={"User-Agent": "Bonfire/1.0.0"}) as s: async with self.session.get(url, headers=self.headers) as r:
async with s.get(url) as r: data = await r.json()
response = await r.text()
# Not everyone has all these settings, so use this as a way to print information if it does, otherwise ignore it
data = json.loads(response)
things_to_print = ['channel', 'commissions_enabled', 'is_nsfw', 'program', 'tablet', 'followers', things_to_print = ['channel', 'commissions_enabled', 'is_nsfw', 'program', 'tablet', 'followers',
'content_type'] 'content_type']
# Using title and replace to provide a nice way to print the data
fmt = "\n".join( fmt = "\n".join(
"{}: {}".format(i.title().replace("_", " "), r) for i, r in data.items() if i in things_to_print) "{}: {}".format(i.title().replace("_", " "), r) for i, r in data.items() if i in things_to_print)
# Social URL's can be given if a user wants them to show, print them if they exist, otherwise don't try to include them
social_links = data.get('social_urls') social_links = data.get('social_urls')
if social_links: if social_links:
fmt2 = "\n".join("\t{}: {}".format(i.title().replace("_", " "), r) for i, r in social_links.items()) fmt2 = "\n".join("\t{}: {}".format(i.title().replace("_", " "), r) for i, r in social_links.items())
@ -105,6 +128,13 @@ class Picarto:
@checks.custom_perms(send_messages=True) @checks.custom_perms(send_messages=True)
async def add_picarto_url(self, ctx, url: str): async def add_picarto_url(self, ctx, url: str):
"""Saves your user's picarto URL""" """Saves your user's picarto URL"""
# This uses a lookbehind to check if picarto.tv exists in the url given
# If it does, it matches picarto.tv/user and sets the url as that
# Then (in the else) add https://www. to that
# Otherwise if it doesn't match, we'll hit an AttributeError due to .group(0)
# This means that the url was just given as a user (or something complete invalid)
# So set URL as https://www.picarto.tv/[url]
# Even if this was invalid such as https://www.picarto.tv/twitch.tv/user for example, our next check handles that
try: try:
url = re.search("((?<=://)?picarto.tv/)+(.*)", url).group(0) url = re.search("((?<=://)?picarto.tv/)+(.*)", url).group(0)
except AttributeError: except AttributeError:
@ -113,26 +143,28 @@ class Picarto:
url = "https://www.{}".format(url) url = "https://www.{}".format(url)
api_url = '{}/channel/{}?key={}'.format(base_url, re.search("https://www.picarto.tv/(.*)", url).group(1), key) api_url = '{}/channel/{}?key={}'.format(base_url, re.search("https://www.picarto.tv/(.*)", url).group(1), key)
with aiohttp.ClientSession() as s: # Check if we can find a user with the provided information, if we can't just return
async with s.get(api_url) as r: async with self.session.get(api_url, headers=self.headers) as r:
if not r.status == 200: if not r.status == 200:
await self.bot.say("That Picarto user does not exist! " await self.bot.say("That Picarto user does not exist! "
"What would be the point of adding a nonexistant Picarto user? Silly") "What would be the point of adding a nonexistant Picarto user? Silly")
return return
picarto_urls = config.get_content('picarto') or {} picarto_urls = config.get_content('picarto') or {}
result = picarto_urls.get(ctx.message.author.id) result = picarto_urls.get(ctx.message.author.id)
# If information for this user already exists, override just the url, and not the information
# Otherwise create the information with notications on, and that they're not live. The next time it's checked, they'll go 'online'
if result is not None: if result is not None:
picarto_urls[ctx.message.author.id]['picarto_url'] = url picarto_urls[ctx.message.author.id]['picarto_url'] = url
else: else:
picarto_urls[ctx.message.author.id] = {'picarto_url': url, picarto_urls[ctx.message.author.id] = {'picarto_url': url,
'servers': {ctx.message.server.id: ctx.message.channel.id}, 'servers': [ctx.message.server.id],
'notifications_on': 1, 'live': 0} 'notifications_on': 1, 'live': 0}
config.save_content('picarto', picarto_urls) config.save_content('picarto', picarto_urls)
await self.bot.say( await self.bot.say(
"I have just saved your Picarto url {}, this channel will now send a notification when you go live".format( "I have just saved your Picarto url {}, this server will now be notified when you go live".format(
ctx.message.author.mention)) ctx.message.author.mention))
@picarto.command(name='remove', aliases=['delete'], pass_context=True, no_pm=True) @picarto.command(name='remove', aliases=['delete'], pass_context=True, no_pm=True)
@ -146,25 +178,26 @@ class Picarto:
await self.bot.say("I am no longer saving your picarto URL {}".format(ctx.message.author.mention)) await self.bot.say("I am no longer saving your picarto URL {}".format(ctx.message.author.mention))
else: else:
await self.bot.say( await self.bot.say(
"I do not have your picarto URL added {}. You can save your picarto url with !picarto add".format( "I do not have your picarto URL added {}. You can save your picarto url with {}picarto add".format(
ctx.message.author.mention)) ctx.message.author.mention, ctx.prefix))
@picarto.group(pass_context=True, no_pm=True, invoke_without_command=True) @picarto.group(pass_context=True, no_pm=True, invoke_without_command=True)
@checks.custom_perms(send_messages=True) @checks.custom_perms(send_messages=True)
async def notify(self, ctx, channel: discord.Channel = None): async def notify(self, ctx):
"""This can be used to turn picarto notifications on or off """This can be used to turn picarto notifications on or off
Call this command by itself, with a channel name, to change which one has the notification sent to it""" Call this command by itself, to add this server to the list of servers to be notified"""
channel = channel or ctx.message.channel
member = ctx.message.author member = ctx.message.author
# If this user's picarto URL is not saved, no use in adding this server to the list that doesn't exist
picarto = config.get_content('picarto') or {} picarto = config.get_content('picarto') or {}
result = picarto.get(member.id) result = picarto.get(member.id)
if result is None: if result is None:
await self.bot.say( await self.bot.say(
"I do not have your picarto URL added {}. You can save your picarto url with !picarto add".format( "I do not have your picarto URL added {}. You can save your picarto url with {}picarto add".format(
member.mention)) member.mention, ctx.prefix))
picarto[member.id]['servers'][ctx.message.server.id] = channel.id # Append this server's ID and save the new content
picarto[member.id]['servers'].append(ctx.message.server.id)
config.save_content('picarto', picarto) config.save_content('picarto', picarto)
await self.bot.say( await self.bot.say(
"I have just changed which channel will be notified when you go live, to `{}`".format(channel.name)) "I have just changed which channel will be notified when you go live, to `{}`".format(channel.name))
@ -175,10 +208,12 @@ class Picarto:
"""Turns picarto notifications on""" """Turns picarto notifications on"""
picarto = config.get_content('picarto') or {} picarto = config.get_content('picarto') or {}
result = picarto.get(ctx.message.author.id) result = picarto.get(ctx.message.author.id)
# Check if this user has saved their picarto URL first
if result is None: if result is None:
await self.bot.say( await self.bot.say(
"I do not have your picarto URL added {}. You can save your picarto url with !picarto add".format( "I do not have your picarto URL added {}. You can save your picarto url with !picarto add".format(
ctx.message.author.mention)) ctx.message.author.mention))
# Next check if they are already set to notify
elif result['notifications_on']: elif result['notifications_on']:
await self.bot.say("What do you want me to do, send two notifications? Not gonna happen {}".format( await self.bot.say("What do you want me to do, send two notifications? Not gonna happen {}".format(
ctx.message.author.mention)) ctx.message.author.mention))
@ -193,10 +228,12 @@ class Picarto:
async def notify_off(self, ctx): async def notify_off(self, ctx):
"""Turns picarto notifications off""" """Turns picarto notifications off"""
picarto = config.get_content('picarto') or {} picarto = config.get_content('picarto') or {}
# Check if this user has saved their picarto URL first
if picarto.get(ctx.message.author.id) is None: if picarto.get(ctx.message.author.id) is None:
await self.bot.say( await self.bot.say(
"I do not have your picarto URL added {}. You can save your picarto url with !picarto add".format( "I do not have your picarto URL added {}. You can save your picarto url with !picarto add".format(
ctx.message.author.mention)) ctx.message.author.mention))
# Next check if they are already set to not notify
elif not picarto.get(ctx.message.author.id)['notifications_on']: elif not picarto.get(ctx.message.author.id)['notifications_on']:
await self.bot.say("I am already set to not notify if you go live! Pay attention brah {}".format( await self.bot.say("I am already set to not notify if you go live! Pay attention brah {}".format(
ctx.message.author.mention)) ctx.message.author.mention))

View file

@ -28,18 +28,20 @@ class VoiceState:
self.voice = None self.voice = None
self.bot = bot self.bot = bot
self.play_next_song = asyncio.Event() self.play_next_song = asyncio.Event()
self.songs = asyncio.Queue(maxsize=10) self.songs = asyncio.Queue(maxsize=10) # This is the queue that holds all VoiceEntry's
self.skip_votes = set() # a set of user_ids that voted self.skip_votes = set() # a set of user_ids that voted
self.audio_player = self.bot.loop.create_task(self.audio_player_task()) self.audio_player = self.bot.loop.create_task(self.audio_player_task()) # Our actual task that handles the queue system
self.opts = { self.opts = {
'default_search': 'auto', 'default_search': 'auto',
'quiet': True, 'quiet': True,
} }
def is_playing(self): def is_playing(self):
# If our VoiceClient or current VoiceEntry do not exist, then we are not playing a song
if self.voice is None or self.current is None: if self.voice is None or self.current is None:
return False return False
# If they do exist, check if the current player has finished
player = self.current.player player = self.current.player
return not player.is_done() return not player.is_done()
@ -48,24 +50,35 @@ class VoiceState:
return self.current.player return self.current.player
def skip(self): def skip(self):
# Make sure we clear the votes, before stopping the player
# When the player is stopped, our toggle_next method is called, so the next song can be played
self.skip_votes.clear() self.skip_votes.clear()
if self.is_playing(): if self.is_playing():
self.player.stop() self.player.stop()
def toggle_next(self): def toggle_next(self):
# Set the Event so that the next song in the queue can be played
self.bot.loop.call_soon_threadsafe(self.play_next_song.set) self.bot.loop.call_soon_threadsafe(self.play_next_song.set)
async def audio_player_task(self): async def audio_player_task(self):
while True: while True:
# At the start of our task, clear the Event, so we can wait till it is next set
self.play_next_song.clear() self.play_next_song.clear()
# Clear the votes skip that were for the last song
self.skip_votes.clear() self.skip_votes.clear()
# Set current to none while we are waiting for the next song in the queue
# If we don't do this and we hit the end of the queue, our current song will remain the song that just finished
self.current = None self.current = None
# Now wait for the next song in the queue
self.current = await self.songs.get() self.current = await self.songs.get()
# Tell the channel that requested the new song that we are now playing
await self.bot.send_message(self.current.channel, 'Now playing ' + str(self.current)) await self.bot.send_message(self.current.channel, 'Now playing ' + str(self.current))
# Recreate the player; depending on how long the song has been in the queue, the URL may have expired
self.current.player = await self.voice.create_ytdl_player(self.current.player.url, ytdl_options=self.opts, self.current.player = await self.voice.create_ytdl_player(self.current.player.url, ytdl_options=self.opts,
after=self.toggle_next) after=self.toggle_next)
# Now we can start actually playing the song
self.current.player.start() self.current.player.start()
# Wait till the Event has been set, before doing our task again
await self.play_next_song.wait() await self.play_next_song.wait()
@ -80,6 +93,10 @@ class Music:
def get_voice_state(self, server): def get_voice_state(self, server):
state = self.voice_states.get(server.id) state = self.voice_states.get(server.id)
# Internally handle creating a voice state if there isn't a current state
# This can be used for example, in case something is skipped when not being connected, we create the voice state when checked
# This only creates the state, we are still not playing anything, which can then be handled separately
if state is None: if state is None:
state = VoiceState(self.bot) state = VoiceState(self.bot)
self.voice_states[server.id] = state self.voice_states[server.id] = state
@ -87,11 +104,13 @@ class Music:
return state return state
async def create_voice_client(self, channel): async def create_voice_client(self, channel):
# First join the channel and get the VoiceClient that we'll use to save per server
voice = await self.bot.join_voice_channel(channel) voice = await self.bot.join_voice_channel(channel)
state = self.get_voice_state(channel.server) state = self.get_voice_state(channel.server)
state.voice = voice state.voice = voice
def __unload(self): def __unload(self):
# If this is unloaded, cancel all players and disconnect from all channels
for state in self.voice_states.values(): for state in self.voice_states.values():
try: try:
state.audio_player.cancel() state.audio_player.cancel()
@ -100,19 +119,21 @@ class Music:
except: except:
pass pass
@commands.command(no_pm=True) @commands.command(pass_context=True, no_pm=True)
@checks.custom_perms(send_messages=True) @checks.custom_perms(send_messages=True)
async def join(self, *, channel: discord.Channel): async def join(self, ctx, *, channel: discord.Channel):
"""Joins a voice channel.""" """Joins a voice channel."""
try: try:
await self.create_voice_client(channel) await self.create_voice_client(channel)
# Check if the channel given was an actual voice channel
except discord.InvalidArgument: except discord.InvalidArgument:
await self.bot.say('This is not a voice channel...') await self.bot.say('This is not a voice channel...')
# Check if we failed to join a channel, which means we are already in a channel.
# move_channel needs to be used if we are already in a channel
except discord.ClientException: except discord.ClientException:
await self.bot.say('Already in a voice channel...') state = self.get_voice_state(ctx.message.server)
except Exception as e: await state.voice.move_to(channel)
fmt = 'An error occurred while processing this request: ```py\n{}: {}\n```' await self.bot.say('Ready to play audio in ' + channel.name)
await self.bot.say(fmt.format(type(e).__name__, e))
else: else:
await self.bot.say('Ready to play audio in ' + channel.name) await self.bot.say('Ready to play audio in ' + channel.name)
@ -120,16 +141,21 @@ class Music:
@checks.custom_perms(send_messages=True) @checks.custom_perms(send_messages=True)
async def summon(self, ctx): async def summon(self, ctx):
"""Summons the bot to join your voice channel.""" """Summons the bot to join your voice channel."""
# First check if the author is even in a voice_channel
summoned_channel = ctx.message.author.voice_channel summoned_channel = ctx.message.author.voice_channel
if summoned_channel is None: if summoned_channel is None:
await self.bot.say('You are not in a voice channel.') await self.bot.say('You are not in a voice channel.')
return False return False
# Check if we're in a channel already, if we are then we just need to move channels
# Otherwse, we need to create an actual voice state
state = self.get_voice_state(ctx.message.server) state = self.get_voice_state(ctx.message.server)
if state.voice is None: if state.voice is None:
state.voice = await self.bot.join_voice_channel(summoned_channel) state.voice = await self.bot.join_voice_channel(summoned_channel)
else: else:
await state.voice.move_to(summoned_channel) await state.voice.move_to(summoned_channel)
# Return true so that we can invoke this, and ensure we succeeded
return True return True
@commands.command(pass_context=True, no_pm=True) @commands.command(pass_context=True, no_pm=True)
@ -142,28 +168,38 @@ class Music:
The list of supported sites can be found here: The list of supported sites can be found here:
https://rg3.github.io/youtube-dl/supportedsites.html https://rg3.github.io/youtube-dl/supportedsites.html
""" """
state = self.get_voice_state(ctx.message.server) state = self.get_voice_state(ctx.message.server)
# First check if we are connected to a voice channel at all, if not summon to the channel the author is in
# Since summon checks if the author is in a channel, we don't need to handle that here, just return if it failed
if state.voice is None: if state.voice is None:
success = await ctx.invoke(self.summon) success = await ctx.invoke(self.summon)
if not success: if not success:
return return
# If the queue is full, we ain't adding anything to it
if state.songs.full(): if state.songs.full():
await self.bot.say("The queue is currently full! You'll need to wait to add a new song") await self.bot.say("The queue is currently full! You'll need to wait to add a new song")
return return
author_channel = ctx.message.author.voice.voice_channel author_channel = ctx.message.author.voice.voice_channel
my_channel = ctx.message.server.me.voice.voice_channel my_channel = ctx.message.server.me.voice.voice_channel
# To try to avoid some abuse, ensure the requester is actually in our channel
if my_channel != author_channel: if my_channel != author_channel:
await self.bot.say("You are not currently in the channel; please join before trying to request a song.") await self.bot.say("You are not currently in the channel; please join before trying to request a song.")
return return
# Create the player, and check if this was successful
try: try:
player = await state.voice.create_ytdl_player(song, ytdl_options=state.opts, after=state.toggle_next) player = await state.voice.create_ytdl_player(song, ytdl_options=state.opts, after=state.toggle_next)
except youtube_dl.DownloadError: except youtube_dl.DownloadError:
await self.bot.send_message(ctx.message.channel, "Sorry, that's not a supported URL!") fmt = "Sorry, either I had an issue downloading that video, or that's not a supported URL!"
await self.bot.say(fmt)
return return
# Now we can create a VoiceEntry and queue it
player.volume = 0.6 player.volume = 0.6
entry = VoiceEntry(ctx.message, player) entry = VoiceEntry(ctx.message, player)
await self.bot.say('Enqueued ' + str(entry)) await self.bot.say('Enqueued ' + str(entry))
@ -186,8 +222,7 @@ class Music:
"""Pauses the currently played song.""" """Pauses the currently played song."""
state = self.get_voice_state(ctx.message.server) state = self.get_voice_state(ctx.message.server)
if state.is_playing(): if state.is_playing():
player = state.player state.player.pause()
player.pause()
@commands.command(pass_context=True, no_pm=True) @commands.command(pass_context=True, no_pm=True)
@checks.custom_perms(kick_members=True) @checks.custom_perms(kick_members=True)
@ -195,8 +230,7 @@ class Music:
"""Resumes the currently played song.""" """Resumes the currently played song."""
state = self.get_voice_state(ctx.message.server) state = self.get_voice_state(ctx.message.server)
if state.is_playing(): if state.is_playing():
player = state.player state.player.resume()
player.resume()
@commands.command(pass_context=True, no_pm=True) @commands.command(pass_context=True, no_pm=True)
@checks.custom_perms(kick_members=True) @checks.custom_perms(kick_members=True)
@ -206,11 +240,14 @@ class Music:
""" """
server = ctx.message.server server = ctx.message.server
state = self.get_voice_state(server) state = self.get_voice_state(server)
# Stop playing whatever song is playing.
if state.is_playing(): if state.is_playing():
player = state.player player = state.player
player.stop() player.stop()
# This will stop cancel the audio event we're using to loop through the queue
# Then erase the voice_state entirely, and disconnect from the channel
try: try:
state.audio_player.cancel() state.audio_player.cancel()
del self.voice_states[server.id] del self.voice_states[server.id]
@ -222,23 +259,33 @@ class Music:
@checks.custom_perms(send_messages=True) @checks.custom_perms(send_messages=True)
async def eta(self, ctx): async def eta(self, ctx):
"""Provides an ETA on when your next song will play""" """Provides an ETA on when your next song will play"""
# Note: There is no way to tell how long a song has been playing, or how long there is left on a song
# That is why this is called an "ETA"
state = self.get_voice_state(ctx.message.server) state = self.get_voice_state(ctx.message.server)
author = ctx.message.author author = ctx.message.author
if not state.is_playing(): if not state.is_playing():
await self.bot.say('Not playing any music right now...') await self.bot.say('Not playing any music right now...')
return return
if len(state.songs._queue) == 0: queue = state.songs._queue
if len(queue) == 0:
await self.bot.say("Nothing currently in the queue") await self.bot.say("Nothing currently in the queue")
return return
# Start off by adding the length of the current song
count = state.current.player.duration count = state.current.player.duration
found = False found = False
for song in state.songs._queue: # Loop through the songs in the queue, until the author is found as the requester
# The found bool is used to see if we actually found the author, or we just looped through the whole queue
for song in queue:
if song.requester == author: if song.requester == author:
found = True found = True
break break
count += song.player.duration count += song.player.duration
# This is checking if nothing from the queue has been added to the total
# If it has not, then we have not looped through the queue at all
# Since the queue was already checked to have more than one song in it, this means the author is next
if count == state.current.player.duration: if count == state.current.player.duration:
await self.bot.say("You are next in the queue!") await self.bot.say("You are next in the queue!")
return return
@ -255,7 +302,10 @@ class Music:
if not state.is_playing(): if not state.is_playing():
await self.bot.say('Not playing any music right now...') await self.bot.say('Not playing any music right now...')
return return
if len(state.songs._queue) == 0:
# Asyncio provides no non-private way to access the queue, so we have to use _queue
queue = state.songs._queue
if len(queue) == 0:
fmt = "Nothing currently in the queue" fmt = "Nothing currently in the queue"
else: else:
fmt = "\n\n".join(str(x) for x in state.songs._queue) fmt = "\n\n".join(str(x) for x in state.songs._queue)
@ -279,14 +329,18 @@ class Music:
if not state.is_playing(): if not state.is_playing():
await self.bot.say('Not playing any music right now...') await self.bot.say('Not playing any music right now...')
return return
# Check if the person requesting a skip is the requester of the song, if so automatically skip
voter = ctx.message.author voter = ctx.message.author
if voter == state.current.requester: if voter == state.current.requester:
await self.bot.say('Requester requested skipping song...') await self.bot.say('Requester requested skipping song...')
state.skip() state.skip()
# Otherwise check if the voter has already voted
elif voter.id not in state.skip_votes: elif voter.id not in state.skip_votes:
state.skip_votes.add(voter.id) state.skip_votes.add(voter.id)
total_votes = len(state.skip_votes) total_votes = len(state.skip_votes)
# Now check how many votes have been made, if 3 then go ahead and skip, otherwise add to the list of votes
if total_votes >= 3: if total_votes >= 3:
await self.bot.say('Skip vote passed, skipping song...') await self.bot.say('Skip vote passed, skipping song...')
state.skip() state.skip()

View file

@ -1,7 +1,9 @@
from discord.ext import commands from discord.ext import commands
import discord import discord
from .utils import checks from .utils import checks
import re import re
import asyncio
class Roles: class Roles:
@ -15,6 +17,7 @@ class Roles:
async def role(self, ctx): async def role(self, ctx):
"""This command can be used to modify the roles on the server. """This command can be used to modify the roles on the server.
Pass no subcommands and this will print the roles currently available on this server""" Pass no subcommands and this will print the roles currently available on this server"""
# Simply get a list of all roles in this server and send them
server_roles = [role.name for role in ctx.message.server.roles if not role.is_everyone] server_roles = [role.name for role in ctx.message.server.roles if not role.is_everyone]
await self.bot.say("Your server's roles are: ```\n{}```".format("\n".join(server_roles))) await self.bot.say("Your server's roles are: ```\n{}```".format("\n".join(server_roles)))
@ -22,8 +25,15 @@ class Roles:
@checks.custom_perms(manage_server=True) @checks.custom_perms(manage_server=True)
async def remove_role(self, ctx): async def remove_role(self, ctx):
"""Use this to remove roles from a number of members""" """Use this to remove roles from a number of members"""
# No use in running through everything if the bot cannot manage roles
if not ctx.message.server.me.permissions_in(ctx.message.channel).manage_roles:
await self.bot.say("I can't manage roles in this server, do you not trust me? :c")
return
server_roles = [role for role in ctx.message.server.roles if not role.is_everyone] server_roles = [role for role in ctx.message.server.roles if not role.is_everyone]
# First get the list of all mentioned users
members = ctx.message.mentions members = ctx.message.mentions
# If no users are mentioned, ask the author for a list of the members they want to remove the role from
if len(members) == 0: if len(members) == 0:
await self.bot.say("Please provide the list of members you want to remove a role from") await self.bot.say("Please provide the list of members you want to remove a role from")
msg = await self.bot.wait_for_message(author=ctx.message.author, channel=ctx.message.channel) msg = await self.bot.wait_for_message(author=ctx.message.author, channel=ctx.message.channel)
@ -33,8 +43,10 @@ class Roles:
if len(msg.mentions) == 0: if len(msg.mentions) == 0:
await self.bot.say("I cannot remove a role from someone if you don't provide someone...") await self.bot.say("I cannot remove a role from someone if you don't provide someone...")
return return
# Override members if everything has gone alright, and then continue
members = msg.mentions members = msg.mentions
# This allows the user to remove multiple roles from the list of users, if they want.
await self.bot.say("Alright, please provide the roles you would like to remove from this member. " await self.bot.say("Alright, please provide the roles you would like to remove from this member. "
"Make sure the roles, if more than one is provided, are separate by commas. " "Make sure the roles, if more than one is provided, are separate by commas. "
"Here is a list of this server's roles:" "Here is a list of this server's roles:"
@ -43,17 +55,22 @@ class Roles:
if msg is None: if msg is None:
await self.bot.say("You took too long. I'm impatient, don't make me wait") await self.bot.say("You took too long. I'm impatient, don't make me wait")
return return
# Split the content based on commas, using regex so we can split if a space was not provided or if it was
role_names = re.split(', ?', msg.content) role_names = re.split(', ?', msg.content)
roles = [] roles = []
# This loop is just to get the actual role objects based on the name
for role in role_names: for role in role_names:
_role = discord.utils.get(server_roles, name=role) _role = discord.utils.get(server_roles, name=role)
if _role is not None: if _role is not None:
roles.append(_role) roles.append(_role)
# If no valid roles were given, let them know that and return
if len(roles) == 0: if len(roles) == 0:
await self.bot.say("Please provide a valid role next time!") await self.bot.say("Please provide a valid role next time!")
return return
# Otherwise, remove the roles from each member given
for member in members: for member in members:
await self.bot.remove_roles(member, *roles) await self.bot.remove_roles(member, *roles)
await self.bot.say("I have just removed the following roles:```\n{}``` from the following members:" await self.bot.say("I have just removed the following roles:```\n{}``` from the following members:"
@ -65,6 +82,12 @@ class Roles:
"""Use this to add a role to multiple members. """Use this to add a role to multiple members.
Provide the list of members, and I'll ask for the role Provide the list of members, and I'll ask for the role
If no members are provided, I'll first ask for them""" If no members are provided, I'll first ask for them"""
# No use in running through everything if the bot cannot manage roles
if not ctx.message.server.me.permissions_in(ctx.message.channel).manage_roles:
await self.bot.say("I can't manage roles in this server, do you not trust me? :c")
return
# This is exactly the same as removing roles, except we call add_roles instead.
server_roles = [role for role in ctx.message.server.roles if not role.is_everyone] server_roles = [role for role in ctx.message.server.roles if not role.is_everyone]
members = ctx.message.mentions members = ctx.message.mentions
if len(members) == 0: if len(members) == 0:
@ -107,17 +130,29 @@ class Roles:
@checks.custom_perms(manage_server=True) @checks.custom_perms(manage_server=True)
async def delete_role(self, ctx, *, role: discord.Role = None): async def delete_role(self, ctx, *, role: discord.Role = None):
"""This command can be used to delete one of the roles from the server""" """This command can be used to delete one of the roles from the server"""
# No use in running through everything if the bot cannot manage roles
if not ctx.message.server.me.permissions_in(ctx.message.channel).manage_roles:
await self.bot.say("I can't delete roles in this server, do you not trust me? :c")
return
# If no role was given, get the current roles on the server and ask which ones they'd like to remove
if role is None: if role is None:
server_roles = [role for role in ctx.message.server.roles if not role.is_everyone] server_roles = [role for role in ctx.message.server.roles if not role.is_everyone]
await self.bot.say( await self.bot.say(
"Which role would you like to remove from the server? Here is a list of this server's roles:" "Which role would you like to remove from the server? Here is a list of this server's roles:"
"```\n{}```".format("\n".join([r.name for r in server_roles]))) "```\n{}```".format("\n".join([r.name for r in server_roles])))
# For this method we're only going to delete one role at a time
# This check attempts to find a role based on the content provided, if it can't find one it returns None
# We can use that fact to simply use just that as our check
check = lambda m: discord.utils.get(server_roles, name=m.content) check = lambda m: discord.utils.get(server_roles, name=m.content)
msg = await self.bot.wait_for_message(author=ctx.message.author, channel=ctx.message.channel, check=check) msg = await self.bot.wait_for_message(author=ctx.message.author, channel=ctx.message.channel, check=check)
if msg is None: if msg is None:
await self.bot.say("You took too long. I'm impatient, don't make me wait") await self.bot.say("You took too long. I'm impatient, don't make me wait")
return return
# If we have gotten here, based on our previous check, we know that the content provided is a valid role.
# Due to that, no need for any error checking here
role = discord.utils.get(server_roles, name=msg.content) role = discord.utils.get(server_roles, name=msg.content)
await self.bot.delete_role(ctx.message.server, role) await self.bot.delete_role(ctx.message.server, role)
@ -144,7 +179,7 @@ class Roles:
yes_no_check = lambda m: re.search("(yes|no)", m.content.lower()) is not None yes_no_check = lambda m: re.search("(yes|no)", m.content.lower()) is not None
members_check = lambda m: len(m.mentions) > 0 members_check = lambda m: len(m.mentions) > 0
# Start the checks for the role, get the name of the command first # Start the checks for the role, get the name of the role first
await self.bot.say( await self.bot.say(
"Alright! I'm ready to create a new role, please respond with the name of the role you want to create") "Alright! I'm ready to create a new role, please respond with the name of the role you want to create")
msg = await self.bot.wait_for_message(timeout=60.0, author=author, channel=channel) msg = await self.bot.wait_for_message(timeout=60.0, author=author, channel=channel)
@ -159,7 +194,8 @@ class Roles:
await self.bot.say("Sounds fancy! Here is a list of all the permissions available. Please respond with just " await self.bot.say("Sounds fancy! Here is a list of all the permissions available. Please respond with just "
"the numbers, seperated by commas, of the permissions you want this role to have.\n" "the numbers, seperated by commas, of the permissions you want this role to have.\n"
"```\n{}```".format(fmt)) "```\n{}```".format(fmt))
msg = await self.bot.wait_for_message(timeout=60.0, author=author, channel=channel, check=num_separated_check) # For this we're going to give a couple extra minutes before we timeout, as it might take a bit to figure out which permissions they want
msg = await self.bot.wait_for_message(timeout=180.0, author=author, channel=channel, check=num_separated_check)
if msg is None: if msg is None:
await self.bot.say("You took too long. I'm impatient, don't make me wait") await self.bot.say("You took too long. I'm impatient, don't make me wait")
return return
@ -187,6 +223,7 @@ class Roles:
mentionable = True if msg.content.lower() == "yes" else False mentionable = True if msg.content.lower() == "yes" else False
# Ready to actually create the role # Ready to actually create the role
# First create a permissions object based on the numbers provided
perms = discord.Permissions.none() perms = discord.Permissions.none()
for index in num_permissions: for index in num_permissions:
setattr(perms, all_perms[index], True) setattr(perms, all_perms[index], True)
@ -197,12 +234,17 @@ class Roles:
'hoist': hoist, 'hoist': hoist,
'mentionable': mentionable 'mentionable': mentionable
} }
# Create the role, and wait a second, sometimes it goes too quickly and we get a role with 'new role' to print
role = await self.bot.create_role(server, **payload) role = await self.bot.create_role(server, **payload)
await asyncio.sleep(1)
await self.bot.say("We did it! You just created the new role {}\nIf you want to add this role" await self.bot.say("We did it! You just created the new role {}\nIf you want to add this role"
" to some people, mention them now".format(role.name)) " to some people, mention them now".format(role.name))
msg = await self.bot.wait_for_message(timeout=60.0, author=author, channel=channel, check=members_check) msg = await self.bot.wait_for_message(timeout=60.0, author=author, channel=channel, check=members_check)
# There's no need to mention the users, so don't send a failure message if they didn't, just return
if msg is None: if msg is None:
return return
# Otherwise members were mentioned, add the new role to them now
for member in msg.mentions: for member in msg.mentions:
await self.bot.add_roles(member, role) await self.bot.add_roles(member, role)

View file

@ -18,11 +18,16 @@ class Stats:
if not boops.get(ctx.message.author.id): if not boops.get(ctx.message.author.id):
await self.bot.say("You have not booped anyone {} Why the heck not...?".format(ctx.message.author.mention)) await self.bot.say("You have not booped anyone {} Why the heck not...?".format(ctx.message.author.mention))
return return
# First get a list of the ID's of all members in this server, for use in list comprehension
server_member_ids = [member.id for member in ctx.message.server.members] server_member_ids = [member.id for member in ctx.message.server.members]
# Then get a sorted list, based on the amount of times they've booped the member
# Reverse needs to be true, as we want it to go from highest to lowest
sorted_boops = sorted(boops.get(ctx.message.author.id).items(), key=lambda x: x[1], reverse=True) sorted_boops = sorted(boops.get(ctx.message.author.id).items(), key=lambda x: x[1], reverse=True)
# Then override the same list, checking if the member they've booped is in this server, using our previous list comprehension
sorted_boops = [x for x in sorted_boops if x[0] in server_member_ids] sorted_boops = [x for x in sorted_boops if x[0] in server_member_ids]
# Since this is sorted, we just need to get the following information on the first user in the list
most_boops = sorted_boops[0][1] most_boops = sorted_boops[0][1]
most_id = sorted_boops[0][0] most_id = sorted_boops[0][0]
member = discord.utils.find(lambda m: m.id == most_id, self.bot.get_all_members()) member = discord.utils.find(lambda m: m.id == most_id, self.bot.get_all_members())
@ -38,7 +43,9 @@ class Stats:
if booped_members is None: if booped_members is None:
await self.bot.say("You have not booped anyone {} Why the heck not...?".format(ctx.message.author.mention)) await self.bot.say("You have not booped anyone {} Why the heck not...?".format(ctx.message.author.mention))
return return
# Same concept as the mostboops method
# TODO: Sort that shit
server_member_ids = [member.id for member in ctx.message.server.members] server_member_ids = [member.id for member in ctx.message.server.members]
booped_members = {m_id: amt for m_id, amt in booped_members.items() if m_id in server_member_ids} booped_members = {m_id: amt for m_id, amt in booped_members.items() if m_id in server_member_ids}
@ -52,9 +59,11 @@ class Stats:
async def leaderboard(self, ctx): async def leaderboard(self, ctx):
"""Prints a leaderboard of everyone in the server's battling record""" """Prints a leaderboard of everyone in the server's battling record"""
battles = config.get_content('battle_records') or {} battles = config.get_content('battle_records') or {}
# Same concept as mostboops
server_member_ids = [member.id for member in ctx.message.server.members] server_member_ids = [member.id for member in ctx.message.server.members]
server_members = {member_id: stats for member_id, stats in battles.items() if member_id in server_member_ids} server_members = {member_id: stats for member_id, stats in battles.items() if member_id in server_member_ids}
# Only real difference is the key, the key needs to be based on the rating in the member's dictionary of stats
sorted_members = sorted(server_members.items(), key=lambda k: k[1]['rating'], reverse=True) sorted_members = sorted(server_members.items(), key=lambda k: k[1]['rating'], reverse=True)
fmt = "" fmt = ""
@ -78,14 +87,17 @@ class Stats:
await self.bot.say("That user has not battled yet!") await self.bot.say("That user has not battled yet!")
return return
# Same concept as the leaderboard
server_member_ids = [member.id for member in ctx.message.server.members] server_member_ids = [member.id for member in ctx.message.server.members]
server_members = {member_id: stats for member_id, stats in all_members.items() if server_members = {member_id: stats for member_id, stats in all_members.items() if
member_id in server_member_ids} member_id in server_member_ids}
sorted_server_members = sorted(server_members.items(), key=lambda x: x[1]['rating'], reverse=True) sorted_server_members = sorted(server_members.items(), key=lambda x: x[1]['rating'], reverse=True)
sorted_all_members = sorted(all_members.items(), key=lambda x: x[1]['rating'], reverse=True) sorted_all_members = sorted(all_members.items(), key=lambda x: x[1]['rating'], reverse=True)
# Enumurate the list so that we can go through, find the user's place in the list, and get just that for the rank
server_rank = [i for i, x in enumerate(sorted_server_members) if x[0] == member.id][0] + 1 server_rank = [i for i, x in enumerate(sorted_server_members) if x[0] == member.id][0] + 1
total_rank = [i for i, x in enumerate(sorted_all_members) if x[0] == member.id][0] + 1 total_rank = [i for i, x in enumerate(sorted_all_members) if x[0] == member.id][0] + 1
# The rest of this is straight forward, just formatting
rating = server_members[member.id]['rating'] rating = server_members[member.id]['rating']
record = "{}-{}".format(server_members[member.id]['wins'], server_members[member.id]['losses']) record = "{}-{}".format(server_members[member.id]['wins'], server_members[member.id]['losses'])
fmt = 'Stats for {}:\n\tRecord: {}\n\tServer Rank: {}/{}\n\tOverall Rank: {}/{}\n\tRating: {}' fmt = 'Stats for {}:\n\tRecord: {}\n\tServer Rank: {}/{}\n\tOverall Rank: {}/{}\n\tRating: {}'

View file

@ -24,6 +24,8 @@ class Strawpoll:
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.url = 'https://strawpoll.me/api/v2/polls' self.url = 'https://strawpoll.me/api/v2/polls'
# In this class we'll only be sending POST requests when creating a poll
# Strawpoll requires the content-type, so just add that to the default headers
self.headers = {'User-Agent': 'Bonfire/1.0.0', self.headers = {'User-Agent': 'Bonfire/1.0.0',
'Content-Type': 'application/json'} 'Content-Type': 'application/json'}
self.session = aiohttp.ClientSession() self.session = aiohttp.ClientSession()
@ -32,27 +34,34 @@ class Strawpoll:
@checks.custom_perms(send_messages=True) @checks.custom_perms(send_messages=True)
async def strawpolls(self, ctx, poll_id: str = None): async def strawpolls(self, ctx, poll_id: str = None):
"""This command can be used to show a strawpoll setup on this server""" """This command can be used to show a strawpoll setup on this server"""
# Strawpolls cannot be 'deleted' so to handle whether a poll is running or not on a server
# Just save the poll in the config file, which can then be removed when it should not be "running" anymore
all_polls = config.get_content('strawpolls') or {} all_polls = config.get_content('strawpolls') or {}
server_polls = all_polls.get(ctx.message.server.id) or {} server_polls = all_polls.get(ctx.message.server.id) or {}
if not server_polls: if not server_polls:
await self.bot.say("There are currently no strawpolls running on this server!") await self.bot.say("There are currently no strawpolls running on this server!")
return return
# If no poll_id was provided, print a list of all current running poll's on this server
if not poll_id: if not poll_id:
fmt = "\n".join( fmt = "\n".join(
"{}: https://strawpoll.me/{}".format(data['title'], _id) for _id, data in server_polls.items()) "{}: https://strawpoll.me/{}".format(data['title'], _id) for _id, data in server_polls.items())
await self.bot.say("```\n{}```".format(fmt)) await self.bot.say("```\n{}```".format(fmt))
# Else if a valid poll_id was provided, print info about that poll
elif poll_id in server_polls.keys(): elif poll_id in server_polls.keys():
poll = server_polls[poll_id] poll = server_polls[poll_id]
async with self.session.get("{}/{}".format(self.url, poll_id)) as response: async with self.session.get("{}/{}".format(self.url, poll_id), headers={'User-Agent': 'Bonfire/1.0.0') as response:
data = await response.json() data = await response.json()
# The response for votes and options is provided as two separate lists
# We are enumarting the list of options, to print r (the option) and the votes to match it, based on the index of the option
# The rest is simple formatting
fmt_options = "\n\t".join( fmt_options = "\n\t".join(
"{}: {}".format(r, data['votes'][i]) for i, r in enumerate(data['options'])) "{}: {}".format(r, data['votes'][i]) for i, r in enumerate(data['options']))
author = discord.utils.get(self.bot.get_all_members(), id=poll['author']) author = discord.utils.get(ctx.message.server.members, id=poll['author'])
created_ago = (pendulum.utcnow() - pendulum.parse(poll['date'])).in_words() created_ago = (pendulum.utcnow() - pendulum.parse(poll['date'])).in_words()
link = "https://strawpoll.me/{}".format(poll_id) link = "https://strawpoll.me/{}".format(poll_id)
fmt = "Link: {}\nTitle: {}\nAuthor: {}\nCreated: {}\nOptions:\n\t{}".format(link, data['title'], fmt = "Link: {}\nTitle: {}\nAuthor: {}\nCreated: {} ago\nOptions:\n\t{}".format(link, data['title'],
author.display_name, author.display_name,
created_ago, fmt_options) created_ago, fmt_options)
await self.bot.say("```\n{}```".format(fmt)) await self.bot.say("```\n{}```".format(fmt))
@ -64,24 +73,32 @@ class Strawpoll:
The format needs to be: poll create "title here" all options here The format needs to be: poll create "title here" all options here
Options need to be separated by using either one ` around each option Options need to be separated by using either one ` around each option
Or use a code block (3 ` around the options), each option on it's own line""" Or use a code block (3 ` around the options), each option on it's own line"""
# The following should use regex to search for the options inside of the two types of code blocks with `
# We're using this instead of other things, to allow most used puncation inside the options
match_single = getter.findall(options) match_single = getter.findall(options)
match_multi = multi.findall(options) match_multi = multi.findall(options)
# Since match_single is already going to be a list, we just set the options to match_single and remove any blank entries
if match_single: if match_single:
options = match_single options = match_single
options = [option for option in options if option] options = [option for option in options if option]
# Otherwise, options need to be set based on the list, split by lines. Then remove blank entries like the last one
elif match_multi: elif match_multi:
options = match_multi[0].splitlines() options = match_multi[0].splitlines()
options = [option for option in options if option] options = [option for option in options if option]
# If neither is found, then error out and let them know to use the help command, since this one is a bit finicky
else: else:
await self.bot.say( await self.bot.say(
"Please provide options for a new strawpoll! Use {}help if you do not know the format".format( "Please provide options for a new strawpoll! Use {}help {} if you do not know the format".format(
ctx.prefix)) ctx.prefix, ctx.command.qualified_name))
return return
# Make the post request to strawpoll, creating the poll, and returning the ID
# The ID is all we really need from the returned data, as the rest we already sent/are not going to use ever
payload = {'title': title, payload = {'title': title,
'options': options} 'options': options}
async with self.session.post(self.url, data=json.dumps(payload), headers=self.headers) as response: async with self.session.post(self.url, data=json.dumps(payload), headers=self.headers) as response:
data = await response.json() data = await response.json()
# Save this strawpoll in the list of running strawpolls for a server
all_polls = config.get_content('strawpolls') or {} all_polls = config.get_content('strawpolls') or {}
server_polls = all_polls.get(ctx.message.server.id) or {} server_polls = all_polls.get(ctx.message.server.id) or {}
server_polls[data['id']] = {'author': ctx.message.author.id, 'date': str(pendulum.utcnow()), 'title': title} server_polls[data['id']] = {'author': ctx.message.author.id, 'date': str(pendulum.utcnow()), 'title': title}
@ -98,14 +115,17 @@ class Strawpoll:
all_polls = config.get_content('strawpolls') or {} all_polls = config.get_content('strawpolls') or {}
server_polls = all_polls.get(ctx.message.server.id) or {} server_polls = all_polls.get(ctx.message.server.id) or {}
# Check if a poll_id was provided, if it is then we can continue, if not print the list of current polls
if poll_id: if poll_id:
poll = server_polls.get(poll_id) poll = server_polls.get(poll_id)
# Check if no poll exists with that ID, then print a list of the polls
if not poll: if not poll:
fmt = "\n".join("{}: {}".format(data['title'], _poll_id) for _poll_id, data in server_polls.items()) fmt = "\n".join("{}: {}".format(data['title'], _poll_id) for _poll_id, data in server_polls.items())
await self.bot.say( await self.bot.say(
"There is no poll setup with that ID! Here is a list of the current polls```\n{}```".format(fmt)) "There is no poll setup with that ID! Here is a list of the current polls```\n{}```".format(fmt))
else: else:
# Delete the poll that was just found
del server_polls[poll_id] del server_polls[poll_id]
all_polls[ctx.message.server.id] = server_polls all_polls[ctx.message.server.id] = server_polls
config.save_content('strawpolls', all_polls) config.save_content('strawpolls', all_polls)

View file

@ -14,6 +14,7 @@ except FileNotFoundError:
botDescription = global_config.get("description") botDescription = global_config.get("description")
commandPrefix = global_config.get("command_prefix", "!") commandPrefix = global_config.get("command_prefix", "!")
discord_bots_key = global_config.get('discord_bots_key', "") discord_bots_key = global_config.get('discord_bots_key', "")
dev_server = global_config.get("dev_server", "")
battleWins = global_config.get("battleWins", []) battleWins = global_config.get("battleWins", [])
defaultStatus = global_config.get("default_status", "") defaultStatus = global_config.get("default_status", "")