diff options
author | 2021-06-30 21:20:31 +0200 | |
---|---|---|
committer | 2021-07-06 02:02:12 +0200 | |
commit | 48e989fe07019401eca6caa6d0eb3981f003eddd (patch) | |
tree | 0d366e833a042e11749d18fc7d4958a8f1b35ba3 | |
parent | Move cooldown handling to the Tag class (diff) |
Move tag listing to new design and move it outside of tag display method
The display method was renamed to get_tag_embed and now exclusively handles
embed for a tag/suggestions instead of holding the logic of the whole command
fixup! Move tag listing to new design and move it outside of tag display method
-rw-r--r-- | bot/exts/info/tags.py | 170 |
1 files changed, 97 insertions, 73 deletions
diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 1665275b9..4aa590430 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -1,14 +1,15 @@ from __future__ import annotations +import enum import logging import re import time from pathlib import Path -from typing import Callable, Iterable, List, NamedTuple, Optional +from typing import Callable, Iterable, List, Literal, NamedTuple, Optional, Union import discord import frontmatter -from discord import Colour, Embed, Member +from discord import Embed, Member from discord.ext.commands import Cog, Context, group from bot import constants @@ -28,6 +29,12 @@ REGEX_NON_ALPHABET = re.compile(r"[^a-z]", re.MULTILINE & re.IGNORECASE) FOOTER_TEXT = f"To show a tag, type {constants.Bot.prefix}tags <tagname>." +class COOLDOWN(enum.Enum): + """Sentinel value to signal that a tag is on cooldown.""" + + obj = object() + + class TagIdentifier(NamedTuple): """Stores the group and name used as an identifier for a tag.""" @@ -237,81 +244,80 @@ class Tags(Cog): matching_tags = self._get_tags_via_content(any, keywords or 'any', ctx.author) await self._send_matching_tags(ctx, keywords, matching_tags) - async def display_tag(self, ctx: Context, tag_identifier: TagIdentifier) -> bool: + async def get_tag_embed( + self, + ctx: Context, + tag_identifier: TagIdentifier, + ) -> Optional[Union[Embed, Literal[COOLDOWN.obj]]]: """ - If a tag is not found, display similar tag names as suggestions. - - If a tag is not specified, display a paginated embed of all tags. + Generate an embed of the requested tag or of suggestions if the tag doesn't exist/isn't accessible by the user. - Tags are on cooldowns on a per-tag, per-channel basis. If a tag is on cooldown, display - nothing and return True. + If the requested tag is on cooldown or no suggestions were found, return None. """ - if tag_identifier.name is not None: + if (tag := self._tags.get(tag_identifier)) is not None and tag.accessible_by(ctx.author): - if (tag := self._tags.get(tag_identifier)) is not None and tag.accessible_by(ctx.author): + if tag.on_cooldown_in(ctx.channel): + log.debug(f"Tag {str(tag_identifier)!r} is on cooldown.") + return COOLDOWN.obj + tag.set_cooldown_for(ctx.channel) - if tag.on_cooldown_in(ctx.channel): - log.debug(f"Tag {str(tag_identifier)!r} is on cooldown.") - return True - tag.set_cooldown_for(ctx.channel) - - self.bot.stats.incr( - f"tags.usages" - f"{'.' + tag_identifier.group.replace('-', '_') if tag_identifier.group else ''}" - f".{tag_identifier.name.replace('-', '_')}" - ) - - await wait_for_deletion( - await ctx.send(embed=tag.embed), - [ctx.author.id], - ) - return True - - elif len(tag_identifier.name) >= 3: - suggested_tags = self.get_fuzzy_matches(tag_identifier)[:10] - if not suggested_tags: - return False - suggested_tags_text = "\n".join( - str(identifier) - for identifier, tag in suggested_tags - if tag.accessible_by(ctx.author) - and not tag.on_cooldown_in(ctx.channel) - ) - await wait_for_deletion( - await ctx.send( - embed=Embed( - title="Did you mean ...", - description=suggested_tags_text - ) - ), - [ctx.author.id], - ) - return True + self.bot.stats.incr( + f"tags.usages" + f"{'.' + tag_identifier.group.replace('-', '_') if tag_identifier.group else ''}" + f".{tag_identifier.name.replace('-', '_')}" + ) + return tag.embed + + elif len(tag_identifier.name) >= 3: + suggested_tags = self.get_fuzzy_matches(tag_identifier)[:10] + if not suggested_tags: + return None + suggested_tags_text = "\n".join( + str(identifier) + for identifier, tag in suggested_tags + if tag.accessible_by(ctx.author) + and not tag.on_cooldown_in(ctx.channel) + ) + return Embed( + title="Did you mean ...", + description=suggested_tags_text + ) - else: - tags = self._cache.values() - if not tags: - await ctx.send(embed=Embed( - description="**There are no tags in the database!**", - colour=Colour.red() - )) - return True + async def list_all_tags(self, ctx: Context) -> None: + """Send a paginator with all loaded tags accessible by `ctx.author`, groups first, and alphabetically sorted.""" + def tag_sort_key(tag_item: tuple[TagIdentifier, tag]) -> str: + ident = tag_item[0] + if ident.group is None: + # Max codepoint character to force tags without a group to the end + group = chr(0x10ffff) else: - embed: Embed = Embed(title="**Current tags**") - await LinePaginator.paginate( - sorted( - f"**ยป** {tag['title']}" for tag in tags - if self.check_accessibility(ctx.author, tag) - ), - ctx, - embed, - footer_text=FOOTER_TEXT, - empty=False, - max_lines=15 - ) - return True - - return False + group = ident.group + return group+ident.name + + result_lines = [] + current_group = object() + group_accessible = True + + for identifier, tag in sorted(self._tags.items(), key=tag_sort_key): + + if identifier.group != current_group: + if not group_accessible: + # Remove group separator line if no tags in the previous group were accessible by the user. + result_lines.pop() + # A new group began, add a separator with the group name. + if identifier.group is not None: + group_accessible = False + result_lines.append(f"\n\N{BULLET} **{identifier.group}**") + else: + result_lines.append("\n\N{BULLET}") + current_group = identifier.group + + if tag.accessible_by(ctx.author): + result_lines.append(f"**\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}** {identifier.name}") + group_accessible = True + + embed = Embed(title="Current tags") + await LinePaginator.paginate(result_lines, ctx, embed=embed, max_lines=15, empty=False, footer_text=FOOTER_TEXT) @tags_group.command(name='get', aliases=('show', 'g')) async def get_command( @@ -322,15 +328,33 @@ class Tags(Cog): """ Get a specified tag, or a list of all tags if no tag is specified. - Returns True if something can be sent, or if the tag is on cooldown. - Returns False if no matches are found. + Returns True if something was sent, or if the tag is on cooldown. + Returns False if no message was sent. """ + if tag_name_or_group is None and tag_name is None: + if self._tags: + await self.list_all_tags(ctx) + return True + else: + await ctx.send(embed=Embed(description="**There are no tags!**")) + return True + if tag_name is None: tag_name = tag_name_or_group tag_group = None else: tag_group = tag_name_or_group - return await self.display_tag(ctx, TagIdentifier(tag_group, tag_name)) + + embed = await self.get_tag_embed(ctx, TagIdentifier(tag_group, tag_name)) + if embed is not None: + if embed is not COOLDOWN.obj: + await wait_for_deletion( + await ctx.send(embed=embed), + (ctx.author.id,) + ) + return True + else: + return False def setup(bot: Bot) -> None: |