From 13ce1d861ef19d240ab49c45837d17928b5bb9e5 Mon Sep 17 00:00:00 2001 From: phxntxm Date: Sun, 2 Jul 2017 23:55:27 -0500 Subject: [PATCH 01/18] Pillow changes --- cogs/utils/images.py | 3 ++- requirements.txt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cogs/utils/images.py b/cogs/utils/images.py index 5d653f2..43cc7b2 100644 --- a/cogs/utils/images.py +++ b/cogs/utils/images.py @@ -92,7 +92,8 @@ async def create_banner(member, image_title, data): stat_offset = draw.textsize(text, font=font, spacing=0) font = ImageFont.truetype(whitneyMedium, 96) - draw.text((360, -4), text, (255, 255, 255), font=font, align="center") + # draw.text((360, -4), text, (255, 255, 255), font=font, align="center") + draw.text((360, -4), text, (255, 255, 255), font=font) draw.text((360 + stat_offset[0], -4), stat_text, (0, 402, 504), font=font) save_me = text_bar.resize((350, 20), Image.ANTIALIAS) offset += 20 diff --git a/requirements.txt b/requirements.txt index e9f5e4f..722f2d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ beautifulsoup4==4.6.0 -Pillow==3.4.1 +Pillow==4.2.0 rethinkdb==2.3.0.post6 ruamel.yaml==0.14.12 youtube-dl From a4a47cded30269519b259771e3be60eeb53c7f4f Mon Sep 17 00:00:00 2001 From: phxntxm Date: Sun, 9 Jul 2017 14:52:09 -0500 Subject: [PATCH 02/18] Correct accidental override of info --- cogs/voice_utilities/source.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cogs/voice_utilities/source.py b/cogs/voice_utilities/source.py index a4d3311..56c20f7 100644 --- a/cogs/voice_utilities/source.py +++ b/cogs/voice_utilities/source.py @@ -3,9 +3,10 @@ import time import asyncio from .exceptions import ExtractionError, WrongEntryTypeError, LiveStreamError +from .entry import get_header + class YoutubeDLSource(discord.FFmpegPCMAudio): - def __init__(self, playlist, url): self.playlist = playlist self.loop = playlist.loop @@ -33,10 +34,10 @@ class YoutubeDLSource(discord.FFmpegPCMAudio): # Otherwise get the first result else: info = info['entries'][0] - self.url = info['webpage_url'] # If this isn't a search, then it is a playlist, this can't be done else: - raise WrongEntryTypeError("This is a playlist.", True, info.get('webpage_url', None) or info.get('url', None)) + raise WrongEntryTypeError("This is a playlist.", True, + info.get('webpage_url', None) or info.get('url', None)) if info['extractor'] in ['generic', 'Dropbox']: try: @@ -55,7 +56,6 @@ class YoutubeDLSource(discord.FFmpegPCMAudio): if headers.get('ice-audio-info'): raise LiveStreamError("Cannot download from a livestream") - if info.get('is_live', False): raise LiveStreamError("Cannot download from a livestream") From eaef4754109eeb263ebe638c4d02005902c1fa45 Mon Sep 17 00:00:00 2001 From: phxntxm Date: Sun, 9 Jul 2017 15:03:16 -0500 Subject: [PATCH 03/18] Use predownloaded info for filename --- cogs/voice_utilities/source.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/voice_utilities/source.py b/cogs/voice_utilities/source.py index 56c20f7..3a5c84d 100644 --- a/cogs/voice_utilities/source.py +++ b/cogs/voice_utilities/source.py @@ -15,7 +15,6 @@ class YoutubeDLSource(discord.FFmpegPCMAudio): self.info = None self.ready = False self.error = False - asyncio.run_coroutine_threadsafe(self.download(), self.loop) async def get_info(self): try: @@ -64,6 +63,7 @@ class YoutubeDLSource(discord.FFmpegPCMAudio): async def prepare(self): await self.get_info() + asyncio.run_coroutine_threadsafe(self.download(), self.loop) return self.info async def download(self): @@ -78,7 +78,7 @@ class YoutubeDLSource(discord.FFmpegPCMAudio): 'before_options': '-nostdin', 'options': '-vn -b:a 128k' } - super().__init__(self.downloader.ytdl.prepare_filename(result), **opts) + super().__init__(self.downloader.ytdl.prepare_filename(self.info), **opts) @property def title(self): From 187c6d3ba7ee58b6578a14c99afb1e80ec6dff51 Mon Sep 17 00:00:00 2001 From: phxntxm Date: Sun, 9 Jul 2017 15:04:48 -0500 Subject: [PATCH 04/18] Force disconnect on stop --- cogs/music.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/music.py b/cogs/music.py index 620d9f5..6005e24 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -633,7 +633,7 @@ class Music: # Then stop playing, and disconnect if voice: voice.stop() - await voice.disconnect() + await voice.disconnect(force=True) @commands.command() @commands.guild_only() From 76a6af23839d87e478c745b54d919a388a01812c Mon Sep 17 00:00:00 2001 From: phxntxm Date: Sun, 9 Jul 2017 23:42:01 -0500 Subject: [PATCH 05/18] Include patreon info in config --- cogs/utils/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cogs/utils/config.py b/cogs/utils/config.py index 731bbad..5b08903 100644 --- a/cogs/utils/config.py +++ b/cogs/utils/config.py @@ -45,6 +45,9 @@ dev_server = global_config.get("dev_server", "") user_agent = global_config.get('user_agent', None) # The URL to proxy youtube_dl's requests through ytdl_proxy = global_config.get('youtube_dl_proxy', None) +# The patreon key, as well as the patreon ID to use +patreon_key = global_config.get('patreon_key', None) +patreon_id = global_config.get('patreon_id', None) # The extensions to load extensions = [ 'cogs.interaction', From e42381b5b5918dc455c1d5ec7829f60a4b6fe957 Mon Sep 17 00:00:00 2001 From: phxntxm Date: Sun, 9 Jul 2017 23:42:58 -0500 Subject: [PATCH 06/18] Add a donator command --- cogs/stats.py | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/cogs/stats.py b/cogs/stats.py index d59707c..9508dd7 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -4,6 +4,7 @@ from discord.ext import commands from . import utils import re +import asyncio class Stats: @@ -11,6 +12,52 @@ class Stats: def __init__(self, bot): self.bot = bot + self.donators = [] + self.bot.loop.create_task(self.donator_task()) + + async def donator_task(self): + while True: + await self.get_donators() + await asyncio.sleep(60) + + async def get_donators(self): + # Set our base URL for the pagination task + url = "https://api.patreon.com/oauth2/api/campaigns/{}/pledges".format(utils.patreon_id) + # Set our headers with our bearer token + headers = {'Authorization': 'Bearer {}'.format(utils.patreon_key)} + # We need the names of all of them, and the names are embeded a bit so lets append while looping + names = [] + # We need to page through, so lets create a loop and break when we find out we're done + while True: + # Simply get data based on the URL + data = await utils.request(url, headers=headers) + # First check if the data failed to retrieve, if so just return + if data is None: + return + + # Loop through the includes, as that's all we need + for include in data['included']: + # We only carry about the user's + if include['type'] != 'user': + continue + # This check checks the user's connected campaign (should only exist for *our* user) and checks if it matches + if include.get('relationshipos', {}).get('campaign', {}).get('data', {}).get('id', {}) == str(utils.patreon_id): + continue + + # Otherwuse the only way this user was included, was if they are a patron, so include them + name = include['attributes']['full_name'] + if name: + names.append(name) + + # Now, lets get our "next" link and request that + url = data['links'].get('next') + # If there is no None, that means there should only be a "first" and our pagination is done + if url is None: + break + + # Now just set the names + self.donators = names + @commands.command() @commands.guild_only() @@ -277,6 +324,20 @@ class Stats: fmt = fmt.format(member.display_name, record, server_rank, overall_rank, rating) await ctx.send('```\n{}```'.format(fmt)) + @commands.command(aliases=['patrons']) + @utils.custom_perms(send_messages=True) + @utils.check_restricted() + async def donators(self, ctx): + """Prints a list of all the patrons for Bonfire + + EXAMPLE: !donators + RESULT: A list of the donators""" + try: + pages = utils.Pages(self.bot, message=ctx.message, entries=self.names) + await pages.paginate() + except utils.CannotPaginate as e: + await ctx.send(str(e)) + def setup(bot): bot.add_cog(Stats(bot)) From 28e912728b77e7f335622e415d0c6ae2e13590b1 Mon Sep 17 00:00:00 2001 From: phxntxm Date: Sun, 9 Jul 2017 23:46:02 -0500 Subject: [PATCH 07/18] Include the correct entry name --- cogs/stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/stats.py b/cogs/stats.py index 9508dd7..ebb2210 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -333,7 +333,7 @@ class Stats: EXAMPLE: !donators RESULT: A list of the donators""" try: - pages = utils.Pages(self.bot, message=ctx.message, entries=self.names) + pages = utils.Pages(self.bot, message=ctx.message, entries=self.donators) await pages.paginate() except utils.CannotPaginate as e: await ctx.send(str(e)) From 46a1280256d2c12a134a837b12df9f8c13d4e1d0 Mon Sep 17 00:00:00 2001 From: phxntxm Date: Mon, 10 Jul 2017 00:20:30 -0500 Subject: [PATCH 08/18] Allow the use of forcing content_type for json --- cogs/stats.py | 7 ++++--- cogs/utils/utilities.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/cogs/stats.py b/cogs/stats.py index ebb2210..0ff5052 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -30,7 +30,7 @@ class Stats: # We need to page through, so lets create a loop and break when we find out we're done while True: # Simply get data based on the URL - data = await utils.request(url, headers=headers) + data = await utils.request(url, headers=headers, force_content_type_json=True) # First check if the data failed to retrieve, if so just return if data is None: return @@ -40,11 +40,12 @@ class Stats: # We only carry about the user's if include['type'] != 'user': continue - # This check checks the user's connected campaign (should only exist for *our* user) and checks if it matches + # This check checks the user's connected campaign (should only exist for *our* user) and checks if it + # matches if include.get('relationshipos', {}).get('campaign', {}).get('data', {}).get('id', {}) == str(utils.patreon_id): continue - # Otherwuse the only way this user was included, was if they are a patron, so include them + # Otherwise the only way this user was included, was if they are a patron, so include them name = include['attributes']['full_name'] if name: names.append(name) diff --git a/cogs/utils/utilities.py b/cogs/utils/utilities.py index fd81fa0..9c301d2 100644 --- a/cogs/utils/utilities.py +++ b/cogs/utils/utilities.py @@ -60,7 +60,7 @@ async def download_image(url): return image -async def request(url, *, headers=None, payload=None, method='GET', attr='json'): +async def request(url, *, headers=None, payload=None, method='GET', attr='json', force_content_type_json=False): # Make sure our User Agent is what's set, and ensure it's sent even if no headers are passed if headers is None: headers = {} @@ -83,7 +83,13 @@ async def request(url, *, headers=None, payload=None, method='GET', attr='json') return_value = getattr(response, attr) # Next check if this can be called if callable(return_value): - return_value = return_value() + # This is use for json; it checks the mimetype instead of checking if the actual data + # This causes some places with different mimetypes to fail, even if it's valid json + # This check allows us to force the content_type to use whatever content type is given + if force_content_type_json: + return_value = return_value(content_type=response.headers['content-type']) + else: + return_value = return_value() # If this is awaitable, await it if inspect.isawaitable(return_value): return_value = await return_value From 00b4d82c4e9b163814e2ac9f6dd69be25643e950 Mon Sep 17 00:00:00 2001 From: phxntxm Date: Mon, 10 Jul 2017 00:39:03 -0500 Subject: [PATCH 09/18] Pep8 --- cogs/stats.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/stats.py b/cogs/stats.py index 0ff5052..cb9b841 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -42,7 +42,8 @@ class Stats: continue # This check checks the user's connected campaign (should only exist for *our* user) and checks if it # matches - if include.get('relationshipos', {}).get('campaign', {}).get('data', {}).get('id', {}) == str(utils.patreon_id): + if include.get('relationshipos', {}).get('campaign', {}).get('data', {}).get('id', {}) == str( + utils.patreon_id): continue # Otherwise the only way this user was included, was if they are a patron, so include them @@ -59,7 +60,6 @@ class Stats: # Now just set the names self.donators = names - @commands.command() @commands.guild_only() @utils.custom_perms(send_messages=True) From 9c25e032b596e3850e5a1ec7bffa431150016883 Mon Sep 17 00:00:00 2001 From: phxntxm Date: Mon, 10 Jul 2017 00:43:32 -0500 Subject: [PATCH 10/18] Rearrange the command and alias since there is already a donator attribute --- cogs/stats.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/stats.py b/cogs/stats.py index cb9b841..d376710 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -325,10 +325,10 @@ class Stats: fmt = fmt.format(member.display_name, record, server_rank, overall_rank, rating) await ctx.send('```\n{}```'.format(fmt)) - @commands.command(aliases=['patrons']) + @commands.command(aliases=['donators']) @utils.custom_perms(send_messages=True) @utils.check_restricted() - async def donators(self, ctx): + async def patrons(self, ctx): """Prints a list of all the patrons for Bonfire EXAMPLE: !donators From e307a5262cd26e66ed745d1f2ba6096fd98c11db Mon Sep 17 00:00:00 2001 From: phxntxm Date: Tue, 11 Jul 2017 12:54:30 -0500 Subject: [PATCH 11/18] Don't log large image errors --- bot.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index f2244a4..4b2a463 100644 --- a/bot.py +++ b/bot.py @@ -80,7 +80,10 @@ async def on_command_error(ctx, error): try: if isinstance(error.original, discord.Forbidden): return - elif isinstance(error.original, discord.HTTPException) and ('empty message' in str(error.original) or 'INTERNAL SERVER ERROR' in str(error.original)): + elif isinstance(error.original, discord.HTTPException) and ( + 'empty message' in str(error.original) or + 'INTERNAL SERVER ERROR' in str(error.original) or + 'REQUEST ENTITY TOO LARGE' in str(error.original)): return elif isinstance(error.original, aiohttp.ClientOSError): return @@ -93,7 +96,7 @@ async def on_command_error(ctx, error): await ctx.message.channel.send(fmt) elif isinstance(error, commands.CheckFailure): fmt = "You can't tell me what to do!" - #await ctx.message.channel.send(fmt) + # await ctx.message.channel.send(fmt) elif isinstance(error, commands.CommandOnCooldown): m, s = divmod(error.retry_after, 60) fmt = "This command is on cooldown! Hold your horses! >:c\nTry again in {} minutes and {} seconds" \ From 7f261a81fadc3b21b5caaf6926f65daaaced8ce6 Mon Sep 17 00:00:00 2001 From: phxntxm Date: Tue, 11 Jul 2017 12:56:55 -0500 Subject: [PATCH 12/18] Save volume entries; handle some playlist quirks --- cogs/music.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/cogs/music.py b/cogs/music.py index 6005e24..5848541 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -1,5 +1,5 @@ from .voice_utilities import * -from discord import FFmpegPCMAudio, PCMVolumeTransformer +from discord import PCMVolumeTransformer import discord from discord.ext import commands @@ -21,7 +21,7 @@ if not discord.opus.is_loaded(): class VoiceState: - def __init__(self, guild, bot, user_queue=False): + def __init__(self, guild, bot, user_queue=False, volume=None): self.guild = guild self.songs = Playlist(bot) self.djs = deque() @@ -31,7 +31,7 @@ class VoiceState: self.skip_votes = set() self.user_queue = user_queue self.loop = bot.loop - self._volume = .5 + self._volume = volume or .5 @property def volume(self): @@ -377,8 +377,10 @@ class Music: # If we have connnected, create our voice state queue_type = self.bot.db.load('server_settings', key=channel.guild.id, pluck='queue_type') + volume = self.bot.db.load('server_settings', key=channel.guild.id, pluck='volume') user_queue = queue_type == "user" - self.voice_states[channel.guild.id] = VoiceState(channel.guild, self.bot, user_queue=user_queue) + self.voice_states[channel.guild.id] = VoiceState(channel.guild, self.bot, user_queue=user_queue, + volume=volume) # If we can send messages, edit it to let the channel know we have succesfully joined if msg: @@ -469,7 +471,8 @@ class Music: """Imports a song into the current voice queue""" # If we don't have a voice state yet, create one if not self.bot.db.load('server_settings', key=ctx.message.guild.id, pluck='playlists_allowed'): - await ctx.send("You cannot import playlists at this time; the {}allowplaylists command can be used to change this setting".format(ctx.prefix)) + await ctx.send("You cannot import playlists at this time; the {}allowplaylists command can be used to " + "change this setting".format(ctx.prefix)) return if ctx.message.guild.id not in self.voice_states: if not await ctx.invoke(self.join): @@ -490,7 +493,16 @@ class Music: return song = re.sub('[<>\[\]]', '', song) - await self.import_playlist(song, ctx) + # Check if we've got the list variable in the URL, if so lets just use this + playlist_id = re.search(r'list=(.+)', song) + if playlist_id: + song = playlist_id.group(1) + try: + await self.import_playlist(song, ctx) + except WrongEntryTypeError: + await ctx.send("This URL is not a playlist! If you want to play this song just use `play`") + except ExtractionError: + await ctx.send("Failed to download {}! If this is not a playlist, use the `play` command".format(song)) @commands.command() @commands.guild_only() @@ -537,6 +549,9 @@ class Music: try: entry = await self.add_entry(song, ctx) + # This error only happens if Discord has derped, and the voice state didn't get created succesfully + except KeyError: + await ctx.send("Sorry, but I failed to connect! Please try again!") except LiveStreamError as e: await ctx.send(str(e)) except WrongEntryTypeError: @@ -586,6 +601,8 @@ class Music: await ctx.send("Sorry but the max volume is 100%") else: state.volume = value + entry = {'server_id': str(ctx.message.guild.id), 'volume': value} + self.bot.db.save('server_settings', entry) await ctx.send('Set the volume to {:.0%}'.format(state.volume)) @commands.command() From c33b16136f2580043368c76abec75d879e526cab Mon Sep 17 00:00:00 2001 From: phxntxm Date: Tue, 11 Jul 2017 12:57:21 -0500 Subject: [PATCH 13/18] Change raffles so they're included in one table, per server --- cogs/raffle.py | 135 +++++++++++++++++++++++++------------------------ 1 file changed, 68 insertions(+), 67 deletions(-) diff --git a/cogs/raffle.py b/cogs/raffle.py index b7176c1..48f7826 100644 --- a/cogs/raffle.py +++ b/cogs/raffle.py @@ -8,7 +8,6 @@ import pendulum import re import asyncio import traceback -import rethinkdb as r class Raffle: @@ -36,63 +35,66 @@ class Raffle: for raffle in raffles: server = self.bot.get_guild(int(raffle['server_id'])) - title = raffle['title'] - entrants = raffle['entrants'] - raffle_id = raffle['id'] + for r in raffle['raffles']: + title = r['title'] + entrants = r['entrants'] + raffle_id = r['id'] - # Check to see if this cog can find the server in question - if server is None: - await self.bot.db.query(r.table('raffles').get(raffle_id).delete()) - continue + # Check to see if this cog can find the server in question + if server is None: + await self.bot.db.query(r.table('raffles').get(raffle_id).delete()) + continue - now = pendulum.utcnow() - expires = pendulum.parse(raffle['expires']) + now = pendulum.utcnow() + expires = pendulum.parse(r['expires']) - # Now lets compare and see if this raffle has ended, if not just continue - if expires > now: - continue + # Now lets compare and see if this raffle has ended, if not just continue + if expires > now: + continue - # Make sure there are actually entrants - if len(entrants) == 0: - fmt = 'Sorry, but there were no entrants for the raffle `{}`!'.format(title) - else: - winner = None - count = 0 - while winner is None: - winner = server.get_member(int(random.SystemRandom().choice(entrants))) - - # Lets make sure we don't get caught in an infinite loop - # Realistically having more than 50 random entrants found that aren't in the server anymore - # Isn't something that should be an issue, but better safe than sorry - count += 1 - if count >= 50: - break - - if winner is None: - fmt = 'I couldn\'t find an entrant that is still in this server, for the raffle `{}`!'.format(title) + # Make sure there are actually entrants + if len(entrants) == 0: + fmt = 'Sorry, but there were no entrants for the raffle `{}`!'.format(title) else: - fmt = 'The raffle `{}` has just ended! The winner is {}!'.format(title, winner.display_name) + winner = None + count = 0 + while winner is None: + winner = server.get_member(int(random.SystemRandom().choice(entrants))) - # Get the notifications settings, get the raffle setting - notifications = self.bot.db.load('server_settings', key=server.id, pluck='notifications') or {} - # Set our default to either the one set, or the default channel of the server - default_channel_id = notifications.get('default') or server.id - # If it is has been overriden by picarto notifications setting, use this - channel_id = notifications.get('raffle') or default_channel_id - channel = self.bot.get_channel(int(channel_id)) - if channel is None: - channel = server.default_channel - try: - await channel.send(fmt) - except (discord.Forbidden, AttributeError): - pass + # Lets make sure we don't get caught in an infinite loop + # Realistically having more than 50 random entrants found that aren't in the server anymore + # Isn't something that should be an issue, but better safe than sorry + count += 1 + if count >= 50: + break - # No matter which one of these matches were met, the raffle has ended and we want to remove it - await self.bot.db.query(r.table('raffles').get(raffle_id).delete()) - # Now...this is an ugly idea yes, but due to the way raffles are setup currently (they'll be changed in - # the future) The cache does not update, and leaves behind this deletion....so we need to manually update - # the cache here - await self.bot.db.cache.get('raffles').refresh() + if winner is None: + fmt = 'I couldn\'t find an entrant that is still in this server, for the raffle `{}`!'.format( + title) + else: + fmt = 'The raffle `{}` has just ended! The winner is {}!'.format(title, winner.display_name) + + # Get the notifications settings, get the raffle setting + notifications = self.bot.db.load('server_settings', key=server.id, pluck='notifications') or {} + # Set our default to either the one set, or the default channel of the server + default_channel_id = notifications.get('default') or server.id + # If it is has been overriden by picarto notifications setting, use this + channel_id = notifications.get('raffle') or default_channel_id + channel = self.bot.get_channel(int(channel_id)) + if channel is None: + channel = server.default_channel + try: + await channel.send(fmt) + except (discord.Forbidden, AttributeError): + pass + + # No matter which one of these matches were met, the raffle has ended and we want to remove it + raffle['raffles'].remove(r) + entry = { + 'server_id': raffle['server_id'], + 'raffles': raffle['raffles'] + } + self.bot.db.save('raffles', entry) @commands.command() @commands.guild_only() @@ -103,8 +105,7 @@ class Raffle: EXAMPLE: !raffles RESULT: A list of the raffles setup on this server""" - r_filter = {'server_id': str(ctx.message.guild.id)} - raffles = self.bot.db.load('raffles', table_filter=r_filter) + raffles = self.bot.db.load('raffles', key=ctx.message.guild.id, pluck='raffles') if not raffles: await ctx.send("There are currently no raffles setup on this server!") return @@ -133,19 +134,15 @@ class Raffle: RESULT: You've entered the first raffle!""" # Lets let people use 1 - (length of raffles) and handle 0 base ourselves raffle_id -= 1 - r_filter = {'server_id': str(ctx.message.guild.id)} author = ctx.message.author + key = str(ctx.message.guild.id) - raffles = self.bot.db.load('raffles', table_filter=r_filter) + raffles = self.bot.db.load('raffles', key=key, pluck='raffles') if raffles is None: await ctx.send("There are currently no raffles setup on this server!") return - if isinstance(raffles, list): - raffle_count = len(raffles) - else: - raffles = [raffles] - raffle_count = 1 + raffle_count = len(raffles) # There is only one raffle, so use the first's info if raffle_count == 1: @@ -157,8 +154,8 @@ class Raffle: entrants.append(str(author.id)) update = { - 'entrants': entrants, - 'id': raffles[0]['id'] + 'raffles': raffles, + 'server_id': key } self.bot.db.save('raffles', update) await ctx.send("{} you have just entered the raffle!".format(author.mention)) @@ -175,8 +172,8 @@ class Raffle: # Since we have no good thing to filter things off of, lets use the internal rethinkdb id update = { - 'entrants': entrants, - 'id': raffles[raffle_id]['id'] + 'raffles': raffles, + 'server_id': key } self.bot.db.save('raffles', update) await ctx.send("{} you have just entered the raffle!".format(author.mention)) @@ -270,11 +267,15 @@ class Raffle: 'expires': expires.to_datetime_string(), 'entrants': [], 'author': str(author.id), - 'server_id': str(server.id) } - # We don't want to pass a filter to this, because we can have multiple raffles per server - self.bot.db.save('raffles', entry) + raffles = self.bot.db.load('raffles', key=server.id, pluck='raffles') or [] + raffles.append(entry) + update = { + 'server_id': str(server.id), + 'raffles': raffles + } + self.bot.db.save('raffles', update) await ctx.send("I have just saved your new raffle!") @raffle.command(name='alerts') From 5f571567a3cc0814d6a1ad5841e5893b67b57184 Mon Sep 17 00:00:00 2001 From: phxntxm Date: Tue, 11 Jul 2017 12:59:24 -0500 Subject: [PATCH 14/18] Handle if something that's not a playlist is given --- cogs/voice_utilities/playlist.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cogs/voice_utilities/playlist.py b/cogs/voice_utilities/playlist.py index e839700..f35d9eb 100644 --- a/cogs/voice_utilities/playlist.py +++ b/cogs/voice_utilities/playlist.py @@ -51,11 +51,7 @@ class Playlist(EventEmitter): Imports the songs from `playlist_url` and queues them to be played. Returns a list of `entries` that have been enqueued. - - :param playlist_url: The playlist url to be cut into individual urls and added to the playlist """ - position = len(self.entries) + 1 - entry_list = [] try: info = await self.downloader.safe_extract_info(self.loop, playlist_url, download=False) @@ -65,6 +61,9 @@ class Playlist(EventEmitter): if not info: raise ExtractionError('Could not extract information from %s' % playlist_url) + if info.get('playlist') is None: + raise WrongEntryTypeError('This is not a playlist!') + # Once again, the generic extractor fucks things up. if info.get('extractor', None) == 'generic': url_field = 'url' From 368914a7c774b5cb49c5ccc6295471fe7e5c68fa Mon Sep 17 00:00:00 2001 From: phxntxm Date: Tue, 11 Jul 2017 14:34:45 -0500 Subject: [PATCH 15/18] Rework the info command --- cogs/misc.py | 78 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/cogs/misc.py b/cogs/misc.py index 0cdb367..5b07d9a 100644 --- a/cogs/misc.py +++ b/cogs/misc.py @@ -171,52 +171,68 @@ class Miscallaneous: # in this command is changed # Create the original embed object - opts = {'title': 'Dev Server', - 'description': 'Join the server above for any questions/suggestions about me.', - 'url': utils.dev_server} - embed = discord.Embed(**opts) + # Set the description include dev server (should be required) and the optional patreon link + description = "[Dev Server]({})".format(utils.dev_server) + if utils.patreon_link: + description += "[Patreon]({})".format(utils.patreon_link) + # Now creat the object + opts = {'title': 'Bonfire', + 'description': description, + 'colour': discord.Colour.green()} + + # Set the owner + embed = discord.Embed(**opts) + if hasattr(self.bot, 'owner'): + embed.set_author(name=str(self.bot.owner), icon_url=self.bot.avatar_url) + + # Setup the process statistics + name = "Process stats" + value = "" + + memory_usage = self.process.memory_full_info().uss / 1024 ** 2 + cpu_usage = self.process.cpu_percent() / psutil.cpu_count() + value += 'Memory: {:.2f} MiB'.format(memory_usage) + value += '\nCPU: {}%'.format(cpu_usage) + if hasattr(self.bot, 'uptime'): + value += (pendulum.utcnow() - self.bot.uptime).in_words() + embed.add_field(name=name, value=value, inline=False) + + # Setup the user and guild statistics + name = "User/Guild stats" + value = "" + + value += "Channels: {}".format(len(list(self.bot.get_all_channels()))) + value += "\nUsers: {}".format(len(self.bot.users)) + value += "\nServers: {}".format(len(self.bot.guilds)) + embed.add_field(name=name, value=value, inline=False) + + # The game statistics + name = "Game statistics" + # To get the newlines right, since we're not sure what will and won't be included + # Lets make this one a list and join it at the end + value = [] - # Add the normal values - embed.add_field(name='Total Servers', value=len(self.bot.guilds)) - embed.add_field(name='Total Members', value=len(self.bot.users)) hm = self.bot.get_cog('Hangman') ttt = self.bot.get_cog('TicTacToe') bj = self.bot.get_cog('Blackjack') - interaction = self.bot.get_cog('Blackjack') + interaction = self.bot.get_cog('Interaction') music = self.bot.get_cog('Music') - # Count the variable values; hangman, tictactoe, etc. if hm: - hm_games = len(hm.games) - if hm_games: - embed.add_field(name='Total Hangman games running', value=hm_games) + value.append("Hangman games: {}".format(len(hm.games))) if ttt: - ttt_games = len(ttt.boards) - if ttt_games: - embed.add_field(name='Total TicTacToe games running', value=ttt_games) + value.append("TicTacToe games: {}".format(len(ttt.boards))) if bj: - bj_games = len(bj.games) - if bj_games: - embed.add_field(name='Total blackjack games running', value=bj_games) + value.append("Blackjack games: {}".format(len(bj.games))) if interaction: count_battles = 0 for battles in self.bot.get_cog('Interaction').battles.values(): count_battles += len(battles) - if count_battles: - embed.add_field(name='Total battles games running', value=count_battles) + value.append("Battles running: {}".format(len(bj.games))) if music: songs = len([x for x in music.voice_states.values() if x.playing]) - if songs: - embed.add_field(name='Total songs playing', value=songs) - - if hasattr(self.bot, 'uptime'): - embed.add_field(name='Uptime', value=(pendulum.utcnow() - self.bot.uptime).in_words()) - - memory_usage = self.process.memory_full_info().uss / 1024 ** 2 - cpu_usage = self.process.cpu_percent() / psutil.cpu_count() - embed.add_field(name='Memory Usage', value='{:.2f} MiB'.format(memory_usage)) - embed.add_field(name='CPU Usage', value='{}%'.format(cpu_usage)) - embed.set_footer(text=self.bot.description) + value.append("Total songs playing: {}".format(songs)) + embed.add_field(name=name, value="\n".join(value), inline=False) await ctx.send(embed=embed) From fab533c07adadbe23c97ec380db8f57d19d71a59 Mon Sep 17 00:00:00 2001 From: phxntxm Date: Tue, 11 Jul 2017 14:35:44 -0500 Subject: [PATCH 16/18] Add patreon_link to config --- cogs/utils/config.py | 203 ++++++++++++++++++++++--------------------- 1 file changed, 102 insertions(+), 101 deletions(-) diff --git a/cogs/utils/config.py b/cogs/utils/config.py index 5b08903..04cda24 100644 --- a/cogs/utils/config.py +++ b/cogs/utils/config.py @@ -1,101 +1,102 @@ -import ruamel.yaml as yaml -import asyncio -import rethinkdb as r -import pendulum - -loop = asyncio.get_event_loop() -global_config = {} - -# Ensure that the required config.yml file actually exists -try: - with open("config.yml", "r") as f: - global_config = yaml.safe_load(f) - global_config = {k: v for k, v in global_config.items() if v} -except FileNotFoundError: - print("You have no config file setup! Please use config.yml.sample to setup a valid config file") - quit() - -try: - bot_token = global_config["bot_token"] -except KeyError: - print("You have no bot_token saved, this is a requirement for running a bot.") - print("Please use config.yml.sample to setup a valid config file") - quit() - - -# Default bot's description -bot_description = global_config.get("description") -# Bot's default prefix for commands -default_prefix = global_config.get("command_prefix", "!") -# The key for bots.discord.pw and carbonitex -discord_bots_key = global_config.get('discord_bots_key', "") -carbon_key = global_config.get('carbon_key', "") -# The client ID for twitch requsets -twitch_key = global_config.get('twitch_key', "") -# The key for youtube API calls -youtube_key = global_config.get("youtube_key", "") -# The key for Osu API calls -osu_key = global_config.get('osu_key', '') -# The key for League of Legends API calls -lol_key = global_config.get('lol_key', '') -# The keys needed for deviant art calls -# The invite link for the server made for the bot -dev_server = global_config.get("dev_server", "") -# The User-Agent that we'll use for most requests -user_agent = global_config.get('user_agent', None) -# The URL to proxy youtube_dl's requests through -ytdl_proxy = global_config.get('youtube_dl_proxy', None) -# The patreon key, as well as the patreon ID to use -patreon_key = global_config.get('patreon_key', None) -patreon_id = global_config.get('patreon_id', None) -# The extensions to load -extensions = [ - 'cogs.interaction', - 'cogs.misc', - 'cogs.mod', - 'cogs.admin', - 'cogs.images', - 'cogs.owner', - 'cogs.stats', - 'cogs.picarto', - 'cogs.overwatch', - 'cogs.links', - 'cogs.roles', - 'cogs.tictactoe', - 'cogs.hangman', 'cogs.events', 'cogs.raffle', - 'cogs.twitch', - 'cogs.blackjack', - 'cogs.osu', - 'cogs.tags', - 'cogs.roulette', - 'cogs.music', - 'cogs.polls', - 'cogs.playlist', - 'cogs.dj' -] - - -# The default status the bot will use -default_status = global_config.get("default_status", None) -# The rethinkdb hostname -db_host = global_config.get('db_host', 'localhost') -# The rethinkdb database name -db_name = global_config.get('db_name', 'Discord_Bot') -# The rethinkdb certification -db_cert = global_config.get('db_cert', '') -# The rethinkdb port -db_port = global_config.get('db_port', 28015) -# The user and password assigned -db_user = global_config.get('db_user', 'admin') -db_pass = global_config.get('db_pass', '') -# We've set all the options we need to be able to connect -# so create a dictionary that we can use to unload to connect -# db_opts = {'host': db_host, 'db': db_name, 'port': db_port, 'ssl': -# {'ca_certs': db_cert}, 'user': db_user, 'password': db_pass} -db_opts = {'host': db_host, 'db': db_name, 'port': db_port, 'user': db_user, 'password': db_pass} - - -def command_prefix(bot, message): - if not message.guild: - return default_prefix - return bot.db.load('server_settings', key=message.guild.id, pluck='prefix') or default_prefix +import ruamel.yaml as yaml +import asyncio +import rethinkdb as r +import pendulum + +loop = asyncio.get_event_loop() +global_config = {} + +# Ensure that the required config.yml file actually exists +try: + with open("config.yml", "r") as f: + global_config = yaml.safe_load(f) + global_config = {k: v for k, v in global_config.items() if v} +except FileNotFoundError: + print("You have no config file setup! Please use config.yml.sample to setup a valid config file") + quit() + +try: + bot_token = global_config["bot_token"] +except KeyError: + print("You have no bot_token saved, this is a requirement for running a bot.") + print("Please use config.yml.sample to setup a valid config file") + quit() + +# Default bot's description +bot_description = global_config.get("description") +# Bot's default prefix for commands +default_prefix = global_config.get("command_prefix", "!") +# The key for bots.discord.pw and carbonitex +discord_bots_key = global_config.get('discord_bots_key', "") +carbon_key = global_config.get('carbon_key', "") +# The client ID for twitch requsets +twitch_key = global_config.get('twitch_key', "") +# The key for youtube API calls +youtube_key = global_config.get("youtube_key", "") +# The key for Osu API calls +osu_key = global_config.get('osu_key', '') +# The key for League of Legends API calls +lol_key = global_config.get('lol_key', '') +# The keys needed for deviant art calls +# The invite link for the server made for the bot +dev_server = global_config.get("dev_server", "") +# The User-Agent that we'll use for most requests +user_agent = global_config.get('user_agent', None) +# The URL to proxy youtube_dl's requests through +ytdl_proxy = global_config.get('youtube_dl_proxy', None) +# The patreon key, as well as the patreon ID to use +patreon_key = global_config.get('patreon_key', None) +patreon_id = global_config.get('patreon_id', None) +patreon_link = global_config.get('patreon_link', None) + +# The extensions to load +extensions = [ + 'cogs.interaction', + 'cogs.misc', + 'cogs.mod', + 'cogs.admin', + 'cogs.images', + 'cogs.owner', + 'cogs.stats', + 'cogs.picarto', + 'cogs.overwatch', + 'cogs.links', + 'cogs.roles', + 'cogs.tictactoe', + 'cogs.hangman', 'cogs.events', 'cogs.raffle', + 'cogs.twitch', + 'cogs.blackjack', + 'cogs.osu', + 'cogs.tags', + 'cogs.roulette', + 'cogs.music', + 'cogs.polls', + 'cogs.playlist', + 'cogs.dj' +] + + +# The default status the bot will use +default_status = global_config.get("default_status", None) +# The rethinkdb hostname +db_host = global_config.get('db_host', 'localhost') +# The rethinkdb database name +db_name = global_config.get('db_name', 'Discord_Bot') +# The rethinkdb certification +db_cert = global_config.get('db_cert', '') +# The rethinkdb port +db_port = global_config.get('db_port', 28015) +# The user and password assigned +db_user = global_config.get('db_user', 'admin') +db_pass = global_config.get('db_pass', '') +# We've set all the options we need to be able to connect +# so create a dictionary that we can use to unload to connect +# db_opts = {'host': db_host, 'db': db_name, 'port': db_port, 'ssl': +# {'ca_certs': db_cert}, 'user': db_user, 'password': db_pass} +db_opts = {'host': db_host, 'db': db_name, 'port': db_port, 'user': db_user, 'password': db_pass} + + +def command_prefix(bot, message): + if not message.guild: + return default_prefix + return bot.db.load('server_settings', key=message.guild.id, pluck='prefix') or default_prefix From 048af76400dad5cf2691fabaab6f6864a71df0b7 Mon Sep 17 00:00:00 2001 From: phxntxm Date: Tue, 11 Jul 2017 14:36:24 -0500 Subject: [PATCH 17/18] Correct attribute lookup on owner --- cogs/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/misc.py b/cogs/misc.py index 5b07d9a..2c50646 100644 --- a/cogs/misc.py +++ b/cogs/misc.py @@ -183,7 +183,7 @@ class Miscallaneous: # Set the owner embed = discord.Embed(**opts) if hasattr(self.bot, 'owner'): - embed.set_author(name=str(self.bot.owner), icon_url=self.bot.avatar_url) + embed.set_author(name=str(self.bot.owner), icon_url=self.bot.owner.avatar_url) # Setup the process statistics name = "Process stats" From 45d80bf6b937aa3ea179d4b23b8f8162c403f143 Mon Sep 17 00:00:00 2001 From: phxntxm Date: Tue, 11 Jul 2017 14:38:12 -0500 Subject: [PATCH 18/18] Newline corrections --- cogs/misc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/misc.py b/cogs/misc.py index 2c50646..7781112 100644 --- a/cogs/misc.py +++ b/cogs/misc.py @@ -174,7 +174,7 @@ class Miscallaneous: # Set the description include dev server (should be required) and the optional patreon link description = "[Dev Server]({})".format(utils.dev_server) if utils.patreon_link: - description += "[Patreon]({})".format(utils.patreon_link) + description += "\n[Patreon]({})".format(utils.patreon_link) # Now creat the object opts = {'title': 'Bonfire', 'description': description, @@ -194,7 +194,7 @@ class Miscallaneous: value += 'Memory: {:.2f} MiB'.format(memory_usage) value += '\nCPU: {}%'.format(cpu_usage) if hasattr(self.bot, 'uptime'): - value += (pendulum.utcnow() - self.bot.uptime).in_words() + value += "\nUptime: {}".format((pendulum.utcnow() - self.bot.uptime).in_words()) embed.add_field(name=name, value=value, inline=False) # Setup the user and guild statistics