Mass update: Added comments to most of the project. A couple small tweaks made as well
This commit is contained in:
parent
d334c5584d
commit
f26e04b1a4
23
cogs/core.py
23
cogs/core.py
|
@ -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")
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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('"','"',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
|
||||
|
|
66
cogs/mod.py
66
cogs/mod.py
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
113
cogs/picarto.py
113
cogs/picarto.py
|
@ -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))
|
||||
|
|
106
cogs/playlist.py
106
cogs/playlist.py
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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: {}'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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", "")
|
||||
|
|
Loading…
Reference in a new issue