1
0
Fork 0
mirror of synced 2024-05-20 12:32:26 +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):
"""Provides a printout of the current month's calendar
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 = {
"january": 1,
"february": 2,
@ -38,6 +40,7 @@ class Core:
"november": 11,
"december": 12
}
# In month was not passed, use the current month
if month is None:
month = datetime.date.today().month
else:
@ -45,8 +48,10 @@ class Core:
if month is None:
await self.bot.say("Please provide a valid Month!")
return
# If year was not passed, use the current year
if year is None:
year = datetime.datetime.today().year
# Here we create the actual "text" calendar that we are printing
cal = calendar.TextCalendar().formatmonth(year, month)
await self.bot.say("```\n{}```".format(cal))
@ -54,16 +59,20 @@ class Core:
@checks.custom_perms(send_messages=True)
async def info(self):
"""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 = {}
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 = []
for author_id in config.owner_ids:
authors.append(discord.utils.get(all_members, id=author_id).name)
fmt['Official Bot Server'] = config.dev_server
fmt['Author'] = ", ".join(authors)
fmt['Uptime'] = (pendulum.utcnow() - self.bot.uptime).in_words()
fmt['Total Servers'] = len(self.bot.servers)
@ -93,14 +102,16 @@ class Core:
perms.embed_links = True
perms.read_message_history = 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{}"
.format(discord.utils.oauth_url('183748889814237186', perms)))
.format(discord.utils.oauth_url(app_info.id, perms)))
@commands.command(pass_context=True)
@checks.custom_perms(send_messages=True)
async def doggo(self, ctx):
"""Use this to print a random doggo image.
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)]
with open(f, 'rb') as f:
await self.bot.upload(f)
@ -110,6 +121,7 @@ class Core:
async def snek(self, ctx):
"""Use this to print a random snek image.
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)]
with open(f, 'rb') as f:
await self.bot.upload(f)
@ -118,6 +130,7 @@ class Core:
@checks.custom_perms(send_messages=True)
async def joke(self):
"""Prints a random riddle"""
# Use the fortune riddles command because it's funny, I promise
fortune_command = "/usr/bin/fortune riddles"
fortune = subprocess.check_output(fortune_command.split()).decode("utf-8")
await self.bot.say(fortune)
@ -127,15 +140,19 @@ class Core:
async def roll(self, ctx, notation: str="d6"):
"""Rolls a die based on the notation given
Format should be #d#"""
# Use regex to get the notation based on what was provided
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)
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):
await self.bot.say("Please provide the die notation in #d#!")
return
# Dice will be None if d# was provided, assume this means 1d#
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)
if dice > 10:
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):
self.word = word
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.failed_letters = []
self.guessed_letters = []
self.fails = 0
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)
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))
return True
else:
@ -43,6 +48,8 @@ class Game:
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".format("o" if self.fails > 0 else " ")
@ -52,6 +59,7 @@ class Game:
man += " |\n"
man += " ———————\n"
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))
return fmt
@ -61,6 +69,7 @@ class Hangman:
self.games = {}
def create(self, word, ctx):
# Create a new game, then save it as the server's game
game = Game(word, ctx.message.author)
self.games[ctx.message.server.id] = game
return game
@ -78,10 +87,16 @@ class Hangman:
if ctx.message.author == game.creator:
await self.bot.say("You can't guess at your own hangman game! :S")
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 guess in game.guessed_letters:
await self.bot.say("That letter has already been guessed!")
# Return here as we don't want to count this as a failure
return
if game.guess_letter(guess):
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.
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:
await self.bot.say("Sorry but only one Hangman game can be running per server!")
return
# Make sure the phrase is less than 30 characters
check = lambda m: len(m.content) < 30
# Doing this so that while we wait for the phrase, another one cannot be started.
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))
# 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"
"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)
@ -131,6 +150,7 @@ class Hangman:
return
else:
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)))
def setup(bot):

View file

