diff --git a/bot/core/module.py b/bot/core/module.py index e46f9875..daaa4bc7 100644 --- a/bot/core/module.py +++ b/bot/core/module.py @@ -46,11 +46,12 @@ async def preload_guild_configuration(client): Loads the plain guild configuration for all guilds the client is part of into data. """ guildids = [guild.id for guild in client.guilds] - rows = client.data.guild_config.fetch_rows_where(guildid=guildids) - client.log( - "Preloaded guild configuration for {} guilds.".format(len(rows)), - context="CORE_LOADING" - ) + if guildids: + rows = client.data.guild_config.fetch_rows_where(guildid=guildids) + client.log( + "Preloaded guild configuration for {} guilds.".format(len(rows)), + context="CORE_LOADING" + ) @module.launch_task @@ -59,11 +60,12 @@ async def preload_studying_members(client): Loads the member data for all members who are currently in voice channels. """ userids = list(set(member.id for guild in client.guilds for ch in guild.voice_channels for member in ch.members)) - rows = client.data.lions.fetch_rows_where(userid=userids) - client.log( - "Preloaded member data for {} members.".format(len(rows)), - context="CORE_LOADING" - ) + if userids: + rows = client.data.lions.fetch_rows_where(userid=userids) + client.log( + "Preloaded member data for {} members.".format(len(rows)), + context="CORE_LOADING" + ) @module.launch_task diff --git a/bot/modules/accountability/TimeSlot.py b/bot/modules/accountability/TimeSlot.py index 8fc178f3..b87a0df3 100644 --- a/bot/modules/accountability/TimeSlot.py +++ b/bot/modules/accountability/TimeSlot.py @@ -72,6 +72,9 @@ class TimeSlot: connect=False ) + happy_lion = "https://media.discordapp.net/stickers/898266283559227422.png" + sad_lion = "https://media.discordapp.net/stickers/898266548421148723.png" + def __init__(self, guild, start_time, data=None): self.guild: discord.Guild = guild self.start_time: datetime.datetime = start_time @@ -139,13 +142,16 @@ class TimeSlot: else: classifications["Attended"].append(mention) + all_attended = all(mem.has_attended for mem in self.members.values()) bonus_line = ( - "{tick} All members attended, and will get a `{bonus} LC` completion bonus!".format( + "{tick} Everyone attended, and will get a `{bonus} LC` bonus!".format( tick=tick, bonus=GuildSettings(self.guild.id).accountability_bonus.value ) - if all(mem.has_attended for mem in self.members.values()) else "" + if all_attended else "" ) + if all_attended: + embed.set_thumbnail(url=self.happy_lion) embed.description += "\n" + bonus_line for field, value in classifications.items(): @@ -182,16 +188,22 @@ class TimeSlot: else: classifications["Missing"].append(mention) + all_attended = all(mem.has_attended for mem in self.members.values()) + bonus_line = ( - "{tick} All members attended, and received a `{bonus} LC` completion bonus!".format( + "{tick} Everyone attended, and received a `{bonus} LC` bonus!".format( tick=tick, bonus=GuildSettings(self.guild.id).accountability_bonus.value ) - if all(mem.has_attended for mem in self.members.values()) else + if all_attended else "{cross} Some members missed the session, so everyone missed out on the bonus!".format( cross=cross ) ) + if all_attended: + embed.set_thumbnail(url=self.happy_lion) + else: + embed.set_thumbnail(url=self.sad_lion) embed.description += "\n" + bonus_line for field, value in classifications.items(): @@ -292,7 +304,7 @@ class TimeSlot: self.message = await self.lobby.send( embed=self.open_embed ) - except discord.HTTPException as e: + except discord.HTTPException: GuildSettings(self.guild.id).event_log.log( "Failed to post the status message in the accountability lobby {}.\n" "Skipping this session.".format(self.lobby.mention), @@ -322,10 +334,21 @@ class TimeSlot: Update the status message, and launch the DM reminder. """ if self.channel: - await self.channel.edit(name="Accountability Study Room") - await self.channel.set_permissions(self.guild.default_role, view_channel=True, connect=False) + try: + await self.channel.edit(name="Accountability Study Room") + await self.channel.set_permissions(self.guild.default_role, view_channel=True, connect=False) + except discord.HTTPException: + pass asyncio.create_task(self.dm_reminder(delay=60)) - await self.message.edit(embed=self.status_embed) + try: + await self.message.edit(embed=self.status_embed) + except discord.NotFound: + try: + self.message = await self.lobby.send( + embed=self.status_embed + ) + except discord.HTTPException: + self.message = None async def dm_reminder(self, delay=60): """ diff --git a/bot/modules/accountability/tracker.py b/bot/modules/accountability/tracker.py index e1136b39..51713230 100644 --- a/bot/modules/accountability/tracker.py +++ b/bot/modules/accountability/tracker.py @@ -232,7 +232,8 @@ async def turnover(): # Start all the current rooms await asyncio.gather( - *(slot.start() for slot in current_slots) + *(slot.start() for slot in current_slots), + return_exceptions=True ) diff --git a/bot/modules/moderation/tickets/Ticket.py b/bot/modules/moderation/tickets/Ticket.py index 3193a185..4d7ec5ec 100644 --- a/bot/modules/moderation/tickets/Ticket.py +++ b/bot/modules/moderation/tickets/Ticket.py @@ -353,9 +353,10 @@ class Ticket: Method used to revert the ticket action, e.g. unban or remove mute role. Generally called by `pardon` and `_expire`. - Must be overriden by the Ticket type, if they implement any revert logic. + May be overriden by the Ticket type, if they implement any revert logic. + Is a no-op by default. """ - raise NotImplementedError + return async def _expire(self): """ diff --git a/bot/modules/moderation/video/admin.py b/bot/modules/moderation/video/admin.py index fd75573b..1f0ddaab 100644 --- a/bot/modules/moderation/video/admin.py +++ b/bot/modules/moderation/video/admin.py @@ -87,9 +87,9 @@ class video_studyban(settings.Boolean, GuildSetting): @property def success_response(self): if self.value: - "Members will now be study banned if they don't enable their video in the configured video channels." + return "Members will now be study-banned if they don't enable their video in the configured video channels." else: - "Members will not be studybanned if they don't enable their video in video channels." + return "Members will not be study-banned if they don't enable their video in video channels." @GuildSettings.attach_setting diff --git a/bot/modules/study/studybadge_cmd.py b/bot/modules/study/studybadge_cmd.py index b43dae4b..99e28fec 100644 --- a/bot/modules/study/studybadge_cmd.py +++ b/bot/modules/study/studybadge_cmd.py @@ -265,6 +265,13 @@ async def cmd_studybadges(ctx, flags): # Parse the input lines = ctx.args.splitlines() results = [await parse_level(ctx, line) for line in lines] + # Check for duplicates + _set = set() + duplicate = next((time for time, _ in results if time in _set or _set.add(time)), None) + if duplicate: + return await ctx.error_reply( + "Level `{}` provided twice!".format(strfdur(duplicate, short=False)) + ) current_times = set(row.required_time for row in guild_roles) # Split up the provided lines into levels to add and levels to edit diff --git a/bot/settings/setting_types.py b/bot/settings/setting_types.py index 89b1b0a6..25a63faa 100644 --- a/bot/settings/setting_types.py +++ b/bot/settings/setting_types.py @@ -1,6 +1,5 @@ import json import asyncio -import datetime import itertools from io import StringIO from enum import IntEnum @@ -12,7 +11,7 @@ from cmdClient.Context import Context from cmdClient.lib import SafeCancellation from meta import client -from utils.lib import parse_dur, strfdur, strfdelta, prop_tabulate, multiple_replace +from utils.lib import parse_dur, strfdur, prop_tabulate, multiple_replace from .base import UserInputError @@ -100,7 +99,7 @@ class Boolean(SettingType): Looks up the provided string in the truthy and falsey tables. """ _userstr = userstr.lower() - if _userstr == "none": + if not _userstr or _userstr == "none": return None if _userstr in cls._truthy: return True @@ -154,7 +153,7 @@ class Integer(SettingType): """ Relies on integer casting to convert the user string """ - if userstr.lower() == "none": + if not userstr or userstr.lower() == "none": return None try: @@ -222,7 +221,7 @@ class String(SettingType): Check that the user-entered string is of the correct length. Accept "None" to unset. """ - if userstr.lower() == "none": + if not userstr or userstr.lower() == "none": # Unsetting case return None elif cls._maxlen is not None and len(userstr) > cls._maxlen: @@ -284,7 +283,7 @@ class Channel(SettingType): Pass to the channel seeker utility to find the requested channel. Handle `0` and variants of `None` to unset. """ - if userstr.lower() in ('0', 'none'): + if userstr.lower() in ('', '0', 'none'): return None else: channel = await ctx.find_channel(userstr, interactive=True, chan_type=cls._chan_type) @@ -296,13 +295,17 @@ class Channel(SettingType): @classmethod def _format_data(cls, id: int, data: Optional[int], **kwargs): """ - Retrieve an artificially created channel mention. - If the channel does not exist, this will show up as invalid-channel. + Retrieve the channel mention, if the channel still exists. + If the channel no longer exists, or cannot be seen by the client, returns None. """ if data is None: return None else: - return "<#{}>".format(data) + channel = client.get_channel(data) + if channel: + return channel.mention + else: + return None class VoiceChannel(Channel): @@ -375,7 +378,7 @@ class Role(SettingType): Pass to the role seeker utility to find the requested role. Handle `0` and variants of `None` to unset. """ - if userstr.lower() in ('0', 'none'): + if userstr.lower() in ('', '0', 'none'): return None else: role = await ctx.find_role(userstr, create=cls._parse_create, interactive=True) @@ -452,7 +455,7 @@ class Emoji(SettingType): Pass to the emoji string parser to get the emoji. Handle `0` and variants of `None` to unset. """ - if userstr.lower() in ('0', 'none'): + if userstr.lower() in ('', '0', 'none'): return None else: return cls._parse_emoji(userstr) @@ -505,7 +508,7 @@ class Timezone(SettingType): Check that the user-entered string is of the correct length. Accept "None" to unset. """ - if userstr.lower() == "none": + if not userstr or userstr.lower() == "none": # Unsetting case return None try: @@ -585,7 +588,7 @@ class IntegerEnum(SettingType): options = {name.lower(): mem.value for name, mem in cls._enum.__members__.items()} - if userstr == "none": + if not userstr or userstr == "none": # Unsetting case return None elif userstr not in options: @@ -654,7 +657,7 @@ class Duration(SettingType): """ Parse the provided duration. """ - if userstr.lower() == "none": + if not userstr or userstr.lower() == "none": return None if cls._default_multiplier and userstr.isdigit(): @@ -949,12 +952,13 @@ class SettingList(SettingType): Splits the user string across `,` to break up the list. Handle `0` and variants of `None` to unset. """ - if userstr.lower() in ('0', 'none'): + if userstr.lower() in ('', '0', 'none'): return [] else: data = [] - for item in userstr.split(','): - data.append(await cls._setting._parse_userstr(ctx, id, item.strip())) + items = (item.strip() for item in userstr.split(',')) + items = (item for item in items if item) + data = [await cls._setting._parse_userstr(ctx, id, item, **kwargs) for item in items] if cls._force_unique: data = list(set(data))