diff --git a/bot/LionModule.py b/bot/LionModule.py index 2d6fb854..c1fd7256 100644 --- a/bot/LionModule.py +++ b/bot/LionModule.py @@ -13,7 +13,7 @@ class LionCommand(Command): """ Subclass to allow easy attachment of custom hooks and structure to commands. """ - ... + allow_before_ready = False class LionModule(Module): @@ -72,25 +72,38 @@ class LionModule(Module): """ Lion pre-command hook. """ + if not self.ready and not ctx.cmd.allow_before_ready: + try: + await ctx.embed_reply( + "I am currently restarting! Please try again in a couple of minutes." + ) + except discord.HTTPException: + pass + raise SafeCancellation(details="Module '{}' is not ready.".format(self.name)) + + # Check that the channel and guild still exists + if not ctx.client.get_guild(ctx.guild.id) or not ctx.guild.get_channel(ctx.ch.id): + raise SafeCancellation(details='Command channel is no longer reachable.') + # Check global user blacklist if ctx.author.id in ctx.client.objects['blacklisted_users']: - raise SafeCancellation + raise SafeCancellation(details='User is blacklisted.') if ctx.guild: # Check global guild blacklist if ctx.guild.id in ctx.client.objects['blacklisted_guilds']: - raise SafeCancellation + raise SafeCancellation(details='Guild is blacklisted.') # Check guild's own member blacklist if ctx.author.id in ctx.client.objects['ignored_members'][ctx.guild.id]: - raise SafeCancellation + raise SafeCancellation(details='User is ignored in this guild.') # Check channel permissions are sane if not ctx.ch.permissions_for(ctx.guild.me).send_messages: - raise SafeCancellation + raise SafeCancellation(details='I cannot send messages in this channel.') if not ctx.ch.permissions_for(ctx.guild.me).embed_links: await ctx.reply("I need permission to send embeds in this channel before I can run any commands!") - raise SafeCancellation + raise SafeCancellation(details='I cannot send embeds in this channel.') # Start typing await ctx.ch.trigger_typing() diff --git a/bot/cmdClient b/bot/cmdClient index 75410acc..6eb42690 160000 --- a/bot/cmdClient +++ b/bot/cmdClient @@ -1 +1 @@ -Subproject commit 75410acc120b456ff315ff468357b6fb22a0a406 +Subproject commit 6eb426903423d6be8439621eb0b906aa94957efd diff --git a/bot/core/data.py b/bot/core/data.py index c33914d7..992e9ab5 100644 --- a/bot/core/data.py +++ b/bot/core/data.py @@ -53,13 +53,19 @@ def add_pending(pending): guild_config = RowTable( 'guild_config', - ('guildid', 'admin_role', 'mod_role', 'event_log_channel', 'alert_channel', + ('guildid', 'admin_role', 'mod_role', 'event_log_channel', 'mod_log_channel', 'alert_channel', + 'studyban_role', 'min_workout_length', 'workout_reward', 'max_tasks', 'task_reward', 'task_reward_limit', 'study_hourly_reward', 'study_hourly_live_bonus', - 'study_ban_role', 'max_study_bans'), + 'renting_price', 'renting_category', 'renting_cap', 'renting_role', 'renting_sync_perms', + 'accountability_category', 'accountability_lobby', 'accountability_bonus', + 'accountability_reward', 'accountability_price', + 'video_studyban', 'video_grace_period', + 'greeting_channel', 'greeting_message', 'returning_message', + 'starting_funds', 'persist_roles'), 'guildid', - cache=TTLCache(1000, ttl=60*5) + cache=TTLCache(2500, ttl=60*5) ) unranked_roles = Table('unranked_roles') @@ -72,6 +78,7 @@ lions = RowTable( ('guildid', 'userid', 'tracked_time', 'coins', 'workout_count', 'last_workout_start', + 'revision_mute_count', 'last_study_badgeid', 'video_warned', '_timestamp' diff --git a/bot/modules/accountability/TimeSlot.py b/bot/modules/accountability/TimeSlot.py index ac12efea..9464e4e5 100644 --- a/bot/modules/accountability/TimeSlot.py +++ b/bot/modules/accountability/TimeSlot.py @@ -69,7 +69,8 @@ class TimeSlot: _everyone_overwrite = discord.PermissionOverwrite( view_channel=False, - connect=False + connect=False, + speak=False ) happy_lion = "https://media.discordapp.net/stickers/898266283559227422.png" @@ -218,6 +219,9 @@ class TimeSlot: """ Load data and update applicable caches. """ + if not self.guild: + return self + # Load setting data self.category = GuildSettings(self.guild.id).accountability_category.value self.lobby = GuildSettings(self.guild.id).accountability_lobby.value @@ -389,13 +393,14 @@ class TimeSlot: pass # Reward members appropriately - guild_settings = GuildSettings(self.guild.id) - reward = guild_settings.accountability_reward.value - if all(mem.has_attended for mem in self.members.values()): - reward += guild_settings.accountability_bonus.value + if self.guild: + guild_settings = GuildSettings(self.guild.id) + reward = guild_settings.accountability_reward.value + if all(mem.has_attended for mem in self.members.values()): + reward += guild_settings.accountability_bonus.value - for memid in self.members: - Lion.fetch(self.guild.id, memid).addCoins(reward) + for memid in self.members: + Lion.fetch(self.guild.id, memid).addCoins(reward) async def cancel(self): """ diff --git a/bot/modules/accountability/tracker.py b/bot/modules/accountability/tracker.py index 51713230..24e1dc94 100644 --- a/bot/modules/accountability/tracker.py +++ b/bot/modules/accountability/tracker.py @@ -374,14 +374,15 @@ async def _accountability_system_resume(): None, mow.slotid, mow.userid) for mow in slot_members[row.slotid] if mow.last_joined_at ) - slot = TimeSlot(client.get_guild(row.guildid), row.start_at, data=row).load( - memberids=[mow.userid for mow in slot_members[row.slotid]] - ) + if client.get_guild(row.guildid): + slot = TimeSlot(client.get_guild(row.guildid), row.start_at, data=row).load( + memberids=[mow.userid for mow in slot_members[row.slotid]] + ) + try: + await slot.close() + except discord.HTTPException: + pass row.closed_at = now - try: - await slot.close() - except discord.HTTPException: - pass # Load the in-progress room data if current_room_data: @@ -451,7 +452,7 @@ async def launch_accountability_system(client): guilds = tables.guild_config.fetch_rows_where( accountability_category=NOTNULL ) - [AccountabilityGuild(guild.guildid) for guild in guilds] + [AccountabilityGuild(guild.guildid) for guild in guilds if client.get_guild(guild.guildid)] await _accountability_system_resume() asyncio.create_task(_accountability_loop()) diff --git a/bot/modules/guild_admin/reaction_roles/command.py b/bot/modules/guild_admin/reaction_roles/command.py index 61e4a1ed..0f915ffa 100644 --- a/bot/modules/guild_admin/reaction_roles/command.py +++ b/bot/modules/guild_admin/reaction_roles/command.py @@ -485,7 +485,7 @@ async def cmd_reactionroles(ctx, flags): await ctx.error_reply( "The provided channel no longer exists!" ) - elif channel.type != discord.ChannelType.text: + elif not isinstance(channel, discord.TextChannel): await ctx.error_reply( "The provided channel is not a text channel!" ) @@ -821,8 +821,8 @@ async def cmd_reactionroles(ctx, flags): setting = await setting_class.parse(target.messageid, ctx, flags[flag]) except UserInputError as e: return await ctx.error_reply( - title="Couldn't save settings!", - description="{} {}\nNo settings were modified.".format(cross, e.msg) + "{} {}\nNo settings were modified.".format(cross, e.msg), + title="Couldn't save settings!" ) else: update_lines.append( @@ -861,8 +861,8 @@ async def cmd_reactionroles(ctx, flags): setting = await setting_class.parse(reaction.reactionid, ctx, flags[flag]) except UserInputError as e: return await ctx.error_reply( + "{} {}\nNo reaction roles were modified.".format(cross, e.msg), title="Couldn't save reaction role settings!", - description="{} {}\nNo reaction roles were modified.".format(cross, e.msg) ) else: update_lines.append( diff --git a/bot/modules/guild_admin/reaction_roles/settings.py b/bot/modules/guild_admin/reaction_roles/settings.py index b678b804..dbb4b8cf 100644 --- a/bot/modules/guild_admin/reaction_roles/settings.py +++ b/bot/modules/guild_admin/reaction_roles/settings.py @@ -199,6 +199,7 @@ class price(setting_types.Integer, ReactionSetting): ) accepts = "An integer number of coins. Use `0` to make the role free, or `None` to use the message default." + _max = 2 ** 20 @property def default(self): diff --git a/bot/modules/guild_admin/reaction_roles/tracker.py b/bot/modules/guild_admin/reaction_roles/tracker.py index 6d130263..f18e3c34 100644 --- a/bot/modules/guild_admin/reaction_roles/tracker.py +++ b/bot/modules/guild_admin/reaction_roles/tracker.py @@ -272,7 +272,7 @@ class ReactionRoleMessage: # Fetch the number of applicable roles the user has roleids = set(reaction.data.roleid for reaction in self.reactions) member_roleids = set(role.id for role in member.roles) - if len(roleids.intersection(member_roleids)) > maximum: + if len(roleids.intersection(member_roleids)) >= maximum: # Notify the user embed = discord.Embed( title="Maximum group roles reached!", diff --git a/bot/modules/renting/rooms.py b/bot/modules/renting/rooms.py index c83adc29..9e79d5b0 100644 --- a/bot/modules/renting/rooms.py +++ b/bot/modules/renting/rooms.py @@ -178,6 +178,8 @@ class Room: """ Expire the room. """ + guild_settings = GuildSettings(self.data.guildid) + if self.channel: # Delete the discord channel try: @@ -188,7 +190,6 @@ class Room: # Delete the room from data (cascades to member deletion) self.delete() - guild_settings = GuildSettings(self.data.guildid) guild_settings.event_log.log( title="Private study room expired!", description="<@{}>'s private study room expired.".format(self.data.ownerid) diff --git a/bot/modules/study/stats_cmd.py b/bot/modules/study/stats_cmd.py index 90768202..73604658 100644 --- a/bot/modules/study/stats_cmd.py +++ b/bot/modules/study/stats_cmd.py @@ -12,7 +12,8 @@ from .module import module @module.cmd( "stats", group="Statistics", - desc="View a summary of your study statistics!" + desc="View a summary of your study statistics!", + allow_before_ready=True ) @in_guild() async def cmd_stats(ctx): diff --git a/bot/utils/ctx_addons.py b/bot/utils/ctx_addons.py index 931422d9..9697eeec 100644 --- a/bot/utils/ctx_addons.py +++ b/bot/utils/ctx_addons.py @@ -26,21 +26,22 @@ async def embed_reply(ctx, desc, colour=discord.Colour.orange(), **kwargs): @Context.util -async def error_reply(ctx, error_str, **kwargs): +async def error_reply(ctx, error_str, send_args={}, **kwargs): """ Notify the user of a user level error. Typically, this will occur in a red embed, posted in the command channel. """ embed = discord.Embed( colour=discord.Colour.red(), - description=error_str + description=error_str, + **kwargs ) message = None try: message = await ctx.ch.send( embed=embed, reference=ctx.msg.to_reference(fail_if_not_exists=False), - **kwargs + **send_args ) ctx.sent_messages.append(message) return message diff --git a/bot/utils/seekers.py b/bot/utils/seekers.py index 69bb1f46..7f3d49d3 100644 --- a/bot/utils/seekers.py +++ b/bot/utils/seekers.py @@ -182,7 +182,11 @@ async def find_channel(ctx, userstr, interactive=False, collection=None, chan_ty # Create the collection to search from args or guild channels collection = collection if collection else ctx.guild.channels if chan_type is not None: - collection = [chan for chan in collection if chan.type == chan_type] + if chan_type == discord.ChannelType.text: + # Hack to support news channels as text channels + collection = [chan for chan in collection if isinstance(chan, discord.TextChannel)] + else: + collection = [chan for chan in collection if chan.type == chan_type] # If the user input was a number or possible channel mention, extract it chanid = userstr.strip('<#@&!>') @@ -413,7 +417,7 @@ async def find_message(ctx, msgid, chlist=None, ignore=[]): async def _search_in_channel(channel: discord.TextChannel, msgid: int): - if channel.type != discord.ChannelType.text: + if not isinstance(channel, discord.TextChannel): return try: message = await channel.fetch_message(msgid)