@ -7,36 +7,47 @@ import random
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}
config.save_content('battling', battling)
def user_battling(ctx):
def user_battling(ctx, player2=None):
battling = config.get_content('battling')
# If no one is battling, obviously the user is not battling
if battling is None:
return False
# Check if the author is battling
if ctx.message.author.id in battling.values() or ctx.message.author.id in battling.keys():
return True
if str(ctx.command) == 'battle':
return ctx.message.mentions[0].id in battling.values() or ctx.message.mentions[0].id in battling.keys()
# Check if the player2 was provided, if they are check if they're in the list
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
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')
if battles is None:
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_rating = winner_stats.get('rating') or 1000
loser_stats = battles.get(loser.id) or {}
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)
rating_change = 0
count = 25
@ -45,21 +56,24 @@ def update_battle_records(winner, loser):
break
rating_change += 1
count += 25
# 16 is the base change, increased or decreased based on whoever has the higher current rating
if winner_rating > loser_rating:
winner_rating += 16 - rating_change
loser_rating -= 16 - rating_change
else:
winner_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_losses = winner_stats.get('losses') or 0
loser_wins = loser_stats.get('wins') or 0
loser_losses = loser_stats.get('losses') or 0
winner_wins += 1
loser_losses += 1
# Now save the new wins, losses, and ratings
winner_stats = {'wins': winner_wins, 'losses': winner_losses, 'rating': winner_rating}
loser_stats = {'wins': loser_wins, 'losses': loser_losses, 'rating': loser_rating}
battles[winner.id] = winner_stats
@ -79,27 +93,23 @@ class Interaction:
@checks.custom_perms(send_messages=True)
async def battle(self, ctx, player2: discord.Member):
"""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:
await self.bot.say("Why would you want to battle yourself? Suicide is not the answer")
return
if self.bot.user.id == player2.id:
await self.bot.say("I always win, don't even try it.")
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!")
return
# Add the author and player provided in a new battle
battling = config.get_content('battling') or {}
battling[ctx.message.author.id] = ctx.message.mentions[0].id
config.save_content('battling', battling)
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)
await self.bot.say(fmt.format(ctx.message.author, player2))
await self.bot.delete_message(ctx.message)
@ -108,10 +118,13 @@ class Interaction:
@checks.custom_perms(send_messages=True)
async def accept(self, ctx):
"""Accepts the battle challenge"""
# Ensure that the author is actually in a battle, otherwise they can't accept one
if not user_battling(ctx):
await self.bot.say("You are not currently in a battle!")
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 {}
p1 = [p1_id for p1_id, p2_id in battling.items() if p2_id == ctx.message.author.id]
if len(p1) == 0:
@ -120,10 +133,14 @@ class Interaction:
battleP1 = discord.utils.find(lambda m: m.id == p1[0], ctx.message.server.members)
battleP2 = ctx.message.author
# Get a random win message from our list
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)
# 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):
await self.bot.say(fmt.format(battleP1.mention, battleP2.mention))
update_battle_records(battleP1, battleP2)
@ -140,15 +157,20 @@ class Interaction:
if not user_battling(ctx):
await self.bot.say("You are not currently in a battle!")
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 {}
p1 = [p1_id for p1_id, p2_id in battling.items() if p2_id == ctx.message.author.id]
if len(p1) == 0:
await self.bot.say("You are not currently being challenged to a battle!")
return
battleP1 = discord.utils.find(lambda m: m.id == p1[0], ctx.message.server.members)
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)
await self.bot.say("{0} has chickened out! What a loser~".format(battleP2.mention, battleP1.mention))
await self.bot.delete_message(ctx.message)
@ -158,13 +180,6 @@ class Interaction:
@checks.custom_perms(send_messages=True)
async def boop(self, ctx, boopee: discord.Member):
"""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:
await self.bot.say("You can't boop yourself! Silly...")
return
@ -173,14 +188,20 @@ class Interaction:
return
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
# Get all the booped stats for the author
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:
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:
booper_boops[boopee.id] = 1
boops[ctx.message.author.id] = booper_boops
# Otherwise increment how many times they've booped that user
else:
amount = booper_boops.get(boopee.id) + 1
booper_boops[boopee.id] = amount

View file

