diff options
| -rw-r--r-- | bot/cogs/error_handler.py | 31 | ||||
| -rw-r--r-- | bot/cogs/moderation/scheduler.py | 18 | ||||
| -rw-r--r-- | bot/cogs/tags.py | 38 | ||||
| -rw-r--r-- | bot/utils/time.py | 36 |
4 files changed, 80 insertions, 43 deletions
diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py index 52893b2ee..13221799b 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -1,4 +1,5 @@ -import contextlib +# import contextlib +import difflib import logging from discord.ext.commands import ( @@ -75,7 +76,7 @@ class ErrorHandler(Cog): if not ctx.channel.id == Channels.verification: tags_get_command = self.bot.get_command("tags get") ctx.invoked_from_error_handler = True - + command_name = ctx.invoked_with log_msg = "Cancelling attempt to fall back to a tag due to failed checks." try: if not await tags_get_command.can_run(ctx): @@ -87,9 +88,31 @@ class ErrorHandler(Cog): return # Return to not raise the exception - with contextlib.suppress(ResponseCodeError): - await ctx.invoke(tags_get_command, tag_name=ctx.invoked_with) + log.debug("Calling...") + tags_cog = self.bot.get_cog("Tags") + sent = await tags_cog._get_command(ctx, command_name) + # sent = await tags_get_command.callback(tags_get_command.cog, ctx, ctx.invoked_with) + if sent: + log.debug("Found") return + # No similar tag found, or tag on cooldown - + # searching for a similar command + log.debug("Not Found") + raw_commands = [ + (cmd.name, *cmd.aliases) + for cmd in self.bot.walk_commands() + if not cmd.hidden + ] + raw_commands = [c for data in raw_commands for c in data] + similar_command_data = difflib.get_close_matches(command_name, raw_commands, 1) + log.debug(similar_command_data) + similar_command = self.bot.get_command(similar_command_data[0]) + if similar_command.can_run(ctx): + misspelled_content = ctx.message.content + await ctx.send( + f"Did you mean:\n**{misspelled_content.replace(command_name, similar_command.name)}**" + ) + elif isinstance(e, BadArgument): await ctx.send(f"Bad argument: {e}\n") await ctx.invoke(*help_command) diff --git a/bot/cogs/moderation/scheduler.py b/bot/cogs/moderation/scheduler.py index e14c302cb..c0de0e4da 100644 --- a/bot/cogs/moderation/scheduler.py +++ b/bot/cogs/moderation/scheduler.py @@ -309,16 +309,23 @@ class InfractionScheduler(Scheduler): guild = self.bot.get_guild(constants.Guild.id) mod_role = guild.get_role(constants.Roles.moderator) user_id = infraction["user"] + actor = infraction["actor"] type_ = infraction["type"] id_ = infraction["id"] + inserted_at = infraction["inserted_at"] + expiry = infraction["expires_at"] log.info(f"Marking infraction #{id_} as inactive (expired).") + expiry = dateutil.parser.isoparse(expiry).replace(tzinfo=None) if expiry else None + created = time.format_infraction_with_duration(inserted_at, expiry) + log_content = None log_text = { - "Member": str(user_id), - "Actor": str(self.bot.user), - "Reason": infraction["reason"] + "Member": f"<@{user_id}>", + "Actor": str(self.bot.get_user(actor) or actor), + "Reason": infraction["reason"], + "Created": created, } try: @@ -384,14 +391,19 @@ class InfractionScheduler(Scheduler): if send_log: log_title = f"expiration failed" if "Failure" in log_text else "expired" + user = self.bot.get_user(user_id) + avatar = user.avatar_url_as(static_format="png") if user else None + log.trace(f"Sending deactivation mod log for infraction #{id_}.") await self.mod_log.send_log_message( icon_url=utils.INFRACTION_ICONS[type_][1], colour=Colours.soft_green, title=f"Infraction {log_title}: {type_}", + thumbnail=avatar, text="\n".join(f"{k}: {v}" for k, v in log_text.items()), footer=f"ID: {id_}", content=log_content, + ) return log_text diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py index 5f7c810b8..61cf55d65 100644 --- a/bot/cogs/tags.py +++ b/bot/cogs/tags.py @@ -12,8 +12,6 @@ from bot.converters import TagContentConverter, TagNameConverter from bot.decorators import with_role from bot.pagination import LinePaginator -from fuzzywuzzy import process - log = logging.getLogger(__name__) TEST_CHANNELS = ( @@ -94,9 +92,9 @@ class Tags(Cog): """Show all known tags, a single tag, or run a subcommand.""" await ctx.invoke(self.get_command, tag_name=tag_name) - @tags_group.command(name='get', aliases=('show', 'g')) - async def get_command(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None: - """Get a specified tag, or a list of all tags if no tag is specified.""" + async def _get_command(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None: + log.debug(self, ctx, tag_name) + def _command_on_cooldown(tag_name: str) -> bool: """ Check if the command is currently on cooldown, on a per-tag, per-channel basis. @@ -120,7 +118,7 @@ class Tags(Cog): time_left = Cooldowns.tags - (time.time() - self.tag_cooldowns[tag_name]["time"]) log.warning(f"{ctx.author} tried to get the '{tag_name}' tag, but the tag is on cooldown. " f"Cooldown ends in {time_left:.1f} seconds.") - return + return False await self._get_tags() @@ -135,28 +133,13 @@ class Tags(Cog): "channel": ctx.channel.id } await ctx.send(embed=Embed.from_dict(tag['embed'])) + return True elif founds and len(tag_name) >= 3: await ctx.send(embed=Embed( title='Did you mean ...', description='\n'.join(tag['title'] for tag in founds[:10]) )) - else: - # No similar tag found, searching for a similar command - command_name = ctx.invoked_with - raw_commands = [cmd.name for cmd in self.bot.walk_commands()] - similar_command_data = process.extractOne(command_name, raw_commands) - # The match is not very similar, no need to suggest it - log.debug(similar_command_data) - if similar_command_data[1] < 65: - log.debug("No similar commands found") - return - similar_command = self.bot.get_command(similar_command_data[0]) - if similar_command.can_run(ctx): - misspelled_content = ctx.message.content - await ctx.send( - f"Did you mean:\n**{misspelled_content.replace(command_name, similar_command.name)}**" - ) - return + return True else: tags = self._cache.values() @@ -165,6 +148,7 @@ class Tags(Cog): description="**There are no tags in the database!**", colour=Colour.red() )) + return True else: embed: Embed = Embed(title="**Current tags**") await LinePaginator.paginate( @@ -175,6 +159,14 @@ class Tags(Cog): empty=False, max_lines=15 ) + return True + + return False + + @tags_group.command(name='get', aliases=('show', 'g')) + async def get_command(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None: + """Get a specified tag, or a list of all tags if no tag is specified.""" + await self._get_command(ctx, tag_name) @tags_group.command(name='set', aliases=('add', 's')) @with_role(*MODERATION_ROLES) diff --git a/bot/utils/time.py b/bot/utils/time.py index 7416f36e0..77060143c 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -114,30 +114,40 @@ def format_infraction(timestamp: str) -> str: def format_infraction_with_duration( - expiry: Optional[str], + date_to: Optional[str], date_from: Optional[datetime.datetime] = None, - max_units: int = 2 + max_units: int = 2, + absolute: bool = True ) -> Optional[str]: """ - Format an infraction timestamp to a more readable ISO 8601 format WITH the duration. + Return `date_to` formatted as a readable ISO-8601 with the humanized duration since `date_from`. - Returns a human-readable version of the duration between datetime.utcnow() and an expiry. - Unlike `humanize_delta`, this function will force the `precision` to be `seconds` by not passing it. - `max_units` specifies the maximum number of units of time to include (e.g. 1 may include days but not hours). - By default, max_units is 2. + `date_from` must be an ISO-8601 formatted timestamp. The duration is calculated as from + `date_from` until `date_to` with a precision of seconds. If `date_from` is unspecified, the + current time is used. + + `max_units` specifies the maximum number of units of time to include in the duration. For + example, a value of 1 may include days but not hours. + + If `absolute` is True, the absolute value of the duration delta is used. This prevents negative + values in the case that `date_to` is in the past relative to `date_from`. """ - if not expiry: + if not date_to: return None + date_to_formatted = format_infraction(date_to) + date_from = date_from or datetime.datetime.utcnow() - date_to = dateutil.parser.isoparse(expiry).replace(tzinfo=None, microsecond=0) + date_to = dateutil.parser.isoparse(date_to).replace(tzinfo=None, microsecond=0) - expiry_formatted = format_infraction(expiry) + delta = relativedelta(date_to, date_from) + if absolute: + delta = abs(delta) - duration = humanize_delta(relativedelta(date_to, date_from), max_units=max_units) - duration_formatted = f" ({duration})" if duration else '' + duration = humanize_delta(delta, max_units=max_units) + duration_formatted = f" ({duration})" if duration else "" - return f"{expiry_formatted}{duration_formatted}" + return f"{date_to_formatted}{duration_formatted}" def until_expiration( |