aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/cogs/antimalware.py4
-rw-r--r--bot/cogs/tags.py71
-rw-r--r--bot/cogs/utils.py21
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."""