@ -12,6 +12,7 @@ class Links:
def __init__(self, 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.session = aiohttp.ClientSession()
@ -19,17 +20,23 @@ class Links:
@checks.custom_perms(send_messages=True)
async def wiki(self, *, query: str):
"""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="
async with self.session.get("{}/{}".format(base_url, query), headers=self.headers) as r:
data = await r.json()
if len(data['query']['search']) == 0:
await self.bot.say("I could not find any results with that term, I tried my best :c")
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'))
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>','',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))
@commands.command()
@ -38,12 +45,14 @@ class Links:
"""Pulls the top urbandictionary.com definition for a term"""
url = "http://api.urbandictionary.com/v0/define?term={}".format('+'.join(msg))
async with self.session.get(url, headers=self.headers) as r:
response = await r.text()
data = json.loads(response)
data = await r.json()
# Urban dictionary has some long definitions, some might not be able to be sent
try:
# List is the list of definitions found, if it's empty then nothing was found
if len(data['list']) == 0:
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:
await self.bot.say(data['list'][0]['definition'])
except discord.HTTPException:
@ -57,32 +66,40 @@ class Links:
# This sets the url as url?q=search+terms
url = 'https://derpibooru.org/search.json?q={}'.format('+'.join(search))
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:
url += ",+explicit&filter_id=95938"
# Get the response from derpibooru and parse the 'search' result from it
async with self.session.get(url, headers=self.headers) as r:
response = await r.text()
data = json.loads(response)
data = await r.json()
try:
results = data['search']
except KeyError:
await self.bot.say("No results with that search term, {0}!".format(ctx.message.author.mention))
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:
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:
await self.bot.say("No results with that search term, {0}!".format(ctx.message.author.mention))
return
else:
# If no search term was provided, search for a random image
async with self.session.get('https://derpibooru.org/images/random') as r:
imageLink = r.url
await self.bot.say(imageLink)
# .url will be the URl we end up at, not the one requested.
# 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)
@ -91,21 +108,27 @@ class Links:
"""Searches for a random image from e621.net
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"""
# 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(',_', '%20')
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....")
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:
url += "%20rating:explicit"
else:
url += "%20rating:safe"
async with self.session.get(url, headers=self.headers) as r:
response = await r.text()
data = json.loads(response)
data = await r.json()
# Check if there were any results, if there are find a random image based on the length of results
if len(data) == 0:
await self.bot.say("No results with that image {}".format(ctx.message.author.mention))
return

View file

@ -19,6 +19,7 @@ class Mod:
"""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"""
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
await self.bot.say("I have just changed this server's 'notifications' 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 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"""
# 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
notifications = config.get_content('user_notifications') or {}
notifications[ctx.message.server.id] = on_off
@ -39,6 +42,7 @@ class Mod:
@commands.group(pass_context=True, no_pm=True)
async def nsfw(self, ctx):
"""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:
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:
await self.bot.say("This channel is already registered as 'nsfw'!")
else:
# Append instead of setting to a certain channel, so that multiple channels can be nsfw
nsfw_channels.append(ctx.message.channel.id)
config.save_content('nsfw_channels', nsfw_channels)
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:
await self.bot.say("That command has no custom permissions setup on it!")
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)
needed_perm = [perm[0] for perm in permissions if perm[1]][0]
await self.bot.say("You need to have the permission `{}` "
@ -104,18 +112,24 @@ class Mod:
"""Sets up custom permissions on the provided command
Format must be 'perms add <command> <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])
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":
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()
setattr(perm_obj, permissions, True)
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
for part in msg[0:len(msg) - 1]:
try:
@ -124,13 +138,17 @@ class Mod:
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
# 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:
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!")
@ -144,6 +162,7 @@ class Mod:
custom_perms = config.get_content('custom_permissions') 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
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)
@commands.has_permissions(manage_server=True)
async def remove_perms(self, ctx, *command: str):
"""Removes the custom permissions setup on the command specified"""
cmd = " ".join(command)
"""Removes the custom permissions setup on the command specified"""
custom_perms = config.get_content('custom_permissions') or {}
server_perms = custom_perms.get(ctx.message.server.id) or {}
if server_perms is None:
await self.bot.say("There are no custom permissions setup on this server yet!")
return
command_perms = server_perms.get(cmd)
if command_perms is None:
await self.bot.say("You do not have custom permissions setup on this command yet!")
cmd = None
# 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
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]
config.save_content('custom_permissions', custom_perms)
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:
await self.bot.say("This server currently has no rules on it! I see you like to live dangerously...")
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))
await self.bot.say('```\n{}```'.format(fmt))
@ -191,6 +229,7 @@ class Mod:
@checks.custom_perms(manage_server=True)
async def rules_add(self, ctx, *, rule: str):
"""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 {}
server_rules = rules.get(ctx.message.server.id) or []
server_rules.append(rule)
@ -210,12 +249,16 @@ class Mod:
await self.bot.say(
"This server currently has no rules on it! Can't remove something that doesn't exist bro")
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))
if rule is None:
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))
# 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,
check=lambda m: m.content.isdigit())
if msg is None:
@ -224,7 +267,8 @@ class Mod:
del server_rules[int(msg.content) - 1]
rules[ctx.message.server.id] = server_rules
config.save_content('rules', rules)
# This check is just to ensure a number was provided within the list's range
try:
del server_rules[rule - 1]
rules[ctx.message.server.id] = server_rules

View file

@ -7,6 +7,10 @@ import aiohttp
import json
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',
'cards', 'damage_done', 'healing_done', 'multikills']
check_o_stats = ['wins', 'losses']
@ -17,6 +21,8 @@ class Overwatch:
def __init__(self, bot):
self.bot = bot
self.headers = {"User-Agent": "Bonfire/1.0.0"}
self.session = aiohttp.ClientSession()
@commands.group(no_pm=True)
async def ow(self):
@ -40,41 +46,46 @@ class Overwatch:
if bt is None:
await self.bot.say("I do not have this user's battletag saved!")
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....")
if hero == "":
with aiohttp.ClientSession(headers={"User-Agent": "Bonfire/1.0.0"}) as s:
async with s.get(base_url + "{}/stats/general".format(bt)) as r:
result = await r.text()
data = json.loads(result)
# If no hero was provided, we just want the base stats for a player
async with self.session.get(base_url + "{}/stats/general".format(bt), headers=self.headers) as r:
data = await r.json()
# 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"
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(
"Overwatch stats for {}: ```py\n{}```".format(user.name, fmt.title().replace("_", " ")))
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('-', ''))
with aiohttp.ClientSession(headers={"User-Agent": "Bonfire/1.0.0"}) as s:
async with s.get(url) as r:
result = await r.text()
msg = json.loads(result).get('msg')
if msg == 'hero data not found':
fmt = "{} has not used the hero {} before!".format(user.name, hero.title())
await self.bot.say(fmt)
return
elif msg == 'bad hero name':
fmt = "{} is not an actual hero!".format(hero.title())
await self.bot.say(fmt)
return
result = await r.text()
data = json.loads(result)
async with self.session.get(url, headers=self.headers) as r:
data = await r.json()
msg = data.get('msg')
# Check if a user has not used the hero provided before
if msg == 'hero data not found':
fmt = "{} has not used the hero {} before!".format(user.name, hero.title())
await self.bot.say(fmt)
return
# Check if a hero that doesn't exist was provided
elif msg == 'bad hero name':
fmt = "{} is not an actual hero!".format(hero.title())
await self.bot.say(fmt)
return
# 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)
# 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'):
fmt += "\nKill Death Ratio: {0:.2f}".format(data['general_stats'].get('eliminations')/data['general_stats'].get('deaths'))
fmt += "\n"
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{}``` "
.format(user.name, hero.title(), fmt.title().replace("_", " ")))
@ -82,17 +93,23 @@ class Overwatch:
@checks.custom_perms(send_messages=True)
async def add(self, ctx, bt: str):
"""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("#", "-")
# 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....")
url = base_url + "{}/stats/general".format(bt)
with aiohttp.ClientSession(headers={"User-Agent": "Bonfire/1.0.0"}) as s:
async with s.get(url) as r:
if not r.status == 200:
await self.bot.say("Profile does not exist! Battletags are picky, "
"format needs to be `user#xxxx`. Capitalization matters")
return
# All we're doing here is ensuring that the status is 200 when looking up someone's general information
# If it's not, let them know exactly how to format their tag
async with self.session.get(url, headers=self.headers) as r:
if not r.status == 200:
await self.bot.say("Profile does not exist! Battletags are picky, "
"format needs to be `user#xxxx`. Capitalization matters")
return
# Now just save the battletag
ow = config.get_content('overwatch') or {}
ow[ctx.message.author.id] = bt
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):
"""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"""
voice_states = self.bot.get_cog('Music').voice_states
for server_id, state in voice_states.items():
if state.is_playing:
server = self.bot.get_server(server_id)
await self.bot.say("Sorry, it's not safe to restart. I am currently playing a song on the {} server".format(server.name))
return
# I do not want to restart the bot if someone is playing music
# This gets all the exiting VoiceStates that are playing music right now
# If we are, say which server it
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 {} servers".format(len(servers_playing_music)))
ctx.invoke(self.bot.commands.get('restart'))
@commands.command(pass_context=True)
@commands.check(checks.is_owner)
async def restart(self, ctx):
"""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)
await self.bot.say("Restarting; see you in the next life {0}!".format(ctx.message.author.mention))
python = sys.executable
@ -45,42 +45,56 @@ class Owner:
@commands.check(checks.is_owner)
async def adddoggo(self, url: str):
"""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:
async with s.get(url) as r:
val = await r.read()
with open(local_path, "wb") as f:
f.write(val)
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.check(checks.is_owner)
async def addsnek(self, url: str):
"""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:
async with s.get(url) as r:
val = await r.read()
with open(local_path, "wb") as f:
f.write(val)
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.check(checks.is_owner)
async def debug(self, ctx):
"""Executes code"""
# Eval and exec have different useful purposes, so use both
try:
# `Get all content in this format`
match_single = getter.findall(ctx.message.content)
# ```\nGet all content in this format```
match_multi = multi.findall(ctx.message.content)
if not match_multi:
if match_single:
result = eval(match_single[0])
# In case the result needs to be awaited, handle that
if inspect.isawaitable(result):
result = await result
await self.bot.say("```\n{0}```".format(result))
elif match_multi:
# Internal method to send the message to the channel, of whatever is passed
def r(v):
self.bot.loop.create_task(self.bot.say("```\n{}```".format(v)))
exec(match_multi[0])
@ -114,20 +128,22 @@ class Owner:
@commands.command()
@commands.check(checks.is_owner)
async def status(self, *stat: str):
async def status(self, *, status: str):
"""Changes the bot's 'playing' status"""
newStatus = ' '.join(stat)
game = discord.Game(name=newStatus, type=0)
await self.bot.change_status(game)
await self.bot.change_status(discord.Game(name=status, type=0))
await self.bot.say("Just changed my status to '{0}'!".format(newStatus))
@commands.command()
@commands.check(checks.is_owner)
async def load(self, *, module: str):
"""Loads a module"""
# Do this because I'm too lazy to type cogs.module
module = module.lower()
if not module.startswith("cogs"):
module = "cogs.{}".format(module)
# This try catch will catch errors such as syntax errors in the module we are loading
try:
self.bot.load_extension(module)
await self.bot.say("I have just loaded the {} module".format(module))
@ -139,24 +155,27 @@ class Owner:
@commands.check(checks.is_owner)
async def unload(self, *, module: str):
"""Unloads a module"""
# Do this because I'm too lazy to type cogs.module
module = module.lower()
if not module.startswith("cogs"):
module = "cogs.{}".format(module)
try:
self.bot.unload_extension(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))
self.bot.unload_extension(module)
await self.bot.say("I have just unloaded the {} module".format(module))
@commands.command()
@commands.check(checks.is_owner)
async def reload(self, *, module: str):
"""Reloads a module"""
# Do this because I'm too lazy to type cogs.module
module = module.lower()
if not module.startswith("cogs"):
module = "cogs.{}".format(module)
self.bot.unload_extension(module)
# This try block will catch errors such as syntax errors in the module we are loading
try:
self.bot.load_extension(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():
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)
with aiohttp.ClientSession(headers={"User-Agent": "Bonfire/1.0.0"}) as s:
async with s.get(url) as r:
response = await r.text()
return json.loads(response)
return await r.json()
except:
return {}
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()]
return len(matches) > 0
class Picarto:
def __init__(self, bot):
self.bot = bot
self.headers = {"User-Agent": "Bonfire/1.0.0"}
self.session = aiohttp.ClientSession()
async def check_channels(self):
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:
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()
for m_id, r in picarto.items():
url = r['picarto_url']
# This is whether they are detected as live in the saved file
live = r['live']
notify = r['notifications_on']
user = re.search("(?<=picarto.tv/)(.*)", url).group(1)
# This is whether or not they are actually online
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:
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_alerts = config.get_content('server_alerts') or {}
channel_id = server_alerts.get(server_id) or server_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
fmt = "{} has just gone live! View their stream at {}".format(member.display_name, url)
config.save_content('picarto', picarto)
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:
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_alerts = config.get_content('server_alerts') or {}
channel_id = server_alerts.get(server_id) or server_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
fmt = "{} has just gone offline! Catch them next time they stream at {}".format(
member.display_name,
@ -76,25 +95,29 @@ class Picarto:
@checks.custom_perms(send_messages=True)
async def picarto(self, ctx, member: discord.Member = None):
"""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
picarto_urls = config.get_content('picarto') or {}
member_url = picarto_urls.get(member.id)
if not member_url:
try:
member_url = picarto_urls.get(member.id)['picarto_url']
except:
await self.bot.say("That user does not have a picarto url setup!")
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)
url = '{}/channel/{}?key={}'.format(base_url, stream, key)
with aiohttp.ClientSession(headers={"User-Agent": "Bonfire/1.0.0"}) as s:
async with s.get(url) as r:
response = await r.text()
data = json.loads(response)
async with self.session.get(url, headers=self.headers) as r:
data = await r.json()
# Not everyone has all these settings, so use this as a way to print information if it does, otherwise ignore it
things_to_print = ['channel', 'commissions_enabled', 'is_nsfw', 'program', 'tablet', 'followers',
'content_type']
# Using title and replace to provide a nice way to print the data
fmt = "\n".join(
"{}: {}".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')
if social_links:
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)
async def add_picarto_url(self, ctx, url: str):
"""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:
url = re.search("((?<=://)?picarto.tv/)+(.*)", url).group(0)
except AttributeError:
@ -113,26 +143,28 @@ class Picarto:
url = "https://www.{}".format(url)
api_url = '{}/channel/{}?key={}'.format(base_url, re.search("https://www.picarto.tv/(.*)", url).group(1), key)
with aiohttp.ClientSession() as s:
async with s.get(api_url) as r:
if not r.status == 200:
await self.bot.say("That Picarto user does not exist! "
"What would be the point of adding a nonexistant Picarto user? Silly")
return
# Check if we can find a user with the provided information, if we can't just return
async with self.session.get(api_url, headers=self.headers) as r:
if not r.status == 200:
await self.bot.say("That Picarto user does not exist! "
"What would be the point of adding a nonexistant Picarto user? Silly")
return
picarto_urls = config.get_content('picarto') or {}
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:
picarto_urls[ctx.message.author.id]['picarto_url'] = url
else:
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}
config.save_content('picarto', picarto_urls)
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))
@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))
else:
await self.bot.say(
"I do not have your picarto URL added {}. You can save your picarto url with !picarto add".format(
ctx.message.author.mention))
"I do not have your picarto URL added {}. You can save your picarto url with {}picarto add".format(
ctx.message.author.mention, ctx.prefix))
@picarto.group(pass_context=True, no_pm=True, invoke_without_command=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
Call this command by itself, with a channel name, to change which one has the notification sent to it"""
channel = channel or ctx.message.channel
Call this command by itself, to add this server to the list of servers to be notified"""
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 {}
result = picarto.get(member.id)
if result is None:
await self.bot.say(
"I do not have your picarto URL added {}. You can save your picarto url with !picarto add".format(
member.mention))
picarto[member.id]['servers'][ctx.message.server.id] = channel.id
"I do not have your picarto URL added {}. You can save your picarto url with {}picarto add".format(
member.mention, ctx.prefix))
# Append this server's ID and save the new content
picarto[member.id]['servers'].append(ctx.message.server.id)
config.save_content('picarto', picarto)
await self.bot.say(
"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"""
picarto = config.get_content('picarto') or {}
result = picarto.get(ctx.message.author.id)
# Check if this user has saved their picarto URL first
if result is None:
await self.bot.say(
"I do not have your picarto URL added {}. You can save your picarto url with !picarto add".format(
ctx.message.author.mention))
# Next check if they are already set to notify
elif result['notifications_on']:
await self.bot.say("What do you want me to do, send two notifications? Not gonna happen {}".format(
ctx.message.author.mention))
@ -193,10 +228,12 @@ class Picarto:
async def notify_off(self, ctx):
"""Turns picarto notifications off"""
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:
await self.bot.say(
"I do not have your picarto URL added {}. You can save your picarto url with !picarto add".format(
ctx.message.author.mention))
# Next check if they are already set to not notify
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(
ctx.message.author.mention))

