diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..e1f901ff --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Use line ending normalisation on all files +* text=auto diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..e3cd6d9e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,19 @@ +[yapf] +based_on_style = pep8 +column_limit = 127 +indent_width = 4 +dedent_closing_brackets = true +split_all_comma_separated_values = true +split_before_arithmetic_operator = true +split_before_bitwise_operator = true +each_dict_entry_on_separate_line = true + +[flake8] +max-line-length = 127 + +[pycodestyle] +max-line-length = 127 + +[mypy] +show_error_codes = True +disable_error_code = import diff --git a/src/esportsbot/base_functions.py b/src/esportsbot/base_functions.py index 01871d82..37c94e58 100644 --- a/src/esportsbot/base_functions.py +++ b/src/esportsbot/base_functions.py @@ -2,8 +2,7 @@ async def send_to_log_channel(self, guild_id, msg): - db_logging_call = db_gateway().get( - 'guild_info', params={'guild_id': guild_id}) + db_logging_call = db_gateway().get('guild_info', params={'guild_id': guild_id}) if db_logging_call and db_logging_call[0]['log_channel_id']: await self.bot.get_channel(db_logging_call[0]['log_channel_id']).send(msg) @@ -49,12 +48,10 @@ def user_id_from_mention(pre_clean_data: str) -> int: def get_whether_in_vm_master(guild_id, channel_id): - in_master = db_gateway().get('voicemaster_master', params={ - 'guild_id': guild_id, 'channel_id': channel_id}) + in_master = db_gateway().get('voicemaster_master', params={'guild_id': guild_id, 'channel_id': channel_id}) return bool(in_master) def get_whether_in_vm_slave(guild_id, channel_id): - in_slave = db_gateway().get('voicemaster_slave', params={ - 'guild_id': guild_id, 'channel_id': channel_id}) + in_slave = db_gateway().get('voicemaster_slave', params={'guild_id': guild_id, 'channel_id': channel_id}) return bool(in_slave) diff --git a/src/esportsbot/bot.py b/src/esportsbot/bot.py index b4af6686..741ef65d 100644 --- a/src/esportsbot/bot.py +++ b/src/esportsbot/bot.py @@ -13,7 +13,6 @@ from datetime import datetime, timedelta import asyncio - # Value to assign new guilds in their role_ping_cooldown_seconds attribute DEFAULT_ROLE_PING_COOLDOWN = timedelta(hours=5) # Value to assign new guilds in their pingme_create_poll_length_seconds attribute @@ -34,13 +33,17 @@ def make_guild_init_data(guild: discord.Guild) -> Dict[str, Any]: :return: A dictionary with default guild attributes, including the guild ID :rtype: Dict[str, Any] """ - return {'guild_id': guild.id, 'num_running_polls': 0, 'role_ping_cooldown_seconds': int(DEFAULT_ROLE_PING_COOLDOWN.total_seconds()), - "pingme_create_threshold": DEFAULT_PINGME_CREATE_THRESHOLD, "pingme_create_poll_length_seconds": int(DEFAULT_PINGME_CREATE_POLL_LENGTH.total_seconds())} + return { + 'guild_id': guild.id, + 'num_running_polls': 0, + 'role_ping_cooldown_seconds': int(DEFAULT_ROLE_PING_COOLDOWN.total_seconds()), + "pingme_create_threshold": DEFAULT_PINGME_CREATE_THRESHOLD, + "pingme_create_poll_length_seconds": int(DEFAULT_PINGME_CREATE_POLL_LENGTH.total_seconds()) + } async def send_to_log_channel(guild_id, msg): - db_logging_call = db_gateway().get( - 'guild_info', params={'guild_id': guild_id}) + db_logging_call = db_gateway().get('guild_info', params={'guild_id': guild_id}) if db_logging_call and db_logging_call[0]['log_channel_id']: await client.get_channel(db_logging_call[0]['log_channel_id']).send(msg) @@ -50,7 +53,11 @@ async def on_ready(): """Initialize the reactionMenuDB and pingme role cooldowns, since this can't be done synchronously """ await client.init() - await client.change_presence(status=discord.Status.dnd, activity=discord.Activity(type=discord.ActivityType.listening, name="your commands")) + await client.change_presence( + status=discord.Status.dnd, + activity=discord.Activity(type=discord.ActivityType.listening, + name="your commands") + ) @client.event @@ -67,14 +74,15 @@ async def on_guild_remove(guild): @client.event async def on_member_join(member): - default_role_exists = db_gateway().get( - 'guild_info', params={'guild_id': member.guild.id}) + default_role_exists = db_gateway().get('guild_info', params={'guild_id': member.guild.id}) if default_role_exists[0]['default_role_id']: - default_role = member.guild.get_role( - default_role_exists[0]['default_role_id']) + default_role = member.guild.get_role(default_role_exists[0]['default_role_id']) await member.add_roles(default_role) - await send_to_log_channel(member.guild.id, f"{member.mention} has joined the server and received the {default_role.mention} role") + await send_to_log_channel( + member.guild.id, + f"{member.mention} has joined the server and received the {default_role.mention} role" + ) else: await send_to_log_channel(member.guild.id, f"{member.mention} has joined the server") @@ -89,23 +97,38 @@ async def on_voice_state_update(member, before, after): if not before.channel.members: # Nobody else in VC await before.channel.delete() - db_gateway().delete('voicemaster_slave', where_params={ - 'guild_id': member.guild.id, 'channel_id': before_channel_id}) + db_gateway().delete( + 'voicemaster_slave', + where_params={ + 'guild_id': member.guild.id, + 'channel_id': before_channel_id + } + ) await send_to_log_channel(member.guild.id, f"{member.mention} has deleted a VM slave") else: # Still others in VC await before.channel.edit(name=f"{before.channel.members[0].display_name}'s VC") - db_gateway().update('voicemaster_slave', set_params={'owner_id': before.channel.members[0].id}, where_params={ - 'guild_id': member.guild.id, 'channel_id': before_channel_id}) + db_gateway().update( + 'voicemaster_slave', + set_params={'owner_id': before.channel.members[0].id}, + where_params={ + 'guild_id': member.guild.id, + 'channel_id': before_channel_id + } + ) elif after_channel_id and get_whether_in_vm_master(member.guild.id, after_channel_id): # Moved into a master VM VC slave_channel_name = f"{member.display_name}'s VC" new_slave_channel = await member.guild.create_voice_channel(slave_channel_name, category=after.channel.category) - db_gateway().insert('voicemaster_slave', params={'guild_id': member.guild.id, - 'channel_id': new_slave_channel.id, - 'owner_id': member.id, - 'locked': False, - }) + db_gateway().insert( + 'voicemaster_slave', + params={ + 'guild_id': member.guild.id, + 'channel_id': new_slave_channel.id, + 'owner_id': member.id, + 'locked': False, + } + ) await member.move_to(new_slave_channel) await send_to_log_channel(member.guild.id, f"{member.mention} has created a VM slave") @@ -202,15 +225,17 @@ async def on_command_error(ctx: Context, exception: Exception): :param Exception exception: The exception caused by the message in ctx """ if isinstance(exception, MissingRequiredArgument): - await ctx.message.reply("Arguments are required for this command! See `" + client.command_prefix + "help " + ctx.invoked_with + "` for more information.") + await ctx.message.reply( + "Arguments are required for this command! See `" + client.command_prefix + "help " + ctx.invoked_with + + "` for more information." + ) elif isinstance(exception, CommandNotFound): try: await ctx.message.add_reaction(client.unknownCommandEmoji.sendable) except (Forbidden, HTTPException): pass except NotFound: - raise ValueError("Invalid unknownCommandEmoji: " - + client.unknownCommandEmoji.sendable) + raise ValueError("Invalid unknownCommandEmoji: " + client.unknownCommandEmoji.sendable) else: sourceStr = str(ctx.message.id) try: @@ -218,8 +243,10 @@ async def on_command_error(ctx: Context, exception: Exception): + "/" + ctx.guild.name + "#" + str(ctx.guild.id) except AttributeError: sourceStr += "/DM@" + ctx.author.name + "#" + str(ctx.author.id) - print(datetime.now().strftime("%m/%d/%Y %H:%M:%S - Caught " - + type(exception).__name__ + " '") + str(exception) + "' from message " + sourceStr) + print( + datetime.now().strftime("%m/%d/%Y %H:%M:%S - Caught " + type(exception).__name__ + " '") + str(exception) + + "' from message " + sourceStr + ) lib.exceptions.print_exception_trace(exception) @@ -257,8 +284,7 @@ async def on_message(message): @client.command() @commands.has_permissions(administrator=True) async def initialsetup(ctx): - already_in_db = db_gateway().get( - 'guild_info', params={'guild_id': ctx.author.guild.id}) + already_in_db = db_gateway().get('guild_info', params={'guild_id': ctx.author.guild.id}) if already_in_db: await ctx.channel.send("This server is already set up") else: diff --git a/src/esportsbot/cogs/AdminCog.py b/src/esportsbot/cogs/AdminCog.py index 095d756f..1b4b78b2 100644 --- a/src/esportsbot/cogs/AdminCog.py +++ b/src/esportsbot/cogs/AdminCog.py @@ -12,8 +12,13 @@ def __init__(self, bot): @commands.command(aliases=['cls', 'purge', 'delete', 'Cls', 'Purge', 'Delete', 'Clear']) @commands.has_permissions(manage_messages=True) async def clear(self, ctx, amount=5): - await ctx.channel.purge(limit=int(amount)+1) - await send_to_log_channel(self, ctx.author.guild.id, self.STRINGS['channel_cleared'].format(author_mention = ctx.author.mention, message_amount=amount)) + await ctx.channel.purge(limit=int(amount) + 1) + await send_to_log_channel( + self, + ctx.author.guild.id, + self.STRINGS['channel_cleared'].format(author_mention=ctx.author.mention, + message_amount=amount) + ) @commands.command(aliases=['Members']) @commands.has_permissions(manage_messages=True) diff --git a/src/esportsbot/cogs/DefaultRoleCog.py b/src/esportsbot/cogs/DefaultRoleCog.py index de2a3327..86e470c8 100644 --- a/src/esportsbot/cogs/DefaultRoleCog.py +++ b/src/esportsbot/cogs/DefaultRoleCog.py @@ -13,17 +13,20 @@ def __init__(self, bot): @commands.command() @commands.has_permissions(administrator=True) async def setdefaultrole(self, ctx, given_role_id=None): - cleaned_role_id = role_id_from_mention( - given_role_id) if given_role_id else False + cleaned_role_id = role_id_from_mention(given_role_id) if given_role_id else False if cleaned_role_id: - db_gateway().update('guild_info', set_params={ - 'default_role_id': cleaned_role_id}, where_params={'guild_id': ctx.author.guild.id}) + db_gateway().update( + 'guild_info', + set_params={'default_role_id': cleaned_role_id}, + where_params={'guild_id': ctx.author.guild.id} + ) await ctx.channel.send(self.STRINGS['default_role_set'].format(role_id=cleaned_role_id)) default_role = ctx.author.guild.get_role(cleaned_role_id) await send_to_log_channel( - self, - ctx.author.guild.id, - self.STRINGS['default_role_set_log'].format(author=ctx.author.mention, role_mention=default_role.mention) + self, + ctx.author.guild.id, + self.STRINGS['default_role_set_log'].format(author=ctx.author.mention, + role_mention=default_role.mention) ) else: await ctx.channel.send(self.STRINGS['default_role_set_missing_params']) @@ -31,8 +34,7 @@ async def setdefaultrole(self, ctx, given_role_id=None): @commands.command() @commands.has_permissions(administrator=True) async def getdefaultrole(self, ctx): - default_role_exists = db_gateway().get( - 'guild_info', params={'guild_id': ctx.author.guild.id}) + default_role_exists = db_gateway().get('guild_info', params={'guild_id': ctx.author.guild.id}) if default_role_exists[0]['default_role_id']: await ctx.channel.send(self.STRINGS['default_role_get'].format(role_id=default_role_exists[0]['default_role_id'])) @@ -42,14 +44,20 @@ async def getdefaultrole(self, ctx): @commands.command() @commands.has_permissions(administrator=True) async def removedefaultrole(self, ctx): - default_role_exists = db_gateway().get( - 'guild_info', params={'guild_id': ctx.author.guild.id}) + default_role_exists = db_gateway().get('guild_info', params={'guild_id': ctx.author.guild.id}) if default_role_exists[0]['default_role_id']: - db_gateway().update('guild_info', set_params={ - 'default_role_id': 'NULL'}, where_params={'guild_id': ctx.author.guild.id}) + db_gateway().update( + 'guild_info', + set_params={'default_role_id': 'NULL'}, + where_params={'guild_id': ctx.author.guild.id} + ) await ctx.channel.send(self.STRINGS['default_role_removed']) - await send_to_log_channel(self, ctx.author.guild.id, self.STRINGS['default_role_removed_log'].format(author_mention=ctx.author.mention)) + await send_to_log_channel( + self, + ctx.author.guild.id, + self.STRINGS['default_role_removed_log'].format(author_mention=ctx.author.mention) + ) else: await ctx.channel.send(self.STRINGS['default_role_missing']) diff --git a/src/esportsbot/cogs/EventCategoriesCog.py b/src/esportsbot/cogs/EventCategoriesCog.py index 97e5fa81..93b7dba7 100644 --- a/src/esportsbot/cogs/EventCategoriesCog.py +++ b/src/esportsbot/cogs/EventCategoriesCog.py @@ -10,9 +10,21 @@ from ..reactionMenus.reactionRoleMenu import ReactionRoleMenu, ReactionRoleMenuOption # Permissions overrides assigned to the shared role in closed event signin channels -CLOSED_EVENT_SIGNIN_CHANNEL_SHARED_PERMS = PermissionOverwrite(read_messages=False, read_message_history=True, add_reactions=False, send_messages=False, use_slash_commands=False) +CLOSED_EVENT_SIGNIN_CHANNEL_SHARED_PERMS = PermissionOverwrite( + read_messages=False, + read_message_history=True, + add_reactions=False, + send_messages=False, + use_slash_commands=False +) # Permissions overrides assigned to the shared role in open event signin channels -OPEN_EVENT_SIGNIN_CHANNEL_SHARED_PERMS = PermissionOverwrite(read_messages=True, read_message_history=True, add_reactions=False, send_messages=False, use_slash_commands=False) +OPEN_EVENT_SIGNIN_CHANNEL_SHARED_PERMS = PermissionOverwrite( + read_messages=True, + read_message_history=True, + add_reactions=False, + send_messages=False, + use_slash_commands=False +) # Permissions overrides assigned to @everyone in all event category channels EVENT_CATEGORY_EVERYONE_PERMS = PermissionOverwrite(read_messages=False) # Permissions overrides assigned to the shared role in all event category channels @@ -20,7 +32,13 @@ # Permissions overrides assigned to the event role in all event category channels EVENT_CATEGORY_EVENT_ROLE_PERMS = PermissionOverwrite(read_messages=True) # Permissions overrides assigned to the event role in event signin channels -EVENT_SIGNIN_CHANNEL_EVENT_PERMS = PermissionOverwrite(read_messages=True, read_message_history=True, add_reactions=False, send_messages=False, use_slash_commands=False) +EVENT_SIGNIN_CHANNEL_EVENT_PERMS = PermissionOverwrite( + read_messages=True, + read_message_history=True, + add_reactions=False, + send_messages=False, + use_slash_commands=False +) class EventCategoriesCog(commands.Cog): @@ -36,7 +54,6 @@ class EventCategoriesCog(commands.Cog): :var bot: The client instance owning this cog instance :vartype bot: EsportsBot """ - def __init__(self, bot: "EsportsBot"): """ :param EsportsBot bot: The client instance owning this cog instance @@ -67,13 +84,20 @@ async def getGuildEventSettings(self, ctx: Context, eventName: str) -> Tuple[dic if not (allEvents := db.get("event_categories", params={"guild_id": ctx.guild.id})): await ctx.message.reply(self.STRINGS['no_event_categories']) else: - await ctx.message.reply(self.STRINGS['unrecognised_event'].format(events=", ".join(e["event_name"].title() for e in allEvents))) + await ctx.message.reply( + self.STRINGS['unrecognised_event'].format( + events=", ".join(e["event_name"].title() for e in allEvents) + ) + ) else: return (guildData, eventData) return () - - @commands.command(name="open-event", usage="open-event ", help="Reveal the signin channel for the named event channel.") + @commands.command( + name="open-event", + usage="open-event ", + help="Reveal the signin channel for the named event channel." + ) @commands.has_permissions(administrator=True) async def admin_cmd_open_event(self, ctx: Context, *, args): """Admin command: Open the named event category, revealing the signin channel to all users. @@ -93,17 +117,29 @@ async def admin_cmd_open_event(self, ctx: Context, *, args): else: sharedRole = ctx.guild.get_role(guildData["shared_role_id"]) if not eventChannel.overwrites_for(sharedRole).read_messages: - reason = self.STRINGS['event_channel_open_reason'].format(author=ctx.author.name, event_name=eventName, command_prefix=self.bot.command_prefix) + reason = self.STRINGS['event_channel_open_reason'].format( + author=ctx.author.name, + event_name=eventName, + command_prefix=self.bot.command_prefix + ) await eventChannel.set_permissions(sharedRole, read_messages=True, reason=reason) - await ctx.send(self.STRINGS['success_channel'].format(channel_id=eventChannel.id, role_name=sharedRole.name)) + await ctx.send( + self.STRINGS['success_channel'].format(channel_id=eventChannel.id, + role_name=sharedRole.name) + ) admin_message = self.STRINGS['admin_signin_visible'][1].format(channel_id=eventChannel.id) await self.bot.adminLog(ctx.message, {self.STRINGS['admin_signin_visible'][0]: admin_message}) else: - await ctx.send(self.STRINGS['channel_already_open'].format(event_name=eventName.title(), channel_id=eventChannel.id)) - - - @commands.command(name="close-event", usage="close-event ", - help="Hide the signin channel for the named event, reset the signin menu, and remove the event's role from users.") + await ctx.send( + self.STRINGS['channel_already_open'].format(event_name=eventName.title(), + channel_id=eventChannel.id) + ) + + @commands.command( + name="close-event", + usage="close-event ", + help="Hide the signin channel for the named event, reset the signin menu, and remove the event's role from users." + ) @commands.has_permissions(administrator=True) async def admin_cmd_close_event(self, ctx: Context, *, args): """Admin command: Close the named event category, making the category invisible to all users and removing @@ -126,36 +162,64 @@ async def admin_cmd_close_event(self, ctx: Context, *, args): else: eventRole = ctx.guild.get_role(eventData["role_id"]) if eventRole.position >= ctx.guild.self_role.position: - await ctx.send(self.STRINGS['role_edit_perms_bad_order'].format(event_role=eventRole.name, role_name=ctx.guild.self_role.name)) + await ctx.send( + self.STRINGS['role_edit_perms_bad_order'].format( + event_role=eventRole.name, + role_name=ctx.guild.self_role.name + ) + ) else: eventName = args.lower() sharedRole = ctx.guild.get_role(guildData["shared_role_id"]) channelEdited = eventChannel.overwrites_for(sharedRole).read_messages usersEdited = len(eventRole.members) # signinMenu.msg = await signinMenu.msg.channel.fetch_message(signinMenu.msg.id) - menuReset = True # len(signinMenu.msg.reactions) > 1 + menuReset = True # len(signinMenu.msg.reactions) > 1 if True not in (channelEdited, usersEdited, menuReset): - await ctx.message.reply(self.STRINGS['nothing_to_do'].format(channel_id=signinMenu.msg.channel.id, shared_role=sharedRole.name, event_role=eventRole.name)) + await ctx.message.reply( + self.STRINGS['nothing_to_do'].format( + channel_id=signinMenu.msg.channel.id, + shared_role=sharedRole.name, + event_role=eventRole.name + ) + ) else: - loadingTxts = ["Closing channel... " + ("⏳" if channelEdited else "✅"), - "Unassigning role" + ((" from " + str(usersEdited) + " users... ⏳") if usersEdited else "... ✅"), - "Resetting signin menu... " + ("⏳" if menuReset else "✅")] + loadingTxts = [ + "Closing channel... " + ("⏳" if channelEdited else "✅"), + "Unassigning role" + ((" from " + str(usersEdited) + " users... ⏳") if usersEdited else "... ✅"), + "Resetting signin menu... " + ("⏳" if menuReset else "✅") + ] loadingMsg = await ctx.send("\n".join(loadingTxts)) adminActions = { - self.STRINGS['admin_event_closed'][0]: self.STRINGS['admin_event_closed'][0].format(event_title=eventName.title()), - self.STRINGS['admin_role_menu_reset'][0]: self.STRINGS['admin_role_menu_reset'][1].format(menu_id=signinMenu.msg.id, menu_type=type(signinMenu).__name__, menu_url=signinMenu.msg.jump_url) + self.STRINGS['admin_event_closed'][0]: + self.STRINGS['admin_event_closed'][0].format(event_title=eventName.title()), + self.STRINGS['admin_role_menu_reset'][0]: + self.STRINGS['admin_role_menu_reset'][1].format( + menu_id=signinMenu.msg.id, + menu_type=type(signinMenu).__name__, + menu_url=signinMenu.msg.jump_url + ) } if channelEdited: - reason = self.STRINGS['event_channel_close_reason'].format(author=ctx.author.name, event_name=eventName, command_prefix=self.bot.command_prefix) + reason = self.STRINGS['event_channel_close_reason'].format( + author=ctx.author.name, + event_name=eventName, + command_prefix=self.bot.command_prefix + ) await eventChannel.set_permissions(sharedRole, read_messages=False, reason=reason) loadingTxts[0] = loadingTxts[0][:-1] + "✅" asyncio.create_task(loadingMsg.edit(content="\n".join(loadingTxts))) - adminActions[self.STRINGS['admin_channel_invisible'][0]] = self.STRINGS['admin_channel_invisible'][1].format(channel_id=eventChannel.id) + adminActions[self.STRINGS['admin_channel_invisible'][0] + ] = self.STRINGS['admin_channel_invisible'][1].format(channel_id=eventChannel.id) membersFutures = set() for member in eventRole.members: - reason = self.STRINGS['event_channel_close_reason'].format(author=ctx.author.name, event_name=eventName, command_prefix=self.bot.command_prefix) + reason = self.STRINGS['event_channel_close_reason'].format( + author=ctx.author.name, + event_name=eventName, + command_prefix=self.bot.command_prefix + ) membersFutures.add(asyncio.create_task(member.remove_roles(eventRole, reason=reason))) if menuReset: @@ -168,13 +232,18 @@ async def admin_cmd_close_event(self, ctx: Context, *, args): loadingTxts[1] = loadingTxts[1][:-1] + "✅" await loadingMsg.edit(content="\n".join(loadingTxts)) adminActions["Event Role Removed"] = f"Users: {usersEdited!s}\n<@&{eventRole.id!s}>" - adminActions[self.STRINGS['admin_role_removed'][0]] = self.STRINGS['admin_role_removed'][1].format(users=usersEdited, event_role_id=eventRole.id) + adminActions[ + self.STRINGS['admin_role_removed'][0] + ] = self.STRINGS['admin_role_removed'][1].format(users=usersEdited, + event_role_id=eventRole.id) await ctx.message.reply(self.STRINGS['success_event_closed']) await self.bot.adminLog(ctx.message, adminActions) - - @commands.command(name="set-event-signin-menu", usage="set-event-signin-menu ", - help="Change the event signin menu to use with `open-event` and `close-event`.") + @commands.command( + name="set-event-signin-menu", + usage="set-event-signin-menu ", + help="Change the event signin menu to use with `open-event` and `close-event`." + ) @commands.has_permissions(administrator=True) async def admin_cmd_set_event_signin_menu(self, ctx: Context, *, args: str): """Admin command: Change the signin menu associated with an event. @@ -192,32 +261,58 @@ async def admin_cmd_set_event_signin_menu(self, ctx: Context, *, args: str): elif int(menuID) not in self.bot.reactionMenus: await ctx.send(self.STRINGS['unrecognised_menu_id'].format(menu_id=menuID)) else: - eventName = args[len(menuID)+1:].lower() + eventName = args[len(menuID) + 1:].lower() db = db_gateway() if not (eventData := db.get("event_categories", {"guild_id": ctx.guild.id, "event_name": eventName})): if not (allEvents := db.get("event_categoriesevent_channels", params={"guild_id": ctx.guild.id})): await ctx.message.reply(self.STRINGS['no_event_categories']) else: - await ctx.message.reply(self.STRINGS['unrecognised_event'].format(events=", ".join(e["event_name"].title() for e in allEvents))) + await ctx.message.reply( + self.STRINGS['unrecognised_event'].format( + events=", ".join(e["event_name"].title() for e in allEvents) + ) + ) else: eventRole = ctx.guild.get_role(eventData["role_id"]) menu = self.bot.reactionMenus[int(menuID)] if not isinstance(menu, ReactionRoleMenu): - await ctx.message.reply(self.STRINGS['invalid_signin_menu'].format(role_name=eventRole.name if eventRole else 'event')) + await ctx.message.reply( + self.STRINGS['invalid_signin_menu'].format(role_name=eventRole.name if eventRole else 'event') + ) else: try: next(o for o in menu.options if isinstance(o, ReactionRoleMenuOption) and o.role == eventRole) except StopIteration: - await ctx.message.reply(self.STRINGS['invalid_signin_menu'].format(role_name=eventRole.name if eventRole else 'event')) + await ctx.message.reply( + self.STRINGS['invalid_signin_menu'].format(role_name=eventRole.name if eventRole else 'event') + ) else: - db.update('event_categories', set_params={"signin_menu_id": menu.msg.id}, where_params={"guild_id": ctx.guild.id, "event_name": eventName}) - await ctx.send(self.STRINGS['success_menu'].format(event_name=eventName.title, menu_url=menu.msg.jump_url)) - admin_message = self.STRINGS['admin_menu_updated'][1].format(event_title=eventName.title(), menu_id=menuID, menu_type=type(menu).__name__, menu_url=menu.msg.jump_url) + db.update( + 'event_categories', + set_params={"signin_menu_id": menu.msg.id}, + where_params={ + "guild_id": ctx.guild.id, + "event_name": eventName + } + ) + await ctx.send( + self.STRINGS['success_menu'].format(event_name=eventName.title, + menu_url=menu.msg.jump_url) + ) + admin_message = self.STRINGS['admin_menu_updated'][1].format( + event_title=eventName.title(), + menu_id=menuID, + menu_type=type(menu).__name__, + menu_url=menu.msg.jump_url + ) await self.bot.adminLog(ctx.message, {self.STRINGS['admin_menu_updated'][0]: admin_message}) - - @commands.command(name="set-shared-role", usage="set-shared-role ", - help="Change the role to admit/deny into *all* event signin menus. This should NOT be the same as any event role. Role can be given as either a mention or an ID.") + @commands.command( + name="set-shared-role", + usage="set-shared-role ", + help= + "Change the role to admit/deny into *all* event signin menus. This should NOT be the same as any event role. Role can be given as either a mention or an ID." + ) @commands.has_permissions(administrator=True) async def admin_cmd_set_shared_role(self, ctx: Context, *, args: str): """Admin command: Set the guild's "shared" role, which is denied visibility of the event category channels when not @@ -237,13 +332,26 @@ async def admin_cmd_set_shared_role(self, ctx: Context, *, args: str): if role is None: await ctx.send(self.STRINGS['unrecognised_role']) else: - db_gateway().update('guild_info', set_params={"shared_role_id": roleID}, where_params={"guild_id": ctx.guild.id}) + db_gateway().update( + 'guild_info', + set_params={"shared_role_id": roleID}, + where_params={"guild_id": ctx.guild.id} + ) await ctx.send(self.STRINGS['success_shared_role'].format(role_name=role.name)) - await self.bot.adminLog(ctx.message, {self.STRINGS['admin_shared_role_set'][0]: self.STRINGS['admin_shared_role_set'][1].format(role_id=roleID)}) - - - @commands.command(name="set-event-role", usage="set-event-role ", - help="Change the role to remove during `close-event`. This should NOT be the same as your shared role. Role can be given as either a mention or an ID.") + await self.bot.adminLog( + ctx.message, + { + self.STRINGS['admin_shared_role_set'][0]: + self.STRINGS['admin_shared_role_set'][1].format(role_id=roleID) + } + ) + + @commands.command( + name="set-event-role", + usage="set-event-role ", + help= + "Change the role to remove during `close-event`. This should NOT be the same as your shared role. Role can be given as either a mention or an ID." + ) @commands.has_permissions(administrator=True) async def admin_cmd_set_event_role(self, ctx: Context, *, args: str): """Admin command: Set the named event's event role, which is granted visibility of the event category channels. @@ -263,21 +371,44 @@ async def admin_cmd_set_event_role(self, ctx: Context, *, args: str): if role is None: await ctx.send(self.STRINGS['unrecognised_role']) else: - eventName = args[len(roleStr)+1:].lower() + eventName = args[len(roleStr) + 1:].lower() db = db_gateway() if not db.get("event_categories", {"guild_id": ctx.guild.id, "event_name": eventName}): if not (allEvents := db.get("event_categories", params={"guild_id": ctx.guild.id})): await ctx.message.reply(self.STRINGS['no_event_categories']) else: - await ctx.message.reply(self.STRINGS['unrecognised_event'].format(events=", ".join(e["event_name"].title() for e in allEvents))) + await ctx.message.reply( + self.STRINGS['unrecognised_event'].format( + events=", ".join(e["event_name"].title() for e in allEvents) + ) + ) else: - db.update('event_categories', set_params={"role_id": roleID}, where_params={"guild_id": ctx.guild.id, "event_name": eventName}) - await ctx.send(self.STRINGS['success_event_role'].format(event_name=eventName.title(), role_name=role.name)) - await self.bot.adminLog(ctx.message, {self.STRINGS['admin_event_role_set'][0]: self.STRINGS['admin_event_role_set'][1].format(role_id=roleID)}) - - - @commands.command(name="register-event-category", usage="register-event-category ", - help="Register an existing event category, menu, and role, for use with `open-event` and `close-event`. This does not setup permissions for the category or channels.") + db.update( + 'event_categories', + set_params={"role_id": roleID}, + where_params={ + "guild_id": ctx.guild.id, + "event_name": eventName + } + ) + await ctx.send( + self.STRINGS['success_event_role'].format(event_name=eventName.title(), + role_name=role.name) + ) + await self.bot.adminLog( + ctx.message, + { + self.STRINGS['admin_event_role_set'][0]: + self.STRINGS['admin_event_role_set'][1].format(role_id=roleID) + } + ) + + @commands.command( + name="register-event-category", + usage="register-event-category ", + help= + "Register an existing event category, menu, and role, for use with `open-event` and `close-event`. This does not setup permissions for the category or channels." + ) @commands.has_permissions(administrator=True) async def admin_cmd_register_event_category(self, ctx: Context, *, args: str): """Admin command: Register an existing category as an event category. @@ -305,20 +436,39 @@ async def admin_cmd_register_event_category(self, ctx: Context, *, args: str): if role is None: await ctx.send(self.STRINGS['unrecognised_role']) else: - eventName = args[len(roleStr)+len(menuIDStr)+2:].lower() + eventName = args[len(roleStr) + len(menuIDStr) + 2:].lower() db = db_gateway() if db.get("event_categories", {"guild_id": ctx.guild.id, "event_name": eventName}): await ctx.message.reply(self.STRINGS['event_exists'].format(event_name=eventName.title())) else: menu = self.bot.reactionMenus[int(menuIDStr)] - db.insert('event_categories', {"guild_id": ctx.guild.id, "event_name": eventName, "role_id": roleID, "signin_menu_id": menu.msg.id}) + db.insert( + 'event_categories', + { + "guild_id": ctx.guild.id, + "event_name": eventName, + "role_id": roleID, + "signin_menu_id": menu.msg.id + } + ) await ctx.send(self.STRINGS['success_event_category'].format(event_name=eventName.title())) - admin_message = self.STRINGS['admin_existing_event_registered'][1].format(event_title=eventName.title(), menu_id=menuIDStr, role_id=roleID, menu_url=menu.msg.jump_url) - await self.bot.adminLog(ctx.message, {self.STRINGS['admin_existing_event_registered'][0]: admin_message}) - - - @commands.command(name="create-event-category", usage="create-event-category ", - help="Create a new event category with a signin channel and menu, event role, general channel and correct permissions, and automatically register them for use with `open-event` and `close-event`.") + admin_message = self.STRINGS['admin_existing_event_registered'][1].format( + event_title=eventName.title(), + menu_id=menuIDStr, + role_id=roleID, + menu_url=menu.msg.jump_url + ) + await self.bot.adminLog( + ctx.message, + {self.STRINGS['admin_existing_event_registered'][0]: admin_message} + ) + + @commands.command( + name="create-event-category", + usage="create-event-category ", + help= + "Create a new event category with a signin channel and menu, event role, general channel and correct permissions, and automatically register them for use with `open-event` and `close-event`." + ) @commands.has_permissions(administrator=True) async def admin_cmd_create_event_category(self, ctx: Context, *, args: str): """Admin command: Automatically create and fully set up a new event category with the given name. @@ -339,43 +489,116 @@ async def admin_cmd_create_event_category(self, ctx: Context, *, args: str): await ctx.message.reply(self.STRINGS['no_shared_role'].format(command_prefix=self.bot.command_prefix)) else: if not (sharedRole := ctx.guild.get_role(guildData["shared_role_id"])): - await ctx.message.reply(self.STRINGS['missing_shared_role'].format(command_prefix=self.bot.command_prefix)) + await ctx.message.reply( + self.STRINGS['missing_shared_role'].format(command_prefix=self.bot.command_prefix) + ) else: emojiSelectorMsg = await ctx.message.reply(self.STRINGS['react_start']) + def emojiSelectorCheck(data: RawReactionActionEvent) -> bool: - return data.message_id == emojiSelectorMsg.id and data.user_id == ctx.author.id and (data.emoji.is_unicode_emoji or self.bot.get_emoji(data.emoji.id)) + return data.message_id == emojiSelectorMsg.id and data.user_id == ctx.author.id and ( + data.emoji.is_unicode_emoji or self.bot.get_emoji(data.emoji.id) + ) try: - signinEmoji = lib.emotes.Emote.fromPartial((await self.bot.wait_for("raw_reaction_add", check=emojiSelectorCheck, timeout=60)).emoji, rejectInvalid=True) + signinEmoji = lib.emotes.Emote.fromPartial( + (await self.bot.wait_for("raw_reaction_add", + check=emojiSelectorCheck, + timeout=60)).emoji, + rejectInvalid=True + ) except asyncio.TimeoutError: await emojiSelectorMsg.reply(self.STRINGS['react_no_time']) except lib.exceptions.UnrecognisedCustomEmoji: await emojiSelectorMsg.reply(self.STRINGS['react_error']) else: - creationReason = self.STRINGS['event_category_create_reason'].format(event_name=eventName, command_prefix=self.bot.command_prefix) + creationReason = self.STRINGS['event_category_create_reason'].format( + event_name=eventName, + command_prefix=self.bot.command_prefix + ) eventRole = await ctx.guild.create_role(name=eventName.title(), reason=creationReason) - categoryOverwrites = {sharedRole: EVENT_CATEGORY_SHARED_ROLE_PERMS, eventRole: EVENT_CATEGORY_EVENT_ROLE_PERMS} - signinOverwrites = {sharedRole: CLOSED_EVENT_SIGNIN_CHANNEL_SHARED_PERMS, eventRole: EVENT_SIGNIN_CHANNEL_EVENT_PERMS} + categoryOverwrites = { + sharedRole: EVENT_CATEGORY_SHARED_ROLE_PERMS, + eventRole: EVENT_CATEGORY_EVENT_ROLE_PERMS + } + signinOverwrites = { + sharedRole: CLOSED_EVENT_SIGNIN_CHANNEL_SHARED_PERMS, + eventRole: EVENT_SIGNIN_CHANNEL_EVENT_PERMS + } if eventRole != ctx.guild.default_role: categoryOverwrites[ctx.guild.default_role] = EVENT_CATEGORY_EVERYONE_PERMS signinOverwrites[ctx.guild.default_role] = EVENT_CATEGORY_EVERYONE_PERMS - newCategory = await ctx.guild.create_category(eventName.title(), reason=creationReason, overwrites=categoryOverwrites) - signinChannel = await ctx.guild.create_text_channel(f"{eventName}-signin", reason=creationReason, category=newCategory, overwrites=signinOverwrites) - eventGeneral = await ctx.guild.create_text_channel(f"{eventName}-general", reason=creationReason, category=newCategory, overwrites=categoryOverwrites) - eventVoice = await ctx.guild.create_voice_channel(f"{eventName}-voice", reason=creationReason, category=newCategory, overwrites=categoryOverwrites) + newCategory = await ctx.guild.create_category( + eventName.title(), + reason=creationReason, + overwrites=categoryOverwrites + ) + signinChannel = await ctx.guild.create_text_channel( + f"{eventName}-signin", + reason=creationReason, + category=newCategory, + overwrites=signinOverwrites + ) + eventGeneral = await ctx.guild.create_text_channel( + f"{eventName}-general", + reason=creationReason, + category=newCategory, + overwrites=categoryOverwrites + ) + eventVoice = await ctx.guild.create_voice_channel( + f"{eventName}-voice", + reason=creationReason, + category=newCategory, + overwrites=categoryOverwrites + ) signinMenuMsg = await signinChannel.send(embed=Embed()) - signinMenu = ReactionRoleMenu(signinMenuMsg, self.bot, {signinEmoji: eventRole}, col=Colour.blue(), titleTxt=self.STRINGS['menu_title'].format(event_name=eventName.title()), - desc=self.STRINGS['menu_description'].format(event_role=eventRole.name)) + signinMenu = ReactionRoleMenu( + signinMenuMsg, + self.bot, + {signinEmoji: eventRole}, + col=Colour.blue(), + titleTxt=self.STRINGS['menu_title'].format(event_name=eventName.title()), + desc=self.STRINGS['menu_description'].format(event_role=eventRole.name) + ) await signinMenu.updateMessage() self.bot.reactionMenus.add(signinMenu) - db.insert('event_categories', {"guild_id": ctx.guild.id, "event_name": eventName, "role_id": eventRole.id, "signin_menu_id": signinMenuMsg.id}) - await ctx.send(self.STRINGS['success_event'].format(event_title=eventName.title(), signin_menu_id=signinMenuMsg.id, signin_channel_mention=signinChannel.mention, shared_role_name=sharedRole.name, command_prefix=self.bot.command_prefix, event_name=eventName, event_general_mention=eventGeneral.mention)) - admin_message = self.STRINGS['admin_event_category_updated'][1].format(event_title=eventName.title(), menu_id=signinMenuMsg.id, role_id=eventRole.id, menu_url=signinMenuMsg.jump_url) - await self.bot.adminLog(ctx.message, {self.STRINGS['admin_event_category_updated'][0]: admin_message}) - - - @commands.command(name="unregister-event-category", usage="unregister-event-category ", - help="Unregister an event category and role so that it can no longer be used with `open-event` and `close-event`, but without deleting the channels.") + db.insert( + 'event_categories', + { + "guild_id": ctx.guild.id, + "event_name": eventName, + "role_id": eventRole.id, + "signin_menu_id": signinMenuMsg.id + } + ) + await ctx.send( + self.STRINGS['success_event'].format( + event_title=eventName.title(), + signin_menu_id=signinMenuMsg.id, + signin_channel_mention=signinChannel.mention, + shared_role_name=sharedRole.name, + command_prefix=self.bot.command_prefix, + event_name=eventName, + event_general_mention=eventGeneral.mention + ) + ) + admin_message = self.STRINGS['admin_event_category_updated'][1].format( + event_title=eventName.title(), + menu_id=signinMenuMsg.id, + role_id=eventRole.id, + menu_url=signinMenuMsg.jump_url + ) + await self.bot.adminLog( + ctx.message, + {self.STRINGS['admin_event_category_updated'][0]: admin_message} + ) + + @commands.command( + name="unregister-event-category", + usage="unregister-event-category ", + help= + "Unregister an event category and role so that it can no longer be used with `open-event` and `close-event`, but without deleting the channels." + ) @commands.has_permissions(administrator=True) async def admin_cmd_unregister_event_category(self, ctx: Context, *, args: str): """Admin command: Unregister a category, menu and role as an event category, without deleting any of them @@ -393,16 +616,22 @@ async def admin_cmd_unregister_event_category(self, ctx: Context, *, args: str): if not (allEvents := db.get("event_categories", params={"guild_id": ctx.guild.id})): await ctx.message.reply(self.STRINGS['no_event_categories']) else: - await ctx.message.reply(self.STRINGS['unrecognised_event'].format(events=", ".join(e["event_name"].title() for e in allEvents))) + await ctx.message.reply( + self.STRINGS['unrecognised_event'].format( + events=", ".join(e["event_name"].title() for e in allEvents) + ) + ) else: db.delete("event_categories", {"guild_id": ctx.guild.id, "event_name": eventName}) await ctx.message.reply(self.STRINGS['success_event_role_unregister'].format(event_title=eventName.title())) admin_message = self.STRINGS['admin_event_category_unregistered'].format(event_title=eventName.title()) await self.bot.adminLog(ctx.message, {self.STRINGS['admin_event_category_unregistered'][0]: admin_message}) - - @commands.command(name="delete-event-category", usage="delete-event-category ", - help="Delete an event category and its role and channels from the server.") + @commands.command( + name="delete-event-category", + usage="delete-event-category ", + help="Delete an event category and its role and channels from the server." + ) @commands.has_permissions(administrator=True) async def admin_cmd_delete_event_category(self, ctx: Context, *, args: str): """Admin command: Delete an event category, its channels and role from the server entirely. @@ -419,17 +648,31 @@ async def admin_cmd_delete_event_category(self, ctx: Context, *, args: str): if not (allEvents := db.get("event_categories", params={"guild_id": ctx.guild.id})): await ctx.message.reply(self.STRINGS['no_event_categories']) else: - await ctx.message.reply(self.STRINGS['unrecognised_event'].format(events=", ".join(e["event_name"].title() for e in allEvents))) + await ctx.message.reply( + self.STRINGS['unrecognised_event'].format( + events=", ".join(e["event_name"].title() for e in allEvents) + ) + ) else: signinMenuID = eventData[0]["signin_menu_id"] eventCategory = self.bot.reactionMenus[signinMenuID].msg.channel.category numChannels = len(eventCategory.channels) eventRole = ctx.guild.get_role(eventData[0]["role_id"]) - confirmMsg = await ctx.message.reply(self.STRINGS['react_delete_confirm'].format(event_title=eventName.title(), event_segment=(f", the {eventRole.name} role," if eventRole else ""), num_channels=numChannels)) + confirmMsg = await ctx.message.reply( + self.STRINGS['react_delete_confirm'].format( + event_title=eventName.title(), + event_segment=(f", the {eventRole.name} role," if eventRole else ""), + num_channels=numChannels + ) + ) asyncio.create_task(confirmMsg.add_reaction('👍')) asyncio.create_task(confirmMsg.add_reaction('👎')) + def confirmCheck(data: RawReactionActionEvent) -> bool: - return data.message_id == confirmMsg.id and data.user_id == ctx.author.id and (data.emoji.is_unicode_emoji and data.emoji.name in ['👍', '👎']) + return data.message_id == confirmMsg.id and data.user_id == ctx.author.id and ( + data.emoji.is_unicode_emoji and data.emoji.name in ['👍', + '👎'] + ) try: confirmResult = (await self.bot.wait_for("raw_reaction_add", check=confirmCheck, timeout=60)).emoji @@ -439,7 +682,10 @@ def confirmCheck(data: RawReactionActionEvent) -> bool: if confirmResult.name == "👎": await ctx.send(self.STRINGS['react_event_delete_cancel']) else: - deletionReason = self.STRINGS['event_category_delete_reason'].format(event_name=eventName, command_prefix=self.bot.command_prefix) + deletionReason = self.STRINGS['event_category_delete_reason'].format( + event_name=eventName, + command_prefix=self.bot.command_prefix + ) self.bot.reactionMenus.removeID(signinMenuID) deletionTasks = set() if eventRole: @@ -450,7 +696,10 @@ def confirmCheck(data: RawReactionActionEvent) -> bool: await asyncio.wait(deletionTasks) db.delete("event_categories", {"guild_id": ctx.guild.id, "event_name": eventName}) await ctx.message.reply(self.STRINGS['success_event_deleted'].format(event_title=eventName.title())) - admin_message = self.STRINGS['admin_event_category_deleted'][1].format(event_title=eventName.title(), num_channels=numChannels) + (f"\nRole deleted: #{eventData[0]['role_id']!s}" if eventData[0]['role_id'] else "") + admin_message = self.STRINGS['admin_event_category_deleted'][1].format( + event_title=eventName.title(), + num_channels=numChannels + ) + (f"\nRole deleted: #{eventData[0]['role_id']!s}" if eventData[0]['role_id'] else "") await self.bot.adminLog(ctx.message, {self.STRINGS['admin_event_category_deleted'][0]: admin_message}) diff --git a/src/esportsbot/cogs/LogChannelCog.py b/src/esportsbot/cogs/LogChannelCog.py index 693e60d2..3b874fc3 100644 --- a/src/esportsbot/cogs/LogChannelCog.py +++ b/src/esportsbot/cogs/LogChannelCog.py @@ -4,6 +4,7 @@ from ..base_functions import channel_id_from_mention from ..base_functions import send_to_log_channel + class LogChannelCog(commands.Cog): def __init__(self, bot): self.bot = bot @@ -12,37 +13,36 @@ def __init__(self, bot): @commands.command() @commands.has_permissions(administrator=True) async def setlogchannel(self, ctx, given_channel_id=None): - cleaned_channel_id = channel_id_from_mention( - given_channel_id) if given_channel_id else ctx.channel.id - log_channel_exists = db_gateway().get( - 'guild_info', params={'guild_id': ctx.author.guild.id}) + cleaned_channel_id = channel_id_from_mention(given_channel_id) if given_channel_id else ctx.channel.id + log_channel_exists = db_gateway().get('guild_info', params={'guild_id': ctx.author.guild.id}) if bool(log_channel_exists): if log_channel_exists[0]['log_channel_id'] != cleaned_channel_id: - db_gateway().update('guild_info', set_params={ - 'log_channel_id': cleaned_channel_id}, where_params={'guild_id': ctx.author.guild.id}) + db_gateway().update( + 'guild_info', + set_params={'log_channel_id': cleaned_channel_id}, + where_params={'guild_id': ctx.author.guild.id} + ) await ctx.channel.send(self.STRINGS["channel_set"].format(channel_id=cleaned_channel_id)) await send_to_log_channel( - self, - ctx.author.guild.id, + self, + ctx.author.guild.id, self.STRINGS["channel_set_notify_in_channel"].format(author_mention=ctx.author.mention), ) else: await ctx.channel.send(self.STRINGS["channel_set_already"]) else: - db_gateway().insert('guild_info', params={ - 'guild_id': ctx.author.guild.id, 'log_channel_id': cleaned_channel_id}) + db_gateway().insert('guild_info', params={'guild_id': ctx.author.guild.id, 'log_channel_id': cleaned_channel_id}) await ctx.channel.send(self.STRINGS["channel_set"].format(channel_id=cleaned_channel_id)) await send_to_log_channel( - self, - ctx.author.guild.id, + self, + ctx.author.guild.id, self.STRINGS["channel_set_notify_in_channel"].format(author_mention=ctx.author.mention), ) @commands.command() @commands.has_permissions(administrator=True) async def getlogchannel(self, ctx): - log_channel_exists = db_gateway().get( - 'guild_info', params={'guild_id': ctx.author.guild.id}) + log_channel_exists = db_gateway().get('guild_info', params={'guild_id': ctx.author.guild.id}) if (channel_id := log_channel_exists[0]['log_channel_id']) is not None: await ctx.channel.send(self.STRINGS["channel_get"].format(channel_id=channel_id)) @@ -52,12 +52,14 @@ async def getlogchannel(self, ctx): @commands.command() @commands.has_permissions(administrator=True) async def removelogchannel(self, ctx): - log_channel_exists = db_gateway().get( - 'guild_info', params={'guild_id': ctx.author.guild.id}) + log_channel_exists = db_gateway().get('guild_info', params={'guild_id': ctx.author.guild.id}) if log_channel_exists[0]['log_channel_id']: - db_gateway().update('guild_info', set_params={ - 'log_channel_id': 'NULL'}, where_params={'guild_id': ctx.author.guild.id}) + db_gateway().update( + 'guild_info', + set_params={'log_channel_id': 'NULL'}, + where_params={'guild_id': ctx.author.guild.id} + ) await ctx.channel.send(self.STRINGS["channel_removed"]) else: await ctx.channel.send(self.STRINGS["channel_get_notfound"]) diff --git a/src/esportsbot/cogs/MenusCog.py b/src/esportsbot/cogs/MenusCog.py index bc86045c..df6813da 100644 --- a/src/esportsbot/cogs/MenusCog.py +++ b/src/esportsbot/cogs/MenusCog.py @@ -7,7 +7,6 @@ from ..reactionMenus import reactionRoleMenu, reactionPollMenu from datetime import timedelta - # Maximum number of polls which can be running at once in a given guild, for performance MAX_POLLS_PER_GUILD = 5 # Maximum length of time a poll can run for, for performance @@ -32,12 +31,15 @@ class MenusCog(commands.Cog): :var bot: The client instance owning this cog instance :vartype bot: EsportsBot """ - def __init__(self, bot: "EsportsBot"): self.bot: "EsportsBot" = bot - - @commands.command(name="del-menu", usage="del-menu ", help="Remove the specified reaction menu. You can also just delete the message, if you have permissions.\nTo get the ID of a reaction menu, enable discord's developer mode, right click on the menu, and click Copy ID.") + @commands.command( + name="del-menu", + usage="del-menu ", + help= + "Remove the specified reaction menu. You can also just delete the message, if you have permissions.\nTo get the ID of a reaction menu, enable discord's developer mode, right click on the menu, and click Copy ID." + ) @commands.has_permissions(administrator=True) async def admin_cmd_del_reaction_menu(self, ctx: Context, *, args: str): """Admin command: Unregister the specified reaction menu for interactions and delete the containing message. @@ -62,8 +64,12 @@ async def admin_cmd_del_reaction_menu(self, ctx: Context, *, args: str): else: await ctx.send(":x: Unrecognised reaction menu!") - - @commands.command(name="del-role-menu-option", usage="del-role-menu-option ", help="Remove a role from a role menu.\nTo get the ID of a reaction menu, enable discord's developer mode, right click on the menu, and click Copy ID.\nYour emoji must be an option in the menu.") + @commands.command( + name="del-role-menu-option", + usage="del-role-menu-option ", + help= + "Remove a role from a role menu.\nTo get the ID of a reaction menu, enable discord's developer mode, right click on the menu, and click Copy ID.\nYour emoji must be an option in the menu." + ) @commands.has_permissions(administrator=True) async def admin_cmd_remove_role_menu_option(self, ctx: Context, *, args: str): """Admin command: Remove an option from a reaction role menu, by its emoji. @@ -84,13 +90,19 @@ async def admin_cmd_remove_role_menu_option(self, ctx: Context, *, args: str): try: roleEmoji = lib.emotes.Emote.fromStr(argsSplit[1], rejectInvalid=True) except lib.exceptions.UnrecognisedCustomEmoji: - await ctx.send(":x: I don't know that emoji!\nYou can only use built in emojis, or custom emojis that are in this server.") + await ctx.send( + ":x: I don't know that emoji!\nYou can only use built in emojis, or custom emojis that are in this server." + ) else: if not menu.hasEmojiRegistered(roleEmoji): await ctx.send(":x: That emoji is not in the menu!") else: optionRole = menu.options[roleEmoji].role - adminActions = {"Reaction menu option removed": "id: " + str(menu.msg.id) + "\ntype: " + type(menu).__name__ + "\nOption: " + roleEmoji.sendable + " <@&" + str(optionRole.id) + ">\n[Menu](" + menu.msg.jump_url + ")"} + adminActions = { + "Reaction menu option removed": + "id: " + str(menu.msg.id) + "\ntype: " + type(menu).__name__ + "\nOption: " + roleEmoji.sendable + + " <@&" + str(optionRole.id) + ">\n[Menu](" + menu.msg.jump_url + ")" + } if len(menu.options) == 1: await menu.delete() await ctx.send("The menu has no more options! Menu deleted.") @@ -105,8 +117,12 @@ async def admin_cmd_remove_role_menu_option(self, ctx: Context, *, args: str): await ctx.send("✅ Removed option " + roleEmoji.sendable + " from menu " + str(menu.msg.id) + "!") await self.bot.adminLog(ctx.message, adminActions) - - @commands.command(name="add-role-menu-option", usage="add-role-menu-option <@role mention>", help="Add a role to a role menu.\nTo get the ID of a reaction menu, enable discord's developer mode, right click on the menu, and click Copy ID.\nYour emoji must not be in the menu already.\nGive your role to grant/remove as a mention.") + @commands.command( + name="add-role-menu-option", + usage="add-role-menu-option <@role mention>", + help= + "Add a role to a role menu.\nTo get the ID of a reaction menu, enable discord's developer mode, right click on the menu, and click Copy ID.\nYour emoji must not be in the menu already.\nGive your role to grant/remove as a mention." + ) @commands.has_permissions(administrator=True) async def admin_cmd_add_role_menu_option(self, ctx: Context, *, args: str): """Admin command: Add a new option to a reaction role menu, by its emoji and role to grant/remove. @@ -128,7 +144,9 @@ async def admin_cmd_add_role_menu_option(self, ctx: Context, *, args: str): try: roleEmoji = lib.emotes.Emote.fromStr(argsSplit[1], rejectInvalid=True) except lib.exceptions.UnrecognisedCustomEmoji: - await ctx.send(":x: I don't know that emoji!\nYou can only use built in emojis, or custom emojis that are in this server.") + await ctx.send( + ":x: I don't know that emoji!\nYou can only use built in emojis, or custom emojis that are in this server." + ) except TypeError: await ctx.send(":x: Invalid emoji: " + argsSplit[1]) else: @@ -149,10 +167,13 @@ async def admin_cmd_add_role_menu_option(self, ctx: Context, *, args: str): + "\ntype: " + type(menu).__name__ \ + "\nOption: " + roleEmoji.sendable + " <@&" + str(role.id) \ + ">\n[Menu](" + menu.msg.jump_url + ")"}) - - - @commands.command(name="make-role-menu", usage="make-role-menu \n<option1 emoji> <@option1 role>\n... ...", help="Create a reaction role menu. Each option must be on its own new line, as an emoji, followed by a space, followed by a mention of the role to grant. The `title` is displayed at the top of the meny and is optional, to exclude your title simply give a new line.") + @commands.command( + name="make-role-menu", + usage="make-role-menu <title>\n<option1 emoji> <@option1 role>\n... ...", + help= + "Create a reaction role menu. Each option must be on its own new line, as an emoji, followed by a space, followed by a mention of the role to grant. The `title` is displayed at the top of the meny and is optional, to exclude your title simply give a new line." + ) @commands.has_permissions(administrator=True) async def admin_cmd_make_role_menu(self, ctx: Context, *, args: str): """Admin command: Create a reaction role menu, allowing users to self-assign or remove roles by adding and removing reactions. @@ -179,7 +200,10 @@ async def admin_cmd_make_role_menu(self, ctx: Context, *, args: str): argsSplit = args.split("\n") if len(argsSplit) < 2: - await ctx.send(":x: Invalid arguments! Please provide your menu title, followed by a new line, then a new line-separated series of options.\nFor more info, see `" + self.bot.command_prefix + "admin-help`") + await ctx.send( + ":x: Invalid arguments! Please provide your menu title, followed by a new line, then a new line-separated series of options.\nFor more info, see `" + + self.bot.command_prefix + "admin-help`" + ) return menuSubject = argsSplit[0] argPos = 0 @@ -199,11 +223,17 @@ async def admin_cmd_make_role_menu(self, ctx: Context, *, args: str): await ctx.send(":x: Invalid emoji: " + e.val) return except lib.exceptions.UnrecognisedCustomEmoji: - await ctx.send(":x: I don't know your " + str(argPos) + lib.stringTyping.getNumExtension(argPos) + " emoji!\nYou can only use built in emojis, or custom emojis that are in this server.") + await ctx.send( + ":x: I don't know your " + str(argPos) + lib.stringTyping.getNumExtension(argPos) + + " emoji!\nYou can only use built in emojis, or custom emojis that are in this server." + ) return else: if dumbReact.sendable == "None": - await ctx.send(":x: I don't know your " + str(argPos) + lib.stringTyping.getNumExtension(argPos) + " emoji!\nYou can only use built in emojis, or custom emojis that are in this server.") + await ctx.send( + ":x: I don't know your " + str(argPos) + lib.stringTyping.getNumExtension(argPos) + + " emoji!\nYou can only use built in emojis, or custom emojis that are in this server." + ) return if dumbReact is None: await ctx.send(":x: Invalid emoji: " + arg.strip(" ").split(" ")[0]) @@ -215,7 +245,10 @@ async def admin_cmd_make_role_menu(self, ctx: Context, *, args: str): localEmoji = True break if not localEmoji: - await ctx.send(":x: I don't know your " + str(argPos) + lib.stringTyping.getNumExtension(argPos) + " emoji!\nYou can only use built in emojis, or custom emojis that are in this server.") + await ctx.send( + ":x: I don't know your " + str(argPos) + lib.stringTyping.getNumExtension(argPos) + + " emoji!\nYou can only use built in emojis, or custom emojis that are in this server." + ) return if dumbReact in reactionRoles: @@ -235,13 +268,18 @@ async def admin_cmd_make_role_menu(self, ctx: Context, *, args: str): await ctx.send(":x: Unrecognised role: " + roleStr) return elif role.position >= botRole.position: - await ctx.send(":x: I can't grant the **" + role.name + "** role!\nMake sure it's below my '" + botRole.name + "' role in the server roles list.") + await ctx.send( + ":x: I can't grant the **" + role.name + "** role!\nMake sure it's below my '" + botRole.name + + "' role in the server roles list." + ) return elif role.is_bot_managed(): await ctx.send(":x: I can't grant the **" + role.name + "** role!\nThis role is managed by a bot.") return elif role.is_integration(): - await ctx.send(":x: I can't grant the **" + role.name + "** role!\nThis role is managed by an integration.") + await ctx.send( + ":x: I can't grant the **" + role.name + "** role!\nThis role is managed by an integration." + ) return reactionRoles[dumbReact] = role @@ -271,13 +309,24 @@ async def admin_cmd_make_role_menu(self, ctx: Context, *, args: str): self.bot.reactionMenus.add(menu) await ctx.send("Role menu " + str(menuMsg.id) + " has been created!") try: - await self.bot.adminLog(ctx.message, {"Reaction Role Menu Created": "id: " + str(menu.msg.id) + "\ntype: " + type(menu).__name__ + "\n" + str(len(reactionRoles)) + " Options: " + "".join(e.sendable for e in reactionRoles) + "\n[Menu](" + menu.msg.jump_url + ")"}) + await self.bot.adminLog( + ctx.message, + { + "Reaction Role Menu Created": + "id: " + str(menu.msg.id) + "\ntype: " + type(menu).__name__ + "\n" + str(len(reactionRoles)) + + " Options: " + "".join(e.sendable for e in reactionRoles) + "\n[Menu](" + menu.msg.jump_url + ")" + } + ) except Exception as e: print(e) raise e - - @commands.command(name="poll", usage="poll <subject>\n<option1 emoji> <option1 name>\n... ...\n<optional args>", help="Start a reaction-based poll. Each option must be on its own new line, as an emoji, followed by a space, followed by the option name. The `subject` is the question that users answer in the poll and is optional, to exclude your subject simply give a new line.\n\n__Optional Arguments__\nOptional arguments should be given by `name=value`, with each arg on a new line.\n- Give `multiplechoice=no` to only allow one vote per person (default: yes).\n- Give the length of the poll, with each time division on a new line. Acceptable time divisions are: `seconds`, `minutes`, `hours`, `days`. (default: minutes=5)") + @commands.command( + name="poll", + usage="poll <subject>\n<option1 emoji> <option1 name>\n... ...\n<optional args>", + help= + "Start a reaction-based poll. Each option must be on its own new line, as an emoji, followed by a space, followed by the option name. The `subject` is the question that users answer in the poll and is optional, to exclude your subject simply give a new line.\n\n__Optional Arguments__\nOptional arguments should be given by `name=value`, with each arg on a new line.\n- Give `multiplechoice=no` to only allow one vote per person (default: yes).\n- Give the length of the poll, with each time division on a new line. Acceptable time divisions are: `seconds`, `minutes`, `hours`, `days`. (default: minutes=5)" + ) async def cmd_poll(self, ctx: Context, *, args: str): """User command: Run a reaction-based poll, allowing users to choose between several named options. Users may not create more than one poll at a time, anywhere. @@ -325,7 +374,7 @@ async def cmd_poll(self, ctx: Context, *, args: str): argSplit = arg.split(" ") argPos += 1 try: - optionName, dumbReact = arg[arg.index(" ")+1:], lib.emotes.Emote.fromStr(argSplit[0], rejectInvalid=True) + optionName, dumbReact = arg[arg.index(" ") + 1:], lib.emotes.Emote.fromStr(argSplit[0], rejectInvalid=True) except (ValueError, IndexError): for kwArg in ["days=", "hours=", "seconds=", "minutes=", "multiplechoice="]: if arg.lower().startswith(kwArg): @@ -364,7 +413,7 @@ async def cmd_poll(self, ctx: Context, *, args: str): await ctx.message.reply(":x: You need to give some options to vote on!\nFor more info, see `" \ + self.bot.command_prefix + "help poll`") return - + timeoutDict = {} for timeName in ["days", "hours", "minutes", "seconds"]: diff --git a/src/esportsbot/cogs/MusicCog.py b/src/esportsbot/cogs/MusicCog.py index 8ab01d2f..028863e9 100644 --- a/src/esportsbot/cogs/MusicCog.py +++ b/src/esportsbot/cogs/MusicCog.py @@ -46,10 +46,11 @@ class MessageTypeEnum(IntEnum): ESPORTS_LOGO_URL = "http://fragsoc.co.uk/wpsite/wp-content/uploads/2020/08/logo1-450x450.png" -EMPTY_PREVIEW_MESSAGE = Embed(title="No song currently playing", - colour=EmbedColours.music, - footer="Use the prefix ! for commands" - ) +EMPTY_PREVIEW_MESSAGE = Embed( + title="No song currently playing", + colour=EmbedColours.music, + footer="Use the prefix ! for commands" +) EMPTY_PREVIEW_MESSAGE.set_image(url=ESPORTS_LOGO_URL) EMPTY_PREVIEW_MESSAGE.set_footer(text="Definitely not made by fuxticks#1809 on discord") @@ -63,14 +64,12 @@ class MessageTypeEnum(IntEnum): BOT_INACTIVE_MINUTES = 2 - # TODO: Change usage of db to use of bot dict of Music Channels # TODO: Update preview message to include volume and reaction controls # TODO: Add move song command to move a song from one position in the queue to another class MusicCog(commands.Cog): - def __init__(self, bot: EsportsBot, max_search_results=100): print("Loaded music module") self._bot = bot @@ -111,7 +110,7 @@ async def setmusicchannel(self, ctx: Context, args: str = None, given_channel_id message = Embed(title=self.user_strings["music_channel_set_missing_channel"], colour=EmbedColours.red) await send_timed_message(ctx.channel, embed=message, timer=30) return False - + try: cleaned_channel_id = channel_id_from_mention(given_channel_id) except ValueError: @@ -133,17 +132,24 @@ async def setmusicchannel(self, ctx: Context, args: str = None, given_channel_id await send_timed_message(ctx.channel, embed=message, timer=30) return False - current_channel_for_guild = self.__db_accessor.get('music_channels', params={ - 'guild_id': ctx.guild.id}) + current_channel_for_guild = self.__db_accessor.get('music_channels', params={'guild_id': ctx.guild.id}) if len(current_channel_for_guild) > 0: # There is already a channel set.. update - self.__db_accessor.update('music_channels', set_params={ - 'channel_id': cleaned_channel_id}, where_params={'guild_id': ctx.guild.id}) + self.__db_accessor.update( + 'music_channels', + set_params={'channel_id': cleaned_channel_id}, + where_params={'guild_id': ctx.guild.id} + ) else: # No channel for guild.. insert - self.__db_accessor.insert('music_channels', params={ - 'channel_id': int(cleaned_channel_id), 'guild_id': int(ctx.guild.id)}) + self.__db_accessor.insert( + 'music_channels', + params={ + 'channel_id': int(cleaned_channel_id), + 'guild_id': int(ctx.guild.id) + } + ) await self.__setup_channel(ctx, int(cleaned_channel_id), args) self._bot.update_music_channels() @@ -160,8 +166,7 @@ async def getmusicchannel(self, ctx: Context) -> Message: :rtype: discord.Message """ - current_channel_for_guild = self.__db_accessor.get('music_channels', params={ - 'guild_id': ctx.guild.id}) + current_channel_for_guild = self.__db_accessor.get('music_channels', params={'guild_id': ctx.guild.id}) if current_channel_for_guild and current_channel_for_guild[0].get('channel_id'): # If the music channel has been set in the guild @@ -183,8 +188,7 @@ async def resetmusicchannel(self, ctx: Context) -> Message: :rtype: discord.Message """ - current_channel_for_guild = self.__db_accessor.get('music_channels', params={ - 'guild_id': ctx.guild.id}) + current_channel_for_guild = self.__db_accessor.get('music_channels', params={'guild_id': ctx.guild.id}) if current_channel_for_guild and current_channel_for_guild[0].get('channel_id'): # If the music channel has been set for the guild @@ -212,8 +216,12 @@ async def setvolume(self, ctx: Context, volume_level) -> bool: if not self._currently_active.get(ctx.guild.id): # Not currently active - await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["bot_inactive"], - colour=EmbedColours.music), timer=10) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=self.user_strings["bot_inactive"], + colour=EmbedColours.music), + timer=10 + ) return False if not await self.__check_valid_user_vc(ctx): @@ -221,10 +229,12 @@ async def setvolume(self, ctx: Context, volume_level) -> bool: return False if not strIsInt(volume_level): - await send_timed_message(channel=ctx.channel, - embed=Embed(title=self.user_strings["volume_set_invalid_value"], - colour=EmbedColours.orange), - timer=10) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=self.user_strings["volume_set_invalid_value"], + colour=EmbedColours.orange), + timer=10 + ) return False if int(volume_level) < 0: @@ -237,9 +247,7 @@ async def setvolume(self, ctx: Context, volume_level) -> bool: client.source.volume = float(volume_level) / float(100) self._currently_active.get(ctx.guild.id)["volume"] = client.source.volume message_title = self.user_strings["volume_set_success"].format(volume_level=volume_level) - await send_timed_message(channel=ctx.channel, - embed=Embed(title=message_title, colour=EmbedColours.music), - timer=10) + await send_timed_message(channel=ctx.channel, embed=Embed(title=message_title, colour=EmbedColours.music), timer=10) return True @commands.command(aliases=["remove", "removeat"]) @@ -256,9 +264,12 @@ async def removesong(self, ctx: Context, song_index=None) -> bool: if not self._currently_active.get(ctx.guild.id): # Not currently active - await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["bot_inactive"], - colour=EmbedColours.music), - timer=10) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=self.user_strings["bot_inactive"], + colour=EmbedColours.music), + timer=10 + ) return False if not await self.__check_valid_user_vc(ctx): @@ -267,35 +278,45 @@ async def removesong(self, ctx: Context, song_index=None) -> bool: if not strIsInt(song_index): message_title = self.user_strings["song_remove_invalid_value"] - await send_timed_message(channel=ctx.channel, - embed=Embed(title=message_title, colour=EmbedColours.orange), - timer=10) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=message_title, + colour=EmbedColours.orange), + timer=10 + ) return False queue_length = len(self._currently_active.get(ctx.guild.id).get("queue")) if queue_length == 0: - await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["bot_inactive"], - colour=EmbedColours.orange), - timer=10) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=self.user_strings["bot_inactive"], + colour=EmbedColours.orange), + timer=10 + ) return False if int(song_index) < 1: message_title = self.user_strings["song_remove_invalid_value"] message_body = self.user_strings["song_remove_valid_options"].format(start_index=1, end_index=queue_length) - await send_timed_message(channel=ctx.channel, - embed=Embed(title=message_title, - colour=EmbedColours.orange), - timer=10) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=message_title, + colour=EmbedColours.orange), + timer=10 + ) return False if int(song_index) == 1: self.__pause_song(ctx.guild.id) current_song = self._currently_active.get(ctx.guild.id).get("current_song").get("title") await self.__check_next_song(ctx.guild.id) - message = Embed(title=self.user_strings["song_remove_success"].format(song_title=current_song, - song_position=song_index), - colour=EmbedColours.green) + message = Embed( + title=self.user_strings["song_remove_success"].format(song_title=current_song, + song_position=song_index), + colour=EmbedColours.green + ) await send_timed_message(channel=ctx.channel, embed=message, timer=10) return True @@ -303,17 +324,17 @@ async def removesong(self, ctx: Context, song_index=None) -> bool: # The index given is out of the bounds of the current queue message_title = self.user_strings["song_remove_invalid_value"] message_body = self.user_strings["song_remove_valid_options"].format(start_index=1, end_index=queue_length) - message = Embed(title=message_title, - description=message_body, - colour=EmbedColours.orange) + message = Embed(title=message_title, description=message_body, colour=EmbedColours.orange) await send_timed_message(channel=ctx.channel, embed=message, timer=10) return False song_popped = self._currently_active[ctx.guild.id]['queue'].pop(int(song_index) - 1) await self.__update_channel_messages(ctx.guild.id) - message = Embed(title=self.user_strings["song_remove_success"].format(song_title=song_popped, - song_position=song_index), - colour=EmbedColours.green) + message = Embed( + title=self.user_strings["song_remove_success"].format(song_title=song_popped, + song_position=song_index), + colour=EmbedColours.green + ) await send_timed_message(channel=ctx.channel, embed=message, timer=10) return True @@ -329,9 +350,12 @@ async def pausesong(self, ctx: Context) -> bool: if not self._currently_active.get(ctx.guild.id): # Not currently active - await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["bot_inactive"], - colour=EmbedColours.music), - timer=10) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=self.user_strings["bot_inactive"], + colour=EmbedColours.music), + timer=10 + ) return False if not await self.__check_valid_user_vc(ctx): @@ -339,9 +363,12 @@ async def pausesong(self, ctx: Context) -> bool: return False if self.__pause_song(ctx.guild.id): - await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["song_pause_success"], - colour=EmbedColours.music), - timer=5) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=self.user_strings["song_pause_success"], + colour=EmbedColours.music), + timer=5 + ) return True return False @@ -358,9 +385,12 @@ async def resumesong(self, ctx: Context) -> bool: if not self._currently_active.get(ctx.guild.id): # Not currently active - await send_timed_message(channel=ctx.channel, - embed=Embed(title=self.user_strings["bot_inactive"], - colour=EmbedColours.music), timer=10) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=self.user_strings["bot_inactive"], + colour=EmbedColours.music), + timer=10 + ) return False if not await self.__check_valid_user_vc(ctx): @@ -368,9 +398,12 @@ async def resumesong(self, ctx: Context) -> bool: return False if self.__resume_song(ctx.guild.id): - await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["song_resume_success"], - colour=EmbedColours.music), - timer=5) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=self.user_strings["song_resume_success"], + colour=EmbedColours.music), + timer=5 + ) return True return False @@ -387,8 +420,12 @@ async def kickbot(self, ctx: Context) -> bool: if not self._currently_active.get(ctx.guild.id): # Not currently active - await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["bot_inactive"], - colour=EmbedColours.music), timer=10) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=self.user_strings["bot_inactive"], + colour=EmbedColours.music), + timer=10 + ) return False if not await self.__check_valid_user_vc(ctx): @@ -396,8 +433,12 @@ async def kickbot(self, ctx: Context) -> bool: return False if await self.__remove_active_channel(ctx.guild.id): - await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["kick_bot_success"], - colour=EmbedColours.music), timer=10) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=self.user_strings["kick_bot_success"], + colour=EmbedColours.music), + timer=10 + ) return True return False @@ -413,9 +454,12 @@ async def skipsong(self, ctx: Context) -> bool: if not self._currently_active.get(ctx.guild.id): # Not currently active - await send_timed_message(channel=ctx.channel, - embed=Embed(title=self.user_strings["bot_inactive"], colour=EmbedColours.music), - timer=10) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=self.user_strings["bot_inactive"], + colour=EmbedColours.music), + timer=10 + ) return False if not await self.__check_valid_user_vc(ctx): @@ -429,9 +473,12 @@ async def skipsong(self, ctx: Context) -> bool: return True await self.__check_next_song(ctx.guild.id) - await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["song_skipped_success"], - colour=EmbedColours.music), - timer=5) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=self.user_strings["song_skipped_success"], + colour=EmbedColours.music), + timer=5 + ) return True @commands.command(aliases=["list", "queue"]) @@ -446,9 +493,12 @@ async def listqueue(self, ctx: Context) -> str: if not self._currently_active.get(ctx.guild.id): # Not currently active - await send_timed_message(channel=ctx.channel, - embed=Embed(title=self.user_strings["bot_inactive"], colour=EmbedColours.music), - timer=10) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=self.user_strings["bot_inactive"], + colour=EmbedColours.music), + timer=10 + ) return "" # We don't want the song channel to be filled with the queue as it already shows it @@ -456,9 +506,12 @@ async def listqueue(self, ctx: Context) -> str: if ctx.message.channel.id == music_channel_in_db[0].get('channel_id'): # Message is in the songs channel message_title = self.user_strings["music_channel_wrong_channel"].format(command_option="cannot") - await send_timed_message(channel=ctx.channel, - embed=Embed(title=message_title, - colour=EmbedColours.music), timer=10) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=message_title, + colour=EmbedColours.music), + timer=10 + ) return "" queue_string = self.__make_queue_list(ctx.guild.id) @@ -488,17 +541,19 @@ async def clearqueue(self, ctx: Context) -> bool: if self._currently_active.get(ctx.guild.id).get('voice_client').is_playing(): # If currently in a song, set the queue to what is currently playing - self._currently_active.get(ctx.guild.id)['queue'] = [ - self._currently_active.get(ctx.guild.id).get('queue').pop(0)] + self._currently_active.get(ctx.guild.id)['queue'] = [self._currently_active.get(ctx.guild.id).get('queue').pop(0)] else: # Else empty the queue and start the inactivity timer self._currently_active.get(ctx.guild.id)['queue'] = [None] await self.__check_next_song(ctx.guild.id) await self.__update_channel_messages(ctx.guild.id) - await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["clear_queue_success"], - colour=EmbedColours.music), - timer=10) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=self.user_strings["clear_queue_success"], + colour=EmbedColours.music), + timer=10 + ) return True @commands.command(aliases=["shuffle", "randomise"]) @@ -514,9 +569,12 @@ async def shufflequeue(self, ctx: Context) -> bool: if not self._currently_active.get(ctx.guild.id): # Not currently active - await send_timed_message(channel=ctx.channel, - embed=Embed(title=self.user_strings["bot_inactive"], colour=EmbedColours.music), - timer=10) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=self.user_strings["bot_inactive"], + colour=EmbedColours.music), + timer=10 + ) return False if not await self.__check_valid_user_vc(ctx): @@ -532,9 +590,12 @@ async def shufflequeue(self, ctx: Context) -> bool: self._currently_active.get(ctx.guild.id).get('queue').insert(0, current_top) await self.__update_channel_messages(ctx.guild.id) - await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["shuffle_queue_success"], - colour=EmbedColours.green), - timer=10) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=self.user_strings["shuffle_queue_success"], + colour=EmbedColours.green), + timer=10 + ) @tasks.loop(seconds=1) async def check_active_channels(self): @@ -618,11 +679,17 @@ async def __setup_channel(self, ctx: Context, channel_id: int, arg: str): default_queue_message = await channel_instance.send(EMPTY_QUEUE_MESSAGE) default_preview_message = await channel_instance.send(embed=temp_default_preview) - self.__db_accessor.update('music_channels', set_params={'queue_message_id': int(default_queue_message.id)}, - where_params={'guild_id': ctx.author.guild.id}) + self.__db_accessor.update( + 'music_channels', + set_params={'queue_message_id': int(default_queue_message.id)}, + where_params={'guild_id': ctx.author.guild.id} + ) - self.__db_accessor.update('music_channels', set_params={'preview_message_id': int(default_preview_message.id)}, - where_params={'guild_id': ctx.author.guild.id}) + self.__db_accessor.update( + 'music_channels', + set_params={'preview_message_id': int(default_preview_message.id)}, + where_params={'guild_id': ctx.author.guild.id} + ) async def __remove_active_channel(self, guild_id: int) -> bool: """ @@ -712,30 +779,43 @@ async def on_message_handle(self, message: Message) -> bool: if not message.author.voice: # User is not in a voice channel.. exit message_title = self.user_strings["no_voice_voice_channel"].format(author=message.author.mention) - await send_timed_message(channel=message.channel, - embed=Embed(title=message_title, - colour=EmbedColours.orange), timer=10) + await send_timed_message( + channel=message.channel, + embed=Embed(title=message_title, + colour=EmbedColours.orange), + timer=10 + ) return True if not message.author.voice.channel.permissions_for(message.guild.me).connect: # The bot does not have permission to join the channel.. exit message_title = self.user_strings["no_perms_voice_channel"].format(author=message.author.mention) - await send_timed_message(channel=message.channel, embed=Embed(title=message_title, - colour=EmbedColours.orange), timer=10) + await send_timed_message( + channel=message.channel, + embed=Embed(title=message_title, + colour=EmbedColours.orange), + timer=10 + ) return True if not self._currently_active.get(message.guild.id): # We aren't in a voice channel in the given guild voice_client = await message.author.voice.channel.connect() - self.__add_new_active_channel(message.guild.id, voice_client=voice_client, - channel_id=message.author.voice.channel.id) + self.__add_new_active_channel( + message.guild.id, + voice_client=voice_client, + channel_id=message.author.voice.channel.id + ) else: if self._currently_active.get(message.guild.id).get('channel_id') != message.author.voice.channel.id: # The bot is already being used in the current guild. message_title = self.user_strings["wrong_voice_voice_channel"].format(author=message.author.mention) - await send_timed_message(channel=message.channel, - embed=Embed(title=message_title, - colour=EmbedColours.orange), timer=10) + await send_timed_message( + channel=message.channel, + embed=Embed(title=message_title, + colour=EmbedColours.orange), + timer=10 + ) return True # Check if the loops for marked and active channels are running. @@ -844,9 +924,15 @@ def __format_api_data(self, data: List[dict]) -> List[dict]: formatted_data = [] for item in data: snippet = item.get("snippet") - info = {"title": snippet.get("title", "Unable to get title, this is a bug"), - "thumbnail": snippet.get("thumbnails", {}).get("maxres", {}).get("url", "Unable to get thumbnail " - "this is a bug")} + info = { + "title": snippet.get("title", + "Unable to get title, this is a bug"), + "thumbnail": snippet.get("thumbnails", + {}).get("maxres", + {}).get("url", + "Unable to get thumbnail " + "this is a bug") + } if item.get("kind", None) == "youtube#video": info["link"] = item.get("id", "Unable to get link, this is a bug") else: @@ -858,8 +944,11 @@ def __format_api_data(self, data: List[dict]) -> List[dict]: # Generate the url from the video id if the video id was gotten successfully. if not self.__is_url(info.get("thumbnail")): - thumbnail = snippet.get("thumbnails", {}).get("maxres", {}).get("url", "Unable to get thumbnail this " - "is a bug") + thumbnail = snippet.get("thumbnails", + {}).get("maxres", + {}).get("url", + "Unable to get thumbnail this " + "is a bug") info["thumbnail"] = thumbnail formatted_data.append(info) @@ -930,9 +1019,12 @@ def __make_update_preview_message(self, guild_id: int) -> Embed: updated_preview_message = EMPTY_PREVIEW_MESSAGE.copy() else: current_song = self._currently_active.get(guild_id).get('current_song') - updated_preview_message = Embed(title="Currently Playing: " + current_song.get('title'), - colour=EmbedColours.music, url=current_song.get('link'), - video=current_song.get('link')) + updated_preview_message = Embed( + title="Currently Playing: " + current_song.get('title'), + colour=EmbedColours.music, + url=current_song.get('link'), + video=current_song.get('link') + ) thumbnail = current_song.get('thumbnail') # If the current thumbnail isn't a url, just use the default image. if not self.__is_url(current_song.get('thumbnail')): @@ -1036,22 +1128,34 @@ async def __check_valid_user_vc(self, ctx: Context) -> bool: if ctx.message.channel.id != music_channel_in_db[0].get('channel_id'): # Message is not in the songs channel message_title = self.user_strings["music_channel_wrong_channel"].format(command_option="can only") - await send_timed_message(channel=ctx.channel, embed=Embed(title=message_title, - colour=EmbedColours.music), timer=10) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=message_title, + colour=EmbedColours.music), + timer=10 + ) return False if not ctx.author.voice: # User is not in a voice channel message_title = self.user_strings["no_voice_voice_channel"].format(author=ctx.author.mention) - await send_timed_message(channel=ctx.channel, embed=Embed(title=message_title, - colour=EmbedColours.music), timer=10) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=message_title, + colour=EmbedColours.music), + timer=10 + ) return False if self._currently_active.get(ctx.guild.id).get('channel_id') != ctx.author.voice.channel.id: # The user is not in the same voice channel as the bot message_title = self.user_strings["wrong_voice_voice_channel"].format(author=ctx.author.mention) - await send_timed_message(channel=ctx.channel, embed=Embed(title=message_title, - colour=EmbedColours.music), timer=10) + await send_timed_message( + channel=ctx.channel, + embed=Embed(title=message_title, + colour=EmbedColours.music), + timer=10 + ) return False return True @@ -1110,10 +1214,12 @@ def __format_download_data(self, download_data: dict) -> dict: """ stream, rate = self.__get_opus_stream(download_data.get('formats')) - useful_data = {'length': download_data.get('duration'), - 'stream': stream, - 'bitrate': rate, - 'filename': download_data.get('filename')} + useful_data = { + 'length': download_data.get('duration'), + 'stream': stream, + 'bitrate': rate, + 'filename': download_data.get('filename') + } return useful_data @staticmethod @@ -1207,9 +1313,12 @@ async def __play_queue(self, guild_id: int) -> bool: # voice_client.play(FFmpegOpusAudio(next_song.get("stream"), before_options=FFMPEG_BEFORE_OPT, # bitrate=int(next_song.get("bitrate")) + 10)) - source = PCMVolumeTransformer(FFmpegPCMAudio(next_song.get("stream"), - before_options=FFMPEG_BEFORE_OPT, options="-vn"), - volume=self._currently_active.get(guild_id).get("volume")) + source = PCMVolumeTransformer( + FFmpegPCMAudio(next_song.get("stream"), + before_options=FFMPEG_BEFORE_OPT, + options="-vn"), + volume=self._currently_active.get(guild_id).get("volume") + ) voice_client.play(source) return True @@ -1232,7 +1341,7 @@ def __make_queue_list(self, guild_id: int) -> str: extra = len(self._currently_active.get(guild_id).get('queue')) - 20 first_string = self.__song_list_to_string(first_part) - last_string = self.__song_list_to_string(last_part, start_index=extra+10) + last_string = self.__song_list_to_string(last_part, start_index=extra + 10) queue_string += f"{first_string}\n\n... and **`{extra}`** more ... \n\n{last_string}" else: @@ -1250,8 +1359,7 @@ def __song_list_to_string(songs: List[dict], start_index: int = 0) -> str: :rtype: str """ - return "\n".join(str(songNum + 1 + start_index) + ". " + - song.get('title') for songNum, song in enumerate(songs)) + return "\n".join(str(songNum + 1 + start_index) + ". " + song.get('title') for songNum, song in enumerate(songs)) def __find_query(self, message: str) -> dict: """ @@ -1292,11 +1400,12 @@ def __clean_query_results(self, results: List[dict]) -> List[dict]: # Gets the data that is actually useful and discards the rest of the data for result in results: - new_result = {'title': result.get('title'), - 'thumbnail': result.get('thumbnails')[-1].get('url'), - 'link': result.get('link'), - 'viewCount': result.get('viewCount') - } + new_result = { + 'title': result.get('title'), + 'thumbnail': result.get('thumbnails')[-1].get('url'), + 'link': result.get('link'), + 'viewCount': result.get('viewCount') + } filename = re.sub(r'\W+', '', new_result.get('title')) + f"{new_result.get('id')}.mp3" new_result['localfile'] = self._song_location + filename @@ -1317,10 +1426,15 @@ def __query_youtube(self, message: str) -> List[dict]: results = VideosSearch(message, limit=self._max_results).result().get('result') # Sort the list by view count - top_results = sorted(results, - key=lambda k: 0 if k["viewCount"]["text"] is None or "No" in k["viewCount"]["text"] else - int(re.sub(r'view(s)?', '', k['viewCount']['text']).replace(',', '')), - reverse=True) + top_results = sorted( + results, + key=lambda k: 0 if k["viewCount"]["text"] is None or "No" in k["viewCount"]["text"] else + int(re.sub(r'view(s)?', + '', + k['viewCount']['text']).replace(',', + '')), + reverse=True + ) music_results = [] diff --git a/src/esportsbot/cogs/PingablesCog.py b/src/esportsbot/cogs/PingablesCog.py index cfaf540b..347037d8 100644 --- a/src/esportsbot/cogs/PingablesCog.py +++ b/src/esportsbot/cogs/PingablesCog.py @@ -8,12 +8,11 @@ from datetime import timedelta from ..reactionMenus import reactionPollMenu - # The default role colour for pingable roles -DEFAULT_PINGABLE_COLOUR = 0x15e012 # green +DEFAULT_PINGABLE_COLOUR = 0x15e012 # green # The maximum amount of time pingable roles can be set to cool down for, for performance MAX_ROLE_PING_TIMEOUT = timedelta(days=30) -# The maximum amount of time pingme role creation polls can be set to run for, for performance +# The maximum amount of time pingme role creation polls can be set to run for, for performance MAX_PINGME_CREATE_POLL_LENGTH = timedelta(days=30) # The maximum number of votes that can be set as the role creation poll threshold, as a sanity check MAX_PINGME_CREATE_THRESHOLD = 100 @@ -49,7 +48,6 @@ class PingablesCog(commands.Cog): :var bot: The client instance owning this cog instance :vartype bot: EsportsBot """ - def __init__(self, bot: "EsportsBot"): """ :param EsportsBot bot: The client instance owning this cog instance @@ -65,8 +63,11 @@ async def pingme(self, ctx: Context, *, args: str): """ pass - - @pingme.command(name="register", usage="pingme register <@role> <name>", help="Convert an existing role into a !pingme role") + @pingme.command( + name="register", + usage="pingme register <@role> <name>", + help="Convert an existing role into a !pingme role" + ) @commands.has_permissions(administrator=True) async def admin_cmd_add_pingable_role(self, ctx: Context, *, args: str): """Admin command: Register an existing role for use with pingme. The role defaults to pingable (i.e not on cooldown) @@ -82,7 +83,7 @@ async def admin_cmd_add_pingable_role(self, ctx: Context, *, args: str): elif len(ctx.message.role_mentions) != 1: await ctx.message.reply("Please only give one role mention!") else: - roleName = args[len(argsSplit[0])+1:].lower() + roleName = args[len(argsSplit[0]) + 1:].lower() role = ctx.message.role_mentions[0] db = db_gateway() if db.get("pingable_roles", {"role_id": role.id}): @@ -90,17 +91,34 @@ async def admin_cmd_add_pingable_role(self, ctx: Context, *, args: str): elif db.get("pingable_roles", {"name": roleName}): await ctx.message.reply("A `!pingme` role already exists with the name '" + roleName + "'!") else: - db.insert("pingable_roles", {"guild_id": ctx.guild.id, "role_id": role.id, "on_cooldown": False, - "last_ping": -1, "ping_count": 0, "monthly_ping_count": 0, - "creator_id": ctx.author.id, "colour": DEFAULT_PINGABLE_COLOUR, - "name": roleName}) + db.insert( + "pingable_roles", + { + "guild_id": ctx.guild.id, + "role_id": role.id, + "on_cooldown": False, + "last_ping": -1, + "ping_count": 0, + "monthly_ping_count": 0, + "creator_id": ctx.author.id, + "colour": DEFAULT_PINGABLE_COLOUR, + "name": roleName + } + ) if not role.mentionable: await role.edit(mentionable=True, colour=discord.Colour.green(), reason="setting up new pingable role") await ctx.message.reply("pingable role setup complete!") - await self.bot.adminLog(ctx.message, {"New !pingme Role Registered", "Name: " + roleName + "\nRole: " + role.mention}) - - - @pingme.command(name="unregister", usage="pingme unregister <@role>", help="Unregister a role from !pingme without removing it from the server") + await self.bot.adminLog( + ctx.message, + {"New !pingme Role Registered", + "Name: " + roleName + "\nRole: " + role.mention} + ) + + @pingme.command( + name="unregister", + usage="pingme unregister <@role>", + help="Unregister a role from !pingme without removing it from the server" + ) @commands.has_permissions(administrator=True) async def admin_cmd_remove_pingable_role(self, ctx: Context, *, args: str): """Admin command: Unregister a pingme role without deleting it from the server. @@ -120,7 +138,6 @@ async def admin_cmd_remove_pingable_role(self, ctx: Context, *, args: str): await ctx.message.reply("✅ Role successfully unregistered for `!pingme`.") await self.bot.adminLog(ctx.message, {"!pingme Role Unregistered", "Role: " + role.mention}) - @pingme.command(name="delete", usage="pingme delete <@role>", help="Delete a !pingme role from the server") @commands.has_permissions(administrator=True) async def admin_cmd_delete_pingable_role(self, ctx: Context, *, args: str): @@ -142,8 +159,11 @@ async def admin_cmd_delete_pingable_role(self, ctx: Context, *, args: str): await ctx.message.reply("The role as been deleted!") await self.bot.adminLog(ctx.message, {"!pingme Role Deleted", "Name: " + role.name + "\nID: " + str(role.id)}) - - @pingme.command(name="reset-cooldown", usage="pingme reset-cooldown <@role>", help="Reset the pinging cooldown for a !pingme role, making it pingable again instantly") + @pingme.command( + name="reset-cooldown", + usage="pingme reset-cooldown <@role>", + help="Reset the pinging cooldown for a !pingme role, making it pingable again instantly" + ) @commands.has_permissions(administrator=True) async def admin_cmd_reset_role_ping_cooldown(self, ctx: Context, *, args: str): """Admin command: Reset the given pingme role's pinging cooldown, forcing it to become pingable again by anyone. @@ -164,12 +184,19 @@ async def admin_cmd_reset_role_ping_cooldown(self, ctx: Context, *, args: str): else: db.update("pingable_roles", {"on_cooldown": False}, {"role_id": role.id}) if not role.mentionable: - await role.edit(mentionable=True, colour=discord.Colour.green(), reason="manual cooldown reset by user " + str(ctx.author.name) + "#" + str(ctx.author.id)) + await role.edit( + mentionable=True, + colour=discord.Colour.green(), + reason="manual cooldown reset by user " + str(ctx.author.name) + "#" + str(ctx.author.id) + ) await ctx.message.reply("The " + role.name + " role is now pingable again!") await self.bot.adminLog(ctx.message, {"Ping Cooldown Manually Reset For !pingme Role": role.mention}) - - @pingme.command(name="set-cooldown", usage="pingme set-cooldown [seconds=seconds] [minutes=minutes] [hours=hours] [days=days]", help="Set the cooldown between !pingme role pings") + @pingme.command( + name="set-cooldown", + usage="pingme set-cooldown [seconds=seconds] [minutes=minutes] [hours=hours] [days=days]", + help="Set the cooldown between !pingme role pings" + ) @commands.has_permissions(administrator=True) async def admin_cmd_set_role_ping_cooldown(self, ctx: Context, *, args: str): """Admin command: Set the amount of time for which pingable roles are to cooldown for between pings. @@ -190,7 +217,7 @@ async def admin_cmd_set_role_ping_cooldown(self, ctx: Context, *, args: str): if arg.startswith(kwArg): kwArgs[kwArg[:-1]] = arg[len(kwArg):] break - + timeoutDict = {} for timeName in ["days", "hours", "minutes", "seconds"]: if timeName in kwArgs: @@ -199,18 +226,28 @@ async def admin_cmd_set_role_ping_cooldown(self, ctx: Context, *, args: str): return timeoutDict[timeName] = int(kwArgs[timeName]) - + timeoutTD = lib.timeUtil.timeDeltaFromDict(timeoutDict) if timeoutTD > MAX_ROLE_PING_TIMEOUT: await ctx.message.reply(":x: The maximum ping cooldown is " + lib.timeUtil.td_format_noYM(MAX_ROLE_PING_TIMEOUT)) return - - db_gateway().update("guild_info", {"role_ping_cooldown_seconds": int(timeoutTD.total_seconds())}, {"guild_id": ctx.guild.id}) - await ctx.message.reply("Cooldown for !pingme roles now updated to " + lib.timeUtil.td_format_noYM(timeoutTD) + "!") - await self.bot.adminLog(ctx.message, {"Cooldown For !pingme Role Pings Updated": lib.timeUtil.td_format_noYM(timeoutTD)}) - - @pingme.command(name="set-create-threshold", usage="pingme set-create-threshold <num_votes>", help="Set minimum number of votes required to create a new role during !pingme create") + db_gateway().update( + "guild_info", + {"role_ping_cooldown_seconds": int(timeoutTD.total_seconds())}, + {"guild_id": ctx.guild.id} + ) + await ctx.message.reply("Cooldown for !pingme roles now updated to " + lib.timeUtil.td_format_noYM(timeoutTD) + "!") + await self.bot.adminLog( + ctx.message, + {"Cooldown For !pingme Role Pings Updated": lib.timeUtil.td_format_noYM(timeoutTD)} + ) + + @pingme.command( + name="set-create-threshold", + usage="pingme set-create-threshold <num_votes>", + help="Set minimum number of votes required to create a new role during !pingme create" + ) @commands.has_permissions(administrator=True) async def admin_cmd_set_pingme_create_threshold(self, ctx: Context, *, args: str): """Admin command: Set the minimum number of votes which must be reached for a pingme role creation to be approved. @@ -223,14 +260,22 @@ async def admin_cmd_set_pingme_create_threshold(self, ctx: Context, *, args: str elif not lib.stringTyping.strIsInt(args): await ctx.message.reply(":x: Invalid threshold! It must be a number.") elif int(args) < 1 or int(args) > MAX_PINGME_CREATE_THRESHOLD: - await ctx.message.reply(":x: Invalid threshold! It must be between 1 and " + str(MAX_PINGME_CREATE_THRESHOLD) + ", inclusive.") + await ctx.message.reply( + ":x: Invalid threshold! It must be between 1 and " + str(MAX_PINGME_CREATE_THRESHOLD) + ", inclusive." + ) else: db_gateway().update("guild_info", {"pingme_create_threshold": int(args)}, {"guild_id": ctx.guild.id}) await ctx.message.reply("✅ Minimum votes for `!pingme create` successfully updated to " + args + " users.") - await self.bot.adminLog(ctx.message, {"Votes Required For !pingme create Updated": "Minimum votes for new roles: " + args}) - - - @pingme.command(name="set-create-poll-length", usage="pingme set-create-poll-length [seconds=seconds] [minutes=minutes] [hours=hours] [days=days]", help="Set the amount of time which !pingme create polls run for") + await self.bot.adminLog( + ctx.message, + {"Votes Required For !pingme create Updated": "Minimum votes for new roles: " + args} + ) + + @pingme.command( + name="set-create-poll-length", + usage="pingme set-create-poll-length [seconds=seconds] [minutes=minutes] [hours=hours] [days=days]", + help="Set the amount of time which !pingme create polls run for" + ) @commands.has_permissions(administrator=True) async def admin_cmd_set_pingme_create_poll_length(self, ctx: Context, *, args: str): """Admin command: Set the amount of time for which user-triggered pingable role creation polls @@ -251,7 +296,7 @@ async def admin_cmd_set_pingme_create_poll_length(self, ctx: Context, *, args: s if arg.startswith(kwArg): kwArgs[kwArg[:-1]] = arg[len(kwArg):] break - + timeoutDict = {} for timeName in ["days", "hours", "minutes", "seconds"]: if timeName in kwArgs: @@ -260,18 +305,32 @@ async def admin_cmd_set_pingme_create_poll_length(self, ctx: Context, *, args: s return timeoutDict[timeName] = int(kwArgs[timeName]) - + timeoutTD = lib.timeUtil.timeDeltaFromDict(timeoutDict) if timeoutTD > MAX_PINGME_CREATE_POLL_LENGTH: - await ctx.message.reply(":x: The maximum `!pingme create` poll length is " + lib.timeUtil.td_format_noYM(MAX_ROLE_PING_TIMEOUT)) + await ctx.message.reply( + ":x: The maximum `!pingme create` poll length is " + lib.timeUtil.td_format_noYM(MAX_ROLE_PING_TIMEOUT) + ) return - - db_gateway().update("guild_info", {"pingme_create_poll_length_seconds": int(timeoutTD.total_seconds())}, {"guild_id": ctx.guild.id}) - await ctx.message.reply("✅ Poll length for `!pingme create` successfully updated to " + lib.timeUtil.td_format_noYM(timeoutTD) + ".") - await self.bot.adminLog(ctx.message, {"Poll length For !pingme create Pings Updated": lib.timeUtil.td_format_noYM(timeoutTD)}) - - @pingme.command(name="set-role-emoji", usage="pingme set-role-emoji <emoji>", help="Set the emoji which appears before the names of !pingme roles. Must be a built in emoji, not custom.") + db_gateway().update( + "guild_info", + {"pingme_create_poll_length_seconds": int(timeoutTD.total_seconds())}, + {"guild_id": ctx.guild.id} + ) + await ctx.message.reply( + "✅ Poll length for `!pingme create` successfully updated to " + lib.timeUtil.td_format_noYM(timeoutTD) + "." + ) + await self.bot.adminLog( + ctx.message, + {"Poll length For !pingme create Pings Updated": lib.timeUtil.td_format_noYM(timeoutTD)} + ) + + @pingme.command( + name="set-role-emoji", + usage="pingme set-role-emoji <emoji>", + help="Set the emoji which appears before the names of !pingme roles. Must be a built in emoji, not custom." + ) @commands.has_permissions(administrator=True) async def admin_cmd_set_pingme_role_emoji(self, ctx: Context, *, args: str): """Admin command: Set an emoji to prefix all pingme role names with. @@ -293,15 +352,18 @@ async def admin_cmd_set_pingme_role_emoji(self, ctx: Context, *, args: str): renamerTasks = set() for roleData in rolesData: renamerTasks.add(changePingablePrefix(args, ctx.guild, roleData["role_id"], roleData["name"])) - + await asyncio.wait(renamerTasks) await progressMsg.edit(content="Renaming " + str(len(rolesData)) + " roles... ✅") await ctx.message.reply("Emoji prefix for `!pingme create` roles now updated to " + args + "!") await self.bot.adminLog(ctx.message, {"Emoji Prefix For !pingme roles Updated": "New emoji: " + args}) - - @pingme.command(name="remove-role-emoji", usage="pingme remove-role-emoji <emoji>", help="Remove the emoji which appears before the names of !pingme roles.") + @pingme.command( + name="remove-role-emoji", + usage="pingme remove-role-emoji <emoji>", + help="Remove the emoji which appears before the names of !pingme roles." + ) @commands.has_permissions(administrator=True) async def admin_cmd_remove_pingme_role_emoji(self, ctx: Context, *, args: str): """Admin command: Remove the pingme role prefix emoji set with admin_cmd_set_pingme_role_emoji @@ -322,15 +384,18 @@ async def admin_cmd_remove_pingme_role_emoji(self, ctx: Context, *, args: str): renamerTasks = set() for roleData in rolesData: renamerTasks.add(changePingablePrefix("", ctx.guild, roleData["role_id"], roleData["name"])) - + await asyncio.wait(renamerTasks) await progressMsg.edit(content="Renaming " + str(len(rolesData)) + " roles... ✅") await ctx.message.reply("Emoji prefix for `!pingme create` roles has been removed!") await self.bot.adminLog(ctx.message, {"Emoji Prefix For !pingme roles Removed": "‎"}) - - @pingme.command(name="create", usage="pingme create <new role name>", help="Start a poll for the creation of a new !pingme role") + @pingme.command( + name="create", + usage="pingme create <new role name>", + help="Start a poll for the creation of a new !pingme role" + ) async def pingme_create(self, ctx: Context, *, args: str): """User command: Trigger a poll for the creation of a new pingme role with the given name. If the guild's configured minimum number of votes is reached, then the role will be created automatically. If the poll @@ -343,7 +408,7 @@ async def pingme_create(self, ctx: Context, *, args: str): if not args: await ctx.message.reply(":x: Please give the name of your new role!") else: - db = db_gateway() + db = db_gateway() roleData = db.get("pingable_roles", {"name": args.lower()}) if roleData and roleData[0]["guild_id"] == ctx.guild.id: await ctx.message.reply(":x: A `!pingme` role already exists with that name!") @@ -351,25 +416,57 @@ async def pingme_create(self, ctx: Context, *, args: str): pollMsg = await ctx.send("‎") guildData = db.get("guild_info", {"guild_id": ctx.guild.id})[0] requiredVotes = guildData["pingme_create_threshold"] - rolePoll = reactionPollMenu.InlineSingleOptionPollMenu(pollMsg, guildData["pingme_create_poll_length_seconds"], requiredVotes, - pollStarter=ctx.author, authorName=ctx.author.display_name + " wants to make a new !pingme role!", - desc="Name: " + args + "\nRequired votes: " + str(requiredVotes) + "\n\nReact if you want the role to be created!", - footerTxt="This menu will expire in " + lib.timeUtil.td_format_noYM(timedelta(seconds=guildData["pingme_create_poll_length_seconds"])) + ".") + rolePoll = reactionPollMenu.InlineSingleOptionPollMenu( + pollMsg, + guildData["pingme_create_poll_length_seconds"], + requiredVotes, + pollStarter=ctx.author, + authorName=ctx.author.display_name + " wants to make a new !pingme role!", + desc="Name: " + args + "\nRequired votes: " + str(requiredVotes) + + "\n\nReact if you want the role to be created!", + footerTxt="This menu will expire in " + + lib.timeUtil.td_format_noYM(timedelta(seconds=guildData["pingme_create_poll_length_seconds"])) + "." + ) await rolePoll.doMenu() if rolePoll.yesesReceived >= requiredVotes: - roleName = (guildData["pingme_role_emoji"] + args.title()) if guildData["pingme_role_emoji"] else args.title() - newRole = await ctx.guild.create_role(name=roleName, colour=DEFAULT_PINGABLE_COLOUR, mentionable=True, reason="New !pingme role creation requested via poll") - db.insert("pingable_roles", {"name": args.lower(), "guild_id": ctx.guild.id, "role_id": newRole.id, - "on_cooldown": False, "last_ping": -1, "ping_count": 0, - "monthly_ping_count": 0, "creator_id": ctx.author.id, "colour": DEFAULT_PINGABLE_COLOUR}) + roleName = (guildData["pingme_role_emoji"] + + args.title()) if guildData["pingme_role_emoji"] else args.title() + newRole = await ctx.guild.create_role( + name=roleName, + colour=DEFAULT_PINGABLE_COLOUR, + mentionable=True, + reason="New !pingme role creation requested via poll" + ) + db.insert( + "pingable_roles", + { + "name": args.lower(), + "guild_id": ctx.guild.id, + "role_id": newRole.id, + "on_cooldown": False, + "last_ping": -1, + "ping_count": 0, + "monthly_ping_count": 0, + "creator_id": ctx.author.id, + "colour": DEFAULT_PINGABLE_COLOUR + } + ) await ctx.message.reply("✅ The role has been created! Get it with `!pingme for " + args.lower() + "`") - await self.bot.adminLog(pollMsg, {"New !pingme Role Created": "Role: " + newRole.mention + "\nName: " + args}) + await self.bot.adminLog( + pollMsg, + {"New !pingme Role Created": "Role: " + newRole.mention + "\nName: " + args} + ) else: - await pollMsg.reply(ctx.author.mention + " The role has not been created, as the poll did not receive enough votes.") + await pollMsg.reply( + ctx.author.mention + " The role has not been created, as the poll did not receive enough votes." + ) await pollMsg.edit(content="This poll has now ended.", embed=rolePoll.getMenuEmbed()) - - @pingme.command(name="for", usage="pingme for <role name>", help="Get yourself a !pingme role, to be notified about events and games.") + @pingme.command( + name="for", + usage="pingme for <role name>", + help="Get yourself a !pingme role, to be notified about events and games." + ) async def pingme_for(self, ctx: Context, *, args: str): """User command: Self-(un)assign a pingme role. Pingme roles do not have to be subscribed to through this command. For example, reaction role menus work fine. @@ -394,7 +491,6 @@ async def pingme_for(self, ctx: Context, *, args: str): await ctx.author.add_roles(role, reason="User subscribed to !pingme role via command") await ctx.message.reply("✅ You got the " + role.name + " role!") - @pingme.command(name="list", usage="pingme list", help="List all available `!pingme` roles") async def pingme_for(self, ctx: Context): """User command: List all available pingme roles. @@ -405,18 +501,22 @@ async def pingme_for(self, ctx: Context): """ allRolesData = db_gateway().get("pingable_roles", {"guild_id": ctx.guild.id}) if not allRolesData: - await ctx.message.reply(f":x: This guild has no `!pingme` roles! Make a new one with `{self.bot.command_prefix}pingme create`.") + await ctx.message.reply( + f":x: This guild has no `!pingme` roles! Make a new one with `{self.bot.command_prefix}pingme create`." + ) else: reportEmbed = discord.Embed(title="All !pingme Roles", desc=ctx.guild.name) reportEmbed.colour = discord.Colour.random() reportEmbed.set_thumbnail(url=self.bot.user.avatar_url_as(size=128)) for roleData in allRolesData: - reportEmbed.add_field(name=roleData["name"].title(), - value="<@&" + str(roleData["role_id"]) + ">\nCreated by: <@" + str(roleData["creator_id"]) + ">\nTotal pings: " + str(roleData["ping_count"])) + reportEmbed.add_field( + name=roleData["name"].title(), + value="<@&" + str(roleData["role_id"]) + ">\nCreated by: <@" + str(roleData["creator_id"]) + + ">\nTotal pings: " + str(roleData["ping_count"]) + ) await ctx.reply(embed=reportEmbed) - - @pingme.command(name="clear", usage="pingme clear", help="Unsubscribe from all !pingme roles, if you have any.") + @pingme.command(name="clear", usage="pingme clear", help="Unsubscribe from all !pingme roles, if you have any.") async def pingme_clear(self, ctx: Context, *, args: str): """User command: Unsubscribe from all assigned pingme roles. diff --git a/src/esportsbot/cogs/TwitchIntegrationCog.py b/src/esportsbot/cogs/TwitchIntegrationCog.py index a519970d..6be87d57 100644 --- a/src/esportsbot/cogs/TwitchIntegrationCog.py +++ b/src/esportsbot/cogs/TwitchIntegrationCog.py @@ -21,23 +21,34 @@ def cog_unload(self): async def addtwitch(self, ctx, twitch_handle=None, announce_channel=None): if twitch_handle is not None and announce_channel is not None: # Check if Twitch channel has already been added - twitch_in_db = db_gateway().get('twitch_info', params={ - 'guild_id': ctx.author.guild.id, 'twitch_handle': twitch_handle.lower()}) - cleaned_channel_id = channel_id_from_mention( - announce_channel) + twitch_in_db = db_gateway().get( + 'twitch_info', + params={ + 'guild_id': ctx.author.guild.id, + 'twitch_handle': twitch_handle.lower() + } + ) + cleaned_channel_id = channel_id_from_mention(announce_channel) channel_mention = "<#" + str(cleaned_channel_id) + ">" if not twitch_in_db: # Check user exists - user_exists = bool( - self.twitch_handler.request_user(twitch_handle)) + user_exists = bool(self.twitch_handler.request_user(twitch_handle)) if user_exists: # Get live status of the channel - live_status = bool(self.twitch_handler.request_data( - [twitch_handle])) + live_status = bool(self.twitch_handler.request_data([twitch_handle])) # Insert Twitch channel into DB - db_gateway().insert('twitch_info', params={ - 'guild_id': ctx.author.guild.id, 'channel_id': cleaned_channel_id, 'twitch_handle': twitch_handle.lower(), 'currently_live': live_status}) - await ctx.channel.send(f"{twitch_handle} is valid and has been added, their notifications will be placed in {channel_mention}") + db_gateway().insert( + 'twitch_info', + params={ + 'guild_id': ctx.author.guild.id, + 'channel_id': cleaned_channel_id, + 'twitch_handle': twitch_handle.lower(), + 'currently_live': live_status + } + ) + await ctx.channel.send( + f"{twitch_handle} is valid and has been added, their notifications will be placed in {channel_mention}" + ) else: await ctx.channel.send(f"{twitch_handle} is not a valid Twitch handle") else: @@ -49,25 +60,41 @@ async def addtwitch(self, ctx, twitch_handle=None, announce_channel=None): async def addcustomtwitch(self, ctx, twitch_handle=None, announce_channel=None, custom_message=None): if None not in (twitch_handle, announce_channel, custom_message): # Check if Twitch channel has already been added - twitch_in_db = db_gateway().get('twitch_info', params={ - 'guild_id': ctx.author.guild.id, 'twitch_handle': twitch_handle.lower()}) - cleaned_channel_id = channel_id_from_mention( - announce_channel) + twitch_in_db = db_gateway().get( + 'twitch_info', + params={ + 'guild_id': ctx.author.guild.id, + 'twitch_handle': twitch_handle.lower() + } + ) + cleaned_channel_id = channel_id_from_mention(announce_channel) channel_mention = "<#" + str(cleaned_channel_id) + ">" if not twitch_in_db: # Check user exists - user_exists = bool( - self.twitch_handler.request_user(twitch_handle)) + user_exists = bool(self.twitch_handler.request_user(twitch_handle)) if user_exists: # Get live status of the channel - live_status = bool(self.twitch_handler.request_data( - [twitch_handle])) + live_status = bool(self.twitch_handler.request_data([twitch_handle])) # Insert Twitch channel into DB - db_gateway().insert('twitch_info', params={ - 'guild_id': ctx.author.guild.id, 'channel_id': cleaned_channel_id, 'twitch_handle': twitch_handle.lower(), 'currently_live': live_status, 'custom_message': custom_message}) - await ctx.channel.send(f"{twitch_handle} is valid and has been added, their notifications will be placed in {channel_mention}") + db_gateway().insert( + 'twitch_info', + params={ + 'guild_id': ctx.author.guild.id, + 'channel_id': cleaned_channel_id, + 'twitch_handle': twitch_handle.lower(), + 'currently_live': live_status, + 'custom_message': custom_message + } + ) + await ctx.channel.send( + f"{twitch_handle} is valid and has been added, their notifications will be placed in {channel_mention}" + ) sample_message = custom_message.format( - handle="TwitchHandle", game="Game/Genre", link="StreamLink", title="Title") + handle="TwitchHandle", + game="Game/Genre", + link="StreamLink", + title="Title" + ) await ctx.channel.send(f"Sample custom message below\n {sample_message}") else: await ctx.channel.send(f"{twitch_handle} is not a valid Twitch handle") @@ -80,14 +107,29 @@ async def addcustomtwitch(self, ctx, twitch_handle=None, announce_channel=None, async def editcustomtwitch(self, ctx, twitch_handle=None, custom_message=None): if twitch_handle is not None and custom_message is not None: # Check if Twitch channel has already been added - twitch_in_db = db_gateway().get('twitch_info', params={ - 'guild_id': ctx.author.guild.id, 'twitch_handle': twitch_handle.lower()}) + twitch_in_db = db_gateway().get( + 'twitch_info', + params={ + 'guild_id': ctx.author.guild.id, + 'twitch_handle': twitch_handle.lower() + } + ) if twitch_in_db: # Make DB edit - db_gateway().update('twitch_info', set_params={ - 'custom_message': custom_message}, where_params={'guild_id': ctx.author.guild.id, 'twitch_handle': twitch_handle.lower()}) + db_gateway().update( + 'twitch_info', + set_params={'custom_message': custom_message}, + where_params={ + 'guild_id': ctx.author.guild.id, + 'twitch_handle': twitch_handle.lower() + } + ) sample_message = custom_message.format( - handle="TwitchHandle", game="Game/Genre", link="StreamLink", title="Title") + handle="TwitchHandle", + game="Game/Genre", + link="StreamLink", + title="Title" + ) await ctx.channel.send(f"Sample custom message below\n {sample_message}") else: await ctx.channel.send("That Twitch handle is not configured in this server") @@ -98,15 +140,25 @@ async def editcustomtwitch(self, ctx, twitch_handle=None, custom_message=None): async def edittwitch(self, ctx, twitch_handle=None, announce_channel=None): if twitch_handle is not None and announce_channel is not None: # Check if Twitch channel has already been added - twitch_in_db = db_gateway().get('twitch_info', params={ - 'guild_id': ctx.author.guild.id, 'twitch_handle': twitch_handle.lower()}) - cleaned_channel_id = channel_id_from_mention( - announce_channel) + twitch_in_db = db_gateway().get( + 'twitch_info', + params={ + 'guild_id': ctx.author.guild.id, + 'twitch_handle': twitch_handle.lower() + } + ) + cleaned_channel_id = channel_id_from_mention(announce_channel) channel_mention = "<#" + str(cleaned_channel_id) + ">" if twitch_in_db: # Make DB edit - db_gateway().update('twitch_info', set_params={ - 'channel_id': cleaned_channel_id}, where_params={'guild_id': ctx.author.guild.id, 'twitch_handle': twitch_handle.lower()}) + db_gateway().update( + 'twitch_info', + set_params={'channel_id': cleaned_channel_id}, + where_params={ + 'guild_id': ctx.author.guild.id, + 'twitch_handle': twitch_handle.lower() + } + ) await ctx.channel.send(f"Changed the alerts for {twitch_handle} to {channel_mention}") else: await ctx.channel.send("The Twitch user mentioned is not configured in this server") @@ -118,12 +170,22 @@ async def removetwitch(self, ctx, twitch_handle=None): if twitch_handle is not None: # Entered a Twitter handle twitch_handle = twitch_handle.lower() - handle_exists = db_gateway().get('twitch_info', params={ - 'guild_id': ctx.author.guild.id, 'twitch_handle': twitch_handle.lower()}) + handle_exists = db_gateway().get( + 'twitch_info', + params={ + 'guild_id': ctx.author.guild.id, + 'twitch_handle': twitch_handle.lower() + } + ) if handle_exists: # Handle exists - db_gateway().delete('twitch_info', - where_params={'guild_id': ctx.author.guild.id, 'twitch_handle': twitch_handle.lower()}) + db_gateway().delete( + 'twitch_info', + where_params={ + 'guild_id': ctx.author.guild.id, + 'twitch_handle': twitch_handle.lower() + } + ) await ctx.channel.send(f"Alerts for {twitch_handle} have been removed from this server") else: await ctx.channel.send("Entered Twitch handle is not configured in this server") @@ -132,14 +194,12 @@ async def removetwitch(self, ctx, twitch_handle=None): @commands.command() async def removealltwitch(self, ctx): - db_gateway().delete('twitch_info', where_params={ - 'guild_id': ctx.author.guild.id}) + db_gateway().delete('twitch_info', where_params={'guild_id': ctx.author.guild.id}) await ctx.channel.send("Removed all Twitch alerts from this server") @commands.command() async def getalltwitch(self, ctx): - returned_val = db_gateway().get('twitch_info', params={ - 'guild_id': ctx.author.guild.id}) + returned_val = db_gateway().get('twitch_info', params={'guild_id': ctx.author.guild.id}) all_handles = "** **\n__**Twitch Alerts**__\n" for each in returned_val: channel_mention = "<#" + str(each['channel_id']) + ">" @@ -162,75 +222,68 @@ async def before_live_checker(self): async def get_and_compare_statuses(self, alert): start_time = time.time() - all_twitch_handles = db_gateway().pure_return( - 'SELECT DISTINCT twitch_handle FROM "twitch_info"') + all_twitch_handles = db_gateway().pure_return('SELECT DISTINCT twitch_handle FROM "twitch_info"') if all_twitch_handles: # Create list of all twitch handles in the database - twitch_handle_arr = list(x['twitch_handle'] - for x in all_twitch_handles) + twitch_handle_arr = list(x['twitch_handle'] for x in all_twitch_handles) # Create dict consisting of twitch handles and live statuses twitch_status_dict = dict() - all_twitch_statuses = db_gateway().pure_return( - 'SELECT DISTINCT twitch_handle, currently_live FROM "twitch_info"') + all_twitch_statuses = db_gateway().pure_return('SELECT DISTINCT twitch_handle, currently_live FROM "twitch_info"') for twitch_user in all_twitch_statuses: - twitch_status_dict[twitch_user['twitch_handle'] - ] = twitch_user['currently_live'] + twitch_status_dict[twitch_user['twitch_handle']] = twitch_user['currently_live'] # Query Twitch to receive array of all live users - returned_data = self.twitch_handler.request_data( - twitch_handle_arr) + returned_data = self.twitch_handler.request_data(twitch_handle_arr) # Loop through all users comparing them to the live list for twitch_handle in twitch_handle_arr: # if any(obj['user_name'].lower() == twitch_handle for obj in returned_data): - handle_live = (next( - (obj for obj in returned_data if obj['user_name'].lower() == twitch_handle), False)) + handle_live = (next((obj for obj in returned_data if obj['user_name'].lower() == twitch_handle), False)) print(handle_live) if handle_live: # User is live if not twitch_status_dict[f'{twitch_handle}']: # User was not live before but now is - db_gateway().update('twitch_info', set_params={ - 'currently_live': True}, where_params={'twitch_handle': twitch_handle.lower()}) + db_gateway().update( + 'twitch_info', + set_params={'currently_live': True}, + where_params={'twitch_handle': twitch_handle.lower()} + ) if alert: # Grab all channels to be alerted - all_channels = db_gateway().get('twitch_info', params={ - 'twitch_handle': twitch_handle.lower()}) + all_channels = db_gateway().get('twitch_info', params={'twitch_handle': twitch_handle.lower()}) for each in all_channels: # Send alert to specified channel to each['channel_id'] custom_message = each['custom_message'].format( - handle=handle_live['user_name'], game=handle_live['game_name'], link=f"https://twitch.tv/{handle_live['user_name']}", title=handle_live['title']) if each['custom_message'] != '' else f"{handle_live['user_name']} has just gone live with {handle_live['game_name']}, check them out here: https://twitch.tv/{handle_live['user_name']}" + handle=handle_live['user_name'], + game=handle_live['game_name'], + link=f"https://twitch.tv/{handle_live['user_name']}", + title=handle_live['title'] + ) if each[ + 'custom_message' + ] != '' else f"{handle_live['user_name']} has just gone live with {handle_live['game_name']}, check them out here: https://twitch.tv/{handle_live['user_name']}" await self.bot.get_channel(each['channel_id']).send(custom_message) else: # User is not live - db_gateway().update('twitch_info', set_params={ - 'currently_live': False}, where_params={'twitch_handle': twitch_handle.lower()}) - return round(time.time()-start_time, 3) + db_gateway().update( + 'twitch_info', + set_params={'currently_live': False}, + where_params={'twitch_handle': twitch_handle.lower()} + ) + return round(time.time() - start_time, 3) class TwitchAPIHandler: - def __init__(self): self.client_id = os.getenv('TWITCH_CLIENT_ID') self.client_secret = os.getenv('TWITCH_CLIENT_SECRET') - self.params = { - 'client_id': self.client_id, - 'client_secret': self.client_secret, - 'grant_type': 'client_credentials' - } + self.params = {'client_id': self.client_id, 'client_secret': self.client_secret, 'grant_type': 'client_credentials'} self.token = None def base_headers(self): - return { - 'Authorization': f'Bearer {self.token.get("access_token")}', - 'Client-ID': self.client_id - } + return {'Authorization': f'Bearer {self.token.get("access_token")}', 'Client-ID': self.client_id} def generate_new_oauth(self): OAuthURL = 'https://id.twitch.tv/oauth2/token' - params = { - 'client_id': self.client_id, - 'client_secret': self.client_secret, - 'grant_type': 'client_credentials' - } + params = {'client_id': self.client_id, 'client_secret': self.client_secret, 'grant_type': 'client_credentials'} oauth_response = requests.post(OAuthURL, params) if oauth_response.status_code == 200: oauth_response_json = oauth_response.json() @@ -243,9 +296,8 @@ def request_data(self, twitch_handles): if self.token is None or self.token['expires_in'] < time.time(): self.generate_new_oauth() data_url = 'https://api.twitch.tv/helix/streams?' - data_url = data_url+"user_login="+("&user_login=".join(twitch_handles)) - data_response = requests.get( - data_url, headers=self.base_headers(), params=self.params) + data_url = data_url + "user_login=" + ("&user_login=".join(twitch_handles)) + data_response = requests.get(data_url, headers=self.base_headers(), params=self.params) return data_response.json()['data'] def request_user(self, twitch_handle): @@ -253,8 +305,7 @@ def request_user(self, twitch_handle): self.generate_new_oauth() data_url = f'https://api.twitch.tv/helix/users?login={twitch_handle}' #data_url = data_url+"user_login="+("&user_login=".join(twitch_handles)) - data_response = requests.get( - data_url, headers=self.base_headers(), params=self.params) + data_response = requests.get(data_url, headers=self.base_headers(), params=self.params) return data_response.json()['data'] diff --git a/src/esportsbot/cogs/TwitterIntegrationCog.py b/src/esportsbot/cogs/TwitterIntegrationCog.py index 4ae3c3a5..0c1c9074 100644 --- a/src/esportsbot/cogs/TwitterIntegrationCog.py +++ b/src/esportsbot/cogs/TwitterIntegrationCog.py @@ -6,7 +6,6 @@ class TwitterIntegrationCog(commands.Cog): - def __init__(self, bot): self.bot = bot self.tweet_checker.start() @@ -19,18 +18,33 @@ def cog_unload(self): async def addtwitter(self, ctx, twitter_handle=None, announce_channel=None): if twitter_handle is not None and announce_channel is not None: if (twitter_handle.replace('_', '')).isalnum(): - twitter_in_db = db_gateway().get('twitter_info', params={ - 'guild_id': ctx.author.guild.id, 'twitter_handle': twitter_handle.lower()}) + twitter_in_db = db_gateway().get( + 'twitter_info', + params={ + 'guild_id': ctx.author.guild.id, + 'twitter_handle': twitter_handle.lower() + } + ) if not bool(twitter_in_db): cleaned_channel_id = channel_id_from_mention(announce_channel) channel_mention = "<#" + str(cleaned_channel_id) + ">" - previous_tweet_id = self.get_tweets( - twitter_handle)[0]['id'] - db_gateway().insert('twitter_info', params={ - 'guild_id': ctx.author.guild.id, 'channel_id': cleaned_channel_id, 'twitter_handle': twitter_handle.lower(), 'previous_tweet_id': previous_tweet_id}) - await ctx.channel.send(f"{twitter_handle} is valid and has been added, their Tweets will be placed in {channel_mention}") + previous_tweet_id = self.get_tweets(twitter_handle)[0]['id'] + db_gateway().insert( + 'twitter_info', + params={ + 'guild_id': ctx.author.guild.id, + 'channel_id': cleaned_channel_id, + 'twitter_handle': twitter_handle.lower(), + 'previous_tweet_id': previous_tweet_id + } + ) + await ctx.channel.send( + f"{twitter_handle} is valid and has been added, their Tweets will be placed in {channel_mention}" + ) else: - await ctx.channel.send(f"{twitter_handle} is already configured to output to <#{str(twitter_in_db['channel_id'])}>") + await ctx.channel.send( + f"{twitter_handle} is already configured to output to <#{str(twitter_in_db['channel_id'])}>" + ) else: await ctx.channel.send("You need to provide a correct Twitter handle") else: @@ -41,11 +55,21 @@ async def addtwitter(self, ctx, twitter_handle=None, announce_channel=None): async def removetwitter(self, ctx, twitter_handle=None): if twitter_handle is not None: if (twitter_handle.replace('_', '')).isalnum(): - twitter_in_db = db_gateway().get('twitter_info', params={ - 'guild_id': ctx.author.guild.id, 'twitter_handle': twitter_handle.lower()}) + twitter_in_db = db_gateway().get( + 'twitter_info', + params={ + 'guild_id': ctx.author.guild.id, + 'twitter_handle': twitter_handle.lower() + } + ) if bool(twitter_in_db): - db_gateway().delete('twitter_info', where_params={ - 'guild_id': ctx.author.guild.id, 'twitter_handle': twitter_handle.lower()}) + db_gateway().delete( + 'twitter_info', + where_params={ + 'guild_id': ctx.author.guild.id, + 'twitter_handle': twitter_handle.lower() + } + ) await ctx.channel.send(f"Removed alerts for @{twitter_handle}") else: await ctx.channel.send(f"No alerts set for @{twitter_handle}") @@ -59,14 +83,25 @@ async def removetwitter(self, ctx, twitter_handle=None): async def changetwitterchannel(self, ctx, twitter_handle=None, announce_channel=None): if twitter_handle is not None and announce_channel is not None: if (twitter_handle.replace('_', '')).isalnum(): - twitter_in_db = db_gateway().get('twitter_info', params={ - 'guild_id': ctx.author.guild.id, 'twitter_handle': twitter_handle.lower()}) + twitter_in_db = db_gateway().get( + 'twitter_info', + params={ + 'guild_id': ctx.author.guild.id, + 'twitter_handle': twitter_handle.lower() + } + ) if bool(twitter_in_db): # In DB cleaned_channel_id = channel_id_from_mention(announce_channel) channel_mention = "<#" + str(cleaned_channel_id) + ">" - db_gateway().update('twitter_info', set_params={'channel_id': cleaned_channel_id}, where_params={ - 'guild_id': ctx.author.guild.id, 'twitter_handle': twitter_handle.lower()}) + db_gateway().update( + 'twitter_info', + set_params={'channel_id': cleaned_channel_id}, + where_params={ + 'guild_id': ctx.author.guild.id, + 'twitter_handle': twitter_handle.lower() + } + ) await ctx.channel.send(f"{twitter_handle} has been updated and will now notify in {channel_mention}") else: # Not set up @@ -79,8 +114,7 @@ async def changetwitterchannel(self, ctx, twitter_handle=None, announce_channel= @commands.command() @commands.has_permissions(administrator=True) async def getalltwitters(self, ctx): - all_guild_twitters = db_gateway().get( - 'twitter_info', params={'guild_id': ctx.author.guild.id}) + all_guild_twitters = db_gateway().get('twitter_info', params={'guild_id': ctx.author.guild.id}) if all_guild_twitters: all_twitters_str = str() for twitter in all_guild_twitters: @@ -96,8 +130,7 @@ def get_tweets(self, given_username, tweet_number=1): for index, tweet_data in enumerate(sntwitter.TwitterSearchScraper(f'from:{given_username}').get_items()): #tweet_is_reply = True if tweet_data.content[0] == '@' else False if tweet_data.content[0] != '@': - tweets_list.append( - {'id': tweet_data.id, 'content': tweet_data.content, 'link': str(tweet_data)}) + tweets_list.append({'id': tweet_data.id, 'content': tweet_data.content, 'link': str(tweet_data)}) if len(tweets_list) == tweet_number: break return tweets_list @@ -115,9 +148,17 @@ async def tweet_checker(self): print(f"{each['twitter_handle']} - Same") else: print(f"{each['twitter_handle']} - Different") - await self.bot.get_channel(each['channel_id']).send(f"@{each['twitter_handle']} has just tweeted! Link - {single_tweet[0]['link']}") - db_gateway().update('twitter_info', set_params={'previous_tweet_id': int(single_tweet[0]['id'])}, where_params={ - 'guild_id': each['guild_id'], 'twitter_handle': each['twitter_handle']}) + await self.bot.get_channel( + each['channel_id'] + ).send(f"@{each['twitter_handle']} has just tweeted! Link - {single_tweet[0]['link']}") + db_gateway().update( + 'twitter_info', + set_params={'previous_tweet_id': int(single_tweet[0]['id'])}, + where_params={ + 'guild_id': each['guild_id'], + 'twitter_handle': each['twitter_handle'] + } + ) end_time = time.time() print(f'Checking tweets took: {round(end_time-start_time, 3)}s') @@ -127,8 +168,14 @@ async def before_tweet_checker(self): returned_val = db_gateway().getall('twitter_info') for each in returned_val: single_tweet = self.get_tweets(each['twitter_handle'], 1) - db_gateway().update('twitter_info', set_params={'previous_tweet_id': int(single_tweet[0]['id'])}, where_params={ - 'guild_id': each['guild_id'], 'twitter_handle': each['twitter_handle']}) + db_gateway().update( + 'twitter_info', + set_params={'previous_tweet_id': int(single_tweet[0]['id'])}, + where_params={ + 'guild_id': each['guild_id'], + 'twitter_handle': each['twitter_handle'] + } + ) print('Waiting on bot to become ready before start Twitter cog') await self.bot.wait_until_ready() diff --git a/src/esportsbot/cogs/VoicemasterCog.py b/src/esportsbot/cogs/VoicemasterCog.py index 8c45dba9..80b97b8d 100644 --- a/src/esportsbot/cogs/VoicemasterCog.py +++ b/src/esportsbot/cogs/VoicemasterCog.py @@ -10,25 +10,41 @@ def __init__(self, bot): @commands.command() @commands.has_permissions(administrator=True) async def setvmmaster(self, ctx, given_channel_id=None): - is_a_valid_id = given_channel_id and given_channel_id.isdigit( - ) and len(given_channel_id) == 18 + is_a_valid_id = given_channel_id and given_channel_id.isdigit() and len(given_channel_id) == 18 if is_a_valid_id: - is_a_master = db_gateway().get('voicemaster_master', params={ - 'guild_id': ctx.author.guild.id, 'channel_id': given_channel_id}) - is_voice_channel = hasattr(self.bot.get_channel( - int(given_channel_id)), 'voice_states') - is_a_slave = db_gateway().get('voicemaster_slave', params={ - 'guild_id': ctx.author.guild.id, 'channel_id': given_channel_id}) + is_a_master = db_gateway().get( + 'voicemaster_master', + params={ + 'guild_id': ctx.author.guild.id, + 'channel_id': given_channel_id + } + ) + is_voice_channel = hasattr(self.bot.get_channel(int(given_channel_id)), 'voice_states') + is_a_slave = db_gateway().get( + 'voicemaster_slave', + params={ + 'guild_id': ctx.author.guild.id, + 'channel_id': given_channel_id + } + ) if is_voice_channel and not (is_a_master or is_a_slave): # Not currently a Master and is voice channel, add it - db_gateway().insert('voicemaster_master', params={ - 'guild_id': ctx.author.guild.id, 'channel_id': given_channel_id}) + db_gateway().insert( + 'voicemaster_master', + params={ + 'guild_id': ctx.author.guild.id, + 'channel_id': given_channel_id + } + ) await ctx.channel.send("This VC has now been set as a VM master") - new_vm_master_channel = self.bot.get_channel( - int(given_channel_id)) - await send_to_log_channel(self, ctx.author.guild.id, f"{ctx.author.mention} has made {new_vm_master_channel.name} - {new_vm_master_channel.id} a VM master VC") + new_vm_master_channel = self.bot.get_channel(int(given_channel_id)) + await send_to_log_channel( + self, + ctx.author.guild.id, + f"{ctx.author.mention} has made {new_vm_master_channel.name} - {new_vm_master_channel.id} a VM master VC" + ) elif is_a_master: # This already exists as a master await ctx.channel.send("This VC is already set as a VM master") @@ -49,8 +65,7 @@ async def setvmmaster(self, ctx, given_channel_id=None): @commands.command() @commands.has_permissions(administrator=True) async def getvmmasters(self, ctx): - master_vm_exists = db_gateway().get('voicemaster_master', params={ - 'guild_id': ctx.author.guild.id}) + master_vm_exists = db_gateway().get('voicemaster_master', params={'guild_id': ctx.author.guild.id}) if master_vm_exists: master_vm_str = str() @@ -64,13 +79,27 @@ async def getvmmasters(self, ctx): @commands.has_permissions(administrator=True) async def removevmmaster(self, ctx, given_channel_id=None): if given_channel_id: - channel_exists = db_gateway().get('voicemaster_master', params={ - 'guild_id': ctx.author.guild.id, 'channel_id': given_channel_id}) + channel_exists = db_gateway().get( + 'voicemaster_master', + params={ + 'guild_id': ctx.author.guild.id, + 'channel_id': given_channel_id + } + ) if channel_exists: - db_gateway().delete('voicemaster_master', where_params={ - 'guild_id': ctx.author.guild.id, 'channel_id': given_channel_id}) + db_gateway().delete( + 'voicemaster_master', + where_params={ + 'guild_id': ctx.author.guild.id, + 'channel_id': given_channel_id + } + ) await ctx.channel.send("This VC is no longer a VM master") - await send_to_log_channel(self, ctx.author.guild.id, f"{ctx.author.mention} has removed {new_vm_master_channel.name} - {new_vm_master_channel.id} from VM master VC") + await send_to_log_channel( + self, + ctx.author.guild.id, + f"{ctx.author.mention} has removed {new_vm_master_channel.name} - {new_vm_master_channel.id} from VM master VC" + ) else: await ctx.channel.send("This VC is not currently a VM master") else: @@ -79,38 +108,45 @@ async def removevmmaster(self, ctx, given_channel_id=None): @commands.command() @commands.has_permissions(administrator=True) async def removeallmasters(self, ctx): - all_vm_masters = db_gateway().get('voicemaster_master', params={ - 'guild_id': ctx.author.guild.id}) + all_vm_masters = db_gateway().get('voicemaster_master', params={'guild_id': ctx.author.guild.id}) for vm_master in all_vm_masters: - db_gateway().delete('voicemaster_master', where_params={ - 'channel_id': vm_master['channel_id']}) + db_gateway().delete('voicemaster_master', where_params={'channel_id': vm_master['channel_id']}) await ctx.channel.send("Cleared all VM masters from this server") await send_to_log_channel(self, ctx.author.guild.id, f"{ctx.author.mention} has removed all VM masters") @commands.command() @commands.has_permissions(administrator=True) async def killallslaves(self, ctx): - all_vm_slaves = db_gateway().get('voicemaster_slave', params={ - 'guild_id': ctx.author.guild.id}) + all_vm_slaves = db_gateway().get('voicemaster_slave', params={'guild_id': ctx.author.guild.id}) for vm_slave in all_vm_slaves: vm_slave_channel = self.bot.get_channel(vm_slave['channel_id']) if vm_slave_channel: await vm_slave_channel.delete() - db_gateway().delete('voicemaster_slave', where_params={ - 'channel_id': vm_slave['channel_id']}) + db_gateway().delete('voicemaster_slave', where_params={'channel_id': vm_slave['channel_id']}) await ctx.channel.send("Cleared all VM slaves from this server") await send_to_log_channel(self, ctx.author.guild.id, f"{ctx.author.mention} has removed all VM slaves") @commands.command() async def lockvm(self, ctx): - in_vm_slave = db_gateway().get('voicemaster_slave', params={ - 'guild_id': ctx.author.guild.id, 'channel_id': ctx.author.voice.channel.id}) + in_vm_slave = db_gateway().get( + 'voicemaster_slave', + params={ + 'guild_id': ctx.author.guild.id, + 'channel_id': ctx.author.voice.channel.id + } + ) if in_vm_slave: if in_vm_slave[0]['owner_id'] == ctx.author.id: if not in_vm_slave[0]['locked']: - db_gateway().update('voicemaster_slave', set_params={'locked': True}, where_params={ - 'guild_id': ctx.author.guild.id, 'channel_id': ctx.author.voice.channel.id}) + db_gateway().update( + 'voicemaster_slave', + set_params={'locked': True}, + where_params={ + 'guild_id': ctx.author.guild.id, + 'channel_id': ctx.author.voice.channel.id + } + ) await ctx.author.voice.channel.edit(user_limit=len(ctx.author.voice.channel.members)) await ctx.channel.send("Your VM slave has been locked 🔒") await send_to_log_channel(self, ctx.author.guild.id, f"{ctx.author.mention} has locked their VM slave") @@ -123,14 +159,25 @@ async def lockvm(self, ctx): @commands.command() async def unlockvm(self, ctx): - in_vm_slave = db_gateway().get('voicemaster_slave', params={ - 'guild_id': ctx.author.guild.id, 'channel_id': ctx.author.voice.channel.id}) + in_vm_slave = db_gateway().get( + 'voicemaster_slave', + params={ + 'guild_id': ctx.author.guild.id, + 'channel_id': ctx.author.voice.channel.id + } + ) if in_vm_slave: if in_vm_slave[0]['owner_id'] == ctx.author.id: if in_vm_slave[0]['locked']: - db_gateway().update('voicemaster_slave', set_params={'locked': False}, where_params={ - 'guild_id': ctx.author.guild.id, 'channel_id': ctx.author.voice.channel.id}) + db_gateway().update( + 'voicemaster_slave', + set_params={'locked': False}, + where_params={ + 'guild_id': ctx.author.guild.id, + 'channel_id': ctx.author.voice.channel.id + } + ) await ctx.author.voice.channel.edit(user_limit=0) await ctx.channel.send("Your VM slave has been unlocked 🔓") await send_to_log_channel(self, ctx.author.guild.id, f"{ctx.author.mention} has unlocked their VM slave") diff --git a/src/esportsbot/db_gateway.py b/src/esportsbot/db_gateway.py index 63d8639b..d6f0e49c 100644 --- a/src/esportsbot/db_gateway.py +++ b/src/esportsbot/db_gateway.py @@ -6,11 +6,12 @@ class db_connection(): def __init__(self, database=None): - self.conn = psycopg2.connect(host=os.getenv('PG_HOST'), - database=os.getenv( - 'PG_DATABASE') if database is None else database, - user=os.getenv('PG_USER'), - password=os.getenv('PG_PWD')) + self.conn = psycopg2.connect( + host=os.getenv('PG_HOST'), + database=os.getenv('PG_DATABASE') if database is None else database, + user=os.getenv('PG_USER'), + password=os.getenv('PG_PWD') + ) self.conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) self.cur = self.conn.cursor() diff --git a/src/esportsbot/generate_schema.py b/src/esportsbot/generate_schema.py index 610e205b..3985dbce 100644 --- a/src/esportsbot/generate_schema.py +++ b/src/esportsbot/generate_schema.py @@ -4,14 +4,16 @@ def generate_schema(): # Does the esportsbot DB exist? esportsbot_exists = db_gateway().pure_return( - "SELECT datname FROM pg_catalog.pg_database WHERE lower(datname) = lower('esportsbot')", "postgres") + "SELECT datname FROM pg_catalog.pg_database WHERE lower(datname) = lower('esportsbot')", + "postgres" + ) if not esportsbot_exists: # Esportsbot DB doesn't exist db_gateway().pure_query("CREATE DATABASE esportsbot", "postgres") # Does the guild_id table exist? - guild_id_exists = db_gateway().pure_return( - "SELECT true::BOOLEAN FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename = 'guild_info'") + guild_id_exists = db_gateway( + ).pure_return("SELECT true::BOOLEAN FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename = 'guild_info'") if not guild_id_exists: # Does not exist query_string = """ @@ -33,7 +35,8 @@ def generate_schema(): # Does the pingable_roles table exist? pingable_roles_exists = db_gateway().pure_return( - "SELECT true::BOOLEAN FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename = 'pingable_roles'") + "SELECT true::BOOLEAN FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename = 'pingable_roles'" + ) if not pingable_roles_exists: # Does not exist query_string = """ @@ -57,7 +60,8 @@ def generate_schema(): # Does the event_categories exist? event_categories_exists = db_gateway().pure_return( - "SELECT true::BOOLEAN FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename = 'event_categories'") + "SELECT true::BOOLEAN FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename = 'event_categories'" + ) if not event_categories_exists: # Does not exist query_string = """ @@ -76,7 +80,8 @@ def generate_schema(): # Does the reaction_menus table exist? reaction_menus_exists = db_gateway().pure_return( - "SELECT true::BOOLEAN FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename = 'reaction_menus'") + "SELECT true::BOOLEAN FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename = 'reaction_menus'" + ) if not reaction_menus_exists: # Does not exist query_string = """ @@ -91,7 +96,8 @@ def generate_schema(): # Does the voicemaster_master table exist? voicemaster_master_exists = db_gateway().pure_return( - "SELECT true::BOOLEAN FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename = 'voicemaster_master'") + "SELECT true::BOOLEAN FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename = 'voicemaster_master'" + ) if not voicemaster_master_exists: # Does not exist query_string = """ @@ -115,7 +121,8 @@ def generate_schema(): # Does the voicemaster_slave table exist? voicemaster_slave_exists = db_gateway().pure_return( - "SELECT true::BOOLEAN FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename = 'voicemaster_slave'") + "SELECT true::BOOLEAN FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename = 'voicemaster_slave'" + ) if not voicemaster_slave_exists: # Does not exist query_string = """ @@ -140,8 +147,8 @@ def generate_schema(): db_gateway().pure_query(query_string) # Does the twitch_info table exist? - twitch_info_exists = db_gateway().pure_return( - "SELECT true::BOOLEAN FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename = 'twitch_info'") + twitch_info_exists = db_gateway( + ).pure_return("SELECT true::BOOLEAN FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename = 'twitch_info'") if not twitch_info_exists: # Does not exist query_string = """ @@ -167,8 +174,8 @@ def generate_schema(): db_gateway().pure_query(query_string) # Does the twitter_info table exist? - twitter_info_exists = db_gateway().pure_return( - "SELECT true::BOOLEAN FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename = 'twitter_info'") + twitter_info_exists = db_gateway( + ).pure_return("SELECT true::BOOLEAN FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename = 'twitter_info'") if not twitter_info_exists: # Does not exist query_string = """ @@ -194,7 +201,8 @@ def generate_schema(): # Does the music_channels_info table exist? music_channels_info_exists = db_gateway().pure_return( - "SELECT true::BOOLEAN FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename = 'music_channels'") + "SELECT true::BOOLEAN FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename = 'music_channels'" + ) if not music_channels_info_exists: query_string = """ CREATE TABLE public.music_channels( diff --git a/src/esportsbot/lib/client.py b/src/esportsbot/lib/client.py index a351c656..c53b7af7 100644 --- a/src/esportsbot/lib/client.py +++ b/src/esportsbot/lib/client.py @@ -18,6 +18,7 @@ # Type alias to be used for user facing strings. Allows for multi-level tables. StringTable = MutableMapping[str, Union[str, "StringTable"]] + class EsportsBot(commands.Bot): """A discord.commands.Bot subclass, adding a dictionary of active reaction menus. @@ -31,7 +32,6 @@ class EsportsBot(commands.Bot): The second level is another dict-like object, mapping string names to parameter-formattable user-facing strings. :vartype STRINGS: str """ - def __init__(self, command_prefix: str, unknownCommandEmoji: Emote, userStringsFile: str, **options): """ :param str command_prefix: The prefix to use for bot commands when evoking from discord. @@ -44,8 +44,8 @@ def __init__(self, command_prefix: str, unknownCommandEmoji: Emote, userStringsF self.STRINGS: StringTable = toml.load(userStringsFile) self.MUSIC_CHANNELS = {} - signal.signal(signal.SIGINT, self.interruptReceived) # keyboard interrupt - signal.signal(signal.SIGTERM, self.interruptReceived) # graceful exit request + signal.signal(signal.SIGINT, self.interruptReceived) # keyboard interrupt + signal.signal(signal.SIGTERM, self.interruptReceived) # graceful exit request def update_music_channels(self): self.MUSIC_CHANNELS = {} @@ -64,14 +64,12 @@ def interruptReceived(self, signum: signal.Signals, frame: FrameType): print("[EsportsBot] Interrupt received.") asyncio.ensure_future(self.shutdown()) - async def shutdown(self): """Shut down the bot gracefully. """ print("[EsportsBot] Shutting down...") await self.logout() - async def rolePingCooldown(self, role: Role, cooldownSeconds: int): """wait cooldownSeconds seconds, then set role back to pingable. role must be registered in the pingable_roles table. @@ -87,7 +85,6 @@ async def rolePingCooldown(self, role: Role, cooldownSeconds: int): if role.guild.get_role(role.id) is not None: await role.edit(mentionable=True, colour=roleData[0]["colour"], reason="role ping cooldown complete") - @tasks.loop(hours=24) async def monthlyPingablesReport(self): """Send a report to all joined servers, summarising the number of times each !pingme @@ -96,8 +93,10 @@ async def monthlyPingablesReport(self): """ if datetime.now().day == 1: loggingTasks = set() - baseEmbed = Embed(title="Monthly !pingme Report", - description="The number of times each !pingme role was pinged last month:") + baseEmbed = Embed( + title="Monthly !pingme Report", + description="The number of times each !pingme role was pinged last month:" + ) baseEmbed.colour = Colour.random() baseEmbed.set_thumbnail(url=self.user.avatar_url_as(size=128)) baseEmbed.set_footer(text=datetime.now().strftime("%m/%d/%Y")) @@ -107,7 +106,10 @@ async def monthlyPingablesReport(self): if pingableRoles: guild = self.get_guild(guildData["guild_id"]) if guild is None: - print("[Esportsbot.monthlyPingablesReport] Unknown guild id in guild_info table: #" + str(guildData["guild_id"])) + print( + "[Esportsbot.monthlyPingablesReport] Unknown guild id in guild_info table: #" + + str(guildData["guild_id"]) + ) elif guildData["log_channel_id"] is not None: reportEmbed = baseEmbed.copy() rolesAdded = False @@ -118,18 +120,22 @@ async def monthlyPingablesReport(self): + str(roleData["role_id"]) + " in guild #" + str(guildData["guild_id"])) db.delete("pingable_roles", {"role_id": roleData["role_id"]}) else: - reportEmbed.add_field(name=role.name, value=role.mention + "\n" + str(roleData["monthly_ping_count"]) + " pings") + reportEmbed.add_field( + name=role.name, + value=role.mention + "\n" + str(roleData["monthly_ping_count"]) + " pings" + ) db.update("pingable_roles", {"monthly_ping_count": 0}, {"role_id": role.id}) rolesAdded = True if rolesAdded: - loggingTasks.add(asyncio.create_task(guild.get_channel(guildData['log_channel_id']).send(embed=reportEmbed))) + loggingTasks.add( + asyncio.create_task(guild.get_channel(guildData['log_channel_id']).send(embed=reportEmbed)) + ) if loggingTasks: await asyncio.wait(loggingTasks) for task in loggingTasks: if e := task.exception(): exceptions.print_exception_trace(e) - async def init(self): """Load in all of the reaction menus registered in SQL, and also restarts all pingme role cooldown tasks @@ -142,19 +148,25 @@ async def init(self): try: menusData = db.getall('reaction_menus') except Exception as e: - print("failed to load menus from SQL",e) + print("failed to load menus from SQL", e) raise e for menuData in menusData: msgID, menuDict = menuData['message_id'], menuData['menu'] if 'type' in menuDict: if reactionMenu.isSaveableMenuTypeName(menuDict['type']): try: - self.reactionMenus.add(reactionMenu.saveableMenuClassFromName(menuDict['type']).fromDict(self, menuDict)) + self.reactionMenus.add( + reactionMenu.saveableMenuClassFromName(menuDict['type']).fromDict(self, + menuDict) + ) except UnrecognisedReactionMenuMessage: - print("Unrecognised message for " + menuDict['type'] + ", removing from the database: " + str(menuDict["msg"])) + print( + "Unrecognised message for " + menuDict['type'] + ", removing from the database: " + + str(menuDict["msg"]) + ) db.delete('reaction_menus', where_params={'message_id': msgID}) else: - print("Non saveable menu in database:",msgID,menuDict["type"]) + print("Non saveable menu in database:", msgID, menuDict["type"]) else: print("no type for menu " + str(msgID)) @@ -177,7 +189,10 @@ async def init(self): + str(roleData["role_id"]) + " in guild #" + str(guildData["guild_id"])) db.delete("pingable_roles", {"role_id": roleData["role_id"]}) else: - remainingCooldown = max(0, int((datetime.fromtimestamp(roleData["last_ping"]) + guildPingCooldown - now).total_seconds())) + remainingCooldown = max( + 0, + int((datetime.fromtimestamp(roleData["last_ping"]) + guildPingCooldown - now).total_seconds()) + ) roleUpdateTasks.add(asyncio.create_task(self.rolePingCooldown(role, remainingCooldown))) if not self.monthlyPingablesReport.is_running(): @@ -190,7 +205,6 @@ async def init(self): if e := task.exception(): exceptions.print_exception_trace(e) - async def adminLog(self, message: Message, actions: Dict[str, str], *args, guildID=None, **kwargs): """Log an event or series of events to the server's admin logging channel. To log an administration action which was not due to a user command, give message as None, and specify the guild in @@ -212,7 +226,12 @@ async def adminLog(self, message: Message, actions: Dict[str, str], *args, guild if message is None: logEmbed = Embed(description="Responsible user unknown. Check the server's audit log.") else: - logEmbed = Embed(description=" | ".join((message.author.mention, "#" + message.channel.name, "[message](" + message.jump_url + ")"))) + logEmbed = Embed( + description=" | ". + join((message.author.mention, + "#" + message.channel.name, + "[message](" + message.jump_url + ")")) + ) logEmbed.set_author(icon_url=self.user.avatar_url_as(size=64), name="Admin Log") logEmbed.set_footer(text=datetime.now().strftime("%m/%d/%Y, %H:%M:%S")) logEmbed.colour = Colour.random() @@ -221,7 +240,6 @@ async def adminLog(self, message: Message, actions: Dict[str, str], *args, guild kwargs["embed"] = logEmbed await self.get_channel(db_logging_call[0]['log_channel_id']).send(*args, **kwargs) - def handleRoleMentions(self, message: Message) -> Set[asyncio.Task]: """Handle !pingme behaviour for the given message. Places mentioned roles on cooldown if they are not already on cooldown. @@ -238,19 +256,42 @@ def handleRoleMentions(self, message: Message) -> Set[asyncio.Task]: for role in message.role_mentions: roleData = db.get('pingable_roles', params={'role_id': role.id}) if roleData and not roleData[0]["on_cooldown"]: - roleUpdateTasks.add(asyncio.create_task(role.edit(mentionable=False, colour=Colour.darker_grey(), reason="placing pingable role on ping cooldown"))) + roleUpdateTasks.add( + asyncio.create_task( + role.edit( + mentionable=False, + colour=Colour.darker_grey(), + reason="placing pingable role on ping cooldown" + ) + ) + ) db.update('pingable_roles', {'on_cooldown': True}, {'role_id': role.id}) db.update('pingable_roles', {"last_ping": datetime.now().timestamp()}, {'role_id': role.id}) db.update('pingable_roles', {"ping_count": roleData[0]["ping_count"] + 1}, {'role_id': role.id}) - db.update('pingable_roles', {"monthly_ping_count": roleData[0]["monthly_ping_count"] + 1}, {'role_id': role.id}) - roleUpdateTasks.add(asyncio.create_task(self.rolePingCooldown(role, guildInfo[0]["role_ping_cooldown_seconds"]))) - roleUpdateTasks.add(asyncio.create_task(self.adminLog(message, {"!pingme Role Pinged": "Role: " + role.mention + "\nUser: " + message.author.mention}))) + db.update( + 'pingable_roles', + {"monthly_ping_count": roleData[0]["monthly_ping_count"] + 1}, + {'role_id': role.id} + ) + roleUpdateTasks.add( + asyncio.create_task(self.rolePingCooldown(role, + guildInfo[0]["role_ping_cooldown_seconds"])) + ) + roleUpdateTasks.add( + asyncio.create_task( + self.adminLog( + message, + {"!pingme Role Pinged": "Role: " + role.mention + "\nUser: " + message.author.mention} + ) + ) + ) return roleUpdateTasks # Singular class instance of EsportsBot _instance: EsportsBot = None + def instance() -> EsportsBot: """Get the singular instance of the discord client. EsportsBot is singular to allow for global client instance references outside of coges, e.g emoji validation in lib diff --git a/src/esportsbot/lib/discordUtil.py b/src/esportsbot/lib/discordUtil.py index 580fbd91..49877217 100644 --- a/src/esportsbot/lib/discordUtil.py +++ b/src/esportsbot/lib/discordUtil.py @@ -2,12 +2,15 @@ from typing import Tuple, Union from . import emotes, exceptions - # Link to an empty image, to allow for an author name in embeds without providing an icon. EMPTY_IMAGE = "https://i.imgur.com/sym17F7.png" -async def reactionFromRaw(client: Client, payload: RawReactionActionEvent) -> Tuple[Message, Union[User, Member], emotes.Emote]: +async def reactionFromRaw(client: Client, + payload: RawReactionActionEvent) -> Tuple[Message, + Union[User, + Member], + emotes.Emote]: """Retrieve complete Reaction and user info from a RawReactionActionEvent payload. :param RawReactionActionEvent payload: Payload describing the reaction action diff --git a/src/esportsbot/lib/emotes.py b/src/esportsbot/lib/emotes.py index 4bb3c329..e3ecec02 100644 --- a/src/esportsbot/lib/emotes.py +++ b/src/esportsbot/lib/emotes.py @@ -14,7 +14,6 @@ if TYPE_CHECKING: from discord import PartialEmoji, Emoji - # Emoji to send in place of unrecognised emojis, when forced to. This should be used as a last resort as it its ambiguous. err_UnknownEmoji = "❓" # True to raise an UnrecognisedCustomEmoji exception when requesting an unknown custom emoji @@ -106,10 +105,12 @@ def __init__(self, id: int = -1, unicode: str = "", rejectInvalid: bool = False) if logUnknownEmojis: print("Unrecognised custom emoji ID in Emote constructor: " + str(self.id)) if raiseUnkownEmojis or rejectInvalid: - raise exceptions.UnrecognisedCustomEmoji("Unrecognised custom emoji ID in Emote constructor: " + str(self.id), self.id) + raise exceptions.UnrecognisedCustomEmoji( + "Unrecognised custom emoji ID in Emote constructor: " + str(self.id), + self.id + ) self.sendable = err_UnknownEmoji - def toDict(self, **kwargs) -> dict: """Serialize this emoji to dictionary format for saving to file. @@ -120,7 +121,6 @@ def toDict(self, **kwargs) -> dict: return {"unicode": self.unicode} return {"id": self.id} - def __repr__(self) -> str: """Get a string uniquely identifying this object, specifying what type of emoji it represents and the emoji itself. @@ -129,7 +129,6 @@ def __repr__(self) -> str: """ return "<Emote-" + ("id" if self.isID else "unicode") + ":" + (str(self.id) if self.isID else self.unicode) + ">" - def __hash__(self) -> int: """Calculate a hash of this emoji, based on its repr string. Two Emote objects representing the same emoji will have the same repr and hash. @@ -139,7 +138,6 @@ def __hash__(self) -> int: """ return hash(repr(self)) - def __eq__(self, other: Emote) -> bool: """Decide if this Emote is equal to another. Two Emotes are equal if they represent the same emoji (i.e ID/unicode) of the same type (custom/unicode) @@ -150,7 +148,6 @@ def __eq__(self, other: Emote) -> bool: """ return isinstance(other, Emote) and self.sendable == other.sendable - def __str__(self) -> str: """Get the object's 'sendable' string. @@ -159,7 +156,6 @@ def __str__(self) -> str: """ return self.sendable - @classmethod def fromDict(cls, emojiDict: dict, **kwargs) -> Emote: """Construct a Emote object from its dictionary representation. @@ -185,7 +181,6 @@ def fromDict(cls, emojiDict: dict, **kwargs) -> Emote: else: return Emote(unicode=emojiDict["unicode"], rejectInvalid=rejectInvalid) - @classmethod def fromPartial(cls, e: PartialEmoji, rejectInvalid: bool = False) -> Emote: """Construct a new Emote object from a given discord.PartialEmoji. @@ -204,7 +199,6 @@ def fromPartial(cls, e: PartialEmoji, rejectInvalid: bool = False) -> Emote: else: return Emote(id=e.id, rejectInvalid=rejectInvalid) - @classmethod def fromReaction(cls, e: Union[Emoji, PartialEmoji, str], rejectInvalid: bool = False) -> Emote: """Construct a new Emote object from a given discord.PartialEmoji, discord.Emoji, or string. @@ -232,7 +226,6 @@ def fromReaction(cls, e: Union[Emoji, PartialEmoji, str], rejectInvalid: bool = else: return Emote(id=e.id, rejectInvalid=rejectInvalid) - @classmethod def fromStr(cls, s: str, rejectInvalid: bool = False) -> Emote: """Construct a Emote object from a string containing either a unicode emoji or a discord custom emoji. diff --git a/src/esportsbot/lib/exceptions.py b/src/esportsbot/lib/exceptions.py index 15bbc22c..9afa96ec 100644 --- a/src/esportsbot/lib/exceptions.py +++ b/src/esportsbot/lib/exceptions.py @@ -7,13 +7,13 @@ import traceback + class UnrecognisedCustomEmoji(Exception): """Exception raised when creating an Emote instance, but the client could not match an emoji to the given ID. :var id: The ID that could not be matched :vartype id: int """ - def __init__(self, comment: str, id: int): """ :param str comment: Description of the exception @@ -29,7 +29,6 @@ class InvalidStringEmoji(Exception): :var val: The string that could not be matched :vartype val: str """ - def __init__(self, comment: str, val: str): """ :param str comment: Description of the exception @@ -43,7 +42,6 @@ class UnrecognisedReactionMenuMessage(Exception): """Exception to indicate that a reaction menu failed to initialize as its message could not be fetched from discord. This could be for a number of reasons, for example a change in permissions, or the message was deleted. """ - def __init__(self, guild: int, channel: int, msg: int): """ :param int guild: The id of the guild containing the requested message @@ -53,7 +51,10 @@ def __init__(self, guild: int, channel: int, msg: int): self.guild = guild self.channel = channel self.msg = msg - super().__init__("Failed to fetch message for reaction menu: guild " + str(guild) + " channel " + str(channel) + " message " + str(msg)) + super().__init__( + "Failed to fetch message for reaction menu: guild " + str(guild) + " channel " + str(channel) + " message " + + str(msg) + ) def print_exception_trace(e: Exception): diff --git a/src/esportsbot/lib/stringTyping.py b/src/esportsbot/lib/stringTyping.py index 63fec109..a6794c04 100644 --- a/src/esportsbot/lib/stringTyping.py +++ b/src/esportsbot/lib/stringTyping.py @@ -5,6 +5,7 @@ .. codeauthor:: Trimatix """ + def strIsInt(x) -> bool: """Decide whether or not something is either an integer, or is castable to integer. diff --git a/src/esportsbot/lib/timeUtil.py b/src/esportsbot/lib/timeUtil.py index 466d0650..61492232 100644 --- a/src/esportsbot/lib/timeUtil.py +++ b/src/esportsbot/lib/timeUtil.py @@ -20,12 +20,7 @@ def td_format_noYM(td_object: timedelta) -> str: :rtype: str """ seconds = int(td_object.total_seconds()) - periods = [ - ('day', 60 * 60 * 24), - ('hour', 60 * 60), - ('minute', 60), - ('second', 1) - ] + periods = [('day', 60 * 60 * 24), ('hour', 60 * 60), ('minute', 60), ('second', 1)] strings = [] for period_name, period_seconds in periods: @@ -46,10 +41,12 @@ def timeDeltaFromDict(timeDict: Dict[str, int]) -> timedelta: :return: a timedelta with all of the attributes requested in the dictionary. :rtype: datetime.timedelta """ - return timedelta(weeks=timeDict["weeks"] if "weeks" in timeDict else 0, - days=timeDict["days"] if "days" in timeDict else 0, - hours=timeDict["hours"] if "hours" in timeDict else 0, - minutes=timeDict["minutes"] if "minutes" in timeDict else 0, - seconds=timeDict["seconds"] if "seconds" in timeDict else 0, - microseconds=timeDict["microseconds"] if "microseconds" in timeDict else 0, - milliseconds=timeDict["milliseconds"] if "milliseconds" in timeDict else 0) + return timedelta( + weeks=timeDict["weeks"] if "weeks" in timeDict else 0, + days=timeDict["days"] if "days" in timeDict else 0, + hours=timeDict["hours"] if "hours" in timeDict else 0, + minutes=timeDict["minutes"] if "minutes" in timeDict else 0, + seconds=timeDict["seconds"] if "seconds" in timeDict else 0, + microseconds=timeDict["microseconds"] if "microseconds" in timeDict else 0, + milliseconds=timeDict["milliseconds"] if "milliseconds" in timeDict else 0 + ) diff --git a/src/esportsbot/reactionMenus/reactionMenu.py b/src/esportsbot/reactionMenus/reactionMenu.py index 2d637651..f73827f3 100644 --- a/src/esportsbot/reactionMenus/reactionMenu.py +++ b/src/esportsbot/reactionMenus/reactionMenu.py @@ -28,7 +28,6 @@ async def deleteReactionMenu(menu: "ReactionMenu"): menu.client.reactionMenus.remove(menu) - class ReactionMenuOption: """An abstract class representing an option in a reaction menu. Reaction menu options must have a name and emoji. They may optionally have a function to call when added, @@ -60,9 +59,15 @@ class ReactionMenuOption: :var removeHasArgs: Whether removeFunc takes arguments, and removeArgs should be attempt to be passed :vartype removeHasArgs: bool """ - - def __init__(self, name: str, emoji: "lib.emotes.Emote", addFunc: FunctionType = None, addArgs: Any = None, - removeFunc: FunctionType = None, removeArgs: Any = None): + def __init__( + self, + name: str, + emoji: "lib.emotes.Emote", + addFunc: FunctionType = None, + addArgs: Any = None, + removeFunc: FunctionType = None, + removeArgs: Any = None + ): """ :param str name: The name of this option, as displayed in the menu embed. :param lib.emotes.Emote emoji: The emoji that a user must react with to trigger this option @@ -80,16 +85,15 @@ def __init__(self, name: str, emoji: "lib.emotes.Emote", addFunc: FunctionType = self.addArgs = addArgs self.addIsCoroutine = addFunc is not None and inspect.iscoroutinefunction(addFunc) self.addIncludeUser = addFunc is not None and 'reactingUser' in inspect.signature(addFunc).parameters - self.addHasArgs = addFunc is not None and len(inspect.signature( - addFunc).parameters) != (1 if self.addIncludeUser else 0) + self.addHasArgs = addFunc is not None and len(inspect.signature(addFunc).parameters + ) != (1 if self.addIncludeUser else 0) self.removeFunc = removeFunc self.removeArgs = removeArgs self.removeIsCoroutine = removeFunc is not None and inspect.iscoroutinefunction(removeFunc) self.removeIncludeUser = removeFunc is not None and 'reactingUser' in inspect.signature(addFunc).parameters - self.removeHasArgs = removeFunc is not None and len(inspect.signature( - removeFunc).parameters) != (1 if self.removeIncludeUser else 0) - + self.removeHasArgs = removeFunc is not None and len(inspect.signature(removeFunc).parameters + ) != (1 if self.removeIncludeUser else 0) async def add(self, member: Union[Member, User]) -> Any: """Invoke this option's 'reaction added' functionality. @@ -110,7 +114,6 @@ async def add(self, member: Union[Member, User]) -> Any: return await self.addFunc(self.addArgs) if self.addIsCoroutine else self.addFunc(self.addArgs) return await self.addFunc() if self.addIsCoroutine else self.addFunc() - async def remove(self, member: Union[Member, User]) -> Any: """Invoke this option's 'reaction removed' functionality. This method is called by the owning reaction menu whenever this option is removed by any user @@ -130,7 +133,6 @@ async def remove(self, member: Union[Member, User]) -> Any: return await self.removeFunc(self.removeArgs) if self.removeIsCoroutine else self.removeFunc(self.removeArgs) return await self.removeFunc() if self.removeIsCoroutine else self.removeFunc() - def __hash__(self) -> int: """Calculate a hash of this menu option from its repr string. As of writing, this is based on the object's memory location. @@ -140,7 +142,6 @@ def __hash__(self) -> int: """ return hash(repr(self)) - @abstractmethod def toDict(self, **kwargs) -> dict: """Serialize this menu option into dictionary format for saving to file. @@ -166,7 +167,6 @@ def toDict(self, **kwargs) -> dict: """ return {"name": self.name, "emoji": self.emoji.toDict(**kwargs)} - @classmethod @abstractmethod def fromDict(cls, data: dict, **kwargs) -> "ReactionMenuOption": @@ -186,9 +186,15 @@ class NonSaveableReactionMenuOption(ReactionMenuOption): When creating a ReactionMenuOption subclass that can be saved to file, do not inherit from this class. Instead, inherit directly from ReactionMenuOption or another suitable subclass that is not marked as unsaveable. """ - - def __init__(self, name: str, emoji: "lib.emotes.Emote", addFunc: FunctionType = None, addArgs: Any = None, - removeFunc: FunctionType = None, removeArgs: Any = None): + def __init__( + self, + name: str, + emoji: "lib.emotes.Emote", + addFunc: FunctionType = None, + addArgs: Any = None, + removeFunc: FunctionType = None, + removeArgs: Any = None + ): """ :param str name: The name of this option, as displayed in the menu embed. :param lib.emotes.Emote emoji: The emoji that a user must react with to trigger this option @@ -198,9 +204,13 @@ def __init__(self, name: str, emoji: "lib.emotes.Emote", addFunc: FunctionType = but a dict is recommended as a close replacement for keyword args. :param removeArgs: The arguments to pass to removeFunc. """ - super(NonSaveableReactionMenuOption, self).__init__(name, emoji, addFunc=addFunc, addArgs=addArgs, - removeFunc=removeFunc, removeArgs=removeArgs) - + super(NonSaveableReactionMenuOption, + self).__init__(name, + emoji, + addFunc=addFunc, + addArgs=addArgs, + removeFunc=removeFunc, + removeArgs=removeArgs) def toDict(self, **kwargs) -> dict: """Unimplemented. @@ -210,7 +220,6 @@ def toDict(self, **kwargs) -> dict: """ raise NotImplementedError("Attempted to call toDict on a non-saveable reaction menu option") - @classmethod def fromDict(cls, data: dict, **kwargs): """fromDict is not defined for NonSaveableReactionMenuOption. @@ -225,7 +234,6 @@ class DummyReactionMenuOption(ReactionMenuOption): """A reaction menu option with no function calls. A prime example is ReactionPollMenu, where adding and removing options need not have any functionality. """ - def __init__(self, name: str, emoji: "lib.emotes.Emote"): """ :param str name: The name of this option, as displayed in the menu embed. @@ -233,7 +241,6 @@ def __init__(self, name: str, emoji: "lib.emotes.Emote"): """ super(DummyReactionMenuOption, self).__init__(name, emoji) - def toDict(self, **kwargs) -> dict: """Serialize this menu option into dictionary format for saving to file. Since dummy reaction menu options have no on-toggle functionality, the resulting base dictionary contains @@ -244,7 +251,6 @@ def toDict(self, **kwargs) -> dict: """ return super(DummyReactionMenuOption, self).toDict(**kwargs) - def fromDict(cls, data: dict, **kwargs) -> "DummyReactionMenuOption": """Deserialize a dictionary representing a DummyReactionMenuOption into a functioning object. @@ -314,11 +320,23 @@ class ReactionMenu: :var targetRole: In order to interact with this menu, users must possess this role. All other reactions are ignored :vartype targetRole: discord.Role """ - - def __init__(self, msg: Message, client: Client, options: Dict["lib.emotes.Emote", ReactionMenuOption] = None, - titleTxt: str = "", desc: str = "", col: Colour = Colour.blue(), - footerTxt: str = "", img: str = "", thumb: str = "", icon: str = None, - authorName: str = "", targetMember: Member = None, targetRole: Role = None): + def __init__( + self, + msg: Message, + client: Client, + options: Dict["lib.emotes.Emote", + ReactionMenuOption] = None, + titleTxt: str = "", + desc: str = "", + col: Colour = Colour.blue(), + footerTxt: str = "", + img: str = "", + thumb: str = "", + icon: str = None, + authorName: str = "", + targetMember: Member = None, + targetRole: Role = None + ): """ :param discord.Message msg: the message where this menu is embedded :param discord.Client client: The client that instanced this menu @@ -355,7 +373,6 @@ def __init__(self, msg: Message, client: Client, options: Dict["lib.emotes.Emote self.targetMember = targetMember self.targetRole = targetRole - def hasEmojiRegistered(self, emoji: "lib.emotes.Emote") -> bool: """Decide whether or not the given emoji is an option in this menu @@ -365,7 +382,6 @@ def hasEmojiRegistered(self, emoji: "lib.emotes.Emote") -> bool: """ return emoji in self.options - async def reactionAdded(self, emoji: "lib.emotes.Emote", member: Union[Member, User]): """Invoke an option's behaviour when it is selected by a user. This method should be called during your discord client's on_reaction_add or on_raw_reaction_add event. @@ -390,7 +406,6 @@ async def reactionAdded(self, emoji: "lib.emotes.Emote", member: Union[Member, U return await self.options[emoji].add(member) - async def reactionRemoved(self, emoji: "lib.emotes.Emote", member: Union[Member, User]): """Invoke an option's behaviour when it is deselected by a user. This method should be called during your discord client's on_reaction_remove or on_raw_reaction_remove event. @@ -415,7 +430,6 @@ async def reactionRemoved(self, emoji: "lib.emotes.Emote", member: Union[Member, return await self.options[emoji].remove(member) - def getMenuEmbed(self) -> Embed: """Generate the discord.Embed representing the reaction menu, and that should be embedded into the menu's message. @@ -438,7 +452,6 @@ def getMenuEmbed(self) -> Embed: return menuEmbed - async def updateMessage(self, noRefreshOptions: bool = False): """Update the menu message by removing all reactions, replacing any existing embed with up to date embed content, and re-add all of the menu's option reactions. @@ -462,13 +475,11 @@ async def updateMessage(self, noRefreshOptions: bool = False): for option in self.options: await self.msg.add_reaction(option.sendable) - async def delete(self): """Forcibly delete the menu and its message. """ await deleteReactionMenu(self) - def toDict(self, **kwargs) -> dict: """Serialize this ReactionMenu into dictionary format for saving to file. This is a base, concrete implementation that saves all information required to recreate a ReactionMenu instance; @@ -482,8 +493,13 @@ def toDict(self, **kwargs) -> dict: for reaction in self.options: optionsDict[reaction.sendable] = self.options[reaction].toDict(**kwargs) - data = {"channel": self.msg.channel.id, "msg": self.msg.id, "options": optionsDict, - "type": self.__class__.__name__, "guild": self.msg.channel.guild.id} + data = { + "channel": self.msg.channel.id, + "msg": self.msg.id, + "options": optionsDict, + "type": self.__class__.__name__, + "guild": self.msg.channel.guild.id + } if self.titleTxt != "": data["titleTxt"] = self.titleTxt @@ -517,7 +533,6 @@ def toDict(self, **kwargs) -> dict: return data - @classmethod @abstractmethod def fromDict(cls, data: dict, **kwargs) -> "ReactionMenu": @@ -547,12 +562,25 @@ class InlineReactionMenu(ReactionMenu): :var timeoutSeconds: The number of seconds that this menu should last before timing out :vartype timeoutSeconds: int """ - - def __init__(self, client: Client, msg: Message, targetMember: Union[Member, User], timeoutSeconds: int, - options: Dict["lib.emotes.Emote", ReactionMenuOption] = None, - returnTriggers: List[ReactionMenuOption] = [], titleTxt: str = "", desc: str = "", - col: Colour = Colour.blue(), footerTxt: str = "", img: str = "", thumb: str = "", - icon: str = None, authorName: str = ""): + def __init__( + self, + client: Client, + msg: Message, + targetMember: Union[Member, + User], + timeoutSeconds: int, + options: Dict["lib.emotes.Emote", + ReactionMenuOption] = None, + returnTriggers: List[ReactionMenuOption] = [], + titleTxt: str = "", + desc: str = "", + col: Colour = Colour.blue(), + footerTxt: str = "", + img: str = "", + thumb: str = "", + icon: str = None, + authorName: str = "" + ): """ :param returnTriggers: A list of options which, when selected, trigger the expiry of the menu. :type returnTriggers: List[ReactionMenuOption] @@ -560,12 +588,23 @@ def __init__(self, client: Client, msg: Message, targetMember: Union[Member, Use """ if footerTxt == "": footerTxt = "This menu will expire in " + str(timeoutSeconds) + " seconds." - super().__init__(msg, client, targetMember=targetMember, options=options, titleTxt=titleTxt, desc=desc, col=col, - footerTxt=footerTxt, img=img, thumb=thumb, icon=icon, authorName=authorName) + super().__init__( + msg, + client, + targetMember=targetMember, + options=options, + titleTxt=titleTxt, + desc=desc, + col=col, + footerTxt=footerTxt, + img=img, + thumb=thumb, + icon=icon, + authorName=authorName + ) self.returnTriggers = returnTriggers self.timeoutSeconds = timeoutSeconds - def reactionClosesMenu(self, reactPL: RawReactionActionEvent) -> bool: """Decide whether a reaction should trigger the expiry of the menu. The reaction should be given in the form of a RawReactionActionEvent payload, from a discord.on_raw_reaction_add event @@ -580,7 +619,6 @@ def reactionClosesMenu(self, reactPL: RawReactionActionEvent) -> bool: except lib.exceptions.UnrecognisedCustomEmoji: return False - async def doMenu(self) -> List["lib.emotes.Emote"]: """Coroutine that executes the menu. @@ -593,8 +631,11 @@ async def doMenu(self) -> List["lib.emotes.Emote"]: """ await self.updateMessage() try: - await lib.client.instance().wait_for("raw_reaction_add", - check=self.reactionClosesMenu, timeout=self.timeoutSeconds) + await lib.client.instance().wait_for( + "raw_reaction_add", + check=self.reactionClosesMenu, + timeout=self.timeoutSeconds + ) self.msg.embeds[0].set_footer(text="This menu has now expired.") await self.msg.edit(embed=self.msg.embeds[0]) except asyncio.TimeoutError: @@ -646,6 +687,7 @@ def isSaveableMenuClass(cls: type) -> bool: """ return issubclass(cls, ReactionMenu) and cls in _saveableNameMenuTypes + def isSaveableMenuInstance(o: ReactionMenu) -> bool: """Decide if o is an instance of a saveable reaction menu class. @@ -655,6 +697,7 @@ def isSaveableMenuInstance(o: ReactionMenu) -> bool: """ return isinstance(o, ReactionMenu) and type(o) in _saveableMenuTypeNames + def isSaveableMenuTypeName(clsName: str) -> bool: """Decide if clsName is the name of a saveable reaction menu class. @@ -664,6 +707,7 @@ def isSaveableMenuTypeName(clsName: str) -> bool: """ return clsName in _saveableNameMenuTypes + def saveableMenuClassFromName(clsName: str) -> type: """Retreive the saveable ReactionMenu subclass that as the given class name. clsName must correspond to a ReactionMenu subclass that has been registered as saveble with the saveableMenu decorator. diff --git a/src/esportsbot/reactionMenus/reactionMenuDB.py b/src/esportsbot/reactionMenus/reactionMenuDB.py index 49946afa..6551efe3 100644 --- a/src/esportsbot/reactionMenus/reactionMenuDB.py +++ b/src/esportsbot/reactionMenus/reactionMenuDB.py @@ -24,11 +24,9 @@ class ReactionMenuDB(dict): :var initializing: Whether or not the DB has been initialized yet """ - def __init__(self): self.initializing = True - def __contains__(self, menu: Union[ReactionMenu, int]) -> bool: """decide whether a menu or menu ID is registered in the database. Overrides 'if x in db:' @@ -46,7 +44,6 @@ def __contains__(self, menu: Union[ReactionMenu, int]) -> bool: else: raise TypeError("ReactionMenuDB can only contain ReactionMenus, given type " + type(menu).__name__) - def __getitem__(self, k: int) -> ReactionMenu: """Get the registered ReactionMenu instance for the given menu ID. Overrides getting through 'db[id]' @@ -58,7 +55,6 @@ def __getitem__(self, k: int) -> ReactionMenu: """ return super().__getitem__(k) - def __setitem__(self, menuID: int, menu: ReactionMenu) -> None: """Registers the given menu into the database. Overrides setting through 'db[id] = menu' @@ -69,16 +65,24 @@ def __setitem__(self, menuID: int, menu: ReactionMenu) -> None: :raise KeyError: If a menu with the given ID is already registered """ if menuID != menu.msg.id: - raise ValueError("Attempted to register a menu with key " + str(menuID) + ", but the message ID for the given menu is " + str(menu.msg.id)) - + raise ValueError( + "Attempted to register a menu with key " + str(menuID) + ", but the message ID for the given menu is " + + str(menu.msg.id) + ) + if menu.msg.id in self: raise KeyError("A menu is already registered with the given ID: " + str(menu.msg.id)) super().__setitem__(menuID, menu) if not self.initializing and isSaveableMenuInstance(menu): - db_gateway().insert('reaction_menus', params={'message_id': menu.msg.id, 'menu': str(Json(menu.toDict())).lstrip("'").rstrip("'")}) - + db_gateway().insert( + 'reaction_menus', + params={ + 'message_id': menu.msg.id, + 'menu': str(Json(menu.toDict())).lstrip("'").rstrip("'") + } + ) def __delitem__(self, menu: Union[ReactionMenu, int]) -> None: """Unregisters the given menu or menu ID from the database. @@ -101,7 +105,6 @@ def __delitem__(self, menu: Union[ReactionMenu, int]) -> None: if isSaveableMenuInstance(menu): db_gateway().delete('reaction_menus', where_params={'message_id': menu.msg.id}) - def add(self, menu: ReactionMenu): """Register a ReactionMenu with the database, and save to SQL. @@ -110,7 +113,6 @@ def add(self, menu: ReactionMenu): """ self[menu.msg.id] = menu - def remove(self, menu: ReactionMenu): """Unregister the given menu, preventing menu interaction through reactions. @@ -119,7 +121,6 @@ def remove(self, menu: ReactionMenu): """ del self[menu.msg.id] - def removeID(self, menuID: int): """Unregister menu with the given ID, preventing menu interaction through reactions. @@ -130,7 +131,6 @@ def removeID(self, menuID: int): raise KeyError("No menu is registered with the given ID: " + str(menuID)) self.remove(self[menuID]) - def updateDB(self, menu: ReactionMenu): """Update the database's record for the given menu, for example when changing the content of a menu. @@ -140,4 +140,8 @@ def updateDB(self, menu: ReactionMenu): if menu.msg.id not in self: raise KeyError("The given menu is not registered: " + str(menu.msg.id)) if isSaveableMenuInstance(menu): - db_gateway().update('reaction_menus', set_params={'menu': str(Json(menu.toDict())).lstrip("'").rstrip("'")}, where_params={'message_id': menu.msg.id}) + db_gateway().update( + 'reaction_menus', + set_params={'menu': str(Json(menu.toDict())).lstrip("'").rstrip("'")}, + where_params={'message_id': menu.msg.id} + ) diff --git a/src/esportsbot/reactionMenus/reactionPollMenu.py b/src/esportsbot/reactionMenus/reactionPollMenu.py index d0470346..2a9b151d 100644 --- a/src/esportsbot/reactionMenus/reactionPollMenu.py +++ b/src/esportsbot/reactionMenus/reactionPollMenu.py @@ -10,7 +10,6 @@ from discord import Colour, Message, Embed, User, Member, RawReactionActionEvent from typing import Dict, Union - # Used as the default author icon in poll embeds BALLOT_BOX_IMAGE = "https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/120/twitter/259/ballot-box-with-ballot_1f5f3.png" @@ -60,7 +59,7 @@ async def showPollResults(menu: InlineReactionPollMenu): # Reject custom emojis that are not accessible to the bot except lib.exceptions.UnrecognisedCustomEmoji: continue - + # Validate emotes if currentEmoji is None: print("[reactionPollMenu.showPollResults] Failed to fetch Emote for reaction: " + str(reaction)) @@ -76,11 +75,11 @@ async def showPollResults(menu: InlineReactionPollMenu): if currentOption.emoji == currentEmoji: menuOption = currentOption break - + # Ignore reactions which do not correspond to poll options if menuOption is None: continue - + # Collate votes for this poll option async for user in reaction.users(): if user != client.user: @@ -95,7 +94,7 @@ async def showPollResults(menu: InlineReactionPollMenu): # Record this user's vote if validVote and user not in results[menuOption]: results[menuOption].append(user) - + # Mark the poll as expired pollEmbed = menu.msg.embeds[0] pollEmbed.set_footer(text="This poll has ended.") @@ -123,7 +122,7 @@ async def showPollResults(menu: InlineReactionPollMenu): for reaction in menu.msg.reactions: await reaction.remove(menu.msg.guild.me) - + class InlineReactionPollMenu(reactionMenu.InlineReactionMenu): """A non-saveable inline reaction menu taking a vote from its participants on a selection of option strings. @@ -135,11 +134,23 @@ class InlineReactionPollMenu(reactionMenu.InlineReactionMenu): vote per poll. :vartype multipleChoice: bool """ - - def __init__(self, msg: Message, pollOptions: Dict[lib.emotes.Emote: str], timeoutSeconds: int, - pollStarter: Union[User, Member] = None, multipleChoice: bool = False, titleTxt: str = "", - desc: str = "", col: Colour = Colour.blue(), footerTxt: str = "", - img: str = "", thumb: str = "", icon: str = None, authorName: str = ""): + def __init__( + self, + msg: Message, + pollOptions: Dict[lib.emotes.Emote:str], + timeoutSeconds: int, + pollStarter: Union[User, + Member] = None, + multipleChoice: bool = False, + titleTxt: str = "", + desc: str = "", + col: Colour = Colour.blue(), + footerTxt: str = "", + img: str = "", + thumb: str = "", + icon: str = None, + authorName: str = "" + ): """ :param discord.Message msg: the message where this menu is embedded :param pollOptions: A dictionary of Emote: str, defining all of the poll options @@ -180,10 +191,21 @@ def __init__(self, msg: Message, pollOptions: Dict[lib.emotes.Emote: str], timeo pollOptions = {e: reactionMenu.DummyReactionMenuOption(n, e) for e, n in pollOptions.items()} - super().__init__(lib.client.instance(), msg, pollStarter, timeoutSeconds, - options=pollOptions, titleTxt=titleTxt, desc=desc, col=col, footerTxt=footerTxt, img=img, - thumb=thumb, icon=icon, authorName=authorName) - + super().__init__( + lib.client.instance(), + msg, + pollStarter, + timeoutSeconds, + options=pollOptions, + titleTxt=titleTxt, + desc=desc, + col=col, + footerTxt=footerTxt, + img=img, + thumb=thumb, + icon=icon, + authorName=authorName + ) def getMenuEmbed(self) -> Embed: """Generate the discord.Embed representing the reaction menu, and that should be embedded into the menu's message. @@ -195,11 +217,17 @@ def getMenuEmbed(self) -> Embed: """ baseEmbed = super().getMenuEmbed() if self.multipleChoice: - baseEmbed.add_field(name="This is a multiple choice poll!", value="Voting for more than one option is allowed.", - inline=False) + baseEmbed.add_field( + name="This is a multiple choice poll!", + value="Voting for more than one option is allowed.", + inline=False + ) else: - baseEmbed.add_field(name="This is a single choice poll!", - value="If you vote for more than one option, only one will be counted.", inline=False) + baseEmbed.add_field( + name="This is a single choice poll!", + value="If you vote for more than one option, only one will be counted.", + inline=False + ) return baseEmbed @@ -212,11 +240,22 @@ class InlineSingleOptionPollMenu(reactionMenu.InlineReactionMenu): Votes are not counted through an option's addFunc, but instead through the reactionClosesMenu override, for efficiency. """ - - def __init__(self, msg: Message, timeoutSeconds: int, requiredVotes: int, - pollStarter: Union[User, Member] = None, titleTxt: str = "", desc: str = "", - col: Colour = Colour.blue(), footerTxt: str = "", img: str = "", thumb: str = "", - icon: str = None, authorName: str = ""): + def __init__( + self, + msg: Message, + timeoutSeconds: int, + requiredVotes: int, + pollStarter: Union[User, + Member] = None, + titleTxt: str = "", + desc: str = "", + col: Colour = Colour.blue(), + footerTxt: str = "", + img: str = "", + thumb: str = "", + icon: str = None, + authorName: str = "" + ): """ :param discord.Message msg: the message where this menu is embedded :param int timeoutSeconds: The number of seconds until the poll ends @@ -255,10 +294,21 @@ def __init__(self, msg: Message, timeoutSeconds: int, requiredVotes: int, self.requiredVotes = requiredVotes pollOptions = {self.yesOption.emoji: self.yesOption} - super().__init__(lib.client.instance(), msg, pollStarter, timeoutSeconds, - options=pollOptions, titleTxt=titleTxt, desc=desc, col=col, footerTxt=footerTxt, img=img, - thumb=thumb, icon=icon, authorName=authorName) - + super().__init__( + lib.client.instance(), + msg, + pollStarter, + timeoutSeconds, + options=pollOptions, + titleTxt=titleTxt, + desc=desc, + col=col, + footerTxt=footerTxt, + img=img, + thumb=thumb, + icon=icon, + authorName=authorName + ) def reactionClosesMenu(self, reactPL: RawReactionActionEvent) -> bool: """An InlineReactionMenu override which checks the number of yes votes received. @@ -273,6 +323,6 @@ def reactionClosesMenu(self, reactPL: RawReactionActionEvent) -> bool: self.yesesReceived += 1 return self.yesesReceived >= self.requiredVotes return False - + except lib.exceptions.UnrecognisedCustomEmoji: return False diff --git a/src/esportsbot/reactionMenus/reactionRoleMenu.py b/src/esportsbot/reactionMenus/reactionRoleMenu.py index 107f870f..f6a489f4 100644 --- a/src/esportsbot/reactionMenus/reactionRoleMenu.py +++ b/src/esportsbot/reactionMenus/reactionRoleMenu.py @@ -56,15 +56,25 @@ class ReactionRoleMenuOption(reactionMenu.ReactionMenuOption): :var role: The role to toggle on reactions :vartype role: discord.Role """ - def __init__(self, emoji: lib.emotes.Emote, role: Role, menu: reactionMenu.ReactionMenu): """ :param lib.emotes.Emote emoji: The emoji to react to the menu with to trigger role updates :param Role role: The role to (un)assign reacting users """ self.role = role - super(ReactionRoleMenuOption, self).__init__(self.role.name, emoji, addFunc=giveRole, addArgs=(menu.msg.guild, self.role, menu.msg.id), removeFunc=removeRole, removeArgs=(menu.msg.guild, self.role, menu.msg.id)) - + super(ReactionRoleMenuOption, + self).__init__( + self.role.name, + emoji, + addFunc=giveRole, + addArgs=(menu.msg.guild, + self.role, + menu.msg.id), + removeFunc=removeRole, + removeArgs=(menu.msg.guild, + self.role, + menu.msg.id) + ) def toDict(self) -> dict: """Serialize the option into dictionary format for saving. @@ -75,7 +85,6 @@ def toDict(self) -> dict: """ return {"role": self.role.id} - @classmethod def fromDict(self, data: dict, **kwargs) -> "ReactionRoleMenuOption": """Deserialize a dictionary representation of a ReactionRoleMenuOption into a functioning object. @@ -92,7 +101,11 @@ def fromDict(self, data: dict, **kwargs) -> "ReactionRoleMenuOption": params = {"dcGuild": Guild, "emoji": lib.emotes.Emote, "menu": reactionMenu.ReactionMenu} for oName, oType in params.items(): if oName not in kwargs: raise NameError("Missing required kwarg: " + oName) - if not isinstance(kwargs[oName], oType): raise TypeError("Expected type " + oType.__name__ + " for parameter " + oName + ", received " + type(kwargs[oName]).__name__) + if not isinstance(kwargs[oName], oType): + raise TypeError( + "Expected type " + oType.__name__ + " for parameter " + oName + ", received " + + type(kwargs[oName]).__name__ + ) return ReactionRoleMenuOption(kwargs["emoji"], kwargs["dcGuild"].get_role(data["role"]), kwargs["menu"]) @@ -100,11 +113,23 @@ def fromDict(self, data: dict, **kwargs) -> "ReactionRoleMenuOption": class ReactionRoleMenu(reactionMenu.ReactionMenu): """A saveable reaction menu that grants and removes roles when interacted with. """ - - def __init__(self, msg: Message, client: Client, reactionRoles: Dict[lib.emotes.Emote, Role], - titleTxt: str = "", desc: str = "", col: Colour = None, - footerTxt: str = "", img: str = "", thumb: str = "", icon: str = "", authorName: str = "", - targetMember: Member = None, targetRole: Role = None): + def __init__( + self, + msg: Message, + client: Client, + reactionRoles: Dict[lib.emotes.Emote, + Role], + titleTxt: str = "", + desc: str = "", + col: Colour = None, + footerTxt: str = "", + img: str = "", + thumb: str = "", + icon: str = "", + authorName: str = "", + targetMember: Member = None, + targetRole: Role = None + ): """ :param discord.Message msg: the message where this menu is embedded :param discord.Client client: The client that instanced this menu @@ -131,8 +156,22 @@ def __init__(self, msg: Message, client: Client, reactionRoles: Dict[lib.emotes. if desc == "": desc = "React for your desired role!" - super(ReactionRoleMenu, self).__init__(msg, client, options=roleOptions, titleTxt=titleTxt, desc=desc, col=col if col is not None else Colour.blue(), footerTxt=footerTxt, img=img, thumb=thumb, icon=icon, authorName=authorName, targetMember=targetMember, targetRole=targetRole) - + super(ReactionRoleMenu, + self).__init__( + msg, + client, + options=roleOptions, + titleTxt=titleTxt, + desc=desc, + col=col if col is not None else Colour.blue(), + footerTxt=footerTxt, + img=img, + thumb=thumb, + icon=icon, + authorName=authorName, + targetMember=targetMember, + targetRole=targetRole + ) def toDict(self) -> dict: """Serialize this menu to dictionary format for saving to file. @@ -145,7 +184,6 @@ def toDict(self) -> dict: baseDict["guild"] = self.msg.guild.id return baseDict - @classmethod def fromDict(csl, client: Client, rmDict: dict) -> "ReactionRoleMenu": """Reconstruct a ReactionRolePicker from its dictionary-serialized representation. @@ -165,15 +203,20 @@ def fromDict(csl, client: Client, rmDict: dict) -> "ReactionRoleMenu": for reaction in rmDict["options"]: reactionRoles[lib.emotes.Emote.fromStr(reaction)] = dcGuild.get_role(rmDict["options"][reaction]["role"]) - - return ReactionRoleMenu(msg, client, reactionRoles, - titleTxt=rmDict["titleTxt"] if "titleTxt" in rmDict else "", - desc=rmDict["desc"] if "desc" in rmDict else "", - col=Colour.from_rgb(rmDict["col"][0], rmDict["col"][1], rmDict["col"][2]) if "col" in rmDict else Colour.blue(), - footerTxt=rmDict["footerTxt"] if "footerTxt" in rmDict else "", - img=rmDict["img"] if "img" in rmDict else "", - thumb=rmDict["thumb"] if "thumb" in rmDict else "", - icon=rmDict["icon"] if "icon" in rmDict else "", - authorName=rmDict["authorName"] if "authorName" in rmDict else "", - targetMember=dcGuild.get_member(rmDict["targetMember"]) if "targetMember" in rmDict else None, - targetRole=dcGuild.get_role(rmDict["targetRole"]) if "targetRole" in rmDict else None) + return ReactionRoleMenu( + msg, + client, + reactionRoles, + titleTxt=rmDict["titleTxt"] if "titleTxt" in rmDict else "", + desc=rmDict["desc"] if "desc" in rmDict else "", + col=Colour.from_rgb(rmDict["col"][0], + rmDict["col"][1], + rmDict["col"][2]) if "col" in rmDict else Colour.blue(), + footerTxt=rmDict["footerTxt"] if "footerTxt" in rmDict else "", + img=rmDict["img"] if "img" in rmDict else "", + thumb=rmDict["thumb"] if "thumb" in rmDict else "", + icon=rmDict["icon"] if "icon" in rmDict else "", + authorName=rmDict["authorName"] if "authorName" in rmDict else "", + targetMember=dcGuild.get_member(rmDict["targetMember"]) if "targetMember" in rmDict else None, + targetRole=dcGuild.get_role(rmDict["targetRole"]) if "targetRole" in rmDict else None + ) diff --git a/src/main.py b/src/main.py index 05ab1fbc..ba2f6f4a 100644 --- a/src/main.py +++ b/src/main.py @@ -1,2 +1,3 @@ from esportsbot import bot + bot.launch()