diff options
author | 2022-10-22 21:52:50 +0300 | |
---|---|---|
committer | 2022-10-22 21:52:50 +0300 | |
commit | 5a4aa1f51875ce12e4484a31b5bdabb2ef659059 (patch) | |
tree | 267d98b58995c34062db57657558ae0d5dd8130d | |
parent | Add a command to query filters by settings (diff) |
Add everyone filter, fix invite filtering
- Add the everyone filter, from a new class of filters - UniqueFilter. These filters should only be run once per context.
These filters can modify any part of their behavior, including which events they respond to.
- The invite filtering had a bug where it could exit early when the context wasn't a valid whitelisting context (which usually means that any unkown filters are allowed), despite there possibly being blocked filters which require addressing.
-rw-r--r-- | bot/exts/filtering/_filter_lists/domain.py | 20 | ||||
-rw-r--r-- | bot/exts/filtering/_filter_lists/extension.py | 2 | ||||
-rw-r--r-- | bot/exts/filtering/_filter_lists/filter_list.py | 104 | ||||
-rw-r--r-- | bot/exts/filtering/_filter_lists/invite.py | 113 | ||||
-rw-r--r-- | bot/exts/filtering/_filter_lists/token.py | 20 | ||||
-rw-r--r-- | bot/exts/filtering/_filter_lists/unique.py | 75 | ||||
-rw-r--r-- | bot/exts/filtering/_filters/filter.py | 14 | ||||
-rw-r--r-- | bot/exts/filtering/_filters/unique/__init__.py | 9 | ||||
-rw-r--r-- | bot/exts/filtering/_filters/unique/everyone.py | 28 | ||||
-rw-r--r-- | bot/exts/filtering/_utils.py | 2 | ||||
-rw-r--r-- | bot/exts/filtering/filtering.py | 8 |
11 files changed, 285 insertions, 110 deletions
diff --git a/bot/exts/filtering/_filter_lists/domain.py b/bot/exts/filtering/_filter_lists/domain.py index 17984e276..d97aa252b 100644 --- a/bot/exts/filtering/_filter_lists/domain.py +++ b/bot/exts/filtering/_filter_lists/domain.py @@ -2,8 +2,6 @@ from __future__ import annotations import re import typing -from functools import reduce -from operator import or_ from bot.exts.filtering._filter_context import Event, FilterContext from bot.exts.filtering._filter_lists.filter_list import FilterList, ListType @@ -18,7 +16,7 @@ if typing.TYPE_CHECKING: URL_RE = re.compile(r"https?://(\S+)", flags=re.IGNORECASE) -class DomainsList(FilterList): +class DomainsList(FilterList[DomainFilter]): """ A list of filters, each looking for a specific domain given by URL. @@ -59,18 +57,6 @@ class DomainsList(FilterList): actions = None messages = [] if triggers: - action_defaults = self[ListType.DENY].defaults.actions - actions = reduce( - or_, - (filter_.actions.fallback_to(action_defaults) if filter_.actions else action_defaults - for filter_ in triggers - ) - ) - if len(triggers) == 1: - message = f"#{triggers[0].id} (`{triggers[0].content}`)" - if triggers[0].description: - message += f" - {triggers[0].description}" - messages = [message] - else: - messages = [f"#{filter_.id} (`{filter_.content}`)" for filter_ in triggers] + actions = self[ListType.DENY].merge_actions(triggers) + messages = self[ListType.DENY].format_messages(triggers) return actions, messages diff --git a/bot/exts/filtering/_filter_lists/extension.py b/bot/exts/filtering/_filter_lists/extension.py index a58c6c45e..3f9d2b287 100644 --- a/bot/exts/filtering/_filter_lists/extension.py +++ b/bot/exts/filtering/_filter_lists/extension.py @@ -33,7 +33,7 @@ DISALLOWED_EMBED_DESCRIPTION = ( ) -class ExtensionsList(FilterList): +class ExtensionsList(FilterList[ExtensionFilter]): """ A list of filters, each looking for a file attachment with a specific extension. diff --git a/bot/exts/filtering/_filter_lists/filter_list.py b/bot/exts/filtering/_filter_lists/filter_list.py index 84a43072b..55204335b 100644 --- a/bot/exts/filtering/_filter_lists/filter_list.py +++ b/bot/exts/filtering/_filter_lists/filter_list.py @@ -1,11 +1,18 @@ +import dataclasses +import typing from abc import abstractmethod +from collections import defaultdict +from collections.abc import Iterable +from dataclasses import dataclass from enum import Enum -from typing import Any, NamedTuple +from functools import reduce +from operator import or_ +from typing import Any from discord.ext.commands import BadArgument -from bot.exts.filtering._filter_context import FilterContext -from bot.exts.filtering._filters.filter import Filter +from bot.exts.filtering._filter_context import Event, FilterContext +from bot.exts.filtering._filters.filter import Filter, UniqueFilter from bot.exts.filtering._settings import ActionSettings, Defaults, create_settings from bot.exts.filtering._utils import FieldRequiring, past_tense from bot.log import get_logger @@ -36,7 +43,8 @@ def list_type_converter(argument: str) -> ListType: raise BadArgument(f"No matching list type found for {argument!r}.") -class AtomicList(NamedTuple): +@dataclass(frozen=True) +class AtomicList: """ Represents the atomic structure of a single filter list as it appears in the database. @@ -68,11 +76,16 @@ class AtomicList(NamedTuple): If the filter is relevant in context, see if it actually triggers. """ - passed_by_default, failed_by_default = self.defaults.validations.evaluate(ctx) + return self._create_filter_list_result(ctx, self.defaults, self.filters.values()) + + @staticmethod + def _create_filter_list_result(ctx: FilterContext, defaults: Defaults, filters: Iterable[Filter]) -> list[Filter]: + """A helper function to evaluate the result of `filter_list_result`.""" + passed_by_default, failed_by_default = defaults.validations.evaluate(ctx) default_answer = not bool(failed_by_default) relevant_filters = [] - for filter_ in self.filters.values(): + for filter_ in filters: if not filter_.validations: if default_answer and filter_.triggered_on(ctx): relevant_filters.append(filter_) @@ -94,8 +107,36 @@ class AtomicList(NamedTuple): raise ValueError(f"Couldn't find a setting named {setting_name!r}.") return value + def merge_actions(self, filters: list[Filter]) -> ActionSettings | None: + """ + Merge the settings of the given filters, with the list's defaults as fallback. + + If `merge_default` is True, include it in the merge instead of using it as a fallback. + """ + try: + result = reduce(or_, (filter_.actions for filter_ in filters if filter_.actions)) + except TypeError: # The sequence fed to reduce is empty. + return None + + return result.fallback_to(self.defaults.actions) + + @staticmethod + def format_messages(triggers: list[Filter], *, expand_single_filter: bool = True) -> list[str]: + """Convert the filters into strings that can be added to the alert embed.""" + if len(triggers) == 1 and expand_single_filter: + message = f"#{triggers[0].id} (`{triggers[0].content}`)" + if triggers[0].description: + message += f" - {triggers[0].description}" + messages = [message] + else: + messages = [f"#{filter_.id} (`{filter_.content}`)" for filter_ in triggers] + return messages + + +T = typing.TypeVar("T", bound=Filter) + -class FilterList(FieldRequiring, dict[ListType, AtomicList]): +class FilterList(dict[ListType, AtomicList], typing.Generic[T], FieldRequiring): """Dispatches events to lists of _filters, and aggregates the responses into a single list of actions to take.""" # Each subclass must define a name matching the filter_list name we're expecting to receive from the database. @@ -110,39 +151,70 @@ class FilterList(FieldRequiring, dict[ListType, AtomicList]): filters = {} for filter_data in list_data["filters"]: - filters[filter_data["id"]] = self._create_filter(filter_data, defaults) + new_filter = self._create_filter(filter_data, defaults) + if new_filter: + filters[filter_data["id"]] = new_filter self[list_type] = AtomicList(list_data["id"], self.name, list_type, defaults, filters) return self[list_type] - def add_filter(self, list_type: ListType, filter_data: dict) -> Filter: + def add_filter(self, list_type: ListType, filter_data: dict) -> T | None: """Add a filter to the list of the specified type.""" new_filter = self._create_filter(filter_data, self[list_type].defaults) - self[list_type].filters[filter_data["id"]] = new_filter + if new_filter: + self[list_type].filters[filter_data["id"]] = new_filter return new_filter @abstractmethod - def get_filter_type(self, content: str) -> type[Filter]: + def get_filter_type(self, content: str) -> type[T]: """Get a subclass of filter matching the filter list and the filter's content.""" @property @abstractmethod - def filter_types(self) -> set[type[Filter]]: + def filter_types(self) -> set[type[T]]: """Return the types of filters used by this list.""" @abstractmethod async def actions_for(self, ctx: FilterContext) -> tuple[ActionSettings | None, list[str]]: """Dispatch the given event to the list's filters, and return actions to take and messages to relay to mods.""" - def _create_filter(self, filter_data: dict, defaults: Defaults) -> Filter: + def _create_filter(self, filter_data: dict, defaults: Defaults) -> T | None: """Create a filter from the given data.""" try: filter_type = self.get_filter_type(filter_data["content"]) - new_filter = filter_type(filter_data, defaults) + if filter_type: + return filter_type(filter_data, defaults) + else: + return None except TypeError as e: log.warning(e) - else: - return new_filter def __hash__(self): return hash(id(self)) + + +@dataclass(frozen=True) +class SubscribingAtomicList(AtomicList): + """ + A base class for a list of unique filters. + + Unique filters are ones that should only be run once in a given context. + Each unique filter is subscribed to a subset of events to respond to. + """ + + subscriptions: defaultdict[Event, list[Filter]] = dataclasses.field(default_factory=lambda: defaultdict(list)) + + def subscribe(self, filter_: UniqueFilter, *events: Event) -> None: + """ + Subscribe a unique filter to the given events. + + The filter is added to a list for each event. When the event is triggered, the filter context will be + dispatched to the subscribed filters. + """ + for event in events: + if filter_ not in self.subscriptions[event]: + self.subscriptions[event].append(filter_) + + def filter_list_result(self, ctx: FilterContext) -> list[Filter]: + """Sift through the list of filters, and return only the ones which apply to the given context.""" + return self._create_filter_list_result(ctx, self.defaults, self.subscriptions[ctx.event]) diff --git a/bot/exts/filtering/_filter_lists/invite.py b/bot/exts/filtering/_filter_lists/invite.py index d35fdd4a4..0b84aec0e 100644 --- a/bot/exts/filtering/_filter_lists/invite.py +++ b/bot/exts/filtering/_filter_lists/invite.py @@ -1,8 +1,6 @@ from __future__ import annotations import typing -from functools import reduce -from operator import or_ from botcore.utils.regex import DISCORD_INVITE from discord import Embed, Invite @@ -20,7 +18,7 @@ if typing.TYPE_CHECKING: from bot.exts.filtering.filtering import Filtering -class InviteList(FilterList): +class InviteList(FilterList[InviteFilter]): """ A list of filters, each looking for guild invites to a specific guild. @@ -52,10 +50,6 @@ class InviteList(FilterList): async def actions_for(self, ctx: FilterContext) -> tuple[ActionSettings | None, list[str]]: """Dispatch the given event to the list's filters, and return actions to take and messages to relay to mods.""" - _, failed = self[ListType.ALLOW].defaults.validations.evaluate(ctx) - if failed: # There's no invite filtering in this context. - return None, [] - text = clean_input(ctx.content) # Avoid escape characters @@ -66,62 +60,73 @@ class InviteList(FilterList): if not invite_codes: return None, [] - # Sort the invites into three categories: - denied_by_default = dict() # Denied unless whitelisted. - allowed_by_default = dict() # Allowed unless blacklisted (partnered or verified servers). - disallowed_invites = dict() # Always denied (invalid invites). + _, failed = self[ListType.ALLOW].defaults.validations.evaluate(ctx) + # If the allowed list doesn't operate in the context, unknown invites are allowed. + check_if_allowed = not failed + + # Sort the invites into two categories: + invites_for_inspection = dict() # Found guild invites requiring further inspection. + unknown_invites = dict() # Either don't resolve or group DMs. for invite_code in invite_codes: try: invite = await bot.instance.fetch_invite(invite_code) except NotFound: - disallowed_invites[invite_code] = None + if check_if_allowed: + unknown_invites[invite_code] = None else: - if not invite.guild: - disallowed_invites[invite_code] = invite - else: - if "PARTNERED" in invite.guild.features or "VERIFIED" in invite.guild.features: - allowed_by_default[invite_code] = invite - else: - denied_by_default[invite_code] = invite - - # Add the disallowed by default unless they're whitelisted. - guilds_for_inspection = {invite.guild.id for invite in denied_by_default.values()} - new_ctx = ctx.replace(content=guilds_for_inspection) - allowed = { - filter_.content for filter_ in self[ListType.ALLOW].filters.values() if filter_.triggered_on(new_ctx) + if invite.guild: + invites_for_inspection[invite_code] = invite + elif check_if_allowed: # Group DM + unknown_invites[invite_code] = invite + + # Find any blocked invites + new_ctx = ctx.replace(content={invite.guild.id for invite in invites_for_inspection.values()}) + triggered = self[ListType.DENY].filter_list_result(new_ctx) + blocked_guilds = {filter_.content for filter_ in triggered} + blocked_invites = { + code: invite for code, invite in invites_for_inspection.items() if invite.guild.id in blocked_guilds } - disallowed_invites.update({ - invite_code: invite for invite_code, invite in denied_by_default.items() if invite.guild.id not in allowed - }) - - # Add the allowed by default only if they're blacklisted. - guilds_for_inspection = {invite.guild.id for invite in allowed_by_default.values()} - new_ctx = ctx.replace(content=guilds_for_inspection) - triggered = self[ListType.ALLOW].filter_list_result(new_ctx) - disallowed_invites.update({ - invite_code: invite for invite_code, invite in allowed_by_default.items() - if invite.guild.id in {filter_.content for filter_ in triggered} - }) - - if not disallowed_invites: + + # Remove the ones which are already confirmed as blocked, or otherwise ones which are partnered or verified. + invites_for_inspection = { + code: invite for code, invite in invites_for_inspection.items() + if invite.guild.id not in blocked_guilds + and "PARTNERED" not in invite.guild.features and "VERIFIED" not in invite.guild.features + } + + # Remove any remaining invites which are allowed + guilds_for_inspection = {invite.guild.id for invite in invites_for_inspection.values()} + + if check_if_allowed: # Whether unknown invites need to be checked. + new_ctx = ctx.replace(content=guilds_for_inspection) + allowed = { + filter_.content for filter_ in self[ListType.ALLOW].filters.values() if filter_.triggered_on(new_ctx) + } + unknown_invites.update({ + code: invite for code, invite in invites_for_inspection.items() if invite.guild.id not in allowed + }) + + if not triggered and not unknown_invites: return None, [] actions = None - if len(disallowed_invites) > len(triggered): # There are invites which weren't allowed but aren't blacklisted. - deny_defaults = self[ListType.DENY].defaults.actions - actions = reduce( - or_, - ( - filter_.actions.fallback_to(deny_defaults) if filter_.actions else deny_defaults - for filter_ in triggered - ), - self[ListType.ALLOW].defaults.actions - ) - elif triggered: - actions = reduce(or_, (filter_.actions for filter_ in triggered)) - ctx.matches += {match[0] for match in matches if match.group("invite") in disallowed_invites} - ctx.alert_embeds += (self._guild_embed(invite) for invite in disallowed_invites.values() if invite) - return actions, [f"`{invite}`" for invite in disallowed_invites] + if unknown_invites: # There are invites which weren't allowed but aren't explicitly blocked. + actions = self[ListType.ALLOW].defaults.actions + # Blocked invites come second so that their actions have preference. + if triggered: + if actions: + actions |= self[ListType.DENY].merge_actions(triggered) + else: + actions = self[ListType.DENY].merge_actions(triggered) + + blocked_invites |= unknown_invites + ctx.matches += {match[0] for match in matches if match.group("invite") in blocked_invites} + ctx.alert_embeds += (self._guild_embed(invite) for invite in blocked_invites.values() if invite) + messages = self[ListType.DENY].format_messages(triggered) + messages += [ + f"`{code} - {invite.guild.id}`" if invite else f"`{code}`" for code, invite in unknown_invites.items() + ] + return actions, messages @staticmethod def _guild_embed(invite: Invite) -> Embed: diff --git a/bot/exts/filtering/_filter_lists/token.py b/bot/exts/filtering/_filter_lists/token.py index 4b161d9b7..c7d7cb444 100644 --- a/bot/exts/filtering/_filter_lists/token.py +++ b/bot/exts/filtering/_filter_lists/token.py @@ -2,8 +2,6 @@ from __future__ import annotations import re import typing -from functools import reduce -from operator import or_ from bot.exts.filtering._filter_context import Event, FilterContext from bot.exts.filtering._filter_lists.filter_list import FilterList, ListType @@ -18,7 +16,7 @@ if typing.TYPE_CHECKING: SPOILER_RE = re.compile(r"(\|\|.+?\|\|)", re.DOTALL) -class TokensList(FilterList): +class TokensList(FilterList[TokenFilter]): """ A list of filters, each looking for a specific token in the given content given as regex. @@ -59,20 +57,8 @@ class TokensList(FilterList): actions = None messages = [] if triggers: - action_defaults = self[ListType.DENY].defaults.actions - actions = reduce( - or_, - (filter_.actions.fallback_to(action_defaults) if filter_.actions else action_defaults - for filter_ in triggers - ) - ) - if len(triggers) == 1: - message = f"#{triggers[0].id} (`{triggers[0].content}`)" - if triggers[0].description: - message += f" - {triggers[0].description}" - messages = [message] - else: - messages = [f"#{filter_.id} (`{filter_.content}`)" for filter_ in triggers] + actions = self[ListType.DENY].merge_actions(triggers) + messages = self[ListType.DENY].format_messages(triggers) return actions, messages @staticmethod diff --git a/bot/exts/filtering/_filter_lists/unique.py b/bot/exts/filtering/_filter_lists/unique.py new file mode 100644 index 000000000..63caa7d36 --- /dev/null +++ b/bot/exts/filtering/_filter_lists/unique.py @@ -0,0 +1,75 @@ +from botcore.utils.logging import get_logger +from discord.ext.commands import Cog + +from bot.exts.filtering._filter_context import FilterContext +from bot.exts.filtering._filter_lists.filter_list import FilterList, ListType, SubscribingAtomicList +from bot.exts.filtering._filters.filter import UniqueFilter +from bot.exts.filtering._filters.unique import unique_filter_types +from bot.exts.filtering._settings import ActionSettings, Defaults, create_settings + +log = get_logger(__name__) + + +class UniquesList(FilterList[UniqueFilter]): + """ + A list of unique filters. + + Unique filters are ones that should only be run once in a given context. + Each unique filter subscribes to a subset of events to respond to. + """ + + name = "unique" + _already_warned = set() + + def __init__(self, filtering_cog: Cog): + super().__init__() + self.filtering_cog = filtering_cog # This is typed as a Cog to avoid a circular import. + self.loaded_types: dict[str, type[UniqueFilter]] = {} + + def add_list(self, list_data: dict) -> SubscribingAtomicList: + """Add a new type of list (such as a whitelist or a blacklist) this filter list.""" + actions, validations = create_settings(list_data["settings"], keep_empty=True) + list_type = ListType(list_data["list_type"]) + defaults = Defaults(actions, validations) + new_list = SubscribingAtomicList(list_data["id"], self.name, list_type, defaults, {}) + self[list_type] = new_list + + filters = {} + events = set() + for filter_data in list_data["filters"]: + new_filter = self._create_filter(filter_data, defaults) + if new_filter: + new_list.subscribe(new_filter, *new_filter.events) + filters[filter_data["id"]] = new_filter + self.loaded_types[new_filter.name] = type(new_filter) + events.update(new_filter.events) + + new_list.filters.update(filters) + if hasattr(self.filtering_cog, "subscribe"): # Subscribe the filter list to any new events found. + self.filtering_cog.subscribe(self, *events) + return new_list + + def get_filter_type(self, content: str) -> type[UniqueFilter] | None: + """Get a subclass of filter matching the filter list and the filter's content.""" + try: + return unique_filter_types[content] + except KeyError: + if content not in self._already_warned: + log.warn(f"A unique filter named {content} was supplied, but no matching implementation found.") + self._already_warned.add(content) + return None + + @property + def filter_types(self) -> set[type[UniqueFilter]]: + """Return the types of filters used by this list.""" + return set(self.loaded_types.values()) + + async def actions_for(self, ctx: FilterContext) -> tuple[ActionSettings | None, list[str]]: + """Dispatch the given event to the list's filters, and return actions to take and messages to relay to mods.""" + triggers = self[ListType.DENY].filter_list_result(ctx) + actions = None + messages = [] + if triggers: + actions = self[ListType.DENY].merge_actions(triggers) + messages = self[ListType.DENY].format_messages(triggers) + return actions, messages diff --git a/bot/exts/filtering/_filters/filter.py b/bot/exts/filtering/_filters/filter.py index 095799781..b0d19d3a8 100644 --- a/bot/exts/filtering/_filters/filter.py +++ b/bot/exts/filtering/_filters/filter.py @@ -1,9 +1,9 @@ -from abc import abstractmethod +from abc import ABC, abstractmethod from typing import Any from pydantic import ValidationError -from bot.exts.filtering._filter_context import FilterContext +from bot.exts.filtering._filter_context import Event, FilterContext from bot.exts.filtering._settings import Defaults, create_settings from bot.exts.filtering._utils import FieldRequiring @@ -79,3 +79,13 @@ class Filter(FieldRequiring): if self.description: string += f" - {self.description}" return string + + +class UniqueFilter(Filter, ABC): + """ + Unique filters are ones that should only be run once in a given context. + + This is as opposed to say running many domain filters on the same message. + """ + + events: tuple[Event, ...] = FieldRequiring.MUST_SET diff --git a/bot/exts/filtering/_filters/unique/__init__.py b/bot/exts/filtering/_filters/unique/__init__.py new file mode 100644 index 000000000..ce78d6922 --- /dev/null +++ b/bot/exts/filtering/_filters/unique/__init__.py @@ -0,0 +1,9 @@ +from os.path import dirname + +from bot.exts.filtering._filters.filter import UniqueFilter +from bot.exts.filtering._utils import subclasses_in_package + +unique_filter_types = subclasses_in_package(dirname(__file__), f"{__name__}.", UniqueFilter) +unique_filter_types = {filter_.name: filter_ for filter_ in unique_filter_types} + +__all__ = [unique_filter_types] diff --git a/bot/exts/filtering/_filters/unique/everyone.py b/bot/exts/filtering/_filters/unique/everyone.py new file mode 100644 index 000000000..06d3a19bb --- /dev/null +++ b/bot/exts/filtering/_filters/unique/everyone.py @@ -0,0 +1,28 @@ +import re + +from bot.constants import Guild +from bot.exts.filtering._filter_context import Event, FilterContext +from bot.exts.filtering._filters.filter import UniqueFilter + +EVERYONE_PING_RE = re.compile(rf"@everyone|<@&{Guild.id}>|@here") +CODE_BLOCK_RE = re.compile( + r"(?P<delim>``?)[^`]+?(?P=delim)(?!`+)" # Inline codeblock + r"|```(.+?)```", # Multiline codeblock + re.DOTALL | re.MULTILINE +) + + +class EveryoneFilter(UniqueFilter): + """Filter messages which contain `@everyone` and `@here` tags outside a codeblock.""" + + name = "everyone" + events = (Event.MESSAGE, Event.MESSAGE_EDIT) + + def triggered_on(self, ctx: FilterContext) -> bool: + """Search for the filter's content within a given context.""" + # First pass to avoid running re.sub on every message + if not EVERYONE_PING_RE.search(ctx.content): + return False + + content_without_codeblocks = CODE_BLOCK_RE.sub("", ctx.content) + return bool(EVERYONE_PING_RE.search(content_without_codeblocks)) diff --git a/bot/exts/filtering/_utils.py b/bot/exts/filtering/_utils.py index 7149f7254..a38fa22e4 100644 --- a/bot/exts/filtering/_utils.py +++ b/bot/exts/filtering/_utils.py @@ -16,7 +16,7 @@ ZALGO_RE = regex.compile(rf"[\p{{NONSPACING MARK}}\p{{ENCLOSING MARK}}--[{VARIAT T = TypeVar('T') -def subclasses_in_package(package: str, prefix: str, parent: type) -> set[type]: +def subclasses_in_package(package: str, prefix: str, parent: T) -> set[T]: """Return all the subclasses of class `parent`, found in the top-level of `package`, given by absolute path.""" subclasses = set() diff --git a/bot/exts/filtering/filtering.py b/bot/exts/filtering/filtering.py index 890b25718..837cd45c1 100644 --- a/bot/exts/filtering/filtering.py +++ b/bot/exts/filtering/filtering.py @@ -954,8 +954,11 @@ class Filtering(Cog): } response = await bot.instance.api_client.post('bot/filter/filters', json=to_serializable(payload)) new_filter = filter_list.add_filter(list_type, response) - extra_msg = Filtering._identical_filters_message(content, filter_list, list_type, new_filter) - await msg.reply(f"✅ Added filter: {new_filter}" + extra_msg) + if new_filter: + extra_msg = Filtering._identical_filters_message(content, filter_list, list_type, new_filter) + await msg.reply(f"✅ Added filter: {new_filter}" + extra_msg) + else: + await msg.reply(":x: Could not create the filter. Are you sure it's implemented?") @staticmethod async def _patch_filter( @@ -990,6 +993,7 @@ class Filtering(Cog): response = await bot.instance.api_client.patch( f'bot/filter/filters/{filter_.id}', json=to_serializable(payload) ) + # Return type can be None, but if it's being edited then it's not supposed to be. edited_filter = filter_list.add_filter(list_type, response) extra_msg = Filtering._identical_filters_message(content, filter_list, list_type, edited_filter) await msg.reply(f"✅ Edited filter: {edited_filter}" + extra_msg) |