View file

@ -28,18 +28,20 @@ class VoiceState:
self.voice = None
self.bot = bot
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.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 = {
'default_search': 'auto',
'quiet': True,
}
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:
return False
# If they do exist, check if the current player has finished
player = self.current.player
return not player.is_done()
@ -48,24 +50,35 @@ class VoiceState:
return self.current.player
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()
if self.is_playing():
self.player.stop()
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)
async def audio_player_task(self):
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()
# Clear the votes skip that were for the last song
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
# Now wait for the next song in the queue
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))
# 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,
after=self.toggle_next)
# Now we can start actually playing the song
self.current.player.start()
# Wait till the Event has been set, before doing our task again
await self.play_next_song.wait()
@ -80,6 +93,10 @@ class Music:
def get_voice_state(self, server):
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:
state = VoiceState(self.bot)
self.voice_states[server.id] = state
@ -87,11 +104,13 @@ class Music:
return state
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)
state = self.get_voice_state(channel.server)
state.voice = voice
def __unload(self):
# If this is unloaded, cancel all players and disconnect from all channels
for state in self.voice_states.values():
try:
state.audio_player.cancel()
@ -100,19 +119,21 @@ class Music:
except:
pass
@commands.command(no_pm=True)
@commands.command(pass_context=True, no_pm=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."""
try:
await self.create_voice_client(channel)
# Check if the channel given was an actual voice channel
except discord.InvalidArgument:
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:
await self.bot.say('Already in a voice channel...')
except Exception as e:
fmt = 'An error occurred while processing this request: ```py\n{}: {}\n```'
await self.bot.say(fmt.format(type(e).__name__, e))
state = self.get_voice_state(ctx.message.server)
await state.voice.move_to(channel)
await self.bot.say('Ready to play audio in ' + channel.name)
else:
await self.bot.say('Ready to play audio in ' + channel.name)
@ -120,16 +141,21 @@ class Music:
@checks.custom_perms(send_messages=True)
async def summon(self, ctx):
"""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
if summoned_channel is None:
await self.bot.say('You are not in a voice channel.')
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)
if state.voice is None:
state.voice = await self.bot.join_voice_channel(summoned_channel)
else:
await state.voice.move_to(summoned_channel)
# Return true so that we can invoke this, and ensure we succeeded
return True
@commands.command(pass_context=True, no_pm=True)
@ -142,28 +168,38 @@ class Music:
The list of supported sites can be found here:
https://rg3.github.io/youtube-dl/supportedsites.html
"""
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:
success = await ctx.invoke(self.summon)
if not success:
return
# If the queue is full, we ain't adding anything to it
if state.songs.full():
await self.bot.say("The queue is currently full! You'll need to wait to add a new song")
return
author_channel = ctx.message.author.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:
await self.bot.say("You are not currently in the channel; please join before trying to request a song.")
return
# Create the player, and check if this was successful
try:
player = await state.voice.create_ytdl_player(song, ytdl_options=state.opts, after=state.toggle_next)
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
# Now we can create a VoiceEntry and queue it
player.volume = 0.6
entry = VoiceEntry(ctx.message, player)
await self.bot.say('Enqueued ' + str(entry))
@ -186,8 +222,7 @@ class Music:
"""Pauses the currently played song."""
state = self.get_voice_state(ctx.message.server)
if state.is_playing():
player = state.player
player.pause()
state.player.pause()
@commands.command(pass_context=True, no_pm=True)
@checks.custom_perms(kick_members=True)
@ -195,8 +230,7 @@ class Music:
"""Resumes the currently played song."""
state = self.get_voice_state(ctx.message.server)
if state.is_playing():
player = state.player
player.resume()
state.player.resume()
@commands.command(pass_context=True, no_pm=True)
@checks.custom_perms(kick_members=True)
@ -206,11 +240,14 @@ class Music:
"""
server = ctx.message.server
state = self.get_voice_state(server)
# Stop playing whatever song is playing.
if state.is_playing():
player = state.player
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:
state.audio_player.cancel()
del self.voice_states[server.id]
@ -222,23 +259,33 @@ class Music:
@checks.custom_perms(send_messages=True)
async def eta(self, ctx):
"""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)
author = ctx.message.author
if not state.is_playing():
await self.bot.say('Not playing any music right now...')
return
if len(state.songs._queue) == 0:
queue = state.songs._queue
if len(queue) == 0:
await self.bot.say("Nothing currently in the queue")
return
# Start off by adding the length of the current song
count = state.current.player.duration
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:
found = True
break
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:
await self.bot.say("You are next in the queue!")
return
@ -255,7 +302,10 @@ class Music:
if not state.is_playing():
await self.bot.say('Not playing any music right now...')
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"
else:
fmt = "\n\n".join(str(x) for x in state.songs._queue)
@ -279,14 +329,18 @@ class Music:
if not state.is_playing():
await self.bot.say('Not playing any music right now...')
return
# Check if the person requesting a skip is the requester of the song, if so automatically skip
voter = ctx.message.author
if voter == state.current.requester:
await self.bot.say('Requester requested skipping song...')
state.skip()
# Otherwise check if the voter has already voted
elif voter.id not in state.skip_votes:
state.skip_votes.add(voter.id)
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:
await self.bot.say('Skip vote passed, skipping song...')
state.skip()

View file

@ -1,7 +1,9 @@
from discord.ext import commands
import discord
from .utils import checks
import re
import asyncio
class Roles:
@ -15,6 +17,7 @@ class Roles:
async def role(self, ctx):
"""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"""
# 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]
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)
async def remove_role(self, ctx):
"""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]
# First get the list of all mentioned users
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:
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)
@ -33,8 +43,10 @@ class Roles:
if len(msg.mentions) == 0:
await self.bot.say("I cannot remove a role from someone if you don't provide someone...")
return
# Override members if everything has gone alright, and then continue
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. "
"Make sure the roles, if more than one is provided, are separate by commas. "
"Here is a list of this server's roles:"
@ -43,17 +55,22 @@ class Roles:
if msg is None:
await self.bot.say("You took too long. I'm impatient, don't make me wait")
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)
roles = []
# This loop is just to get the actual role objects based on the name
for role in role_names:
_role = discord.utils.get(server_roles, name=role)
if _role is not None:
roles.append(_role)
# If no valid roles were given, let them know that and return
if len(roles) == 0:
await self.bot.say("Please provide a valid role next time!")
return
# Otherwise, remove the roles from each member given
for member in members:
await self.bot.remove_roles(member, *roles)
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.
Provide the list of members, and I'll ask for the role
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]
members = ctx.message.mentions
if len(members) == 0:
@ -107,17 +130,29 @@ class Roles:
@checks.custom_perms(manage_server=True)
async def delete_role(self, ctx, *, role: discord.Role = None):
"""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:
server_roles = [role for role in ctx.message.server.roles if not role.is_everyone]
await self.bot.say(
"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])))
# 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)
msg = await self.bot.wait_for_message(author=ctx.message.author, channel=ctx.message.channel, check=check)
if msg is None:
await self.bot.say("You took too long. I'm impatient, don't make me wait")
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)
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
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(
"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)
@ -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 "
"the numbers, seperated by commas, of the permissions you want this role to have.\n"
"```\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:
await self.bot.say("You took too long. I'm impatient, don't make me wait")
return
@ -187,6 +223,7 @@ class Roles:
mentionable = True if msg.content.lower() == "yes" else False
# Ready to actually create the role
# First create a permissions object based on the numbers provided
perms = discord.Permissions.none()
for index in num_permissions:
setattr(perms, all_perms[index], True)
@ -197,12 +234,17 @@ class Roles:
'hoist': hoist,
'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)
await asyncio.sleep(1)
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))
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:
return
# Otherwise members were mentioned, add the new role to them now
for member in msg.mentions:
await self.bot.add_roles(member, role)

View file

@ -18,11 +18,16 @@ class Stats:
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))
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]
# 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)
# 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]
# 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_id = sorted_boops[0][0]
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:
await self.bot.say("You have not booped anyone {} Why the heck not...?".format(ctx.message.author.mention))
return
# Same concept as the mostboops method
# TODO: Sort that shit
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}
@ -52,9 +59,11 @@ class Stats:
async def leaderboard(self, ctx):
"""Prints a leaderboard of everyone in the server's battling record"""
battles = config.get_content('battle_records') or {}
# Same concept as mostboops
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}
# 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)
fmt = ""
@ -78,14 +87,17 @@ class Stats:
await self.bot.say("That user has not battled yet!")
return
# Same concept as the leaderboard
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
member_id in server_member_ids}
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)
# 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
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']
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: {}'

