From 50d08e469d1090f1887234c28dc4c26ae272a05a Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sat, 11 Feb 2017 17:14:31 -0600 Subject: [PATCH 01/23] Added a paginator method to use embeds --- cogs/utils/paginator.py | 192 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 cogs/utils/paginator.py diff --git a/cogs/utils/paginator.py b/cogs/utils/paginator.py new file mode 100644 index 0000000..316729c --- /dev/null +++ b/cogs/utils/paginator.py @@ -0,0 +1,192 @@ +import asyncio +import discord + +class CannotPaginate(Exception): + pass + +class Pages: + """Implements a paginator that queries the user for the + pagination interface. + + Pages are 1-index based, not 0-index based. + + If the user does not reply within 2 minutes, the pagination + interface exits automatically. + """ + def __init__(self, bot, *, message, entries, per_page=12): + self.bot = bot + self.entries = entries + self.message = message + self.author = message.author + self.per_page = per_page + pages, left_over = divmod(len(self.entries), self.per_page) + if left_over: + pages += 1 + self.maximum_pages = pages + self.embed = discord.Embed() + self.paginating = len(entries) > per_page + self.reaction_emojis = [ + ('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.first_page), + ('\N{BLACK LEFT-POINTING TRIANGLE}', self.previous_page), + ('\N{BLACK RIGHT-POINTING TRIANGLE}', self.next_page), + ('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.last_page), + ('\N{INPUT SYMBOL FOR NUMBERS}', self.numbered_page ), + ('\N{BLACK SQUARE FOR STOP}', self.stop_pages), + ('\N{INFORMATION SOURCE}', self.show_help), + ] + + server = self.message.server + if server is not None: + self.permissions = self.message.channel.permissions_for(server.me) + else: + self.permissions = self.message.channel.permissions_for(self.bot.user) + + if not self.permissions.embed_links: + raise CannotPaginate('Bot does not have embed links permission.') + + def get_page(self, page): + base = (page - 1) * self.per_page + return self.entries[base:base + self.per_page] + + async def show_page(self, page, *, first=False): + self.current_page = page + entries = self.get_page(page) + p = [] + for t in enumerate(entries, 1 + ((page - 1) * self.per_page)): + p.append('%s. %s' % t) + + self.embed.set_footer(text='Page %s/%s (%s entries)' % (page, self.maximum_pages, len(self.entries))) + + if not self.paginating: + self.embed.description = '\n'.join(p) + return await self.bot.send_message(self.message.channel, embed=self.embed) + + if not first: + self.embed.description = '\n'.join(p) + await self.bot.edit_message(self.message, embed=self.embed) + return + + # verify we can actually use the pagination session + if not self.permissions.add_reactions: + raise CannotPaginate('Bot does not have add reactions permission.') + + if not self.permissions.read_message_history: + raise CannotPaginate('Bot does not have Read Message History permission.') + + p.append('') + p.append('Confused? React with \N{INFORMATION SOURCE} for more info.') + self.embed.description = '\n'.join(p) + self.message = await self.bot.send_message(self.message.channel, embed=self.embed) + for (reaction, _) in self.reaction_emojis: + if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'): + # no |<< or >>| buttons if we only have two pages + # we can't forbid it if someone ends up using it but remove + # it from the default set + continue + + await self.bot.add_reaction(self.message, reaction) + + async def checked_show_page(self, page): + if page != 0 and page <= self.maximum_pages: + await self.show_page(page) + + async def first_page(self): + """goes to the first page""" + await self.show_page(1) + + async def last_page(self): + """goes to the last page""" + await self.show_page(self.maximum_pages) + + async def next_page(self): + """goes to the next page""" + await self.checked_show_page(self.current_page + 1) + + async def previous_page(self): + """goes to the previous page""" + await self.checked_show_page(self.current_page - 1) + + async def show_current_page(self): + if self.paginating: + await self.show_page(self.current_page) + + async def numbered_page(self): + """lets you type a page number to go to""" + to_delete = [] + to_delete.append(await self.bot.send_message(self.message.channel, 'What page do you want to go to?')) + msg = await self.bot.wait_for_message(author=self.author, channel=self.message.channel, + check=lambda m: m.content.isdigit(), timeout=30.0) + if msg is not None: + page = int(msg.content) + to_delete.append(msg) + if page != 0 and page <= self.maximum_pages: + await self.show_page(page) + else: + to_delete.append(await self.bot.say('Invalid page given. (%s/%s)' % (page, self.maximum_pages))) + await asyncio.sleep(5) + else: + to_delete.append(await self.bot.send_message(self.message.channel, 'Took too long.')) + await asyncio.sleep(5) + + try: + await self.bot.delete_messages(to_delete) + except Exception: + pass + + async def show_help(self): + """shows this message""" + e = discord.Embed() + messages = ['Welcome to the interactive paginator!\n'] + messages.append('This interactively allows you to see pages of text by navigating with ' \ + 'reactions. They are as follows:\n') + + for (emoji, func) in self.reaction_emojis: + messages.append('%s %s' % (emoji, func.__doc__)) + + e.description = '\n'.join(messages) + e.colour = 0x738bd7 # blurple + e.set_footer(text='We were on page %s before this message.' % self.current_page) + await self.bot.edit_message(self.message, embed=e) + + async def go_back_to_current_page(): + await asyncio.sleep(60.0) + await self.show_current_page() + + self.bot.loop.create_task(go_back_to_current_page()) + + async def stop_pages(self): + """stops the interactive pagination session""" + await self.bot.delete_message(self.message) + self.paginating = False + + def react_check(self, reaction, user): + if user is None or user.id != self.author.id: + return False + + for (emoji, func) in self.reaction_emojis: + if reaction.emoji == emoji: + self.match = func + return True + return False + + async def paginate(self): + """Actually paginate the entries and run the interactive loop if necessary.""" + await self.show_page(1, first=True) + + while self.paginating: + react = await self.bot.wait_for_reaction(message=self.message, check=self.react_check, timeout=120.0) + if react is None: + self.paginating = False + try: + await self.bot.clear_reactions(self.message) + except: + pass + finally: + break + + try: + await self.bot.remove_reaction(self.message, react.reaction.emoji, react.user) + except: + pass # can't remove it so don't bother doing so + + await self.match() From 81c2f1b7f6b6740702567d3a26968fc1785fa5c5 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sat, 11 Feb 2017 17:14:47 -0600 Subject: [PATCH 02/23] Added the paginator to the init package file --- cogs/utils/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cogs/utils/__init__.py b/cogs/utils/__init__.py index ceb3694..4570514 100644 --- a/cogs/utils/__init__.py +++ b/cogs/utils/__init__.py @@ -3,3 +3,4 @@ from .checks import is_owner, custom_perms, is_pm from .config import * from .utilities import * from .images import create_banner +from .paginator import Page From baa8c74beb6f2687fc4145d9168276f83f6c7656 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sat, 11 Feb 2017 17:14:57 -0600 Subject: [PATCH 03/23] Used the paginator for the help commands --- cogs/core.py | 159 +++++++-------------------------------------------- 1 file changed, 20 insertions(+), 139 deletions(-) diff --git a/cogs/core.py b/cogs/core.py index 194f358..f2b883b 100644 --- a/cogs/core.py +++ b/cogs/core.py @@ -1,8 +1,7 @@ import discord from discord.ext import commands -from .utils import checks -from .utils import config -from .utils import utilities + +from . import utils import subprocess import glob @@ -24,119 +23,6 @@ class Core: self.results_per_page = 10 self.commands = None - async def on_reaction_add(self, reaction, user): - # Make sure that this is a normal user who pressed the button - # Also make sure that this is even a message we should be paying attention to - if user.bot or reaction.message.id not in self.help_embeds: - return - - # If right is clicked - if '\u27A1' in reaction.emoji: - embed = self.next_page(reaction.message.id) - # If left is clicked - elif '\u2B05' in reaction.emoji: - embed = self.prev_page(reaction.message.id) - else: - return - - await self.bot.edit_message(reaction.message, embed=embed) - try: - await self.bot.remove_reaction(reaction.message, reaction.emoji, user) - except discord.Forbidden: - pass - - async def on_reaction_remove(self, reaction, user): - # Make sure that this is a normal user who pressed the button - # Also make sure that this is even a message we should be paying attention to - # We need reaction_add and reaction_move for cases like PM's, where the bot cannot remove a reaction - # This causes an error when the bot tries to remove the reaction, leaving it in place - # So the next click from a user will remove it, not add it. - if user.bot or reaction.message.id not in self.help_embeds: - return - - # If right is clicked - if '\u27A1' in reaction.emoji: - embed = self.next_page(reaction.message.id) - # If left is clicked - elif '\u2B05' in reaction.emoji: - embed = self.prev_page(reaction.message.id) - else: - return - - await self.bot.edit_message(reaction.message, embed=embed) - try: - await self.bot.remove_reaction(reaction.message, reaction.emoji, user) - except discord.Forbidden: - pass - - def determine_commands(self, page): - """Returns the list of commands to use per page""" - - end_index = self.results_per_page * page - start_index = end_index - self.results_per_page - - return self.commands[start_index:end_index] - - def prev_page(self, message_id): - """Goes to the previus page""" - total_commands = len(self.commands) - # Increase the page count by one - page = self.help_embeds.get(message_id) - 1 - - total_pages = math.ceil(total_commands / self.results_per_page) - - # If we hit the zeroith page, set to the very last page - if page <= 0: - page = total_pages - # Set the new page - self.help_embeds[message_id] = page - # Now create our new embed - return self.create_help_embed(message_id=message_id) - - def next_page(self, message_id): - """Goes to the next page for this message""" - total_commands = len(self.commands) - # Increase the page count by one - page = self.help_embeds.get(message_id) + 1 - - total_pages = math.ceil(total_commands / self.results_per_page) - - # Make sure we don't reach past what we should; if we do, reset to page 1 - if page > total_pages: - page = 1 - - # Set the new page - self.help_embeds[message_id] = page - # Now create our new embed - return self.create_help_embed(message_id=message_id) - - def create_help_embed(self, message_id=None, page=1): - # If a message_id is provided, we need to get the new page (this is being sent by next/prev page buttons) - if message_id is not None: - page = self.help_embeds.get(message_id) - - # Refresh our command list - self.commands = sorted(utilities.get_all_commands(self.bot)) - - # Calculate the total amount of pages needed - total_commands = len(self.commands) - total_pages = math.ceil(total_commands / self.results_per_page) - - # Lets make sure that if a page was provided, it is within our range of pages available - if page < 1 or page > total_pages: - page = 1 - - # First create the embed object - opts = {"title": "Command List [{}/{}]".format(page, total_pages), - "description": "Run help on a specific command for more information on it!"} - embed = discord.Embed(**opts) - - # Add each field for the commands for this page - fmt = "\n".join(self.determine_commands(page)) - embed.add_field(name="Commands", value=fmt, inline=False) - - return embed - def find_command(self, command): # This method ensures the command given is valid. We need to loop through commands # As self.bot.commands only includes parent commands @@ -161,7 +47,7 @@ class Core: return cmd @commands.command(pass_context=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def help(self, ctx, *, message=None): """This command is used to provide a link to the help URL. This can be called on a command to provide more information about that command @@ -189,14 +75,9 @@ class Core: cmd = self.find_command(message) if cmd is None: - embed = self.create_help_embed(page=page) - msg = await self.bot.say(embed=embed) - - # Add the arrows for previous and next page - await self.bot.add_reaction(msg, '\N{LEFTWARDS BLACK ARROW}') - await self.bot.add_reaction(msg, '\N{BLACK RIGHTWARDS ARROW}') - # The only thing we need to record about this message, is the page number, starting at 1 - self.help_embeds[msg.id] = page + entries = sorted(utils.get_all_commands(self.bot)) + pages = utils.Pages(self.bot, message=ctx.message, entires=entries) + await pages.paginate() else: # Get the description for a command description = cmd.help @@ -209,7 +90,7 @@ class Core: example = None result = None # Also get the subcommands for this command, if they exist - subcommands = [x for x in utilities._get_all_commands(cmd) if x != cmd.qualified_name] + subcommands = [x for x in utils._get_all_commands(cmd) if x != cmd.qualified_name] # The rest is simple, create the embed, set the thumbail to me, add all fields if they exist embed = discord.Embed(title=cmd.qualified_name) @@ -226,7 +107,7 @@ class Core: await self.bot.say(embed=embed) @commands.command() - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def motd(self, *, date=None): """This command can be used to print the current MOTD (Message of the day) This will most likely not be updated every day, however messages will still be pushed to this every now and then @@ -234,7 +115,7 @@ class Core: EXAMPLE: !motd RESULT: 'This is an example message of the day!'""" if date is None: - motd = await config.get_content('motd') + motd = await utils.get_content('motd') try: # Lets set this to the first one in the list first latest_motd = motd[0] @@ -256,7 +137,7 @@ class Core: else: try: r_filter = pendulum.parse(date) - motd = await config.get_content('motd', r_filter) + motd = await utils.get_content('motd', r_filter) date = motd[0]['date'] motd = motd[0]['motd'] fmt = "Message of the day for {}:\n\n{}".format(date, motd) @@ -270,7 +151,7 @@ class Core: await self.bot.say("Invalid date format! Try like {}".format(now)) @commands.command() - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) 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 @@ -309,14 +190,14 @@ class Core: await self.bot.say("```\n{}```".format(cal)) @commands.command() - @checks.custom_perms(send_messages=True) + @utils.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 - bot_data = await config.get_content('bot_data') + bot_data = await utils.get_content('bot_data') total_data = {'member_count': 0, 'server_count': 0} for entry in bot_data: @@ -326,7 +207,7 @@ class Core: # Create the original embed object opts = {'title': 'Dev Server', 'description': 'Join the server above for any questions/suggestions about me.', - 'url': config.dev_server} + 'url': utils.dev_server} embed = discord.Embed(**opts) # Add the normal values @@ -359,7 +240,7 @@ class Core: await self.bot.say(embed=embed) @commands.command() - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def uptime(self): """Provides a printout of the current bot's uptime @@ -368,7 +249,7 @@ class Core: await self.bot.say("Uptime: ```\n{}```".format((pendulum.utcnow() - self.bot.uptime).in_words())) @commands.command(aliases=['invite']) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def addbot(self): """Provides a link that you can use to add me to a server @@ -389,7 +270,7 @@ class Core: .format(discord.utils.oauth_url(app_info.id, perms))) @commands.command() - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def doggo(self): """Use this to print a random doggo image. @@ -401,7 +282,7 @@ class Core: await self.bot.upload(f) @commands.command() - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def snek(self): """Use this to print a random snek image. @@ -413,7 +294,7 @@ class Core: await self.bot.upload(f) @commands.command() - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def joke(self): """Prints a random riddle @@ -432,7 +313,7 @@ class Core: break @commands.command(pass_context=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def roll(self, ctx, notation: str = "d6"): """Rolls a die based on the notation given Format should be #d# From c1862d04e8f8479789251b43131ded4022fe3273 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sat, 11 Feb 2017 17:18:40 -0600 Subject: [PATCH 04/23] Corrected syntax error --- cogs/utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utils/__init__.py b/cogs/utils/__init__.py index 4570514..9d964f4 100644 --- a/cogs/utils/__init__.py +++ b/cogs/utils/__init__.py @@ -3,4 +3,4 @@ from .checks import is_owner, custom_perms, is_pm from .config import * from .utilities import * from .images import create_banner -from .paginator import Page +from .paginator import Pages From 2eed78fb159adafa74948a75e800214049cbaf63 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sat, 11 Feb 2017 17:20:55 -0600 Subject: [PATCH 05/23] Corrected misspelling --- cogs/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/core.py b/cogs/core.py index f2b883b..cd3739d 100644 --- a/cogs/core.py +++ b/cogs/core.py @@ -76,7 +76,7 @@ class Core: if cmd is None: entries = sorted(utils.get_all_commands(self.bot)) - pages = utils.Pages(self.bot, message=ctx.message, entires=entries) + pages = utils.Pages(self.bot, message=ctx.message, entries=entries) await pages.paginate() else: # Get the description for a command From 996f30b5b3c0dd80631b1577c966a0c4c4cd989b Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sun, 12 Feb 2017 15:30:50 -0600 Subject: [PATCH 06/23] Used the paginator for the leaderboard --- cogs/stats.py | 51 +++++++++++++++++++++------------------------------ 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/cogs/stats.py b/cogs/stats.py index d150056..b7ef552 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -1,9 +1,8 @@ import discord from discord.ext import commands -from .utils import config -from .utils import checks -from .utils import images + +from . import utils import re @@ -30,7 +29,7 @@ class Stats: return cmd @commands.command(no_pm=True, pass_context=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def serverinfo(self, ctx): """Provides information about the server @@ -65,12 +64,12 @@ class Stats: await self.bot.say(embed=embed) @commands.group(no_pm=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def command(self): pass @command.command(no_pm=True, pass_context=True, name="stats") - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def command_stats(self, ctx, *, command): """This command can be used to view some usage stats about a specific command @@ -82,7 +81,7 @@ class Stats: return r_filter = {'command': cmd.qualified_name} - command_stats = await config.get_content('command_usage', r_filter) + command_stats = await utils.get_content('command_usage', r_filter) try: command_stats = command_stats[0] except TypeError: @@ -109,7 +108,7 @@ class Stats: await self.bot.say(fmt) @command.command(no_pm=True, pass_context=True, name="leaderboard") - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def command_leaderboard(self, ctx, option="server"): """This command can be used to print a leaderboard of commands Provide 'server' to print a leaderboard for this server @@ -120,7 +119,7 @@ class Stats: if re.search('(author|me)', option): author = ctx.message.author # First lets get all the command usage - command_stats = await config.get_content('command_usage') + command_stats = await utils.get_content('command_usage') # Now use a dictionary comprehension to get just the command name, and usage # Based on the author's usage of the command stats = {data['command']: data['member_usage'].get(author.id) for data in command_stats @@ -142,7 +141,7 @@ class Stats: elif re.search('server', option): # This is exactly the same as above, except server usage instead of member usage server = ctx.message.server - command_stats = await config.get_content('command_usage') + command_stats = await utils.get_content('command_usage') stats = {data['command']: data['server_usage'].get(server.id) for data in command_stats if data['server_usage'].get(server.id, 0) > 0} sorted_stats = sorted(stats.items(), key=lambda x: x[1], reverse=True) @@ -154,14 +153,14 @@ class Stats: await self.bot.say("That is not a valid option, valid options are: `server` or `me`") @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def mostboops(self, ctx): """Shows the person you have 'booped' the most, as well as how many times EXAMPLE: !mostboops RESULT: You've booped @OtherPerson 351253897120935712093572193057310298 times!""" r_filter = {'member_id': ctx.message.author.id} - boops = await config.get_content('boops', r_filter) + boops = await utils.get_content('boops', r_filter) if boops is None: await self.bot.say("You have not booped anyone {} Why the heck not...?".format(ctx.message.author.mention)) return @@ -182,17 +181,17 @@ class Stats: member = discord.utils.find(lambda m: m.id == most_id, self.bot.get_all_members()) await self.bot.say("{0} you have booped {1} the most amount of times, coming in at {2} times".format( - ctx.message.author.mention, member.mention, most_boops)) + ctx.message.author.mention, member.display_name, most_boops)) @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def listboops(self, ctx): """Lists all the users you have booped and the amount of times EXAMPLE: !listboops RESULT: The list of your booped members!""" r_filter = {'member_id': ctx.message.author.id} - boops = await config.get_content('boops', r_filter) + boops = await utils.get_content('boops', r_filter) if boops is None: await self.bot.say("You have not booped anyone {} Why the heck not...?".format(ctx.message.author.mention)) return @@ -219,7 +218,7 @@ class Stats: await self.bot.say("You have booped:```\n{}```".format(output)) @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def leaderboard(self, ctx): """Prints a leaderboard of everyone in the server's battling record @@ -227,32 +226,24 @@ class Stats: RESULT: A leaderboard of this server's battle records""" # Create a list of the ID's of all members in this server, for comparison to the records saved server_member_ids = [member.id for member in ctx.message.server.members] - battles = await config.get_content('battle_records') + battles = await utils.get_content('battle_records') battles = [battle for battle in battles if battle['member_id'] in server_member_ids] # Sort the members based on their rating sorted_members = sorted(battles, key=lambda k: k['rating'], reverse=True) output = [] - count = 1 for x in sorted_members: member_id = x['member_id'] rating = x['rating'] member = ctx.message.server.get_member(member_id) - output.append((count, "{} (Rating: {})".format(member.display_name, rating))) - count += 1 - if count >= 11: - break + output.append("{} (Rating: {})".format(member.display_name, rating)) - try: - banner = await images.create_banner(ctx.message.author, "Battling Leaderboard", output) - await self.bot.upload(banner) - except (FileNotFoundError, discord.Forbidden): - fmt = "\n".join("#{}) {}".format(key, value) for key, value in output) - await self.bot.say("Battling leaderboard for this server:```\n{}```".format(fmt)) + pages = utils.Pages(self.bot, message=ctx.message, entries=output, per_page=10) + await pages.paginate() @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def stats(self, ctx, member: discord.Member = None): """Prints the battling stats for you, or the user provided @@ -262,7 +253,7 @@ class Stats: # For this one, we don't want to pass a filter, as we do need all battle records # We need this because we want to make a comparison for overall rank - all_members = await config.get_content('battle_records') + all_members = await utils.get_content('battle_records') # Make a list comprehension to just check if the user has battled if len([entry for entry in all_members if entry['member_id'] == member.id]) == 0: From 541e1a0ce7706feed86d4bbd2327540ae754e37f Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sun, 12 Feb 2017 15:31:18 -0600 Subject: [PATCH 07/23] Updated to use 10 results per page --- cogs/utils/paginator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utils/paginator.py b/cogs/utils/paginator.py index 316729c..66c15be 100644 --- a/cogs/utils/paginator.py +++ b/cogs/utils/paginator.py @@ -13,7 +13,7 @@ class Pages: If the user does not reply within 2 minutes, the pagination interface exits automatically. """ - def __init__(self, bot, *, message, entries, per_page=12): + def __init__(self, bot, *, message, entries, per_page=10): self.bot = bot self.entries = entries self.message = message From 556111544975e2d670ca9781f3c556660a4a7811 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sun, 12 Feb 2017 15:39:57 -0600 Subject: [PATCH 08/23] Changed rules to use the paginator --- cogs/mod.py | 88 ++++++++++++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/cogs/mod.py b/cogs/mod.py index 248f952..ef44eae 100644 --- a/cogs/mod.py +++ b/cogs/mod.py @@ -1,6 +1,6 @@ from discord.ext import commands -from .utils import checks -from .utils import config + +from . import utils import discord import re @@ -40,7 +40,7 @@ class Mod: return cmd @commands.command(pass_context=True, no_pm=True, aliases=['nick']) - @checks.custom_perms(kick_members=True) + @utils.custom_perms(kick_members=True) async def nickname(self, ctx, *, name=None): """Used to set the nickname for Bonfire (provide no nickname and it will reset) @@ -50,7 +50,7 @@ class Mod: await self.bot.say("\N{OK HAND SIGN}") @commands.command(no_pm=True) - @checks.custom_perms(kick_members=True) + @utils.custom_perms(kick_members=True) async def kick(self, member: discord.Member): """Used to kick a member from this server @@ -63,7 +63,7 @@ class Mod: await self.bot.say("But I can't, muh permissions >:c") @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(ban_members=True) + @utils.custom_perms(ban_members=True) async def unban(self, ctx, member_id: int): """Used to unban a member from this server Due to the fact that I cannot find a user without being in a server with them @@ -84,7 +84,7 @@ class Mod: await self.bot.say("Sorry, I failed to unban that user!") @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(ban_members=True) + @utils.custom_perms(ban_members=True) async def ban(self, ctx, *, member): """Used to ban a member This can be used to ban someone preemptively as well. @@ -120,7 +120,7 @@ class Mod: await self.bot.say("Sorry, I failed to ban that user!") @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(kick_members=True) + @utils.custom_perms(kick_members=True) async def alerts(self, ctx, channel: discord.Channel): """This command is used to set a channel as the server's 'notifications' channel Any notifications (like someone going live on Twitch, or Picarto) will go to that channel @@ -130,13 +130,13 @@ class Mod: r_filter = {'server_id': ctx.message.server.id} entry = {'server_id': ctx.message.server.id, 'channel_id': channel.id} - if not await config.add_content('server_alerts', entry, r_filter): - await config.update_content('server_alerts', entry, r_filter) + if not await utils.add_content('server_alerts', entry, r_filter): + await utils.update_content('server_alerts', entry, r_filter) await self.bot.say("I have just changed this server's 'notifications' channel" "\nAll notifications will now go to `{}`".format(channel)) @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(kick_members=True) + @utils.custom_perms(kick_members=True) async def usernotify(self, ctx, on_off: str): """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 @@ -151,8 +151,8 @@ class Mod: r_filter = {'server_id': ctx.message.server.id} entry = {'server_id': ctx.message.server.id, 'channel_id': on_off} - if not await config.add_content('user_notifications', entry, r_filter): - await config.update_content('user_notifications', entry, r_filter) + if not await utils.add_content('user_notifications', entry, r_filter): + await utils.update_content('user_notifications', entry, r_filter) fmt = "notify" if on_off else "not notify" await self.bot.say("This server will now {} if someone has joined or left".format(fmt)) @@ -164,33 +164,33 @@ class Mod: await self.bot.say('Invalid subcommand passed: {0.subcommand_passed}'.format(ctx)) @nsfw.command(name="add", pass_context=True) - @checks.custom_perms(kick_members=True) + @utils.custom_perms(kick_members=True) async def nsfw_add(self, ctx): """Registers this channel as a 'nsfw' channel EXAMPLE: !nsfw add RESULT: ;)""" r_filter = {'channel_id': ctx.message.channel.id} - if await config.add_content('nsfw_channels', r_filter, r_filter): + if await utils.add_content('nsfw_channels', r_filter, r_filter): await self.bot.say("This channel has just been registered as 'nsfw'! Have fun you naughties ;)") else: await self.bot.say("This channel is already registered as 'nsfw'!") @nsfw.command(name="remove", aliases=["delete"], pass_context=True) - @checks.custom_perms(kick_members=True) + @utils.custom_perms(kick_members=True) async def nsfw_remove(self, ctx): """Removes this channel as a 'nsfw' channel EXAMPLE: !nsfw remove RESULT: ;(""" r_filter = {'channel_id': ctx.message.channel.id} - if await config.remove_content('nsfw_channels', r_filter): + if await utils.remove_content('nsfw_channels', r_filter): await self.bot.say("This channel has just been unregistered as a nsfw channel") else: await self.bot.say("This channel is not registered as a ''nsfw' channel!") @commands.command(pass_context=True) - @checks.custom_perms(kick_members=True) + @utils.custom_perms(kick_members=True) async def say(self, ctx, *, msg: str): """Tells the bot to repeat what you say @@ -204,7 +204,7 @@ class Mod: pass @commands.group(pass_context=True, invoke_without_command=True, no_pm=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def perms(self, ctx, *, command: str = None): """This command can be used to print the current allowed permissions on a specific command This supports groups as well as subcommands; pass no argument to print a list of available permissions @@ -217,7 +217,7 @@ class Mod: return r_filter = {'server_id': ctx.message.server.id} - server_perms = await config.get_content('custom_permissions', r_filter) + server_perms = await utils.get_content('custom_permissions', r_filter) try: server_perms = server_perms[0] except TypeError: @@ -231,14 +231,14 @@ class Mod: perms_value = server_perms.get(cmd.qualified_name) if perms_value is None: # If we don't find custom permissions, get the required permission for a command - # based on what we set in checks.custom_perms, if custom_perms isn't found, we'll get an IndexError + # based on what we set in utils.custom_perms, if custom_perms isn't found, we'll get an IndexError try: - custom_perms = [func for func in cmd.checks if "custom_perms" in func.__qualname__][0] + custom_perms = [func for func in cmd.utils if "custom_perms" in func.__qualname__][0] except IndexError: # Loop through and check if there is a check called is_owner # If we loop through and don't find one, this means that the only other choice is to be - # Able to manage the server (for the checks on perm commands) - for func in cmd.checks: + # Able to manage the server (for the utils on perm commands) + for func in cmd.utils: if "is_owner" in func.__qualname__: await self.bot.say("You need to own the bot to run this command") return @@ -304,7 +304,7 @@ class Mod: # The other case is if I'm using the default has_permissions case # Which means I do not want to check custom permissions at all # Currently the second case is only on adding and removing permissions, to avoid abuse on these - for check in cmd.checks: + for check in cmd.utils: 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!") return @@ -317,11 +317,11 @@ class Mod: # In this case, I'm going the other way around, to make the least queries # As custom permissions are probably going to be ran multiple times per server # Whereas in most other cases, the command is probably going to be ran once/few times per server - if not await config.update_content('custom_permissions', entry, r_filter): - await config.add_content('custom_permissions', entry, r_filter) + if not await utils.update_content('custom_permissions', entry, r_filter): + await utils.add_content('custom_permissions', entry, r_filter) # Same case as prefixes, for now, trigger a manual update - self.bot.loop.create_task(config.cache['custom_permissions'].update()) + self.bot.loop.create_task(utils.cache['custom_permissions'].update()) await self.bot.say("I have just added your custom permissions; " "you now need to have `{}` permissions to use the command `{}`".format(permissions, command)) @@ -341,14 +341,14 @@ class Mod: return r_filter = {'server_id': ctx.message.server.id} - await config.replace_content('custom_permissions', r.row.without(cmd.qualified_name), r_filter) + await utils.replace_content('custom_permissions', r.row.without(cmd.qualified_name), r_filter) await self.bot.say("I have just removed the custom permissions for {}!".format(cmd)) # Same case as prefixes, for now, trigger a manual update - self.bot.loop.create_task(config.cache['custom_permissions'].update()) + self.bot.loop.create_task(utils.cache['custom_permissions'].update()) @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(manage_server=True) + @utils.custom_perms(manage_server=True) async def prefix(self, ctx, *, prefix: str): """This command can be used to set a custom prefix per server @@ -361,8 +361,8 @@ class Mod: entry = {'server_id': ctx.message.server.id, 'prefix': prefix} - if not await config.add_content('prefixes', entry, r_filter): - await config.update_content('prefixes', entry, r_filter) + if not await utils.add_content('prefixes', entry, r_filter): + await utils.update_content('prefixes', entry, r_filter) if prefix is None: fmt = "I have just cleared your custom prefix, the default prefix will have to be used now" @@ -372,7 +372,7 @@ class Mod: await self.bot.say(fmt) @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(manage_messages=True) + @utils.custom_perms(manage_messages=True) async def purge(self, ctx, limit: int = 100): """This command is used to a purge a number of messages from the channel @@ -388,7 +388,7 @@ class Mod: " I can only bulk delete messages that are under 14 days old.") @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(manage_messages=True) + @utils.custom_perms(manage_messages=True) async def prune(self, ctx, limit: int = 100): """This command can be used to prune messages from certain members Mention any user you want to prune messages from; if no members are mentioned, the messages removed will be mine @@ -430,14 +430,14 @@ class Mod: pass @commands.group(aliases=['rule'], pass_context=True, no_pm=True, invoke_without_command=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def rules(self, ctx, rule: int = None): """This command can be used to view the current rules on the server EXAMPLE: !rules 5 RESULT: Rule 5 is printed""" r_filter = {'server_id': ctx.message.server.id} - rules = await config.get_content('rules', r_filter) + rules = await utils.get_content('rules', r_filter) try: rules = rules[0]['rules'] except TypeError: @@ -448,9 +448,9 @@ class Mod: return if rule is None: - # 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(rules)) - await self.bot.say('```\n{}```'.format(fmt)) + pages = utils.Pages(self.bot, message=ctx.message, entries=rules, per_page=5) + pages.title = "Rules for {}".format(ctx.message.server.name) + await pages.paginate() else: try: fmt = rules[rule - 1] @@ -460,7 +460,7 @@ class Mod: await self.bot.say("Rule {}: \"{}\"".format(rule, fmt)) @rules.command(name='add', aliases=['create'], pass_context=True, no_pm=True) - @checks.custom_perms(manage_server=True) + @utils.custom_perms(manage_server=True) async def rules_add(self, ctx, *, rule: str): """Adds a rule to this server's rules @@ -470,13 +470,13 @@ class Mod: entry = {'server_id': ctx.message.server.id, 'rules': [rule]} update = {'rules': r.row['rules'].append(rule)} - if not await config.update_content('rules', update, r_filter): - await config.add_content('rules', entry, r_filter) + if not await utils.update_content('rules', update, r_filter): + await utils.add_content('rules', entry, r_filter) await self.bot.say("I have just saved your new rule, use the rules command to view this server's current rules") @rules.command(name='remove', aliases=['delete'], pass_context=True, no_pm=True) - @checks.custom_perms(manage_server=True) + @utils.custom_perms(manage_server=True) async def rules_delete(self, ctx, rule: int): """Removes one of the rules from the list of this server's rules Provide a number to delete that rule @@ -485,7 +485,7 @@ class Mod: RESULT: Freedom from opression!""" r_filter = {'server_id': ctx.message.server.id} update = {'rules': r.row['rules'].delete_at(rule - 1)} - if not await config.update_content('rules', update, r_filter): + if not await utils.update_content('rules', update, r_filter): await self.bot.say("That is not a valid rule number, try running the command again.") else: await self.bot.say("I have just removed that rule from your list of rules!") From a40ca625838378f1322ec0d30bc7f45838276620 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sun, 12 Feb 2017 15:45:11 -0600 Subject: [PATCH 09/23] Corrected issue where the banner creation method wasn't found --- cogs/stats.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cogs/stats.py b/cogs/stats.py index b7ef552..3ade1b1 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -1,7 +1,6 @@ import discord from discord.ext import commands - from . import utils import re @@ -97,7 +96,7 @@ class Stats: ("Total Usage", total_usage), ("Your Usage", member_usage), ("This Server's Usage", server_usage)] - banner = await images.create_banner(ctx.message.author, "Command Stats", data) + banner = await utils.create_banner(ctx.message.author, "Command Stats", data) await self.bot.upload(banner) except (FileNotFoundError, discord.Forbidden): fmt = "The command {} has been used a total of {} times\n" \ @@ -132,7 +131,7 @@ class Stats: # As this can include, for example, all 3 if there are only 3 entries try: top_5 = [(data[0], data[1]) for data in sorted_stats[:5]] - banner = await images.create_banner(ctx.message.author, "Your command usage", top_5) + banner = await utils.create_banner(ctx.message.author, "Your command usage", top_5) await self.bot.upload(banner) except (FileNotFoundError, discord.Forbidden): top_5 = "\n".join("{}: {}".format(data[0], data[1]) for data in sorted_stats[:5]) @@ -209,7 +208,7 @@ class Stats: try: output = [("{0.display_name}".format(ctx.message.server.get_member(m_id)), amt) for m_id, amt in sorted_booped_members] - banner = await images.create_banner(ctx.message.author, "Your booped victims", output) + banner = await utils.create_banner(ctx.message.author, "Your booped victims", output) await self.bot.upload(banner) except (FileNotFoundError, discord.Forbidden): output = "\n".join( @@ -279,7 +278,7 @@ class Stats: title = 'Stats for {}'.format(member.display_name) fmt = [('Record', record), ('Server Rank', '{}/{}'.format(server_rank, len(server_members))), ('Overall Rank', '{}/{}'.format(total_rank, len(all_members))), ('Rating', rating)] - banner = await images.create_banner(member, title, fmt) + banner = await utils.create_banner(member, title, fmt) await self.bot.upload(banner) except (FileNotFoundError, discord.Forbidden): fmt = 'Stats for {}:\n\tRecord: {}\n\tServer Rank: {}/{}\n\tOverall Rank: {}/{}\n\tRating: {}' From 31ed6b32fbe9e41ad0b3c26cc6362fc436bdea49 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sun, 12 Feb 2017 15:54:21 -0600 Subject: [PATCH 10/23] Made get_all_commands available --- cogs/core.py | 2 +- cogs/utils/utilities.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/core.py b/cogs/core.py index cd3739d..522873a 100644 --- a/cogs/core.py +++ b/cogs/core.py @@ -90,7 +90,7 @@ class Core: example = None result = None # Also get the subcommands for this command, if they exist - subcommands = [x for x in utils._get_all_commands(cmd) if x != cmd.qualified_name] + subcommands = [x for x in utils.get_all_commands(cmd) if x != cmd.qualified_name] # The rest is simple, create the embed, set the thumbail to me, add all fields if they exist embed = discord.Embed(title=cmd.qualified_name) diff --git a/cogs/utils/utilities.py b/cogs/utils/utilities.py index 4669fe1..2eb3e08 100644 --- a/cogs/utils/utilities.py +++ b/cogs/utils/utilities.py @@ -19,7 +19,7 @@ def get_all_commands(bot): return all_commands -def _get_all_commands(command): +def get_all_commands(command): yield command.qualified_name try: non_aliases = set(cmd.name for cmd in command.commands.values()) From e90bab4a843ce94ae78a843d7208f0595ff526d8 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sun, 12 Feb 2017 15:55:49 -0600 Subject: [PATCH 11/23] Made get_all_commands available --- cogs/core.py | 2 +- cogs/utils/utilities.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cogs/core.py b/cogs/core.py index 522873a..ba7a646 100644 --- a/cogs/core.py +++ b/cogs/core.py @@ -90,7 +90,7 @@ class Core: example = None result = None # Also get the subcommands for this command, if they exist - subcommands = [x for x in utils.get_all_commands(cmd) if x != cmd.qualified_name] + subcommands = [x for x in utils.get_subcommands(cmd) if x != cmd.qualified_name] # The rest is simple, create the embed, set the thumbail to me, add all fields if they exist embed = discord.Embed(title=cmd.qualified_name) diff --git a/cogs/utils/utilities.py b/cogs/utils/utilities.py index 2eb3e08..cb71d0c 100644 --- a/cogs/utils/utilities.py +++ b/cogs/utils/utilities.py @@ -14,17 +14,17 @@ def get_all_commands(bot): # Only the command itself will be yielded if there are no children for cmd_name in parent_command_names: cmd = bot.commands.get(cmd_name) - for child_cmd in _get_all_commands(cmd): + for child_cmd in get_subcommands(cmd): all_commands.append(child_cmd) return all_commands -def get_all_commands(command): +def get_subcommands(command): yield command.qualified_name try: non_aliases = set(cmd.name for cmd in command.commands.values()) for cmd_name in non_aliases: - yield from _get_all_commands(command.commands[cmd_name]) + yield from get_subcommands(command.commands[cmd_name]) except AttributeError: pass From c560bc4210f21f0926d7a5e908eed928f4e9ca00 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sun, 12 Feb 2017 16:05:51 -0600 Subject: [PATCH 12/23] Added a check to ensure someone who joins after betting doesn't get checked to see if they won or not --- cogs/blackjack.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cogs/blackjack.py b/cogs/blackjack.py index 0b3bd9d..9bc8592 100644 --- a/cogs/blackjack.py +++ b/cogs/blackjack.py @@ -86,7 +86,7 @@ class Blackjack: @blackjack.command(pass_context=True, no_pm=True, name='forcestop', aliases=['stop']) @utils.custom_perms(manage_server=True) async def blackjack_stop(self, ctx): - """Forces the game to stop, mostly for use if someone has gone afk + """Forces the game to stop, mostly for use if someone has gone afk EXAMPLE: !blackjack forcestop RESULT: No more blackjack spam""" @@ -202,7 +202,7 @@ class Game: # The channel we'll send messages to self.channel = message.channel - # People can join in on this game, but lets make sure we don't go over the limit however + # People can join in on this game, but lets make sure we don't go over the limit however self._max_players = 10 # Lets create our main deck, and shuffle it @@ -379,6 +379,11 @@ class Game: for entry in self.players: player = entry['player'] + # Quick check here to ensure the player isn't someone who got added + # Specifically right after the betting phase + if not hasattr(player, 'bet'): + continue + hand = player.hand count = max(player.count) From 540ecf86fb3c9265e5b9dfba44fc386ddea40a2d Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sun, 12 Feb 2017 16:09:41 -0600 Subject: [PATCH 13/23] Added a check to ensure the data is not None. This shouldn't happen that often yet it seems to do so, why is your API so shitty DA? --- cogs/da.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cogs/da.py b/cogs/da.py index a120acf..1b72328 100644 --- a/cogs/da.py +++ b/cogs/da.py @@ -77,6 +77,8 @@ class Deviantart: if result is None: params['username'] = da_name data = await utils.request(self.base_url, payload=params) + if data is None: + continue result = data['results'][0] cache[da_name] = result From 59c48b323e663ff033e115de6b432891d63773ef Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sun, 12 Feb 2017 16:31:18 -0600 Subject: [PATCH 14/23] Added a couple more checks to voice, to catch when Discord says the connection was successful, yet it wasn't --- cogs/music.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cogs/music.py b/cogs/music.py index 8e3e64f..5474ecc 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -146,6 +146,8 @@ class Music: async def create_voice_client(self, channel): """Creates a voice client and saves it""" # First join the channel and get the VoiceClient that we'll use to save per server + await self.remove_voice_client(channel.server) + server = channel.server state = self.get_voice_state(server) voice = self.bot.voice_client_in(server) @@ -154,17 +156,21 @@ class Music: try: if voice is None: state.voice = await self.bot.join_voice_channel(channel) - return True + if state.voice: + return True elif voice.channel == channel: state.voice = voice return True else: + # This shouldn't theoretically ever happen yet it does. Thanks Discord await voice.disconnect() state.voice = await self.bot.join_voice_channel(channel) return True except (discord.ClientException, socket.gaierror): continue + return False + async def remove_voice_client(self, server): """Removes any voice clients from a server @@ -413,6 +419,12 @@ class Music: author_channel = ctx.message.author.voice.voice_channel my_channel = ctx.message.server.me.voice.voice_channel + if my_channel is None: + # If we're here this means that after 3 attempts...4 different "failsafes"... + # Discord has returned saying the connection was successful, and returned a None connection + await self.bot.say("I failed to connect to the channel! Please try again soon") + return + # 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.") From 275f2fad72603f72e46b8634e2e59903969e4781 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sun, 12 Feb 2017 16:40:11 -0600 Subject: [PATCH 15/23] Added a couple more checks to voice, to catch when Discord says the connection was successful, yet it wasn't --- cogs/music.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cogs/music.py b/cogs/music.py index 5474ecc..5f70c52 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -165,7 +165,8 @@ class Music: # This shouldn't theoretically ever happen yet it does. Thanks Discord await voice.disconnect() state.voice = await self.bot.join_voice_channel(channel) - return True + if state.voice: + return True except (discord.ClientException, socket.gaierror): continue From 84acd500421209df6dd3159d5c05088dec6d00f8 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sun, 12 Feb 2017 16:40:28 -0600 Subject: [PATCH 16/23] Added a common exception that's temporary, to stop getting logged --- bot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot.py b/bot.py index 8501ada..55e2187 100644 --- a/bot.py +++ b/bot.py @@ -5,6 +5,7 @@ import logging import datetime import pendulum import os +import aiohttp os.chdir(os.path.dirname(os.path.realpath(__file__))) @@ -88,6 +89,8 @@ async def on_command_error(error, ctx): return elif isinstance(error.original, discord.HTTPException) and 'empty message' in str(error.original): return + elif isinstance(error.original, aiohttp.ClientOSError): + return except AttributeError: pass From f9ce8ef85e1aa8548bd32021585ee80145a11eae Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sun, 12 Feb 2017 17:36:11 -0600 Subject: [PATCH 17/23] Added an exception catch for when permissions aren't given to the bot for pagination --- cogs/core.py | 5 ++++- cogs/mod.py | 5 ++++- cogs/music.py | 4 ++-- cogs/stats.py | 5 ++++- cogs/utils/__init__.py | 2 +- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/cogs/core.py b/cogs/core.py index ba7a646..b6c3c18 100644 --- a/cogs/core.py +++ b/cogs/core.py @@ -77,7 +77,10 @@ class Core: if cmd is None: entries = sorted(utils.get_all_commands(self.bot)) pages = utils.Pages(self.bot, message=ctx.message, entries=entries) - await pages.paginate() + try: + await pages.paginate() + except utils.CannotPaginate as e: + await self.bot.say(str(e)) else: # Get the description for a command description = cmd.help diff --git a/cogs/mod.py b/cogs/mod.py index ef44eae..57869aa 100644 --- a/cogs/mod.py +++ b/cogs/mod.py @@ -450,7 +450,10 @@ class Mod: if rule is None: pages = utils.Pages(self.bot, message=ctx.message, entries=rules, per_page=5) pages.title = "Rules for {}".format(ctx.message.server.name) - await pages.paginate() + try: + await pages.paginate() + except utils.CannotPaginate as e: + await self.bot.say(str(e)) else: try: fmt = rules[rule - 1] diff --git a/cogs/music.py b/cogs/music.py index 5f70c52..3e0fbfc 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -167,13 +167,13 @@ class Music: state.voice = await self.bot.join_voice_channel(channel) if state.voice: return True - except (discord.ClientException, socket.gaierror): + except (discord.ClientException, socket.gaierror, ConnectionResetError): continue return False - async def remove_voice_client(self, server): + async def remove_voice_client(self, server):greendsi_webimsuan """Removes any voice clients from a server This is sometimes needed, due to the unreliability of Discord's voice connection We do not want to end up with a voice client stuck somewhere, so this cancels any found for a server""" diff --git a/cogs/stats.py b/cogs/stats.py index 3ade1b1..a4629f0 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -239,7 +239,10 @@ class Stats: output.append("{} (Rating: {})".format(member.display_name, rating)) pages = utils.Pages(self.bot, message=ctx.message, entries=output, per_page=10) - await pages.paginate() + try: + await pages.paginate() + except utils.CannotPaginate as e: + await self.bot.say(str(e)) @commands.command(pass_context=True, no_pm=True) @utils.custom_perms(send_messages=True) diff --git a/cogs/utils/__init__.py b/cogs/utils/__init__.py index 9d964f4..7d776f3 100644 --- a/cogs/utils/__init__.py +++ b/cogs/utils/__init__.py @@ -3,4 +3,4 @@ from .checks import is_owner, custom_perms, is_pm from .config import * from .utilities import * from .images import create_banner -from .paginator import Pages +from .paginator import Pages, CannotPaginate From 04c07fc08c709410c52d5ff27bef27cc2b846d97 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sun, 12 Feb 2017 17:45:19 -0600 Subject: [PATCH 18/23] Use the full utils package instead of importing each individually --- cogs/music.py | 35 ++++++++++++++++++----------------- cogs/owner.py | 24 +++++++++++------------- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/cogs/music.py b/cogs/music.py index 3e0fbfc..693dfd7 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -1,9 +1,10 @@ -from .utils import * -from .voice_utilities import * +from .voice_utils import * import discord from discord.ext import commands +from . import utils + import math import time import asyncio @@ -321,7 +322,7 @@ class Music: @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def progress(self, ctx): """Provides the progress of the current song""" @@ -345,7 +346,7 @@ class Music: await self.bot.say(fmt) @commands.command(no_pm=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def join(self, *, channel: discord.Channel): """Joins a voice channel.""" try: @@ -361,7 +362,7 @@ class Music: await self.bot.say('Ready to play audio in ' + channel.name) @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def summon(self, ctx): """Summons the bot to join your voice channel.""" # This method will be invoked by other commands, so we should return True or False instead of just returning @@ -388,7 +389,7 @@ class Music: return success @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def play(self, ctx, *, song: str): """Plays a song. If there is a song currently in the queue, then it is @@ -406,7 +407,7 @@ class Music: 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 + # Since summon utils 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: @@ -479,7 +480,7 @@ class Music: await self.bot.say('Enqueued ' + str(_entry)) @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(kick_members=True) + @utils.custom_perms(kick_members=True) async def volume(self, ctx, value: int = None): """Sets the volume of the currently playing song.""" @@ -498,7 +499,7 @@ class Music: await self.bot.say('Set the volume to {:.0%}'.format(player.volume)) @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(kick_members=True) + @utils.custom_perms(kick_members=True) async def pause(self, ctx): """Pauses the currently played song.""" state = self.get_voice_state(ctx.message.server) @@ -506,7 +507,7 @@ class Music: state.player.pause() @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(kick_members=True) + @utils.custom_perms(kick_members=True) async def resume(self, ctx): """Resumes the currently played song.""" state = self.get_voice_state(ctx.message.server) @@ -514,7 +515,7 @@ class Music: state.player.resume() @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(kick_members=True) + @utils.custom_perms(kick_members=True) async def stop(self, ctx): """Stops playing audio and leaves the voice channel. This also clears the queue. @@ -538,7 +539,7 @@ class Music: pass @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(send_messages=True) + @utils.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 @@ -578,7 +579,7 @@ class Music: await self.bot.say("ETA till your next play is: {0[0]}m {0[1]}s".format(divmod(round(count, 0), 60))) @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def queue(self, ctx): """Provides a printout of the songs that are in the queue""" state = self.get_voice_state(ctx.message.server) @@ -594,14 +595,14 @@ class Music: self.bot.loop.create_task(self.queue_embed_task(state, ctx.message.channel, ctx.message.author)) @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def queuelength(self, ctx): """Prints the length of the queue""" await self.bot.say("There are a total of {} songs in the queue" .format(len(self.get_voice_state(ctx.message.server).songs.entries))) @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def skip(self, ctx): """Vote to skip a song. The song requester can automatically skip. approximately 1/3 of the members in the voice channel @@ -633,7 +634,7 @@ class Music: await self.bot.say('You have already voted to skip this song.') @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(kick_members=True) + @utils.custom_perms(kick_members=True) async def modskip(self, ctx): """Forces a song skip, can only be used by a moderator""" state = self.get_voice_state(ctx.message.server) @@ -645,7 +646,7 @@ class Music: await self.bot.say('Song has just been skipped.') @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def playing(self, ctx): """Shows info about the currently played song.""" diff --git a/cogs/owner.py b/cogs/owner.py index 7536f4d..e06418c 100644 --- a/cogs/owner.py +++ b/cogs/owner.py @@ -1,8 +1,6 @@ from discord.ext import commands -from .utils import config -from .utils import checks -from .utils import utilities +from . import utils import re import glob @@ -23,7 +21,7 @@ class Owner: self.bot = bot @commands.command() - @commands.check(checks.is_owner) + @commands.check(utils.is_owner) async def motd_push(self, *, message): """Used to push a new message to the message of the day""" date = pendulum.utcnow().to_date_string() @@ -31,12 +29,12 @@ class Owner: entry = {'motd': message, 'date': date} # Try to add this, if there's an entry for that date, lets update it to make sure only one motd is sent a day # I should be managing this myself, more than one should not be sent in a day - if await config.add_content('motd', entry, r_filter): - await config.update_content('motd', entry, r_filter) + if await utils.add_content('motd', entry, r_filter): + await utils.update_content('motd', entry, r_filter) await self.bot.say("New motd update for {}!".format(date)) @commands.command(pass_context=True) - @commands.check(checks.is_owner) + @commands.check(utils.is_owner) async def debug(self, ctx): """Executes code""" # Eval and exec have different useful purposes, so use both @@ -65,7 +63,7 @@ class Owner: await self.bot.say(fmt.format(type(error).__name__, error)) @commands.command(pass_context=True) - @commands.check(checks.is_owner) + @commands.check(utils.is_owner) async def shutdown(self, ctx): """Shuts the bot down""" fmt = 'Shutting down, I will miss you {0.author.name}' @@ -74,21 +72,21 @@ class Owner: await self.bot.close() @commands.command() - @commands.check(checks.is_owner) + @commands.check(utils.is_owner) async def name(self, newNick: str): """Changes the bot's name""" await self.bot.edit_profile(username=newNick) await self.bot.say('Changed username to ' + newNick) @commands.command() - @commands.check(checks.is_owner) + @commands.check(utils.is_owner) async def status(self, *, status: str): """Changes the bot's 'playing' status""" await self.bot.change_status(discord.Game(name=status, type=0)) await self.bot.say("Just changed my status to '{0}'!".format(status)) @commands.command() - @commands.check(checks.is_owner) + @commands.check(utils.is_owner) async def load(self, *, module: str): """Loads a module""" @@ -106,7 +104,7 @@ class Owner: await self.bot.say(fmt.format(type(error).__name__, error)) @commands.command() - @commands.check(checks.is_owner) + @commands.check(utils.is_owner) async def unload(self, *, module: str): """Unloads a module""" @@ -119,7 +117,7 @@ class Owner: await self.bot.say("I have just unloaded the {} module".format(module)) @commands.command() - @commands.check(checks.is_owner) + @commands.check(utils.is_owner) async def reload(self, *, module: str): """Reloads a module""" From e45f78707721c00ea23877db8e85778c3d75970e Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sun, 12 Feb 2017 17:58:27 -0600 Subject: [PATCH 19/23] Catch exception when we cannot paginate --- cogs/core.py | 9 +-------- cogs/mod.py | 4 ++-- cogs/stats.py | 2 +- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/cogs/core.py b/cogs/core.py index b6c3c18..b442de8 100644 --- a/cogs/core.py +++ b/cogs/core.py @@ -58,13 +58,6 @@ class Core: cmd = None page = 1 - if ctx.message.server: - perms = ctx.message.server.me.permissions_in(ctx.message.channel) - if not (perms.embed_links and perms.add_reactions): - fmt = "I need the permissions `embed_links` and `add_reactions` to send my help message! " \ - " Otherwise you can use this link to view available commands {}".format( - "http://bonfirebot.readthedocs.io/en/latest/") - await self.bot.say(fmt) if message is not None: # If something is provided, it can either be the page number or a command @@ -76,8 +69,8 @@ class Core: if cmd is None: entries = sorted(utils.get_all_commands(self.bot)) - pages = utils.Pages(self.bot, message=ctx.message, entries=entries) try: + pages = utils.Pages(self.bot, message=ctx.message, entries=entries) await pages.paginate() except utils.CannotPaginate as e: await self.bot.say(str(e)) diff --git a/cogs/mod.py b/cogs/mod.py index 57869aa..62b6bbd 100644 --- a/cogs/mod.py +++ b/cogs/mod.py @@ -448,9 +448,9 @@ class Mod: return if rule is None: - pages = utils.Pages(self.bot, message=ctx.message, entries=rules, per_page=5) - pages.title = "Rules for {}".format(ctx.message.server.name) try: + pages = utils.Pages(self.bot, message=ctx.message, entries=rules, per_page=5) + pages.title = "Rules for {}".format(ctx.message.server.name) await pages.paginate() except utils.CannotPaginate as e: await self.bot.say(str(e)) diff --git a/cogs/stats.py b/cogs/stats.py index a4629f0..612c124 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -238,8 +238,8 @@ class Stats: member = ctx.message.server.get_member(member_id) output.append("{} (Rating: {})".format(member.display_name, rating)) - pages = utils.Pages(self.bot, message=ctx.message, entries=output, per_page=10) try: + pages = utils.Pages(self.bot, message=ctx.message, entries=output) await pages.paginate() except utils.CannotPaginate as e: await self.bot.say(str(e)) From 599d8feb3a55d489352cd17d32125acf288328c5 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sun, 12 Feb 2017 20:22:37 -0600 Subject: [PATCH 20/23] How the hell did that get in there? --- cogs/music.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/music.py b/cogs/music.py index 693dfd7..c35b1e9 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -174,7 +174,7 @@ class Music: return False - async def remove_voice_client(self, server):greendsi_webimsuan + async def remove_voice_client(self, server): """Removes any voice clients from a server This is sometimes needed, due to the unreliability of Discord's voice connection We do not want to end up with a voice client stuck somewhere, so this cancels any found for a server""" From 91ff56f621d12ef4bfa799f04b041237482e197a Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sun, 12 Feb 2017 20:25:56 -0600 Subject: [PATCH 21/23] Fixed botched find+replace --- cogs/music.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/music.py b/cogs/music.py index c35b1e9..8f3b19f 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -1,4 +1,4 @@ -from .voice_utils import * +from .voice_utilities import * import discord from discord.ext import commands From a124eaf89aa5ea7dcc4713a8af793df6e148cc22 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sun, 12 Feb 2017 20:33:24 -0600 Subject: [PATCH 22/23] Added a method to convert images to jpeg --- cogs/utils/utilities.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/cogs/utils/utilities.py b/cogs/utils/utilities.py index cb71d0c..b5eb180 100644 --- a/cogs/utils/utilities.py +++ b/cogs/utils/utilities.py @@ -1,8 +1,20 @@ import aiohttp -import io +from io import BytesIO import inspect from . import config +from PIL import Image + +def convert_to_jpeg(pfile): + # Open the file given + img = Image.open(pfile) + # Create the BytesIO object we'll use as our new "file" + new_file = BytesIO() + # Save to this file as jpeg + img.save(new_file, format='JPEG') + # In order to use the file, we need to seek back to the 0th position + new_file.seek(0) + return new_file def get_all_commands(bot): """Returns a list of all command names for the bot""" @@ -61,7 +73,7 @@ async def download_image(url): return None # Then wrap it in a BytesIO object, to be used like an actual file - image = io.BytesIO(bts) + image = BytesIO(bts) return image async def request(url, *, headers=None, payload=None, method='GET', attr='json'): From edf9d9a58728d790f32f25abe34dc019c335b34f Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sun, 12 Feb 2017 20:33:57 -0600 Subject: [PATCH 23/23] Used the jpeg conversion method, since some browsers do not have webp compatibility --- cogs/interaction.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cogs/interaction.py b/cogs/interaction.py index de7c644..342efcf 100644 --- a/cogs/interaction.py +++ b/cogs/interaction.py @@ -142,6 +142,7 @@ class Interaction: filename = 'avatar.gif' else: filename = 'avatar.webp' + file = utils.convert_to_jpeg(file) await self.bot.upload(file, filename=filename) else: await self.bot.say(url)