From 5612f6e25aa37c2448dec683f7856846ac572676 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Thu, 16 Mar 2017 15:30:54 -0500 Subject: [PATCH 01/11] Update to work with new database settings --- cogs/utils/checks.py | 55 ++++++++++++++------------ cogs/utils/config.py | 91 +++++++++++++++++++++----------------------- 2 files changed, 74 insertions(+), 72 deletions(-) diff --git a/cogs/utils/checks.py b/cogs/utils/checks.py index dbfd47a..6aade00 100644 --- a/cogs/utils/checks.py +++ b/cogs/utils/checks.py @@ -7,10 +7,22 @@ from . import config loop = asyncio.get_event_loop() -# The list of tables needed for the database -table_list = ['battle_records', 'battling', 'boops', 'bot_data', 'command_usage', 'custom_permissions', - 'deviantart', 'motd', 'nsfw_channels', 'overwatch', 'picarto', 'prefixes', 'raffles', - 'rules', 'server_alerts', 'strawpolls', 'tags', 'tictactoe', 'twitch', 'user_notifications'] +# The tables needed for the database, as well as their primary keys +required_tables = { + 'battle_records': 'member_id', + 'boops': 'member_id', + 'command_usage': 'command', + 'motd': 'date', + 'overwatch': 'member_id', + 'picarto': 'member_id', + 'server_settings': 'server_id', + 'raffles': 'id', + 'strawpolls': 'server_id', + 'osu': 'member_id', + 'tags': 'server_id', + 'tictactoe': 'member_id', + 'twitch': 'member_id' +} async def db_check(): @@ -24,9 +36,10 @@ async def db_check(): except r.errors.ReqlDriverError: print("Cannot connect to the RethinkDB instance with the following information: {}".format(db_opts)) - print("The RethinkDB instance you have setup may be down, otherwise please ensure you setup a"\ - " RethinkDB instance, and you have provided the correct database information in config.yml") + print("The RethinkDB instance you have setup may be down, otherwise please ensure you setup a" + " RethinkDB instance, and you have provided the correct database information in config.yml") quit() + return # Get the current databases and check if the one we need is there dbs = await r.db_list().run(conn) @@ -35,19 +48,20 @@ async def db_check(): print('Couldn\'t find database {}...creating now'.format(db_opts['db'])) await r.db_create(db_opts['db']).run(conn) # Then add all the tables - for table in table_list: + for table, key in required_tables.items(): print("Creating table {}...".format(table)) - await r.table_create(table).run(conn) + await r.table_create(table, primary_key=key).run(conn) print("Done!") else: # Otherwise, if the database is setup, make sure all the required tables are there tables = await r.table_list().run(conn) - for table in table_list: + for table, key in required_tables.items(): if table not in tables: print("Creating table {}...".format(table)) - await r.table_create(table).run(conn) + await r.table_create(table, primary_key=key).run(conn) print("Done checking tables!") + def is_owner(ctx): return ctx.message.author.id in config.owner_ids @@ -66,24 +80,15 @@ def custom_perms(**perms): for perm, setting in perms.items(): setattr(required_perm, perm, setting) - perm_values = config.cache.get('custom_permissions').values - - # Loop through and find this server's entry for custom permissions - # Find the command we're using, if it exists, then overwrite - # The required permissions, based on the value saved - for x in perm_values: - if x['server_id'] == ctx.message.server.id and x.get(ctx.command.qualified_name): - required_perm = discord.Permissions(x[ctx.command.qualified_name]) + try: + server_settings = config.cache.get('server_settings').values + required_perm_value = [x for x in server_settings if x['server_id'] == ctx.message.server.id][0]['permissions'][ctx.command.qualified_name] + required_perm = discord.Permissions(required_perm_value) + except (TypeError, IndexError, KeyError): + pass # Now just check if the person running the command has these permissions return member_perms >= required_perm predicate.perms = perms return commands.check(predicate) - - -def is_pm(): - def predicate(ctx): - return ctx.message.channel.is_private - - return commands.check(predicate) diff --git a/cogs/utils/config.py b/cogs/utils/config.py index 2e9c3ab..91d5f1d 100644 --- a/cogs/utils/config.py +++ b/cogs/utils/config.py @@ -9,7 +9,7 @@ global_config = {} # Ensure that the required config.yml file actually exists try: with open("config.yml", "r") as f: - global_config = yaml.load(f) + global_config = yaml.safe_load(f) except FileNotFoundError: print("You have no config file setup! Please use config.yml.sample to setup a valid config file") quit() @@ -70,10 +70,6 @@ user_agent = global_config.get('user_agent', "") # The extensions to load extensions = global_config.get('extensions', []) -# The variables needed for sharding -shard_count = global_config.get('shard_count', 1) -shard_id = global_config.get('shard_id', 0) - # The default status the bot will use default_status = global_config.get("default_status", None) # The URL that will be used to link to for the help command @@ -95,10 +91,6 @@ db_pass = global_config.get('db_pass', '') # {'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} -possible_keys = ['prefixes', 'battle_records', 'boops', 'server_alerts', 'user_notifications', 'nsfw_channels', - 'custom_permissions', 'rules', 'overwatch', 'picarto', 'twitch', 'strawpolls', 'tags', - 'tictactoe', 'bot_data', 'command_manage'] - # This will be a dictionary that holds the cache object, based on the key that is saved cache = {} @@ -110,7 +102,7 @@ cache = {} # We still need 'cache' for prefixes and custom permissions however, so for now, just include that cache['prefixes'] = Cache('prefixes') -cache['custom_permissions'] = Cache('custom_permissions') +cache['server_settings'] = Cache('server_settings') async def update_cache(): for value in cache.values(): @@ -123,61 +115,46 @@ def command_prefix(bot, message): # If the prefix does exist in the database and isn't in our cache; too bad, something has messed up # But it is not worth a query for every single message the bot detects, to fix try: - values = cache['prefixes'].values - try: - prefix = [data['prefix'] for data in values if message.server.id == data['server_id']][0] - except IndexError: - prefix = None - except AttributeError: - prefix = None + prefixes = cache['server_settings'].values + prefix = [x for x in prefixes if x['server_id'] == message.guild.id][0]['prefix'] return prefix or default_prefix - except KeyError: + except (KeyError, TypeError, IndexError, AttributeError): return default_prefix -async def add_content(table, content, r_filter=None): +async def add_content(table, content): r.set_loop_type("asyncio") conn = await r.connect(**db_opts) # First we need to make sure that this entry doesn't exist # For all rethinkDB cares, multiple entries can exist with the same content # For our purposes however, we do not want this try: - if r_filter is not None: - cursor = await r.table(table).filter(r_filter).run(conn) - cur_content = await _convert_to_list(cursor) - if len(cur_content) > 0: - await conn.close() - return False - await r.table(table).insert(content).run(conn) + result = await r.table(table).insert(content).run(conn) await conn.close() - return True except r.ReqlOpFailedError: # This means the table does not exist await r.table_create(table).run(conn) await r.table(table).insert(content).run(conn) await conn.close() - return True + result = {} + return result.get('inserted', 0) > 0 -async def remove_content(table, r_filter=None): - if r_filter is None: - r_filter = {} +async def remove_content(table, key): r.set_loop_type("asyncio") conn = await r.connect(**db_opts) try: - result = await r.table(table).filter(r_filter).delete().run(conn) + result = await r.table(table).get(key).delete().run(conn) except r.ReqlOpFailedError: result = {} pass await conn.close() - if table == 'prefixes' or table == 'custom_permissions': + if table == 'prefixes' or table == 'server_settings': loop.create_task(cache[table].update()) return result.get('deleted', 0) > 0 -async def update_content(table, content, r_filter=None): - if r_filter is None: - r_filter = {} +async def update_content(table, content, key): r.set_loop_type("asyncio") conn = await r.connect(**db_opts) # This method is only for updating content, so if we find that it doesn't exist, just return false @@ -185,36 +162,56 @@ async def update_content(table, content, r_filter=None): # Update based on the content and filter passed to us # rethinkdb allows you to do many many things inside of update # This is why we're accepting a variable and using it, whatever it may be, as the query - result = await r.table(table).filter(r_filter).update(content).run(conn) + result = await r.table(table).get(key).update(content).run(conn) except r.ReqlOpFailedError: await conn.close() result = {} await conn.close() - if table == 'prefixes' or table == 'custom_permissions': + if table == 'prefixes' or table == 'server_settings': loop.create_task(cache[table].update()) return result.get('replaced', 0) > 0 or result.get('unchanged', 0) > 0 -async def replace_content(table, content, r_filter=None): +async def replace_content(table, content, key): # This method is here because .replace and .update can have some different functionalities - if r_filter is None: - r_filter = {} r.set_loop_type("asyncio") conn = await r.connect(**db_opts) try: - result = await r.table(table).filter(r_filter).replace(content).run(conn) + result = await r.table(table).get(key).replace(content).run(conn) except r.ReqlOpFailedError: await conn.close() result = {} await conn.close() - if table == 'prefixes' or table == 'custom_permissions': + if table == 'prefixes' or table == 'server_settings': loop.create_task(cache[table].update()) return result.get('replaced', 0) > 0 or result.get('unchanged', 0) > 0 -async def get_content(table: str, r_filter=None): - if r_filter is None: - r_filter = {} +async def get_content(table, key=None): + r.set_loop_type("asyncio") + conn = await r.connect(**db_opts) + + try: + if key: + cursor = await r.table(table).get(key).run(conn) + else: + cursor = await r.table(table).run(conn) + if cursor is None: + content = None + elif type(cursor) is not dict: + content = await _convert_to_list(cursor) + if len(content) == 0: + content = None + else: + content = cursor + except (IndexError, r.ReqlOpFailedError): + content = None + await conn.close() + if table == 'prefixes' or table == 'server_settings': + loop.create_task(cache[table].update()) + return content + +async def filter_content(table: str, r_filter): r.set_loop_type("asyncio") conn = await r.connect(**db_opts) try: @@ -225,7 +222,7 @@ async def get_content(table: str, r_filter=None): except (IndexError, r.ReqlOpFailedError): content = None await conn.close() - if table == 'prefixes' or table == 'custom_permissions': + if table == 'prefixes' or table == 'server_settings': loop.create_task(cache[table].update()) return content From fc1ed00b960b0af155cb73657bdc02ae399bcb6d Mon Sep 17 00:00:00 2001 From: phxntxm Date: Fri, 17 Mar 2017 00:21:30 -0500 Subject: [PATCH 02/11] Update to work with updated database --- bot.py | 12 +-- cogs/core.py | 9 +- cogs/da.py | 166 ------------------------------ cogs/events.py | 39 +++---- cogs/interaction.py | 10 +- cogs/links.py | 15 ++- cogs/mod.py | 177 +++++++++++++++++--------------- cogs/overwatch.py | 79 +++++--------- cogs/owner.py | 6 +- cogs/picarto.py | 222 ++++++++++++++++++---------------------- cogs/raffle.py | 34 +++--- cogs/stats.py | 21 ++-- cogs/strawpoll.py | 3 +- cogs/tags.py | 25 +++-- cogs/twitch.py | 166 ++++++++++++++---------------- cogs/utils/utilities.py | 19 ++++ 16 files changed, 397 insertions(+), 606 deletions(-) delete mode 100644 cogs/da.py diff --git a/bot.py b/bot.py index ddc17f3..09d4cfc 100644 --- a/bot.py +++ b/bot.py @@ -50,12 +50,10 @@ async def process_command(ctx): server = ctx.message.server command = ctx.command - r_filter = {'command': command.qualified_name} - command_usage = await utils.get_content('command_usage', r_filter) + command_usage = await utils.get_content('command_usage', key=command.qualified_name) if command_usage is None: command_usage = {'command': command.qualified_name} - else: - command_usage = command_usage[0] + # Add one to the total usage for this command, basing it off 0 to start with (obviously) total_usage = command_usage.get('total_usage', 0) + 1 command_usage['total_usage'] = total_usage @@ -67,15 +65,15 @@ async def process_command(ctx): command_usage['member_usage'] = total_member_usage # Add one to the server's usage for this command - if ctx.message.server is not None: + if ctx.message.guild is not None: total_server_usage = command_usage.get('server_usage', {}) server_usage = total_server_usage.get(server.id, 0) + 1 total_server_usage[server.id] = server_usage command_usage['server_usage'] = total_server_usage # Save all the changes - if not await utils.update_content('command_usage', command_usage, r_filter): - await utils.add_content('command_usage', command_usage, r_filter) + if not await utils.update_content('command_usage', command_usage, command.qualified_name): + await utils.add_content('command_usage', command_usage) @bot.event diff --git a/cogs/core.py b/cogs/core.py index b1f28d9..59986d0 100644 --- a/cogs/core.py +++ b/cogs/core.py @@ -132,10 +132,9 @@ class Core: await self.bot.say(fmt) else: try: - r_filter = pendulum.parse(date) - motd = await utils.get_content('motd', r_filter) - date = motd[0]['date'] - motd = motd[0]['motd'] + motd = await utils.get_content('motd', str(pendulum.parse(date).date())) + date = motd['date'] + motd = motd['motd'] fmt = "Message of the day for {}:\n\n{}".format(date, motd) await self.bot.say(fmt) # This one will be hit if we return None for that day @@ -185,7 +184,7 @@ class Core: cal = calendar.TextCalendar().formatmonth(year, month) await self.bot.say("```\n{}```".format(cal)) - @commands.command() + @commands.command(enabled=False) @utils.custom_perms(send_messages=True) async def info(self): """This command can be used to print out some of my information""" diff --git a/cogs/da.py b/cogs/da.py deleted file mode 100644 index 7adb2d5..0000000 --- a/cogs/da.py +++ /dev/null @@ -1,166 +0,0 @@ -import aiohttp -import asyncio -import discord -import traceback -import logging - -from discord.ext import commands - -from . import utils - -log = logging.getLogger() - - -class Deviantart: - def __init__(self, bot): - self.base_url = "https://www.deviantart.com/api/v1/oauth2/gallery/all" - self.bot = bot - self.headers = {"User-Agent": utils.user_agent} - self.session = aiohttp.ClientSession() - self.token = None - self.params = None - bot.loop.create_task(self.token_task()) - bot.loop.create_task(self.post_task()) - - async def token_task(self): - while True: - expires_in = await self.get_token() - await asyncio.sleep(expires_in) - - async def post_task(self): - await asyncio.sleep(5) - # Lets start the task a few seconds after, to ensure our token gets set - while True: - await self.check_posts() - await asyncio.sleep(300) - - async def get_token(self): - # We need a token to create requests, it doesn't seem this token goes away - # To get this token, we need to make a request and retrieve that - url = 'https://www.deviantart.com/oauth2/token' - params = {'client_id': utils.da_id, - 'client_secret': utils.da_secret, - 'grant_type': 'client_credentials'} - - data = await utils.request(url, payload=params) - - self.token = data.get('access_token', None) - self.params = {'access_token': self.token} - # Make sure we refresh our token, based on when they tell us it expires - # Ensure we call it a few seconds earlier, to give us enough time to set the new token - # If there was an issue, lets call this in a minute again - return data.get('expires_in', 65) - 5 - - async def check_posts(self): - content = await utils.get_content('deviantart') - # People might sub to the same person, so lets cache every person and their last update - cache = {} - - try: - for entry in content: - user = discord.utils.get(self.bot.get_all_members(), id=entry['member_id']) - - # If we're sharded, we might not be able to find this user. - # If the bot is not in the server with the member either - if user is None: - continue - - params = self.params.copy() - # Now loop through the subscriptions - for da_name in entry['subbed']: - # Check what the last updated content we sent to this user was - # Since we cannot go back in time, if this doesn't match the last uploaded from the user - # Assume we need to notify the user of this post - last_updated_id = entry['last_updated'].get(da_name, None) - # Check if this user has been requested already, if so we don't need to make another request - result = cache.get(da_name, None) - if result is None: - params['username'] = da_name - data = await utils.request(self.base_url, payload=params) - if data is None: - continue - elif not data['results']: - continue - - result = data['results'][0] - cache[da_name] = result - - # This means that our last update to this user, for this author, is not the same - if last_updated_id != result['deviationid']: - # First lets check if the last updated ID was None, if so...then we haven't alerted them yet - # We don't want to alert them in this case - # We just want to act like the artist's most recent update was the last notified - # So just notify the user if this is not None - if last_updated_id is not None: - fmt = "There has been a new post by an artist you are subscribed to!\n\n" \ - "**Title:** {}\n**User:** {}\n**URL:** {}".format( - result['title'], - result['author']['username'], - result['url']) - await self.bot.send_message(user, fmt) - # Now we can update the user's last updated for this DA - # We want to do this whether or not our last if statement was met - r_filter = {'member_id': user.id} - update = {'last_updated': {da_name: result['deviationid']}} - await utils.update_content('deviantart', update, r_filter) - except Exception as e: - tb = traceback.format_exc() - fmt = "{1}\n{0.__class__.__name__}: {0}".format(tb, e) - log.error(fmt) - - @commands.group() - @utils.custom_perms(send_messages=True) - async def da(self): - """This provides a sort of 'RSS' feed for subscribed to artists. - Subscribe to artists, and I will PM you when new posts come out from these artists""" - pass - - @da.command(pass_context=True, name='sub', aliases=['add', 'subscribe']) - @utils.custom_perms(send_messages=True) - async def da_sub(self, ctx, *, username): - """This can be used to add a feed to your notifications. - Provide a username, and when posts are made from this user, you will be notified - - EXAMPLE: !da sub MyFavoriteArtistEva<3 - RESULT: Notifications of amazing pics c:""" - r_filter = {'member_id': ctx.message.author.id} - content = await utils.get_content('deviantart', r_filter) - # TODO: Ensure the user provided is a real user - - if content is None: - entry = {'member_id': ctx.message.author.id, 'subbed': [username], 'last_updated': {}} - await utils.add_content('deviantart', entry, r_filter) - await self.bot.say("You have just subscribed to {}!".format(username)) - elif content[0]['subbed'] is None or username not in content[0]['subbed']: - if content[0]['subbed'] is None: - sub_list = [username] - else: - content[0]['subbed'].append(username) - sub_list = content[0]['subbed'] - await utils.update_content('deviantart', {'subbed': sub_list}, r_filter) - await self.bot.say("You have just subscribed to {}!".format(username)) - else: - await self.bot.say("You are already subscribed to that user!") - - @da.command(pass_context=True, name='unsub', aliases=['delete', 'remove', 'unsubscribe']) - @utils.custom_perms(send_messages=True) - async def da_unsub(self, ctx, *, username): - """This command can be used to unsub from the specified user - - EXAMPLE: !da unsub TheArtistWhoBetrayedMe - RESULT: No more pics from that terrible person!""" - r_filter = {'member_id': ctx.message.author.id} - content = await utils.get_content('deviantart', r_filter) - - if content is None or content[0]['subbed'] is None: - await self.bot.say("You are not subscribed to anyone at the moment!") - elif username in content[0]['subbed']: - content[0]['subbed'].remove(username) - await utils.update_content('deviantart', {'subbed': content[0]['subbed']}, r_filter) - await self.bot.say("You have just unsubscribed from {}!".format(username)) - else: - await self.bot.say("You are not subscribed to that user!") - - -def setup(bot): - bot.add_cog(Deviantart(bot)) diff --git a/cogs/events.py b/cogs/events.py index eb9bd49..894fea6 100644 --- a/cogs/events.py +++ b/cogs/events.py @@ -20,6 +20,8 @@ class StatsUpdate: self.bot.loop.create_task(self.session.close()) async def update(self): + # Currently disabled + return server_count = 0 data = await config.get_content('bot_data') @@ -48,6 +50,7 @@ class StatsUpdate: log.info('bots.discord.pw statistics returned {} for {}'.format(resp.status, payload)) async def on_server_join(self, server): + return r_filter = {'shard_id': config.shard_id} server_count = len(self.bot.servers) member_count = len(set(self.bot.get_all_members())) @@ -58,6 +61,7 @@ class StatsUpdate: self.bot.loop.create_task(self.update()) async def on_server_leave(self, server): + return r_filter = {'shard_id': config.shard_id} server_count = len(self.bot.servers) member_count = len(set(self.bot.get_all_members())) @@ -68,6 +72,7 @@ class StatsUpdate: self.bot.loop.create_task(self.update()) async def on_ready(self): + return r_filter = {'shard_id': config.shard_id} server_count = len(self.bot.servers) member_count = len(set(self.bot.get_all_members())) @@ -79,16 +84,15 @@ class StatsUpdate: async def on_member_join(self, member): server = member.server - r_filter = {'server_id': server.id} - notifications = await config.get_content('user_notifications', r_filter) + server_settings = await config.get_content('server_settings', server.id) try: - channel_id = notifications[0]['channel_id'] - except TypeError: - return - - # By default, notifications should be off unless explicitly turned on - if not channel_id: + join_leave_on = server_settings['join_leave'] + if join_leave_on: + channel_id = server_settings.get('notification_channel') or member.server.id + else: + return + except (IndexError, TypeError, KeyError): return channel = server.get_channel(channel_id) @@ -96,22 +100,19 @@ class StatsUpdate: async def on_member_remove(self, member): server = member.server - r_filter = {'server_id': server.id} - notifications = await config.get_content('user_notifications', r_filter) + server_settings = await config.get_content('server_settings', server.id) try: - channel_id = notifications[0]['channel_id'] - except TypeError: - return - - # By default, notifications should be off unless explicitly turned on - if not channel_id: + join_leave_on = server_settings['join_leave'] + if join_leave_on: + channel_id = server_settings.get('notification_channel') or member.server.id + else: + return + except (IndexError, TypeError, KeyError): return channel = server.get_channel(channel_id) - await self.bot.send_message(channel, - "{0} has left the server, I hope it wasn't because of something I said :c".format( - member.display_name)) + await self.bot.send_message(channel, "{0} has left the server, I hope it wasn't because of something I said :c".format(member.display_name)) def setup(bot): diff --git a/cogs/interaction.py b/cogs/interaction.py index 5e9af9d..fa81104 100644 --- a/cogs/interaction.py +++ b/cogs/interaction.py @@ -258,20 +258,20 @@ class Interaction: await self.bot.say("Why the heck are you booping me? Get away from me >:c") return - r_filter = {'member_id': booper.id} - boops = await utils.get_content('boops', r_filter) + key = booper.id + boops = await utils.get_content('boops', key) if boops is not None: - boops = boops[0]['boops'] + boops = boops['boops'] # If the booper has never booped the member provided, assure it's 0 amount = boops.get(boopee.id, 0) + 1 boops[boopee.id] = amount - await utils.update_content('boops', {'boops': boops}, r_filter) + await utils.update_content('boops', {'boops': boops}, key) else: entry = {'member_id': booper.id, 'boops': {boopee.id: 1}} - await utils.add_content('boops', entry, r_filter) + await utils.add_content('boops', entry) amount = 1 fmt = "{0.mention} has just booped {1.mention}{3}! That's {2} times now!" diff --git a/cogs/links.py b/cogs/links.py index 938acf1..632749f 100644 --- a/cogs/links.py +++ b/cogs/links.py @@ -27,9 +27,8 @@ class Links: url = "https://www.google.com/search" # Turn safe filter on or off, based on whether or not this is a nsfw channel - r_filter = {'channel_id': ctx.message.channel.id} - nsfw_channels = await utils.get_content("nsfw_channels", r_filter) - safe = 'off' if nsfw_channels else 'on' + nsfw = await utils.channel_is_nsfw(ctx.message.channel) + safe = 'off' if nsfw else 'on' params = {'q': query, 'safe': safe, @@ -181,12 +180,11 @@ class Links: query = ' '.join(value for value in search if not re.search('&?filter_id=[0-9]+', value)) params = {'q': query} - r_filter = {'channel_id': ctx.message.channel.id} - nsfw_channels = await utils.get_content("nsfw_channels", r_filter) + nsfw = await utils.channel_is_nsfw(ctx.message.channel) # If this is a nsfw channel, we just need to tack on 'explicit' to the terms # Also use the custom filter that I have setup, that blocks some certain tags # If the channel is not nsfw, we don't need to do anything, as the default filter blocks explicit - if nsfw_channels is not None: + if nsfw: params['q'] += ", (explicit OR suggestive)" params['filter_id'] = 95938 else: @@ -256,12 +254,11 @@ class Links: # Due to this, send a message saying we're looking up the information first await self.bot.say("Looking up an image with those tags....") - r_filter = {'channel_id': ctx.message.channel.id} - nsfw_channels = await utils.get_content("nsfw_channels", r_filter) + nsfw = await utils.channel_is_nsfw(ctx.message.channel) # e621 by default does not filter explicit content, so tack on # safe/explicit based on if this channel is nsfw or not - params['tags'] += " rating:explicit" if nsfw_channels else " rating:safe" + params['tags'] += " rating:explicit" if nsfw else " rating:safe" data = await utils.request(url, payload=params) diff --git a/cogs/mod.py b/cogs/mod.py index 1f8db12..47cd048 100644 --- a/cogs/mod.py +++ b/cogs/mod.py @@ -119,19 +119,22 @@ class Mod: except discord.HTTPException: await self.bot.say("Sorry, I failed to ban that user!") - @commands.command(pass_context=True, no_pm=True) + @commands.command(no_pm=True, aliases=['alerts'], pass_context=True) @utils.custom_perms(kick_members=True) - async def alerts(self, ctx, channel: discord.Channel): + async def notifications(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 - EXAMPLE: !alerts #alerts RESULT: No more alerts spammed in #general!""" - r_filter = {'server_id': ctx.message.server.id} - entry = {'server_id': ctx.message.server.id, - 'channel_id': channel.id} - if not await utils.add_content('server_alerts', entry, r_filter): - await utils.update_content('server_alerts', entry, r_filter) + if str(channel.type) != "text": + await self.bot.say("The notifications channel must be a text channel!") + return + + key = ctx.message.server.id + entry = {'server_id': key, + 'notification_channel': channel.id} + if not await utils.update_content('server_settings', entry, key): + await utils.add_content('server_settings', entry) await self.bot.say("I have just changed this server's 'notifications' channel" "\nAll notifications will now go to `{}`".format(channel)) @@ -139,55 +142,71 @@ class Mod: @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 Provide on, yes, or true to set it on; otherwise it will be turned off - EXAMPLE: !usernotify on RESULT: Annying join/leave notifications! Yay!""" # Join/Leave notifications can be kept separate from normal alerts # So we base this channel on it's own and not from alerts # When mod logging becomes available, that will be kept to it's own channel if wanted as well - on_off = ctx.message.channel.id if re.search("(on|yes|true)", on_off.lower()) else None - r_filter = {'server_id': ctx.message.server.id} - entry = {'server_id': ctx.message.server.id, - 'channel_id': on_off} - if not await utils.add_content('user_notifications', entry, r_filter): - await utils.update_content('user_notifications', entry, r_filter) + on_off = True if re.search("(on|yes|true)", on_off.lower()) else False + key = ctx.message.server.id + entry = {'server_id': key, + 'join_leave': on_off} + if not await utils.update_content('server_settings', entry, key): + await utils.add_content('server_settings', entry) + fmt = "notify" if on_off else "not notify" await self.bot.say("This server will now {} if someone has joined or left".format(fmt)) @commands.group(pass_context=True) async def nsfw(self, ctx): """Handles adding or removing a channel as a nsfw channel""" - # This command isn't meant to do anything, so just send an error if an invalid subcommand is passed - if ctx.invoked_subcommand is None: - await self.bot.say('Invalid subcommand passed: {0.subcommand_passed}'.format(ctx)) + pass @nsfw.command(name="add", pass_context=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 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 ;)") + key = ctx.message.server.id + entry = {'server_id': key, + 'nsfw_channels': [ctx.message.channel.id]} + update = {'nsfw_channels': r.row['nsfw_channels'].append(ctx.message.channel.id)} + + server_settings = await utils.get_content('server_settings', key) + if server_settings and 'nsfw_channels' in server_settings.keys(): + await utils.update_content('server_settings', update, key) + elif server_settings: + await utils.update_content('server_settings', entry, key) else: - await self.bot.say("This channel is already registered as 'nsfw'!") + await utils.add_content('server_settings', entry) + + await self.bot.say("This channel has just been registered as 'nsfw'! Have fun you naughties ;)") @nsfw.command(name="remove", aliases=["delete"], pass_context=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 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!") + + key = ctx.message.server.id + server_settings = await utils.get_content('server_settings', key) + channel = ctx.message.channel.id + try: + channels = server_settings['nsfw_channels'] + if channel in channels: + channels.remove(channel) + + entry = {'nsfw_channels': channels} + await utils.update_content('server_settings', entry, key) + await self.bot.say("This channel has just been unregistered as a nsfw channel") + return + except (TypeError, IndexError): + pass + + await self.bot.say("This channel is not registered as a 'nsfw' channel!") @commands.command(pass_context=True) @utils.custom_perms(kick_members=True) @@ -208,7 +227,6 @@ class Mod: 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 - EXAMPLE: !perms help RESULT: Hopefully a result saying you just need send_messages permissions; otherwise lol this server's admin doesn't like me """ if command is None: @@ -216,18 +234,18 @@ class Mod: "Valid permissions are: ```\n{}```".format("\n".join("{}".format(i) for i in valid_perms))) return - r_filter = {'server_id': ctx.message.server.id} - server_perms = await utils.get_content('custom_permissions', r_filter) - try: - server_perms = server_perms[0] - except TypeError: - server_perms = {} - cmd = self.find_command(command) + cmd = utils.find_command(self.bot, command) if cmd is None: await self.bot.say("That is not a valid command!") return + server_settings = await utils.get_content('server_settings', ctx.message.server.id) + try: + server_perms = server_settings['permissions'] + except (TypeError, IndexError, KeyError): + server_perms = {} + 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 @@ -238,7 +256,7 @@ class Mod: # 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 utils on perm commands) - for func in cmd.utils: + for func in cmd.checks: if "is_owner" in func.__qualname__: await self.bot.say("You need to own the bot to run this command") return @@ -265,7 +283,6 @@ class Mod: """Sets up custom permissions on the provided command Format must be 'perms add ' If you want to open the command to everyone, provide 'none' as the permission - EXAMPLE: !perms add skip ban_members RESULT: No more random people voting to skip a song""" @@ -278,6 +295,13 @@ class Mod: "`perms add `") return + cmd = utils.find_command(self.bot, command) + + if cmd is None: + await self.bot.say( + "That command does not exist! You can't have custom permissions on a non-existant command....") + return + # If a user can run a command, they have to have send_messages permissions; so use this as the base if permissions.lower() == "none": permissions = "send_messages" @@ -293,35 +317,22 @@ class Mod: return perm_value = perm_obj.value - cmd = self.find_command(command) - - if cmd is None: - await self.bot.say( - "That command does not exist! You can't have custom permissions on a non-existant command....") - return - # Two cases I use should never have custom permissions setup on them, is_owner for obvious reasons # The other case is if I'm using the default has_permissions case # Which means I do not want to check custom permissions at all # Currently the second case is only on adding and removing permissions, to avoid abuse on these for check in cmd.checks: - if "is_owner" == check.__name__ or re.search("has_permissions", str(check)) is not None: + if "is_owner" == check.__name__ or "has_permissions" in str(check): await self.bot.say("This command cannot have custom permissions setup!") return - r_filter = {'server_id': ctx.message.server.id} - entry = {'server_id': ctx.message.server.id, - cmd.qualified_name: perm_value} + key = ctx.message.server.id + entry = {'server_id': key, + 'permissions': {cmd.qualified_name: perm_value}} - # In all other cases, I've used add_content before update_content - # 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 utils.update_content('custom_permissions', entry, r_filter): - await utils.add_content('custom_permissions', entry, r_filter) + if not await utils.update_content('server_settings', entry, key): + await utils.add_content('server_settings', entry) - # Same case as prefixes, for now, trigger a manual 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)) @@ -333,20 +344,17 @@ class Mod: EXAMPLE: !perms remove play RESULT: Freedom!""" - cmd = self.find_command(command) + cmd = utils.find_command(self.bot, command) if cmd is None: await self.bot.say( "That command does not exist! You can't have custom permissions on a non-existant command....") return - r_filter = {'server_id': ctx.message.server.id} - await utils.replace_content('custom_permissions', r.row.without(cmd.qualified_name), r_filter) + update = {'permissions': {cmd.qualified_name: None}} + await utils.update_content('server_settings', update, ctx.message.server.id) 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(utils.cache['custom_permissions'].update()) - @commands.command(pass_context=True, no_pm=True) @utils.custom_perms(manage_server=True) async def prefix(self, ctx, *, prefix: str): @@ -354,15 +362,15 @@ class Mod: EXAMPLE: !prefix new_prefix RESULT: You probably screwing it up and not realizing you now need to do new_prefixprefix""" - r_filter = {'server_id': ctx.message.server.id} + key = ctx.message.server.id if prefix.lower().strip() == "none": prefix = None - entry = {'server_id': ctx.message.server.id, + entry = {'server_id': key, 'prefix': prefix} - if not await utils.add_content('prefixes', entry, r_filter): - await utils.update_content('prefixes', entry, r_filter) + if not await utils.update_content('server_settings', entry, key): + await utils.add_content('server_settings', entry) if prefix is None: fmt = "I have just cleared your custom prefix, the default prefix will have to be used now" @@ -390,7 +398,7 @@ class Mod: @commands.command(pass_context=True, no_pm=True) @utils.custom_perms(manage_messages=True) - async def prune(self, ctx, limit = None): + async def prune(self, ctx, limit=None): """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 If no limit is provided, then 100 will be used. This is also the max limit we can use @@ -458,14 +466,10 @@ class Mod: EXAMPLE: !rules 5 RESULT: Rule 5 is printed""" - r_filter = {'server_id': ctx.message.server.id} - rules = await utils.get_content('rules', r_filter) - try: - rules = rules[0]['rules'] - except TypeError: - await self.bot.say("This server currently has no rules on it! I see you like to live dangerously...") - return - if len(rules) == 0: + server_settings = await utils.get_content('server_settings', ctx.message.server.id) + rules = server_settings.get('rules') + + if not rules or len(rules) == 0: await self.bot.say("This server currently has no rules on it! I see you like to live dangerously...") return @@ -491,12 +495,18 @@ class Mod: EXAMPLE: !rules add No fun allowed in this server >:c RESULT: No more fun...unless they break the rules!""" - r_filter = {'server_id': ctx.message.server.id} - entry = {'server_id': ctx.message.server.id, + key = ctx.message.server.id + entry = {'server_id': key, 'rules': [rule]} update = {'rules': r.row['rules'].append(rule)} - if not await utils.update_content('rules', update, r_filter): - await utils.add_content('rules', entry, r_filter) + + server_settings = await utils.get_content('server_settings', key) + if server_settings and 'rules' in server_settings.keys(): + await utils.update_content('server_settings', update, key) + elif server_settings: + await utils.update_content('server_settings', entry, key) + else: + await utils.add_content('server_settings', entry) await self.bot.say("I have just saved your new rule, use the rules command to view this server's current rules") @@ -508,9 +518,8 @@ class Mod: EXAMPLE: !rules delete 5 RESULT: Freedom from opression!""" - r_filter = {'server_id': ctx.message.server.id} update = {'rules': r.row['rules'].delete_at(rule - 1)} - if not await utils.update_content('rules', update, r_filter): + if not await utils.update_content('server_settings', update, ctx.message.server.id): 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!") diff --git a/cogs/overwatch.py b/cogs/overwatch.py index 3680f8b..b477b00 100644 --- a/cogs/overwatch.py +++ b/cogs/overwatch.py @@ -1,13 +1,9 @@ -from .utils import config -from .utils import checks -from .utils import images +from . import utils from discord.ext import commands import discord -import aiohttp - -base_url = "https://api.owapi.net/api/v3/u/" +BASE_URL = "https://api.owapi.net/api/v3/u/" # This is a list of the possible things that we may want to retrieve from the stats # The API returns something if it exists, and leaves it out of the data returned entirely if it does not # For example if you have not won with a character, wins will not exist in the list @@ -15,7 +11,6 @@ base_url = "https://api.owapi.net/api/v3/u/" check_g_stats = ["eliminations", "deaths", 'kpd', 'wins', 'losses', 'time_played', 'cards', 'damage_done', 'healing_done', 'multikills'] check_o_stats = ['wins'] -MAX_RETRIES = 5 class Overwatch: @@ -23,29 +18,6 @@ class Overwatch: def __init__(self, bot): self.bot = bot - self.headers = {"User-Agent": config.user_agent} - self.session = aiohttp.ClientSession() - - async def _request(self, payload, endpoint): - """Handles requesting to the API""" - - # Format the URL we'll need based on the base_url, and the endpoint we want to hit - url = "{}{}".format(base_url, endpoint) - - # Attempt to connect up to our max retries - for x in range(MAX_RETRIES): - try: - async with aiohttp.ClientSession(headers=self.headers) as session: - async with session.get(url, params=payload) as r: - # If we failed to connect, attempt again - if r.status != 200: - continue - - data = await r.json() - return data - # If any error happened when making the request, attempt again - except: - continue @commands.group(no_pm=True) async def ow(self): @@ -56,28 +28,28 @@ class Overwatch: pass @ow.command(name="stats", pass_context=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def ow_stats(self, ctx, user: discord.Member = None, hero: str = ""): """Prints out a basic overview of a member's stats Provide a hero after the member to get stats for that specific hero EXAMPLE: !ow stats @OtherPerson Junkrat RESULT: Whether or not you should unfriend this person because they're a dirty rat""" + user = user or ctx.message.author - r_filter = {'member_id': user.id} - ow_stats = await config.get_content('overwatch', r_filter) + ow_stats = await utils.get_content('overwatch', user.id) if ow_stats is None: await self.bot.say("I do not have this user's battletag saved!") return # This API sometimes takes a while to look up information, so send a message saying we're processing - await self.bot.say("Searching profile information....") - bt = ow_stats[0]['battletag'] + bt = ow_stats['battletag'] if hero == "": # If no hero was provided, we just want the base stats for a player - data = await self._request(None, "{}/stats".format(bt)) + url = BASE_URL + "{}/stats".format(bt) + data = await utils.request(url) region = [x for x in data.keys() if data[x] is not None][0] stats = data[region]['stats']['quickplay'] @@ -86,8 +58,8 @@ class Overwatch: else: # If there was a hero provided, search for a user's data on that hero hero = hero.lower().replace('-', '') - endpoint = "{}/heroes".format(bt) - data = await self._request(None, endpoint) + url = BASE_URL + "{}/heroes".format(bt) + data = await utils.request(url) region = [x for x in data.keys() if data[x] is not None][0] stats = data[region]['heroes']['stats']['quickplay'].get(hero) @@ -104,52 +76,51 @@ class Overwatch: for k, r in stats['hero_stats'].items(): output_data.append((k.title().replace("_", " "), r)) try: - banner = await images.create_banner(user, "Overwatch", output_data) + banner = await utils.create_banner(user, "Overwatch", output_data) await self.bot.upload(banner) except (FileNotFoundError, discord.Forbidden): fmt = "\n".join("{}: {}".format(k, r) for k, r in output_data) await self.bot.say("Overwatch stats for {}: ```py\n{}```".format(user.name, fmt)) @ow.command(pass_context=True, name="add") - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def add(self, ctx, bt: str): """Saves your battletag for looking up information EXAMPLE: !ow add Username#1234 RESULT: Your battletag is now saved""" + await ctx.message.channel.trigger_typing() + # Battletags are normally provided like name#id # However the API needs this to be a -, so repliace # with - if it exists bt = bt.replace("#", "-") - r_filter = {'member_id': ctx.message.author.id} + key = ctx.message.author.id - # This API sometimes takes a while to look up information, so send a message saying we're processing - await self.bot.say("Looking up your profile information....") # All we're doing here is ensuring that the status is 200 when looking up someone's general information # If it's not, let them know exactly how to format their tag - endpoint = "{}/stats".format(bt) - data = await self._request(None, endpoint) + url = BASE_URL + "{}/stats".format(bt) + data = await utils.request(url) if data is None: - await self.bot.say("Profile does not exist! Battletags are picky, " - "format needs to be `user#xxxx`. Capitalization matters") - return + await self.bot.say("Profile does not exist! Battletags are picky, " + "format needs to be `user#xxxx`. Capitalization matters") + return # Now just save the battletag - entry = {'member_id': ctx.message.author.id, 'battletag': bt} + entry = {'member_id': key, 'battletag': bt} update = {'battletag': bt} # Try adding this first, if that fails, update the saved entry - if not await config.add_content('overwatch', entry, r_filter): - await config.update_content('overwatch', update, r_filter) + if not await utils.add_content('overwatch', entry): + await utils.update_content('overwatch', update, key) await self.bot.say("I have just saved your battletag {}".format(ctx.message.author.mention)) @ow.command(pass_context=True, name="delete", aliases=['remove']) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def delete(self, ctx): """Removes your battletag from the records EXAMPLE: !ow delete RESULT: Your battletag is no longer saved""" - r_filter = {'member_id': ctx.message.author.id} - if await config.remove_content('overwatch', r_filter): + if await utils.remove_content('overwatch', ctx.message.author.id): await self.bot.say("I no longer have your battletag saved {}".format(ctx.message.author.mention)) else: await self.bot.say("I don't even have your battletag saved {}".format(ctx.message.author.mention)) diff --git a/cogs/owner.py b/cogs/owner.py index 3a568ad..0528ed8 100644 --- a/cogs/owner.py +++ b/cogs/owner.py @@ -22,12 +22,12 @@ class 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() - r_filter = {'date': date} + key = date 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 utils.add_content('motd', entry, r_filter): - await utils.update_content('motd', entry, r_filter) + if await utils.add_content('motd', entry): + await utils.update_content('motd', entry, key) await self.bot.say("New motd update for {}!".format(date)) @commands.command(pass_context=True) diff --git a/cogs/picarto.py b/cogs/picarto.py index 6eab420..69b1722 100644 --- a/cogs/picarto.py +++ b/cogs/picarto.py @@ -11,159 +11,143 @@ from discord.ext import commands from . import utils log = logging.getLogger() -base_url = 'https://ptvappapi.picarto.tv' +BASE_URL = 'https://ptvappapi.picarto.tv' # This is a public key for use, I don't care if this is seen -key = '03e26294-b793-11e5-9a41-005056984bd4' - - -async def online_users(): - try: - # Someone from picarto contacted me and told me their database queries are odd - # It is more efficent on their end to make a query for all online users, and base checks off that - # In place of requesting for /channel and checking if that is online currently, for each channel - # This method is in place to just return all online_users - url = '{}/online/all?key={}'.format(base_url, key) - with aiohttp.ClientSession(headers={"User-Agent": utils.user_agent}) as s: - async with s.get(url) as response: - return await response.json() - except: - return {} - - -def check_online(online_channels, channel): - # online_channels is the dictionary of all users online currently - # And channel is the name we are checking against that - # This creates a list of all users that match this channel name (should only ever be 1) - # And returns True as long as it is more than 0 - matches = [stream for stream in online_channels if stream['channel_name'].lower() == channel.lower()] - return len(matches) > 0 +api_key = '03e26294-b793-11e5-9a41-005056984bd4' class Picarto: def __init__(self, bot): self.bot = bot - self.headers = {"User-Agent": utils.user_agent} - self.session = aiohttp.ClientSession() + + async def get_online_users(self): + # This method is in place to just return all online users so we can compare against it + url = BASE_URL + '/online/all' + payload = {'key': api_key} + self.online_channels = await utils.request(url, payload=payload) + + def channel_online(self, channel): + # Channel is the name we are checking against that + # This creates a list of all users that match this channel name (should only ever be 1) + # And returns True as long as it is more than 0 + channel = re.search("(?<=picarto.tv/)(.*)", channel).group(1) + matches = [stream for stream in self.online_channels if stream['channel_name'].lower() == channel.lower()] + return len(matches) > 0 async def check_channels(self): await self.bot.wait_until_ready() # This is a loop that runs every 30 seconds, checking if anyone has gone online try: while not self.bot.is_closed: - r_filter = {'notifications_on': 1} - picarto = await utils.get_content('picarto', r_filter) - # Get all online users before looping, so that only one request is needed - online_users_list = await online_users() - old_online_users = {data['member_id']: data for data in picarto if data['live']} - old_offline_users = {data['member_id']: data for data in picarto if not data['live']} - - for m_id, result in old_offline_users.items(): - # Get their url and their user based on that url - url = result['picarto_url'] - user = re.search("(?<=picarto.tv/)(.*)", url).group(1) - # Check if they are online right now - if check_online(online_users_list, user): - for server_id in result['servers']: - # Get the channel to send the message to, based on the saved alert's channel - server = self.bot.get_server(server_id) + await self.get_online_users() + picarto = await utils.filter_content('picarto', {'notifications_on': 1}) + for data in picarto: + m_id = data['member_id'] + url = data['picarto_url'] + # Check if they are online + online = self.channel_online(url) + # If they're currently online, but saved as not then we'll send our notification + if online and data['live'] == 0: + for s_id in data['servers']: + server = self.bot.get_server(s_id) if server is None: continue - server_alerts = await utils.get_content('server_alerts', {'server_id': server_id}) - try: - channel_id = server_alerts[0]['channel_id'] - except (IndexError, TypeError): - channel_id = server_id - channel = self.bot.get_channel(channel_id) - # Get the member that has just gone live - member = discord.utils.get(server.members, id=m_id) + member = server.get_member(m_id) if member is None: continue - - fmt = "{} has just gone live! View their stream at {}".format(member.display_name, url) - await self.bot.send_message(channel, fmt) - await utils.update_content('picarto', {'live': 1}, {'member_id': m_id}) - for m_id, result in old_online_users.items(): - # Get their url and their user based on that url - url = result['picarto_url'] - user = re.search("(?<=picarto.tv/)(.*)", url).group(1) - # Check if they are online right now - if not check_online(online_users_list, user): - for server_id in result['servers']: - # Get the channel to send the message to, based on the saved alert's channel - server = self.bot.get_server(server_id) + server_settings = await utils.get_content('server_settings', s_id) + if server_settings is not None: + channel_id = server_settings.get('notification_channel', s_id) + else: + channel_id = s_id + channel = server.get_channel(channel_id) + await self.bot.send_message(channel, "{} has just gone live! View their stream at <{}>".format(member.display_name, data['picarto_url'])) + self.bot.loop.create_task(utils.update_content('picarto', {'live': 1}, m_id)) + elif not online and data['live'] == 1: + for s_id in data['servers']: + server = self.bot.get_server(s_id) if server is None: continue - server_alerts = await utils.get_content('server_alerts', {'server_id': server_id}) - try: - channel_id = server_alerts[0]['channel_id'] - except (IndexError, TypeError): - channel_id = server_id - channel = self.bot.get_channel(channel_id) - # Get the member that has just gone live - member = discord.utils.get(server.members, id=m_id) - fmt = "{} has just gone offline! Catch them next time they stream at {}".format( - member.display_name, url) - await self.bot.send_message(channel, fmt) - await utils.update_content('picarto', {'live': 0}, {'member_id': m_id}) + member = server.get_member(m_id) + if member is None: + continue + server_settings = await utils.get_content('server_settings', s_id) + if server_settings is not None: + channel_id = server_settings.get('notification_channel', s_id) + else: + channel_id = s_id + channel = server.get_channel(channel_id) + await self.bot.send_message(channel, "{} has just gone offline! View their stream next time at <{}>".format(member.display_name, data['picarto_url'])) + self.bot.loop.create_task(utils.update_content('picarto', {'live': 0}, m_id)) await asyncio.sleep(30) except Exception as e: tb = traceback.format_exc() fmt = "{1}\n{0.__class__.__name__}: {0}".format(tb, e) log.error(fmt) - @commands.group(pass_context=True, invoke_without_command=True, no_pm=True) + @commands.group(invoke_without_command=True, no_pm=True, pass_context=True) @utils.custom_perms(send_messages=True) async def picarto(self, ctx, member: discord.Member = None): """This command can be used to view Picarto stats about a certain member EXAMPLE: !picarto @otherPerson RESULT: Info about their picarto stream""" + # If member is not given, base information on the author member = member or ctx.message.author - r_filter = {'member_id': member.id} - picarto_entry = await utils.get_content('picarto', r_filter) + picarto_entry = await utils.get_content('picarto', member.id) if picarto_entry is None: await self.bot.say("That user does not have a picarto url setup!") return - member_url = picarto_entry[0]['picarto_url'] + + member_url = picarto_entry['picarto_url'] # Use regex to get the actual username so that we can make a request to the API stream = re.search("(?<=picarto.tv/)(.*)", member_url).group(1) - url = '{}/channel/{}?key={}'.format(base_url, stream, key) - async with self.session.get(url, headers=self.headers) as response: - data = await response.json() + url = BASE_URL + '/channel/{}'.format(stream) + payload = {'key': api_key} + + data = await utils.request(url, payload=payload) + if data is None: + await self.bot.say("I couldn't connect to Picarto!") + return # Not everyone has all these settings, so use this as a way to print information if it does, otherwise ignore it things_to_print = ['channel', 'commissions_enabled', 'is_nsfw', 'program', 'tablet', 'followers', 'content_type'] - # Using title and replace to provide a nice way to print the data - fmt = "\n".join( - "{}: {}".format(i.title().replace("_", " "), result) for i, result in data.items() if i in things_to_print) + + embed = discord.Embed(title='{}\'s Picarto'.format(data['channel']), url=url) + if data['avatar_url']: + embed.set_thumbnail(url=data['avatar_url']) + + for i, result in data.items(): + if i in things_to_print and str(result): + i = i.title().replace('_', ' ') + embed.add_field(name=i, value=str(result)) # Social URL's can be given if a user wants them to show # Print them if they exist, otherwise don't try to include them - social_links = data.get('social_urls') - if social_links: - fmt2 = "\n".join( - "\t{}: {}".format(i.title().replace("_", " "), result) for i, result in social_links.items()) - fmt = "{}\nSocial Links:\n{}".format(fmt, fmt2) - await self.bot.say("Picarto stats for {}: ```\n{}```".format(member.display_name, fmt)) + for i, result in data['social_urls'].items(): + embed.add_field(name=i.title(), value=result) - @picarto.command(name='add', pass_context=True, no_pm=True) + await self.bot.say(embed=embed) + + @picarto.command(name='add', no_pm=True, pass_context=True) @utils.custom_perms(send_messages=True) async def add_picarto_url(self, ctx, url: str): """Saves your user's picarto URL EXAMPLE: !picarto add MyUsername RESULT: Your picarto stream is saved, and notifications should go to this server""" + # This uses a lookbehind to check if picarto.tv exists in the url given # If it does, it matches picarto.tv/user and sets the url as that # Then (in the else) add https://www. to that # Otherwise if it doesn't match, we'll hit an AttributeError due to .group(0) # This means that the url was just given as a user (or something complete invalid) # So set URL as https://www.picarto.tv/[url] - # Even if this was invalid such as https://www.picarto.tv/twitch.tv/user + # Even if this was invalid such as https://www.picarto.tv/picarto.tv/user # For example, our next check handles that try: url = re.search("((?<=://)?picarto.tv/)+(.*)", url).group(0) @@ -171,43 +155,42 @@ class Picarto: url = "https://www.picarto.tv/{}".format(url) else: url = "https://www.{}".format(url) + channel = re.search("https://www.picarto.tv/(.*)", url).group(1) + api_url = BASE_URL + '/channel/{}'.format(channel) + payload = {'key': api_key} - api_url = '{}/channel/{}?key={}'.format(base_url, re.search("https://www.picarto.tv/(.*)", url).group(1), key) + data = await utils.request(api_url, payload=payload) + if not data: + await self.bot.say("That Picarto user does not exist! What would be the point of adding a nonexistant " + "Picarto user? Silly") + return - # Check if we can find a user with the provided information, if we can't just return - async with self.session.get(api_url, headers=self.headers) as response: - if not response.status == 200: - await self.bot.say("That Picarto user does not exist! " - "What would be the point of adding a nonexistant Picarto user? Silly") - return - - r_filter = {'member_id': ctx.message.author.id} + key = ctx.message.author.id entry = {'picarto_url': url, 'servers': [ctx.message.server.id], 'notifications_on': 1, 'live': 0, - 'member_id': ctx.message.author.id} - if await utils.add_content('picarto', entry, r_filter): + 'member_id': key} + if await utils.add_content('picarto', entry): await self.bot.say( "I have just saved your Picarto URL {}, this server will now be notified when you go live".format( ctx.message.author.mention)) else: - await utils.update_content('picarto', {'picarto_url': url}, r_filter) + await utils.update_content('picarto', {'picarto_url': url}, key) await self.bot.say("I have just updated your Picarto URL") - @picarto.command(name='remove', aliases=['delete'], pass_context=True, no_pm=True) + @picarto.command(name='remove', aliases=['delete'], no_pm=True, pass_context=True) @utils.custom_perms(send_messages=True) async def remove_picarto_url(self, ctx): """Removes your picarto URL""" - r_filter = {'member_id': ctx.message.author.id} - if await utils.remove_content('picarto', r_filter): + if await utils.remove_content('picarto', ctx.message.author.id): await self.bot.say("I am no longer saving your picarto URL {}".format(ctx.message.author.mention)) else: await self.bot.say( "I do not have your picarto URL added {}. You can save your picarto url with {}picarto add".format( ctx.message.author.mention, ctx.prefix)) - @picarto.group(pass_context=True, no_pm=True, invoke_without_command=True) + @picarto.group(no_pm=True, invoke_without_command=True, pass_context=True) @utils.custom_perms(send_messages=True) async def notify(self, ctx): """This can be used to turn picarto notifications on or off @@ -215,29 +198,27 @@ class Picarto: EXAMPLE: !picarto notify RESULT: This server will now be notified of you going live""" - r_filter = {'member_id': ctx.message.author.id} - result = await utils.get_content('picarto', r_filter) + key = ctx.message.author.id + result = await utils.get_content('picarto', key) # Check if this user is saved at all if result is None: await self.bot.say( "I do not have your Picarto URL added {}. You can save your Picarto url with !picarto add".format( ctx.message.author.mention)) # Then check if this server is already added as one to notify in - elif ctx.message.server.id in result[0]['servers']: + elif ctx.message.server.id in result['servers']: await self.bot.say("I am already set to notify in this server...") else: - await utils.update_content('picarto', {'servers': r.row['servers'].append(ctx.message.server.id)}, - r_filter) + await utils.update_content('picarto', {'servers': r.row['servers'].append(ctx.message.server.id)}, key) - @notify.command(name='on', aliases=['start,yes'], pass_context=True, no_pm=True) + @notify.command(name='on', aliases=['start,yes'], no_pm=True, pass_context=True) @utils.custom_perms(send_messages=True) async def notify_on(self, ctx): """Turns picarto notifications on EXAMPLE: !picarto notify on RESULT: Notifications are sent when you go live""" - r_filter = {'member_id': ctx.message.author.id} - await utils.update_content('picarto', {'notifications_on': 1}, r_filter) + await utils.update_content('picarto', {'notifications_on': 1}, ctx.message.author.id) await self.bot.say("I will notify if you go live {}, you'll get a bajillion followers I promise c:".format( ctx.message.author.mention)) @@ -248,8 +229,7 @@ class Picarto: EXAMPLE: !picarto notify off RESULT: No more notifications sent when you go live""" - r_filter = {'member_id': ctx.message.author.id} - await utils.update_content('picarto', {'notifications_on': 0}, r_filter) + await utils.update_content('picarto', {'notifications_on': 0}, ctx.message.author.id) await self.bot.say( "I will not notify if you go live anymore {}, " "are you going to stream some lewd stuff you don't want people to see?~".format( @@ -259,4 +239,4 @@ class Picarto: def setup(bot): p = Picarto(bot) bot.loop.create_task(p.check_channels()) - bot.add_cog(Picarto(bot)) + bot.add_cog(Picarto(bot)) \ No newline at end of file diff --git a/cogs/raffle.py b/cogs/raffle.py index 411ad6f..e72deff 100644 --- a/cogs/raffle.py +++ b/cogs/raffle.py @@ -1,7 +1,7 @@ from discord.ext import commands -from .utils import config -from .utils import checks +from . import utils +import discord import random import pendulum import re @@ -27,7 +27,7 @@ class Raffle: async def check_raffles(self): # This is used to periodically check the current raffles, and see if they have ended yet # If the raffle has ended, we'll pick a winner from the entrants - raffles = await config.get_content('raffles') + raffles = await utils.get_content('raffles') if raffles is None: return @@ -73,22 +73,25 @@ class Raffle: # No matter which one of these matches were met, the raffle has ended and we want to remove it # We don't have to wait for it however, so create a task for it - r_filter = {'id': raffle_id} - self.bot.loop.create_task(config.remove_content('raffles', r_filter)) + self.bot.loop.create_task(utils.remove_content('raffles', raffle_id )) + + server_settings = await utils.get_content('server_settings', str(server.id)) + channel_id = server_settings.get('notification_channel', server.id) + channel = self.bot.get_channel(channel_id) try: - await self.bot.send_message(server, fmt) + await channel.send(fmt) except discord.Forbidden: pass @commands.command(pass_context=True, no_pm=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def raffles(self, ctx): """Used to print the current running raffles on the server EXAMPLE: !raffles RESULT: A list of the raffles setup on this server""" r_filter = {'server_id': ctx.message.server.id} - raffles = await config.get_content('raffles', r_filter) + raffles = await utils.filter_content('raffles', r_filter) if raffles is None: await self.bot.say("There are currently no raffles setup on this server!") return @@ -101,7 +104,7 @@ class Raffle: await self.bot.say(fmt) @commands.group(pass_context=True, no_pm=True, invoke_without_command=True) - @checks.custom_perms(send_messages=True) + @utils.custom_perms(send_messages=True) async def raffle(self, ctx, raffle_id: int = 0): """Used to enter a raffle running on this server If there is more than one raffle running, provide an ID of the raffle you want to enter @@ -113,7 +116,7 @@ class Raffle: r_filter = {'server_id': ctx.message.server.id} author = ctx.message.author - raffles = await config.get_content('raffles', r_filter) + raffles = await utils.filter_content('raffles', r_filter) if raffles is None: await self.bot.say("There are currently no raffles setup on this server!") return @@ -130,9 +133,8 @@ class Raffle: entrants.append(author.id) # Since we have no good thing to filter things off of, lets use the internal rethinkdb id - r_filter = {'id': raffles[0]['id']} update = {'entrants': entrants} - await config.update_content('raffles', update, r_filter) + await utils.update_content('raffles', update, raffles[0]['id']) await self.bot.say("{} you have just entered the raffle!".format(author.mention)) # Otherwise, make sure the author gave a valid raffle_id elif raffle_id in range(raffle_count - 1): @@ -144,10 +146,8 @@ class Raffle: return entrants.append(author.id) - # Since we have no good thing to filter things off of, lets use the internal rethinkdb id - r_filter = {'id': raffles[raffle_id]['id']} update = {'entrants': entrants} - await config.update_content('raffles', update, r_filter) + await utils.update_content('raffles', update, raffles[raffle_id]['id']) await self.bot.say("{} you have just entered the raffle!".format(author.mention)) else: fmt = "Please provide a valid raffle ID, as there are more than one setup on the server! " \ @@ -156,7 +156,7 @@ class Raffle: await self.bot.say(fmt) @raffle.command(pass_context=True, no_pm=True, name='create', aliases=['start', 'begin', 'add']) - @checks.custom_perms(kick_members=True) + @utils.custom_perms(kick_members=True) async def raffle_create(self, ctx): """This is used in order to create a new server raffle @@ -231,7 +231,7 @@ class Raffle: 'server_id': server.id} # We don't want to pass a filter to this, because we can have multiple raffles per server - await config.add_content('raffles', entry) + await utils.add_content('raffles', entry) await self.bot.say("I have just saved your new raffle!") diff --git a/cogs/stats.py b/cogs/stats.py index 612c124..4a37f10 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -74,16 +74,13 @@ class Stats: EXAMPLE: !command stats play RESULT: The realization that this is the only reason people use me ;-;""" - cmd = self.find_command(command) + cmd = self.bot.get_command(command) if cmd is None: await self.bot.say("`{}` is not a valid command".format(command)) return - r_filter = {'command': cmd.qualified_name} - command_stats = await utils.get_content('command_usage', r_filter) - try: - command_stats = command_stats[0] - except TypeError: + command_stats = await utils.get_content('command_usage', cmd.qualified_name) + if command_stats is None: await self.bot.say("That command has never been used! You know I worked hard on that! :c") return @@ -97,7 +94,7 @@ class Stats: ("Your Usage", member_usage), ("This Server's Usage", server_usage)] banner = await utils.create_banner(ctx.message.author, "Command Stats", data) - await self.bot.upload(banner) + await self.bot.upload(file=banner) except (FileNotFoundError, discord.Forbidden): fmt = "The command {} has been used a total of {} times\n" \ "{} times on this server\n" \ @@ -158,14 +155,13 @@ class Stats: EXAMPLE: !mostboops RESULT: You've booped @OtherPerson 351253897120935712093572193057310298 times!""" - r_filter = {'member_id': ctx.message.author.id} - boops = await utils.get_content('boops', r_filter) + boops = await utils.get_content('boops', ctx.message.author.id) if boops is None: await self.bot.say("You have not booped anyone {} Why the heck not...?".format(ctx.message.author.mention)) return # Just to make this easier, just pay attention to the boops data, now that we have the right entry - boops = boops[0]['boops'] + boops = boops['boops'] # First get a list of the ID's of all members in this server, for use in list comprehension server_member_ids = [member.id for member in ctx.message.server.members] @@ -189,14 +185,13 @@ class Stats: EXAMPLE: !listboops RESULT: The list of your booped members!""" - r_filter = {'member_id': ctx.message.author.id} - boops = await utils.get_content('boops', r_filter) + boops = await utils.get_content('boops', ctx.message.author.id) if boops is None: await self.bot.say("You have not booped anyone {} Why the heck not...?".format(ctx.message.author.mention)) return # Just to make this easier, just pay attention to the boops data, now that we have the right entry - boops = boops[0]['boops'] + boops = boops['boops'] # Same concept as the mostboops method server_member_ids = [member.id for member in ctx.message.server.members] diff --git a/cogs/strawpoll.py b/cogs/strawpoll.py index 9333fd8..ee50563 100644 --- a/cogs/strawpoll.py +++ b/cogs/strawpoll.py @@ -40,8 +40,7 @@ class Strawpoll: RESULT: A list of all polls setup on this server""" # Strawpolls cannot be 'deleted' so to handle whether a poll is running or not on a server # Just save the poll, which can then be removed when it should not be "running" anymore - r_filter = {'server_id': ctx.message.server.id} - polls = await config.get_content('strawpolls', r_filter) + polls = await config.get_content('strawpolls', ctx.message.server.id) # Check if there are any polls setup on this server try: polls = polls[0]['polls'] diff --git a/cogs/tags.py b/cogs/tags.py index 32e61e5..badf66b 100644 --- a/cogs/tags.py +++ b/cogs/tags.py @@ -17,7 +17,7 @@ class Tags: EXAMPLE: !tags RESULT: All tags setup on this server""" - tags = await config.get_content('tags', {'server_id': ctx.message.server.id}) + tags = await config.get_content('tags', ctx.message.server.id) # Simple generator that adds a tag to the list to print, if the tag is for this server try: fmt = "\n".join("{}".format(tag['tag']) for tag in tags) @@ -72,13 +72,10 @@ class Tags: entry = {'server_id': ctx.message.server.id, 'tag': tag, 'result': tag_result} r_filter = lambda row: (row['server_id'] == ctx.message.server.id) & (row['tag'] == tag) # Try to create new entry first, if that fails (it already exists) then we update it - if await config.add_content('tags', entry, r_filter): - await self.bot.say( - "I have just added the tag `{0}`! You can call this tag by entering !tag {0}".format(tag)) - else: - await config.update_content('tags', entry, r_filter) - await self.bot.say( - "I have just updated the tag `{0}`! You can call this tag by entering !tag {0}".format(tag)) + if not await config.update_content('tags', entry, r_filter): + await config.add_content('tags', entry) + await self.bot.say( + "I have just updated the tag `{0}`! You can call this tag by entering !tag {0}".format(tag)) @tag.command(name='delete', aliases=['remove', 'stop'], pass_context=True, no_pm=True) @checks.custom_perms(kick_members=True) @@ -88,12 +85,14 @@ class Tags: EXAMPLE: !tag delete stupid_tag RESULT: Deletes that stupid tag""" - r_filter = lambda row: (row['server_id'] == ctx.message.server.id) & (row['tag'] == tag) - if await config.remove_content('tags', r_filter): - await self.bot.say('I have just removed the tag `{}`'.format(tag)) + await self.bot.say("Temporarily disabled") + # TODO: Fix tags, this will inherently fix this method + """r_filter = lambda row: (row['server_id'] == ctx.message.guild.id) & (row['tag'] == tag) + if await utils.remove_content('tags', r_filter): + await ctx.send('I have just removed the tag `{}`'.format(tag)) else: - await self.bot.say( - "The tag {} does not exist! You can't remove something if it doesn't exist...".format(tag)) + await ctx.send( + "The tag {} does not exist! You can't remove something if it doesn't exist...".format(tag))""" def setup(bot): diff --git a/cogs/twitch.py b/cogs/twitch.py index a9ae4c1..e50f409 100644 --- a/cogs/twitch.py +++ b/cogs/twitch.py @@ -5,7 +5,6 @@ from . import utils import aiohttp import asyncio import discord -import json import re import rethinkdb as r import traceback @@ -23,11 +22,10 @@ class Twitch: self.bot = bot self.key = utils.twitch_key self.params = {'client_id': self.key} - self.headers = {"User-Agent": utils.user_agent, - "Client-ID": self.key} - async def channel_online(self, channel: str): + async def channel_online(self, twitch_url: str): # Check a specific channel's data, and get the response in text format + channel = re.search("(?<=twitch.tv/)(.*)", twitch_url).group(1) url = "https://api.twitch.tv/kraken/streams/{}".format(channel) response = await utils.request(url, payload=self.params) @@ -46,100 +44,96 @@ class Twitch: # Loop through as long as the bot is connected try: while not self.bot.is_closed: - twitch = await utils.get_content('twitch', {'notifications_on': 1}) - # Online/offline is based on whether they are set to such, in the utils file - # This means they were detected as online/offline before and we check for a change - online_users = {data['member_id']: data for data in twitch if data['live']} - offline_users = {data['member_id']: data for data in twitch if not data['live']} - for m_id, result in offline_users.items(): - # Get their url and their user based on that url - url = result['twitch_url'] - user = re.search("(?<=twitch.tv/)(.*)", url).group(1) - # Check if they are online right now - if await self.channel_online(user): - for server_id in result['servers']: - # Get the channel to send the message to, based on the saved alert's channel - server = self.bot.get_server(server_id) + twitch = await utils.filter_content('twitch', {'notifications_on': 1}) + for data in twitch: + m_id = data['member_id'] + url = data['twitch_url'] + # Check if they are online + online = await self.channel_online(url) + # If they're currently online, but saved as not then we'll send our notification + if online and data['live'] == 0: + for s_id in data['servers']: + server = self.bot.get_server(s_id) if server is None: continue - server_alerts = await utils.get_content('server_alerts', {'server_id': server_id}) - try: - channel_id = server_alerts[0]['channel_id'] - except (IndexError, TypeError): - channel_id = server_id - channel = self.bot.get_channel(channel_id) - # Get the member that has just gone live - member = discord.utils.get(server.members, id=m_id) + member = server.get_member(m_id) if member is None: continue - - fmt = "{} has just gone live! View their stream at {}".format(member.display_name, url) - await self.bot.send_message(channel, fmt) - await utils.update_content('twitch', {'live': 1}, {'member_id': m_id}) - for m_id, result in online_users.items(): - # Get their url and their user based on that url - url = result['twitch_url'] - user = re.search("(?<=twitch.tv/)(.*)", url).group(1) - # Check if they are online right now - if not await self.channel_online(user): - for server_id in result['servers']: - # Get the channel to send the message to, based on the saved alert's channel - server = self.bot.get_server(server_id) + server_settings = await utils.get_content('server_settings', s_id) + if server_settings is not None: + channel_id = server_settings.get('notification_channel', s_id) + else: + channel_id = s_id + channel = server.get_channel(channel_id) + await self.bot.send_message(channel, "{} has just gone live! View their stream at <{}>".format(member.display_name, data['twitch_url'])) + self.bot.loop.create_task(utils.update_content('twitch', {'live': 1}, m_id)) + elif not online and data['live'] == 1: + for s_id in data['servers']: + server = self.bot.get_server(s_id) if server is None: continue - server_alerts = await utils.get_content('server_alerts', {'server_id': server_id}) - channel_id = server_id - if server_alerts is not None and len(server_alerts) > 0: - channel_id = server_alerts[0].get('channel_id') - channel = self.bot.get_channel(channel_id) - # Get the member that has just gone live - member = discord.utils.get(server.members, id=m_id) - fmt = "{} has just gone offline! Catch them next time they stream at {}".format( - member.display_name, url) - await self.bot.send_message(channel, fmt) - await utils.update_content('twitch', {'live': 0}, {'member_id': m_id}) + member = server.get_member(m_id) + if member is None: + continue + server_settings = await utils.get_content('server_settings', s_id) + if server_settings is not None: + channel_id = server_settings.get('notification_channel', s_id) + else: + channel_id = s_id + channel = server.get_channel(channel_id) + await self.bot.send_message(channel, "{} has just gone offline! View their stream next time at <{}>".format(member.display_name, data['twitch_url'])) + self.bot.loop.create_task(utils.update_content('twitch', {'live': 0}, m_id)) await asyncio.sleep(30) except Exception as e: tb = traceback.format_exc() fmt = "{1}\n{0.__class__.__name__}: {0}".format(tb, e) log.error(fmt) - @commands.group(no_pm=True, invoke_without_command=True, pass_context=True) + @commands.group(no_pm=True, invoke_without_command=True) @utils.custom_perms(send_messages=True) async def twitch(self, ctx, *, member: discord.Member = None): """Use this command to check the twitch info of a user EXAMPLE: !twitch @OtherPerson RESULT: Information about their twitch URL""" + await ctx.message.channel.trigger_typing() + if member is None: member = ctx.message.author - result = await utils.get_content('twitch', {'member_id': member.id}) + result = await utils.get_content('twitch', member.id) if result is None: await self.bot.say("{} has not saved their twitch URL yet!".format(member.name)) return - result = result[0] url = result['twitch_url'] user = re.search("(?<=twitch.tv/)(.*)", url).group(1) - twitch_url = "https://api.twitch.tv/kraken/channels/{}?client_id={}".format(user, self.key) - with aiohttp.ClientSession() as s: - async with s.get(twitch_url) as response: - data = await response.json() + twitch_url = "https://api.twitch.tv/kraken/channels/{}".format(user) + payload = {'client_id': self.key} + data = await utils.request(twitch_url, payload=payload) - fmt = "Username: {}".format(data['display_name']) - fmt += "\nStatus: {}".format(data['status']) - fmt += "\nFollowers: {}".format(data['followers']) - fmt += "\nURL: {}".format(url) - await self.bot.say("```\n{}```".format(fmt)) + embed = discord.Embed(title=data['display_name'], url=url) + if data['logo']: + embed.set_thumbnail(url=data['logo']) - @twitch.command(name='add', pass_context=True, no_pm=True) + embed.add_field(name='Title', value=data['status']) + embed.add_field(name='Followers', value=data['followers']) + embed.add_field(name='Views', value=data['views']) + if data['game']: + embed.add_field(name='Game', value=data['game']) + embed.add_field(name='Language', value=data['broadcaster_language']) + + await self.bot.say(embed=embed) + + @twitch.command(name='add', no_pm=True) @utils.custom_perms(send_messages=True) async def add_twitch_url(self, ctx, url: str): """Saves your user's twitch URL EXAMPLE: !twitch add MyTwitchName RESULT: Saves your twitch URL; notifications will be sent to this server when you go live""" + await ctx.message.channel.trigger_typing() + # This uses a lookbehind to check if twitch.tv exists in the url given # If it does, it matches twitch.tv/user and sets the url as that # Then (in the else) add https://www. to that @@ -156,29 +150,28 @@ class Twitch: url = "https://www.{}".format(url) # Try to find the channel provided, we'll get a 404 response if it does not exist - with aiohttp.ClientSession() as s: - async with s.get(url) as response: - if not response.status == 200: - await self.bot.say("That twitch user does not exist! " - "What would be the point of adding a nonexistant twitch user? Silly") - return + status = await utils.request(url, attr='status') + if not status == 200: + await self.bot.say("That twitch user does not exist! " + "What would be the point of adding a nonexistant twitch user? Silly") + return - r_filter = {'member_id': ctx.message.author.id} + key = ctx.message.author.id entry = {'twitch_url': url, 'servers': [ctx.message.server.id], 'notifications_on': 1, 'live': 0, - 'member_id': ctx.message.author.id} + 'member_id': key} update = {'twitch_url': url} # Check to see if this user has already saved a twitch URL # If they have, update the URL, otherwise create a new entry # Assuming they're not live, and notifications should be on - if not await utils.add_content('twitch', entry, r_filter): - await utils.update_content('twitch', update, r_filter) + if not await utils.add_content('twitch', entry): + await utils.update_content('twitch', update, key) await self.bot.say("I have just saved your twitch url {}".format(ctx.message.author.mention)) - @twitch.command(name='remove', aliases=['delete'], pass_context=True, no_pm=True) + @twitch.command(name='remove', aliases=['delete'], no_pm=True) @utils.custom_perms(send_messages=True) async def remove_twitch_url(self, ctx): """Removes your twitch URL @@ -186,11 +179,10 @@ class Twitch: EXAMPLE: !twitch remove RESULT: I stop saving your twitch URL""" # Just try to remove it, if it doesn't exist, nothing is going to happen - r_filter = {'member_id': ctx.message.author.id} - await utils.remove_content('twitch', r_filter) + await utils.remove_content('twitch', ctx.message.author.id) await self.bot.say("I am no longer saving your twitch URL {}".format(ctx.message.author.mention)) - @twitch.group(pass_context=True, no_pm=True, invoke_without_command=True) + @twitch.group(no_pm=True, invoke_without_command=True) @utils.custom_perms(send_messages=True) async def notify(self, ctx): """This can be used to modify notification settings for your twitch user @@ -198,43 +190,41 @@ class Twitch: EXAMPLE: !twitch notify RESULT: This server will now be notified when you go live""" - r_filter = {'member_id': ctx.message.author.id} - result = await utils.get_content('twitch', r_filter) + key = ctx.message.author.id + result = await utils.get_content('twitch', key) # Check if this user is saved at all if result is None: await self.bot.say( "I do not have your twitch URL added {}. You can save your twitch url with !twitch add".format( ctx.message.author.mention)) # Then check if this server is already added as one to notify in - elif ctx.message.server.id in result[0]['servers']: + elif ctx.message.server.id in result['servers']: await self.bot.say("I am already set to notify in this server...") else: - await utils.update_content('twitch', {'servers': r.row['servers'].append(ctx.message.server.id)}, r_filter) + await utils.update_content('twitch', {'servers': r.row['servers'].append(ctx.message.server.id)}, key) await self.bot.say("This server will now be notified if you go live") - @notify.command(name='on', aliases=['start,yes'], pass_context=True, no_pm=True) + @notify.command(name='on', aliases=['start,yes'], no_pm=True) @utils.custom_perms(send_messages=True) async def notify_on(self, ctx): """Turns twitch notifications on EXAMPLE: !twitch notify on RESULT: Notifications will be sent when you go live""" - r_filter = {'member_id': ctx.message.author.id} - if await utils.update_content('twitch', {"notifications_on": 1}, r_filter): + if await utils.update_content('twitch', {"notifications_on": 1}, ctx.message.author.id): await self.bot.say("I will notify if you go live {}, you'll get a bajillion followers I promise c:".format( ctx.message.author.mention)) else: await self.bot.say("I can't notify if you go live if I don't know your twitch URL yet!") - @notify.command(name='off', aliases=['stop,no'], pass_context=True, no_pm=True) + @notify.command(name='off', aliases=['stop,no'], no_pm=True) @utils.custom_perms(send_messages=True) async def notify_off(self, ctx): """Turns twitch notifications off EXAMPLE: !twitch notify off RESULT: Notifications will not be sent when you go live""" - r_filter = {'member_id': ctx.message.author.id} - if await utils.update_content('twitch', {"notifications_on": 1}, r_filter): + if await utils.update_content('twitch', {"notifications_on": 0}, ctx.message.author.id): await self.bot.say( "I will not notify if you go live anymore {}, " "are you going to stream some lewd stuff you don't want people to see?~".format( @@ -247,4 +237,4 @@ class Twitch: def setup(bot): t = Twitch(bot) bot.loop.create_task(t.check_channels()) - bot.add_cog(Twitch(bot)) + bot.add_cog(Twitch(bot)) \ No newline at end of file diff --git a/cogs/utils/utilities.py b/cogs/utils/utilities.py index b5eb180..3de6fb6 100644 --- a/cogs/utils/utilities.py +++ b/cogs/utils/utilities.py @@ -5,6 +5,7 @@ import inspect from . import config from PIL import Image + def convert_to_jpeg(pfile): # Open the file given img = Image.open(pfile) @@ -16,6 +17,7 @@ def convert_to_jpeg(pfile): new_file.seek(0) return new_file + def get_all_commands(bot): """Returns a list of all command names for the bot""" # First lets create a set of all the parent names @@ -31,6 +33,7 @@ def get_all_commands(bot): return all_commands + def get_subcommands(command): yield command.qualified_name try: @@ -40,6 +43,19 @@ def get_subcommands(command): except AttributeError: pass + +async def channel_is_nsfw(channel): + server = channel.server.id + channel = channel.id + + server_settings = await config.get_content('server_settings', server) + + try: + return channel in server_settings['nsfw_channels'] + except (TypeError, IndexError, KeyError): + return False + + def find_command(bot, command): """Finds a command (be it parent or sub command) based on string given""" # This method ensures the command given is valid. We need to loop through commands @@ -64,6 +80,7 @@ def find_command(bot, command): return cmd + async def download_image(url): """Returns a file-like object based on the URL provided""" headers = {'User-Agent': config.user_agent} @@ -76,6 +93,7 @@ async def download_image(url): image = BytesIO(bts) return image + async def request(url, *, headers=None, payload=None, method='GET', attr='json'): # Make sure our User Agent is what's set, and ensure it's sent even if no headers are passed if headers == None: @@ -112,6 +130,7 @@ async def request(url, *, headers=None, payload=None, method='GET', attr='json') except: continue + async def update_records(key, winner, loser): # We're using the Harkness scale to rate # http://opnetchessclub.wikidot.com/harkness-rating-system From fdb2ca7974fcd32722f5e9d49a98307278041a4f Mon Sep 17 00:00:00 2001 From: phxntxm Date: Fri, 17 Mar 2017 00:27:21 -0500 Subject: [PATCH 03/11] Removed old check --- cogs/utils/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/utils/__init__.py b/cogs/utils/__init__.py index 4db6c14..74d8602 100644 --- a/cogs/utils/__init__.py +++ b/cogs/utils/__init__.py @@ -1,6 +1,6 @@ from .cards import Deck -from .checks import is_owner, custom_perms, is_pm, db_check +from .checks import is_owner, custom_perms, db_check from .config import * from .utilities import * from .images import create_banner -from .paginator import Pages, CannotPaginate +from .paginator import Pages, CannotPaginate \ No newline at end of file From 8386923199d4dab59f2c352b805ac7beb0875694 Mon Sep 17 00:00:00 2001 From: phxntxm Date: Fri, 17 Mar 2017 00:29:56 -0500 Subject: [PATCH 04/11] Pass context --- cogs/twitch.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cogs/twitch.py b/cogs/twitch.py index e50f409..eea05e5 100644 --- a/cogs/twitch.py +++ b/cogs/twitch.py @@ -89,7 +89,7 @@ class Twitch: fmt = "{1}\n{0.__class__.__name__}: {0}".format(tb, e) log.error(fmt) - @commands.group(no_pm=True, invoke_without_command=True) + @commands.group(no_pm=True, invoke_without_command=True, pass_context=True) @utils.custom_perms(send_messages=True) async def twitch(self, ctx, *, member: discord.Member = None): """Use this command to check the twitch info of a user @@ -125,7 +125,7 @@ class Twitch: await self.bot.say(embed=embed) - @twitch.command(name='add', no_pm=True) + @twitch.command(name='add', no_pm=True, pass_context=True) @utils.custom_perms(send_messages=True) async def add_twitch_url(self, ctx, url: str): """Saves your user's twitch URL @@ -171,7 +171,7 @@ class Twitch: await utils.update_content('twitch', update, key) await self.bot.say("I have just saved your twitch url {}".format(ctx.message.author.mention)) - @twitch.command(name='remove', aliases=['delete'], no_pm=True) + @twitch.command(name='remove', aliases=['delete'], no_pm=True, pass_context=True) @utils.custom_perms(send_messages=True) async def remove_twitch_url(self, ctx): """Removes your twitch URL @@ -182,7 +182,7 @@ class Twitch: await utils.remove_content('twitch', ctx.message.author.id) await self.bot.say("I am no longer saving your twitch URL {}".format(ctx.message.author.mention)) - @twitch.group(no_pm=True, invoke_without_command=True) + @twitch.group(no_pm=True, invoke_without_command=True, pass_context=True) @utils.custom_perms(send_messages=True) async def notify(self, ctx): """This can be used to modify notification settings for your twitch user @@ -204,7 +204,7 @@ class Twitch: await utils.update_content('twitch', {'servers': r.row['servers'].append(ctx.message.server.id)}, key) await self.bot.say("This server will now be notified if you go live") - @notify.command(name='on', aliases=['start,yes'], no_pm=True) + @notify.command(name='on', aliases=['start,yes'], no_pm=True, pass_context=True) @utils.custom_perms(send_messages=True) async def notify_on(self, ctx): """Turns twitch notifications on @@ -217,7 +217,7 @@ class Twitch: else: await self.bot.say("I can't notify if you go live if I don't know your twitch URL yet!") - @notify.command(name='off', aliases=['stop,no'], no_pm=True) + @notify.command(name='off', aliases=['stop,no'], no_pm=True, pass_context=True) @utils.custom_perms(send_messages=True) async def notify_off(self, ctx): """Turns twitch notifications off From fcc8cc0bbe1fd23a47c1e3b63d9ba662c70575f6 Mon Sep 17 00:00:00 2001 From: phxntxm Date: Fri, 17 Mar 2017 01:12:16 -0500 Subject: [PATCH 05/11] Remove erroneous instance of guild --- bot.py | 2 +- cogs/utils/config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index 09d4cfc..b1b3e75 100644 --- a/bot.py +++ b/bot.py @@ -65,7 +65,7 @@ async def process_command(ctx): command_usage['member_usage'] = total_member_usage # Add one to the server's usage for this command - if ctx.message.guild is not None: + if ctx.message.server is not None: total_server_usage = command_usage.get('server_usage', {}) server_usage = total_server_usage.get(server.id, 0) + 1 total_server_usage[server.id] = server_usage diff --git a/cogs/utils/config.py b/cogs/utils/config.py index 91d5f1d..875f5cb 100644 --- a/cogs/utils/config.py +++ b/cogs/utils/config.py @@ -116,7 +116,7 @@ def command_prefix(bot, message): # But it is not worth a query for every single message the bot detects, to fix try: prefixes = cache['server_settings'].values - prefix = [x for x in prefixes if x['server_id'] == message.guild.id][0]['prefix'] + prefix = [x for x in prefixes if x['server_id'] == message.server.id][0]['prefix'] return prefix or default_prefix except (KeyError, TypeError, IndexError, AttributeError): return default_prefix From 03c91c06ff1fd3c4d4a6e56f644d2e089cc5ee56 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Fri, 17 Mar 2017 16:42:20 -0500 Subject: [PATCH 06/11] Update to work with new database settings --- cogs/stats.py | 2 +- cogs/tags.py | 2 +- cogs/utils/utilities.py | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cogs/stats.py b/cogs/stats.py index 4a37f10..3a5b55f 100644 --- a/cogs/stats.py +++ b/cogs/stats.py @@ -94,7 +94,7 @@ class Stats: ("Your Usage", member_usage), ("This Server's Usage", server_usage)] banner = await utils.create_banner(ctx.message.author, "Command Stats", data) - await self.bot.upload(file=banner) + await self.bot.upload(banner) except (FileNotFoundError, discord.Forbidden): fmt = "The command {} has been used a total of {} times\n" \ "{} times on this server\n" \ diff --git a/cogs/tags.py b/cogs/tags.py index badf66b..bd6a20d 100644 --- a/cogs/tags.py +++ b/cogs/tags.py @@ -34,7 +34,7 @@ class Tags: EXAMPLE: !tag butts RESULT: Whatever you setup for the butts tag!!""" r_filter = lambda row: (row['server_id'] == ctx.message.server.id) & (row['tag'] == tag) - tags = await config.get_content('tags', r_filter) + tags = await config.filter_content('tags', r_filter) if tags is None: await self.bot.say('That tag does not exist!') return diff --git a/cogs/utils/utilities.py b/cogs/utils/utilities.py index 3de6fb6..ab7d6c6 100644 --- a/cogs/utils/utilities.py +++ b/cogs/utils/utilities.py @@ -134,8 +134,8 @@ async def request(url, *, headers=None, payload=None, method='GET', attr='json') async def update_records(key, winner, loser): # We're using the Harkness scale to rate # http://opnetchessclub.wikidot.com/harkness-rating-system - r_filter = lambda row: (row['member_id'] == winner.id) | (row['member_id'] == loser.id) - matches = await config.get_content(key, r_filter) + r_filter = lambda row: (row['member_id'] == str(winner.id)) | (row['member_id'] == str(loser.id)) + matches = await config.filter_content(key, r_filter) winner_stats = {} loser_stats = {} @@ -183,9 +183,9 @@ async def update_records(key, winner, loser): winner_stats = {'wins': winner_wins, 'losses': winner_losses, 'rating': winner_rating} loser_stats = {'wins': loser_wins, 'losses': loser_losses, 'rating': loser_rating} - if not await config.update_content(key, winner_stats, {'member_id': winner.id}): + if not await config.update_content(key, winner_stats, winner.id): winner_stats['member_id'] = winner.id - await config.add_content(key, winner_stats, {'member_id': winner.id}) - if not await config.update_content(key, loser_stats, {'member_id': loser.id}): + await config.add_content(key, winner_stats) + if not await config.update_content(key, loser_stats, loser.id): loser_stats['member_id'] = loser.id - await config.add_content(key, loser_stats, {'member_id': loser.id}) + await config.add_content(key, loser_stats) From 542293f8d3da65c3aeb36f8df8fb50c1a2c53703 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Sun, 19 Mar 2017 22:37:45 -0500 Subject: [PATCH 07/11] Fixed an issue where cache would update even on a get request, causing an infinite loop --- cogs/utils/config.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/cogs/utils/config.py b/cogs/utils/config.py index 875f5cb..da4e93a 100644 --- a/cogs/utils/config.py +++ b/cogs/utils/config.py @@ -130,13 +130,15 @@ async def add_content(table, content): # For our purposes however, we do not want this try: result = await r.table(table).insert(content).run(conn) - await conn.close() except r.ReqlOpFailedError: # This means the table does not exist await r.table_create(table).run(conn) await r.table(table).insert(content).run(conn) - await conn.close() result = {} + + await conn.close() + if table == 'prefixes' or table == 'server_settings': + loop.create_task(cache[table].update()) return result.get('inserted', 0) > 0 @@ -148,6 +150,7 @@ async def remove_content(table, key): except r.ReqlOpFailedError: result = {} pass + await conn.close() if table == 'prefixes' or table == 'server_settings': loop.create_task(cache[table].update()) @@ -164,8 +167,8 @@ async def update_content(table, content, key): # This is why we're accepting a variable and using it, whatever it may be, as the query result = await r.table(table).get(key).update(content).run(conn) except r.ReqlOpFailedError: - await conn.close() result = {} + await conn.close() if table == 'prefixes' or table == 'server_settings': loop.create_task(cache[table].update()) @@ -179,8 +182,8 @@ async def replace_content(table, content, key): try: result = await r.table(table).get(key).replace(content).run(conn) except r.ReqlOpFailedError: - await conn.close() result = {} + await conn.close() if table == 'prefixes' or table == 'server_settings': loop.create_task(cache[table].update()) @@ -206,9 +209,8 @@ async def get_content(table, key=None): content = cursor except (IndexError, r.ReqlOpFailedError): content = None + await conn.close() - if table == 'prefixes' or table == 'server_settings': - loop.create_task(cache[table].update()) return content async def filter_content(table: str, r_filter): @@ -221,9 +223,8 @@ async def filter_content(table: str, r_filter): content = None except (IndexError, r.ReqlOpFailedError): content = None + await conn.close() - if table == 'prefixes' or table == 'server_settings': - loop.create_task(cache[table].update()) return content From 77dd53567f65689acdf63058103bdde36313b033 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Fri, 24 Mar 2017 14:19:53 -0500 Subject: [PATCH 08/11] Remove prefixes from cache --- cogs/utils/config.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cogs/utils/config.py b/cogs/utils/config.py index da4e93a..22dae61 100644 --- a/cogs/utils/config.py +++ b/cogs/utils/config.py @@ -101,7 +101,6 @@ cache = {} ca che[k] = Cache(k)""" # We still need 'cache' for prefixes and custom permissions however, so for now, just include that -cache['prefixes'] = Cache('prefixes') cache['server_settings'] = Cache('server_settings') async def update_cache(): @@ -137,7 +136,7 @@ async def add_content(table, content): result = {} await conn.close() - if table == 'prefixes' or table == 'server_settings': + if table == 'server_settings': loop.create_task(cache[table].update()) return result.get('inserted', 0) > 0 @@ -152,7 +151,7 @@ async def remove_content(table, key): pass await conn.close() - if table == 'prefixes' or table == 'server_settings': + if table == 'server_settings': loop.create_task(cache[table].update()) return result.get('deleted', 0) > 0 @@ -170,7 +169,7 @@ async def update_content(table, content, key): result = {} await conn.close() - if table == 'prefixes' or table == 'server_settings': + if table == 'server_settings': loop.create_task(cache[table].update()) return result.get('replaced', 0) > 0 or result.get('unchanged', 0) > 0 @@ -185,7 +184,7 @@ async def replace_content(table, content, key): result = {} await conn.close() - if table == 'prefixes' or table == 'server_settings': + if table == 'server_settings': loop.create_task(cache[table].update()) return result.get('replaced', 0) > 0 or result.get('unchanged', 0) > 0 From 8962218f7db8f8774e7e8ff5e4f1891c8d0aad4f Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Thu, 13 Apr 2017 19:51:10 -0500 Subject: [PATCH 09/11] Remove DA from the examples --- README.md | 2 -- config.yml.sample | 2 -- 2 files changed, 4 deletions(-) diff --git a/README.md b/README.md index 370a7d0..10e718b 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,6 @@ The only required file to modify would be the config.yml.sample file. The entrie - twitch_key: The twitch token that is used for the API calls - youtube_key: The key used for youtube API calls - osu_key: The key used for Osu API calls -- da_id: The deviant art ID retrieved when registering an application, needed for API calls. -- da_secret: The deviant art Secret, given with the da_id above - shard_count: This is the number of shards the bot is split over. 1 needs to be used if the bot is not being sharded - shard_id: This will be the ID of the shard in particular, 0 if sharding is not used - extensions: This is a list of the extensions loaded into the bot (check the cogs folder for the extensions available). The disabled playlist is a special entry....read that file for what its purpose is....most likely you will not need it. Entries in this list need to be separated by ", " like in the example. diff --git a/config.yml.sample b/config.yml.sample index 1b7e5dd..897960d 100644 --- a/config.yml.sample +++ b/config.yml.sample @@ -8,8 +8,6 @@ carbon_key: 'key' twitch_key: 'key' youtube_key: 'key' osu_key: 'key' -da_id: 'id' -da_secret: 'key' dev_server: 'https://discord.gg/123456' user_agent: 'User-Agent/1.0.0 (Comment like link to site)' From e3e971bfdcf0e5f85852c75b61786a3855a11b26 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Thu, 13 Apr 2017 20:11:33 -0500 Subject: [PATCH 10/11] readd sharding info --- cogs/utils/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cogs/utils/config.py b/cogs/utils/config.py index 22dae61..13c8f3b 100644 --- a/cogs/utils/config.py +++ b/cogs/utils/config.py @@ -47,6 +47,9 @@ class Cache: bot_description = global_config.get("description") # Bot's default prefix for commands default_prefix = global_config.get("command_prefix", "!") +# The sharding information +shard_count = global_config.get("shard_count", 1) +shard_id = global_config.get("shard_id", 1) # The key for bots.discord.pw and carbonitex discord_bots_key = global_config.get('discord_bots_key', "") carbon_key = global_config.get('carbon_key', "") From 713eeb1e3ab5b429520a4d0b29fbe9563ac8ec39 Mon Sep 17 00:00:00 2001 From: Phxntxm Date: Thu, 13 Apr 2017 20:31:03 -0500 Subject: [PATCH 11/11] Update modules to match current requirements --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 10e718b..d64604d 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ https://www.rethinkdb.com/docs/install/ I also use a few libraries that aren't included by default, which can be installed using pip. ``` -python3.5 -m pip install discord.py[voice] lxml fuzzywuzzy youtube_dl rethinkdb ruamel.yaml pendulum Pillow==3.4.1 readline +python3.5 -m pip install discord.py[voice] BeautifulSoup4 youtube_dl rethinkdb ruamel.yaml pendulum Pillow==3.4.1 readline # Or on windows -py -3 -m pip install discord.py[voice] lxml fuzzywuzzy youtube_dl rethinkdb ruamel.yaml pendulum Pillow==3.4.1 readline +py -3 -m pip install discord.py[voice] BeautifulSoup4 youtube_dl rethinkdb ruamel.yaml pendulum Pillow==3.4.1 readline ``` Note: ATM of writing this, Pillow 3.4.2 (the stable version...good job Pillow?) is broken, do not use pip's default to install this. This is why we're using Pillow==3.4.1 above, and not just Pillow