View file

@ -24,6 +24,8 @@ class Strawpoll:
def __init__(self, bot):
self.bot = bot
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',
'Content-Type': 'application/json'}
self.session = aiohttp.ClientSession()
@ -32,27 +34,34 @@ class Strawpoll:
@checks.custom_perms(send_messages=True)
async def strawpolls(self, ctx, poll_id: str = None):
"""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 {}
server_polls = all_polls.get(ctx.message.server.id) or {}
if not server_polls:
await self.bot.say("There are currently no strawpolls running on this server!")
return
# If no poll_id was provided, print a list of all current running poll's on this server
if not poll_id:
fmt = "\n".join(
"{}: https://strawpoll.me/{}".format(data['title'], _id) for _id, data in server_polls.items())
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():
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()
# 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(
"{}: {}".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()
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,
created_ago, fmt_options)
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
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"""
# 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_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:
options = match_single
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:
options = match_multi[0].splitlines()
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:
await self.bot.say(
"Please provide options for a new strawpoll! Use {}help if you do not know the format".format(
ctx.prefix))
"Please provide options for a new strawpoll! Use {}help {} if you do not know the format".format(
ctx.prefix, ctx.command.qualified_name))
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,
'options': options}
async with self.session.post(self.url, data=json.dumps(payload), headers=self.headers) as response:
data = await response.json()
# Save this strawpoll in the list of running strawpolls for a server
all_polls = config.get_content('strawpolls') 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}
@ -98,14 +115,17 @@ class Strawpoll:
all_polls = config.get_content('strawpolls') 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:
poll = server_polls.get(poll_id)
# Check if no poll exists with that ID, then print a list of the polls
if not poll:
fmt = "\n".join("{}: {}".format(data['title'], _poll_id) for _poll_id, data in server_polls.items())
await self.bot.say(
"There is no poll setup with that ID! Here is a list of the current polls```\n{}```".format(fmt))
else:
# Delete the poll that was just found
del server_polls[poll_id]
all_polls[ctx.message.server.id] = server_polls
config.save_content('strawpolls', all_polls)

View file

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