diff options
author | 2020-03-09 09:14:49 +0100 | |
---|---|---|
committer | 2020-03-09 09:14:49 +0100 | |
commit | c2de291bc41bfa387308db3ae84bbf0150d494d9 (patch) | |
tree | b6c2dd466fe37510e917b4982637177eb9fe73d4 | |
parent | ModLog: fix posting null attachments for deleted message logs (diff) | |
parent | Merge pull request #825 from python-discord/token-detection-fix (diff) |
Merge branch 'master' into bug/mod/792/null-attachments
-rw-r--r-- | bot/cogs/antimalware.py | 4 | ||||
-rw-r--r-- | bot/cogs/tags.py | 71 | ||||
-rw-r--r-- | bot/cogs/token_remover.py | 13 | ||||
-rw-r--r-- | bot/cogs/utils.py | 21 |
4 files changed, 103 insertions, 6 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/token_remover.py b/bot/cogs/token_remover.py index 82c01ae96..547ba8da0 100644 --- a/bot/cogs/token_remover.py +++ b/bot/cogs/token_remover.py @@ -96,12 +96,19 @@ class TokenRemover(Cog): if msg.author.bot: return False - maybe_match = TOKEN_RE.search(msg.content) - if maybe_match is None: + # Use findall rather than search to guard against method calls prematurely returning the + # token check (e.g. `message.channel.send` also matches our token pattern) + maybe_matches = TOKEN_RE.findall(msg.content) + if not maybe_matches: return False + return any(cls.is_maybe_token(substr) for substr in maybe_matches) + + @classmethod + def is_maybe_token(cls, test_str: str) -> bool: + """Check the provided string to see if it is a seemingly valid token.""" try: - user_id, creation_timestamp, hmac = maybe_match.group(0).split('.') + user_id, creation_timestamp, hmac = test_str.split('.') except ValueError: return False 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.""" |