diff --git a/bot.py b/bot.py index 4b2a463..d4f28bb 100644 --- a/bot.py +++ b/bot.py @@ -83,7 +83,8 @@ async def on_command_error(ctx, error): elif isinstance(error.original, discord.HTTPException) and ( 'empty message' in str(error.original) or 'INTERNAL SERVER ERROR' in str(error.original) or - 'REQUEST ENTITY TOO LARGE' in str(error.original)): + 'REQUEST ENTITY TOO LARGE' in str(error.original) or + 'Unknown Message' in str(error.original)): return elif isinstance(error.original, aiohttp.ClientOSError): return diff --git a/cogs/admin.py b/cogs/admin.py index 0049a4c..6ac1802 100644 --- a/cogs/admin.py +++ b/cogs/admin.py @@ -13,6 +13,27 @@ class Administration: def __init__(self, bot): self.bot = bot + @commands.command() + @commands.guild_only() + @utils.custom_perms(manage_guild=True) + @utils.check_restricted() + async def allowbirthdays(self, ctx, setting): + """Turns on/off the birthday announcements in this server + + EXAMPLE: !allowbirthdays on + RESULT: Playlists can now be used""" + if setting.lower() in ['on', 'yes', 'true']: + allowed = True + else: + allowed = False + entry = { + 'server_id': str(ctx.message.guild.id), + 'birthdays_allowed': allowed + } + self.bot.db.save('server_settings', entry) + fmt = "The birthday announcements have just been turned {}".format("on" if allowed else "off") + await ctx.send(fmt) + @commands.command() @commands.guild_only() @utils.custom_perms(manage_guild=True) diff --git a/cogs/birthday.py b/cogs/birthday.py new file mode 100644 index 0000000..ca71d2d --- /dev/null +++ b/cogs/birthday.py @@ -0,0 +1,154 @@ +import discord +import pendulum +from pendulum.parsing.exceptions import ParserError + +from discord.ext import commands +from . import utils + +class Birthday: + + def __init__(self, bot): + self.bot = bot + self.bot.loop.create_task(self.birthday_task()) + + def get_birthdays_for_server(self, server, today=False): + bds = self.bot.db.load('birthdays') + # Get a list of the ID's to compare against + member_ids = [str(m.id) for m in server.members] + + # Now create a list comparing to the server's list of member IDs + bds = [x for x in bds if x['member_id'] in member_ids] + + _entries = [] + + for bd in bds: + if not bd['birthday']: + continue + + day = pendulum.parse(bd['birthday']) + # Check if it's today, and we want to only get todays birthdays + if (today and day.date() == pendulum.today().date()) or not today: + # If so, get the member and add them to the entry + member = server.get_member(int(bd['member_id'])) + _entries.append({ + 'birthday': day, + 'member': member + }) + + return _entries + + async def birthday_task(self): + while True: + await self.notify_birthdays() + # Every 12 hours, this is not something that needs to happen often + await asyncio.sleep(60 * 60 * 12) + + async def notify_birthdays(self): + tfilter = {'birthdays_allowed': True} + servers = await self.bot.db.actual_load('server_settings', table_filter=tfilter) + for s in servers: + server = bot.get_guild(int(s['server_id'])) + if not server: + continue + + bds = self.get_birthdays_for_server(server, today=True) + for bd in bds: + # Set our default to either the one set, or the default channel of the server + default_channel_id = servers.get('notifications', {}).get('default') or guild.id + # If it is has been overriden by picarto notifications setting, use this + channel_id = servers.get('notifications', {}).get('birthdays') or default_channel_id + # Now get the channel based on that ID + channel = server.get_channel(int(channel_id)) or server.default_channel + try: + await channel.send("It is {}'s birthday today! Wish them a happy birthday! \N{SHORTCAKE}".format(member.mention)) + except (discord.Forbidden, discord.HTTPException): + pass + + @commands.group(aliases=['birthdays'], invoke_without_command=True) + @commands.guild_only() + @utils.custom_perms(send_messages=True) + @utils.check_restricted() + async def birthday(self, ctx, *, member: discord.Member = None): + """A command used to view the birthdays on this server; or a specific member's birthday + + EXAMPLE: !birthdays + RESULT: A printout of the birthdays from everyone on this server""" + if member: + date = self.bot.db.load('birthdays', key=member.id, pluck='birthday') + if date: + await ctx.send("{}'s birthday is {}".format(member.display_name, date)) + else: + await ctx.send("I do not have {}'s birthday saved!".format(member.display_name)) + else: + # Get this server's birthdays + bds = self.get_birthdays_for_server(ctx.message.guild) + # Create entries based on the user's display name and their birthday + entries = ["{} ({})".format(bd['member'].display_name, bd['birthday'].format("%B %-d")) for bd in bds] + # Create our pages object + try: + pages = utils.Pages(self.bot, message=ctx.message, entries=entries, per_page=5) + pages.title = "Birthdays for {}".format(ctx.message.guild.name) + await pages.paginate() + except utils.CannotPaginate as e: + await ctx.send(str(e)) + + + @birthday.command(name='add') + @utils.custom_perms(send_messages=True) + @utils.check_restricted() + async def _add_bday(self, ctx, *, date): + """Used to link your birthday to your account + + EXAMPLE: !birthday add December 1st + RESULT: I now know your birthday is December 1st""" + try: + # Try parsing the date from what was given + date = pendulum.parse(date) + # We'll save in a specific way so that it can be parsed how we want, so do this + date = date.format("%B %-d") + except (ValueError, ParserError): + await ctx.send("Please provide date in a valid format, such as December 1st!") + else: + entry = { + 'member_id': str(ctx.message.author.id), + 'birthday': date + } + self.bot.db.save('birthdays', entry) + await ctx.send("I have just saved your birthday as {}".format(date)) + + @birthday.command(name='remove') + @utils.custom_perms(send_messages=True) + @utils.check_restricted() + async def _remove_bday(self, ctx): + """Used to unlink your birthday to your account + + EXAMPLE: !birthday remove + RESULT: I have magically forgotten your birthday""" + entry = { + 'member_id': str(ctx.message.author.id), + 'birthday': None + } + self.bot.db.save('birthdays', entry) + await ctx.send("I don't know your birthday anymore :(") + + @birthday.command(name='alerts', aliases=['notifications']) + @commands.guild_only() + @utils.custom_perms(manage_guild=True) + @utils.check_restricted() + async def birthday_alerts_channel(self, ctx, channel: discord.TextChannel): + """Sets the notifications channel for birthday notifications + + EXAMPLE: !birthday alerts #birthday + RESULT: birthday notifications will go to this channel + """ + entry = { + 'server_id': str(ctx.message.guild.id), + 'notifications': { + 'birthday': str(channel.id) + } + } + self.bot.db.save('server_settings', entry) + await ctx.send("All birthday notifications will now go to {}".format(channel.mention)) + +def setup(bot): + bot.add_cog(Birthday(bot)) diff --git a/cogs/music.py b/cogs/music.py index 5848541..8f92905 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -521,8 +521,9 @@ class Music: if not await ctx.invoke(self.join): return + state = self.voice_states.get(ctx.message.guild.id) # If this is a user queue, this is the wrong command - if self.voice_states.get(ctx.message.guild.id).user_queue: + if state and state.user_queue: await ctx.send("The current queue type is the DJ queue. " "Use the command {}dj to join this queue".format(ctx.prefix)) return diff --git a/cogs/picarto.py b/cogs/picarto.py index 30b25e6..e7c8777 100644 --- a/cogs/picarto.py +++ b/cogs/picarto.py @@ -28,6 +28,9 @@ class Picarto: self.online_channels = await utils.request(url, payload=payload) async def channel_embed(self, channel): + # First make sure the picarto URL is actually given + if not channel: + return None # Use regex to get the actual username so that we can make a request to the API stream = re.search("(?<=picarto.tv/)(.*)", channel).group(1) url = BASE_URL + '/channel/name/{}'.format(stream) diff --git a/cogs/twitch.py b/cogs/twitch.py index a9daf75..049a5f1 100644 --- a/cogs/twitch.py +++ b/cogs/twitch.py @@ -24,6 +24,8 @@ class Twitch: self.params = {'client_id': self.key} def _form_embed(self, data): + if not data: + return None # I want to make the least API calls possible, however there's a few things to note here: # 1) When requesting /streams and a channel is offline, the channel data is not provided # 2) When requesting /streams and a channel is online, the channel data is provided diff --git a/cogs/utils/checks.py b/cogs/utils/checks.py index 09813bb..f8f30cc 100644 --- a/cogs/utils/checks.py +++ b/cogs/utils/checks.py @@ -22,7 +22,8 @@ required_tables = { 'tags': 'server_id', 'tictactoe': 'member_id', 'twitch': 'member_id', - 'user_playlists': 'member_id' + 'user_playlists': 'member_id', + 'birthdays': 'member_id' } diff --git a/cogs/utils/config.py b/cogs/utils/config.py index 04cda24..052da52 100644 --- a/cogs/utils/config.py +++ b/cogs/utils/config.py @@ -56,6 +56,7 @@ extensions = [ 'cogs.mod', 'cogs.admin', 'cogs.images', + 'cogs.birthday', 'cogs.owner', 'cogs.stats', 'cogs.picarto',