diff options
| -rw-r--r-- | bot/cogs/allow_deny_lists.py | 29 | ||||
| -rw-r--r-- | bot/cogs/filtering.py | 14 | ||||
| -rw-r--r-- | bot/converters.py | 40 | ||||
| -rw-r--r-- | bot/utils/regex.py | 12 |
4 files changed, 78 insertions, 17 deletions
diff --git a/bot/cogs/allow_deny_lists.py b/bot/cogs/allow_deny_lists.py index 8b3c892f5..d82d175cf 100644 --- a/bot/cogs/allow_deny_lists.py +++ b/bot/cogs/allow_deny_lists.py @@ -7,7 +7,7 @@ from discord.ext.commands import BadArgument, Cog, Context, group from bot import constants from bot.api import ResponseCodeError from bot.bot import Bot -from bot.converters import ValidAllowDenyListType +from bot.converters import ValidAllowDenyListType, ValidDiscordServerInvite from bot.pagination import LinePaginator from bot.utils.checks import with_role_check @@ -31,6 +31,24 @@ class AllowDenyLists(Cog): """Add an item to an allow or denylist.""" allow_type = "whitelist" if allowed else "blacklist" + # If this is a server invite, we gotta validate it. + if list_type == "GUILD_INVITE": + log.trace(f"{content} is a guild invite, attempting to validate.") + validator = ValidDiscordServerInvite() + guild_data = await validator.convert(ctx, content) + + # If we make it this far without raising a BadArgument, the invite is + # valid. Let's convert the content to an ID. + log.trace(f"{content} validated as server invite. Converting to ID.") + content = guild_data.get("id") + + # Unless the user has specified another comment, let's + # use the server name as the comment so that the list + # of guild IDs will be more easily readable when we + # display it. + if not comment: + comment = guild_data.get("name") + # Try to add the item to the database log.trace(f"Trying to add the {content} item to the {list_type} {allow_type}") payload = { @@ -100,17 +118,18 @@ class AllowDenyLists(Cog): # Build a list of lines we want to show in the paginator lines = [] for item in result: - line = f"• {item.get('content')}" + line = f"• `{item.get('content')}`" if item.get("comment"): - line += f" ({item.get('comment')})" + line += f" - {item.get('comment')}" lines.append(line) lines = sorted(lines) # Build the embed + list_type_plural = list_type.lower().replace("_", " ").title() + "s" embed = Embed( - title=f"{allow_type.title()}ed {list_type.lower()} items ({len(result)} total)", + title=f"{allow_type.title()}ed {list_type_plural} ({len(result)} total)", colour=Colour.blue() ) log.trace(f"Trying to list {len(result)} items from the {list_type.lower()} {allow_type}") @@ -139,6 +158,7 @@ class AllowDenyLists(Cog): ctx: Context, list_type: ValidAllowDenyListType, content: str, + *, comment: Optional[str] = None, ) -> None: """Add an item to the specified allowlist.""" @@ -150,6 +170,7 @@ class AllowDenyLists(Cog): ctx: Context, list_type: ValidAllowDenyListType, content: str, + *, comment: Optional[str] = None, ) -> None: """Add an item to the specified denylist.""" diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py index 4d51bba2e..3ebb47a0f 100644 --- a/bot/cogs/filtering.py +++ b/bot/cogs/filtering.py @@ -18,22 +18,12 @@ from bot.constants import ( Filter, Icons, URLs ) from bot.utils.redis_cache import RedisCache +from bot.utils.regex import INVITE_RE from bot.utils.scheduling import Scheduler log = logging.getLogger(__name__) # Regular expressions -INVITE_RE = re.compile( - r"(?:discord(?:[\.,]|dot)gg|" # Could be discord.gg/ - r"discord(?:[\.,]|dot)com(?:\/|slash)invite|" # or discord.com/invite/ - r"discordapp(?:[\.,]|dot)com(?:\/|slash)invite|" # or discordapp.com/invite/ - r"discord(?:[\.,]|dot)me|" # or discord.me - r"discord(?:[\.,]|dot)io" # or discord.io. - r")(?:[\/]|slash)" # / or 'slash' - r"([a-zA-Z0-9]+)", # the invite code itself - flags=re.IGNORECASE -) - SPOILER_RE = re.compile(r"(\|\|.+?\|\|)", re.DOTALL) URL_RE = re.compile(r"(https?://[^\s]+)", flags=re.IGNORECASE) ZALGO_RE = re.compile(r"[\u0300-\u036F\u0489]") @@ -478,7 +468,7 @@ class Filtering(Cog): return True guild_id = guild.get("id") - guild_invite_whitelist = self._get_allowlist_items(True, "guild_invite_id") + guild_invite_whitelist = self._get_allowlist_items(True, "guild_invite") if guild_id not in guild_invite_whitelist: guild_icon_hash = guild["icon"] diff --git a/bot/converters.py b/bot/converters.py index edac67be2..7e21c1542 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -9,8 +9,10 @@ import dateutil.tz import discord from aiohttp import ClientConnectorError, ContentTypeError from dateutil.relativedelta import relativedelta -from discord.ext.commands import BadArgument, Context, Converter, UserConverter +from discord.ext.commands import BadArgument, Context, Converter, IDConverter, UserConverter +from bot.constants import URLs +from bot.utils.regex import INVITE_RE log = logging.getLogger(__name__) @@ -34,6 +36,42 @@ def allowed_strings(*values, preserve_case: bool = False) -> t.Callable[[str], s return converter +class ValidDiscordServerInvite(Converter): + """ + A converter that validates whether a given string is a valid Discord server invite. + + Raises 'BadArgument' if: + - The string is not a valid Discord server invite. + - The string is valid, but is an invite for a group DM. + - The string is valid, but is expired. + + Returns a (partial) guild object if: + - The string is a valid vanity + - The string is a full invite URI + - The string contains the invite code (the stuff after discord.gg/) + + See the Discord API docs for documentation on the guild object: + https://discord.com/developers/docs/resources/guild#guild-object + """ + + async def convert(self, ctx: Context, server_invite: str) -> dict: + """Check whether the string is a valid Discord server invite.""" + invite_code = INVITE_RE.match(server_invite) + if invite_code: + response = await ctx.bot.http_session.get( + f"{URLs.discord_invite_api}/{invite_code[1]}" + ) + if response.status != 404: + invite_data = await response.json() + return invite_data.get("guild") + + id_converter = IDConverter() + if id_converter._get_id_match(server_invite): + raise BadArgument("Guild IDs are not supported, only invites.") + + raise BadArgument("This does not appear to be a valid Discord server invite.") + + class ValidAllowDenyListType(Converter): """ A converter that checks whether the given string is a valid AllowDenyList type. diff --git a/bot/utils/regex.py b/bot/utils/regex.py new file mode 100644 index 000000000..d194f93cb --- /dev/null +++ b/bot/utils/regex.py @@ -0,0 +1,12 @@ +import re + +INVITE_RE = re.compile( + r"(?:discord(?:[\.,]|dot)gg|" # Could be discord.gg/ + r"discord(?:[\.,]|dot)com(?:\/|slash)invite|" # or discord.com/invite/ + r"discordapp(?:[\.,]|dot)com(?:\/|slash)invite|" # or discordapp.com/invite/ + r"discord(?:[\.,]|dot)me|" # or discord.me + r"discord(?:[\.,]|dot)io" # or discord.io. + r")(?:[\/]|slash)" # / or 'slash' + r"([a-zA-Z0-9]+)", # the invite code itself + flags=re.IGNORECASE +) |