diff options
| author | 2020-03-09 09:09:05 +0100 | |
|---|---|---|
| committer | 2020-03-09 09:09:05 +0100 | |
| commit | 3de5a481c0461041b52c8952175d596e2956c4b2 (patch) | |
| tree | 549091de781de1d7603e19a97c254dbcdd98926c | |
| parent | Refactor token detection to check all potential substrings in message (diff) | |
| parent | Merge pull request #826 from python-discord/vote (diff) | |
Merge branch 'master' into token-detection-fix
| -rw-r--r-- | bot/cogs/antimalware.py | 4 | ||||
| -rw-r--r-- | bot/cogs/tags.py | 71 | ||||
| -rw-r--r-- | bot/cogs/utils.py | 21 | 
3 files changed, 93 insertions, 3 deletions
diff --git a/bot/cogs/antimalware.py b/bot/cogs/antimalware.py index 373619895..79bf486a4 100644 --- a/bot/cogs/antimalware.py +++ b/bot/cogs/antimalware.py @@ -29,8 +29,9 @@ class AntiMalware(Cog):              return          embed = Embed() -        file_extensions = {splitext(message.filename.lower())[1] for message in message.attachments} +        file_extensions = {splitext(attachment.filename.lower())[1] for attachment in message.attachments}          extensions_blocked = file_extensions - set(AntiMalwareConfig.whitelist) +        blocked_extensions_str = ', '.join(extensions_blocked)          if ".py" in extensions_blocked:              # Short-circuit on *.py files to provide a pastebin link              embed.description = ( @@ -38,7 +39,6 @@ class AntiMalware(Cog):                  f"please use a code-pasting service such as {URLs.site_schema}{URLs.site_paste}"              )          elif extensions_blocked: -            blocked_extensions_str = ', '.join(extensions_blocked)              whitelisted_types = ', '.join(AntiMalwareConfig.whitelist)              meta_channel = self.bot.get_channel(Channels.meta)              embed.description = ( diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py index 5da9a4148..c6b442912 100644 --- a/bot/cogs/tags.py +++ b/bot/cogs/tags.py @@ -1,7 +1,7 @@  import logging  import re  import time -from typing import Dict, List, Optional +from typing import Callable, Dict, Iterable, List, Optional  from discord import Colour, Embed  from discord.ext.commands import Cog, Context, group @@ -86,11 +86,80 @@ class Tags(Cog):              return self._get_suggestions(tag_name)          return found +    async def _get_tags_via_content(self, check: Callable[[Iterable], bool], keywords: str) -> list: +        """ +        Search for tags via contents. + +        `predicate` will be the built-in any, all, or a custom callable. Must return a bool. +        """ +        await self._get_tags() + +        keywords_processed: List[str] = [] +        for keyword in keywords.split(','): +            keyword_sanitized = keyword.strip().casefold() +            if not keyword_sanitized: +                # this happens when there are leading / trailing / consecutive comma. +                continue +            keywords_processed.append(keyword_sanitized) + +        if not keywords_processed: +            # after sanitizing, we can end up with an empty list, for example when keywords is ',' +            # in that case, we simply want to search for such keywords directly instead. +            keywords_processed = [keywords] + +        matching_tags = [] +        for tag in self._cache.values(): +            if check(query in tag['embed']['description'].casefold() for query in keywords_processed): +                matching_tags.append(tag) + +        return matching_tags + +    async def _send_matching_tags(self, ctx: Context, keywords: str, matching_tags: list) -> None: +        """Send the result of matching tags to user.""" +        if not matching_tags: +            pass +        elif len(matching_tags) == 1: +            await ctx.send(embed=Embed().from_dict(matching_tags[0]['embed'])) +        else: +            is_plural = keywords.strip().count(' ') > 0 or keywords.strip().count(',') > 0 +            embed = Embed( +                title=f"Here are the tags containing the given keyword{'s' * is_plural}:", +                description='\n'.join(tag['title'] for tag in matching_tags[:10]) +            ) +            await LinePaginator.paginate( +                sorted(f"**ยป**   {tag['title']}" for tag in matching_tags), +                ctx, +                embed, +                footer_text="To show a tag, type !tags <tagname>.", +                empty=False, +                max_lines=15 +            ) +      @group(name='tags', aliases=('tag', 't'), invoke_without_command=True)      async def tags_group(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None:          """Show all known tags, a single tag, or run a subcommand."""          await ctx.invoke(self.get_command, tag_name=tag_name) +    @tags_group.group(name='search', invoke_without_command=True) +    async def search_tag_content(self, ctx: Context, *, keywords: str) -> None: +        """ +        Search inside tags' contents for tags. Allow searching for multiple keywords separated by comma. + +        Only search for tags that has ALL the keywords. +        """ +        matching_tags = await self._get_tags_via_content(all, keywords) +        await self._send_matching_tags(ctx, keywords, matching_tags) + +    @search_tag_content.command(name='any') +    async def search_tag_content_any_keyword(self, ctx: Context, *, keywords: Optional[str] = None) -> None: +        """ +        Search inside tags' contents for tags. Allow searching for multiple keywords separated by comma. + +        Search for tags that has ANY of the keywords. +        """ +        matching_tags = await self._get_tags_via_content(any, keywords or 'any') +        await self._send_matching_tags(ctx, keywords, matching_tags) +      @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.""" diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py index 8ea972145..024141d62 100644 --- a/bot/cogs/utils.py +++ b/bot/cogs/utils.py @@ -257,6 +257,27 @@ class Utils(Cog):          embed.description = best_match          await ctx.send(embed=embed) +    @command(aliases=("poll",)) +    @with_role(*MODERATION_ROLES) +    async def vote(self, ctx: Context, title: str, *options: str) -> None: +        """ +        Build a quick voting poll with matching reactions with the provided options. + +        A maximum of 20 options can be provided, as Discord supports a max of 20 +        reactions on a single message. +        """ +        if len(options) < 2: +            raise BadArgument("Please provide at least 2 options.") +        if len(options) > 20: +            raise BadArgument("I can only handle 20 options!") + +        codepoint_start = 127462  # represents "regional_indicator_a" unicode value +        options = {chr(i): f"{chr(i)} - {v}" for i, v in enumerate(options, start=codepoint_start)} +        embed = Embed(title=title, description="\n".join(options.values())) +        message = await ctx.send(embed=embed) +        for reaction in options: +            await message.add_reaction(reaction) +  def setup(bot: Bot) -> None:      """Load the Utils cog."""  |