diff options
| -rw-r--r-- | bot/exts/filtering/_filter_lists/domain.py | 4 | ||||
| -rw-r--r-- | bot/exts/filtering/_filter_lists/filter_list.py | 119 | ||||
| -rw-r--r-- | bot/exts/filtering/_filter_lists/invite.py | 4 | ||||
| -rw-r--r-- | bot/exts/filtering/_filter_lists/token.py | 4 | ||||
| -rw-r--r-- | bot/exts/filtering/_ui/filter.py | 6 | ||||
| -rw-r--r-- | bot/exts/filtering/_ui/filter_list.py | 2 | ||||
| -rw-r--r-- | bot/exts/filtering/filtering.py | 4 |
7 files changed, 54 insertions, 89 deletions
diff --git a/bot/exts/filtering/_filter_lists/domain.py b/bot/exts/filtering/_filter_lists/domain.py index 34ab5670c..17984e276 100644 --- a/bot/exts/filtering/_filter_lists/domain.py +++ b/bot/exts/filtering/_filter_lists/domain.py @@ -54,9 +54,7 @@ class DomainsList(FilterList): urls = {match.group(1).lower().rstrip("/") for match in URL_RE.finditer(text)} new_ctx = ctx.replace(content=urls) - triggers = self.filter_list_result( - new_ctx, self[ListType.DENY].filters, self[ListType.DENY].defaults.validations - ) + triggers = self[ListType.DENY].filter_list_result(new_ctx) ctx.notification_domain = new_ctx.notification_domain actions = None messages = [] diff --git a/bot/exts/filtering/_filter_lists/filter_list.py b/bot/exts/filtering/_filter_lists/filter_list.py index daab45b81..9eb907fc1 100644 --- a/bot/exts/filtering/_filter_lists/filter_list.py +++ b/bot/exts/filtering/_filter_lists/filter_list.py @@ -1,7 +1,6 @@ from abc import abstractmethod -from collections.abc import Iterator from enum import Enum -from typing import Any, ItemsView, NamedTuple +from typing import Any, NamedTuple from discord.ext.commands import BadArgument @@ -62,35 +61,53 @@ class AtomicList(NamedTuple): """Provide a short description identifying the list with its name and type.""" return f"{past_tense(self.list_type.name.lower())} {self.name.lower()}" + 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. -class FilterList(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. - # Names must be unique across all filter lists. - name = FieldRequiring.MUST_SET_UNIQUE + The strategy is as follows: + 1. The default settings are evaluated on the given context. The default answer for whether the filter is + relevant in the given context is whether there aren't any validation settings which returned False. + 2. For each filter, its overrides are considered: + - If there are no overrides, then the filter is relevant if that is the default answer. + - Otherwise it is relevant if there are no failed overrides, and any failing default is overridden by a + successful override. - def __init__(self): - self._filter_lists: dict[ListType, AtomicList] = {} + If the filter is relevant in context, see if it actually triggers. + """ + passed_by_default, failed_by_default = self.defaults.validations.evaluate(ctx) + default_answer = not bool(failed_by_default) - def __iter__(self) -> Iterator[ListType]: - return iter(self._filter_lists) + relevant_filters = [] + for filter_ in self.filters.values(): + if not filter_.validations: + if default_answer and filter_.triggered_on(ctx): + relevant_filters.append(filter_) + else: + passed, failed = filter_.validations.evaluate(ctx) + if not failed and failed_by_default < passed: + if filter_.triggered_on(ctx): + relevant_filters.append(filter_) - def __getitem__(self, list_type: ListType) -> AtomicList: - return self._filter_lists[list_type] + return relevant_filters - def __contains__(self, list_type: ListType) -> bool: - return list_type in self._filter_lists + def default(self, setting: str) -> Any: + """Get the default value of a specific setting.""" + missing = object() + value = self.defaults.actions.get_setting(setting, missing) + if value is missing: + value = self.defaults.validations.get_setting(setting, missing) + if value is missing: + raise ValueError(f"Couldn't find a setting named {setting!r}.") + return value - def __bool__(self) -> bool: - return bool(self._filter_lists) - def __len__(self) -> int: - return len(self._filter_lists) +class FilterList(FieldRequiring, dict[ListType, AtomicList]): + """Dispatches events to lists of _filters, and aggregates the responses into a single list of actions to take.""" - def items(self) -> ItemsView[ListType, AtomicList]: - """Return an iterator for the lists' types and values.""" - return self._filter_lists.items() + # Each subclass must define a name matching the filter_list name we're expecting to receive from the database. + # Names must be unique across all filter lists. + name = FieldRequiring.MUST_SET_UNIQUE def add_list(self, list_data: dict) -> AtomicList: """Add a new type of list (such as a whitelist or a blacklist) this filter list.""" @@ -102,24 +119,8 @@ class FilterList(FieldRequiring): for filter_data in list_data["filters"]: filters[filter_data["id"]] = self._create_filter(filter_data) - self._filter_lists[list_type] = AtomicList(list_data["id"], self.name, list_type, defaults, filters) - return self._filter_lists[list_type] - - def remove_list(self, list_type: ListType) -> None: - """Remove the list associated with the given type from the FilterList object.""" - if list_type not in self._filter_lists: - return - self._filter_lists.pop(list_type) - - def default(self, list_type: ListType, setting: str) -> Any: - """Get the default value of a specific setting.""" - missing = object() - value = self._filter_lists[list_type].defaults.actions.get_setting(setting, missing) - if value is missing: - value = self._filter_lists[list_type].defaults.validations.get_setting(setting, missing) - if value is missing: - raise ValueError(f"Could find a setting named {setting}.") - return value + 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: """Add a filter to the list of the specified type.""" @@ -140,39 +141,6 @@ class FilterList(FieldRequiring): 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.""" - @staticmethod - def filter_list_result( - ctx: FilterContext, filters: dict[int, Filter], defaults: ValidationSettings - ) -> list[Filter]: - """ - Sift through the list of filters, and return only the ones which apply to the given context. - - The strategy is as follows: - 1. The default settings are evaluated on the given context. The default answer for whether the filter is - relevant in the given context is whether there aren't any validation settings which returned False. - 2. For each filter, its overrides are considered: - - If there are no overrides, then the filter is relevant if that is the default answer. - - Otherwise it is relevant if there are no failed overrides, and any failing default is overridden by a - successful override. - - If the filter is relevant in context, see if it actually triggers. - """ - passed_by_default, failed_by_default = defaults.evaluate(ctx) - default_answer = not bool(failed_by_default) - - relevant_filters = [] - for filter_ in filters.values(): - if not filter_.validations: - if default_answer and filter_.triggered_on(ctx): - relevant_filters.append(filter_) - else: - passed, failed = filter_.validations.evaluate(ctx) - if not failed and failed_by_default < passed: - if filter_.triggered_on(ctx): - relevant_filters.append(filter_) - - return relevant_filters - def _create_filter(self, filter_data: dict) -> Filter: """Create a filter from the given data.""" try: @@ -182,3 +150,6 @@ class FilterList(FieldRequiring): log.warning(e) else: return new_filter + + def __hash__(self): + return hash(id(self)) diff --git a/bot/exts/filtering/_filter_lists/invite.py b/bot/exts/filtering/_filter_lists/invite.py index 5bb4549ae..d35fdd4a4 100644 --- a/bot/exts/filtering/_filter_lists/invite.py +++ b/bot/exts/filtering/_filter_lists/invite.py @@ -97,9 +97,7 @@ class InviteList(FilterList): # 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.filter_list_result( - new_ctx, self[ListType.ALLOW].filters, self[ListType.DENY].defaults.validations - ) + 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} diff --git a/bot/exts/filtering/_filter_lists/token.py b/bot/exts/filtering/_filter_lists/token.py index c80ccfd68..4b161d9b7 100644 --- a/bot/exts/filtering/_filter_lists/token.py +++ b/bot/exts/filtering/_filter_lists/token.py @@ -55,9 +55,7 @@ class TokensList(FilterList): text = clean_input(text) ctx = ctx.replace(content=text) - triggers = self.filter_list_result( - ctx, self[ListType.DENY].filters, self[ListType.DENY].defaults.validations - ) + triggers = self[ListType.DENY].filter_list_result(ctx) actions = None messages = [] if triggers: diff --git a/bot/exts/filtering/_ui/filter.py b/bot/exts/filtering/_ui/filter.py index e6330329d..38eef3ca6 100644 --- a/bot/exts/filtering/_ui/filter.py +++ b/bot/exts/filtering/_ui/filter.py @@ -278,7 +278,7 @@ class FilterEditView(EditBaseView): default_value = self.filter_type.extra_fields_type().dict()[setting_name] else: dict_to_edit = self.settings_overrides - default_value = self.filter_list.default(self.list_type, setting_name) + default_value = self.filter_list[self.list_type].default(setting_name) # Update the setting override value or remove it if setting_value is not self._REMOVE: if not repr_equals(setting_value, default_value): @@ -405,7 +405,7 @@ def description_and_settings_converter( type_ = loaded_settings[setting][2] try: parsed_value = parse_value(settings.pop(setting), type_) - if not repr_equals(parsed_value, filter_list.default(list_type, setting)): + if not repr_equals(parsed_value, filter_list[list_type].default(setting)): settings[setting] = parsed_value except (TypeError, ValueError) as e: raise BadArgument(e) @@ -431,7 +431,7 @@ def filter_overrides(filter_: Filter, filter_list: FilterList, list_type: ListTy if settings: for _, setting in settings.items(): for setting_name, value in to_serializable(setting.dict()).items(): - if not repr_equals(value, filter_list.default(list_type, setting_name)): + if not repr_equals(value, filter_list[list_type].default(setting_name)): overrides_values[setting_name] = value if filter_.extra_fields_type: diff --git a/bot/exts/filtering/_ui/filter_list.py b/bot/exts/filtering/_ui/filter_list.py index b072e293e..15d81322b 100644 --- a/bot/exts/filtering/_ui/filter_list.py +++ b/bot/exts/filtering/_ui/filter_list.py @@ -234,7 +234,7 @@ class FilterListEditView(EditBaseView): if not setting_name: # Obligatory check to match the signature in the parent class. return - default_value = self.filter_list.default(self.list_type, setting_name) + default_value = self.filter_list[self.list_type].default(setting_name) if not repr_equals(setting_value, default_value): self.settings[setting_name] = setting_value # If there's already a new value, remove it, since the new value is the same as the default. diff --git a/bot/exts/filtering/filtering.py b/bot/exts/filtering/filtering.py index c47ba653f..563bdacb5 100644 --- a/bot/exts/filtering/filtering.py +++ b/bot/exts/filtering/filtering.py @@ -646,7 +646,7 @@ class Filtering(Cog): file = discord.File(BytesIO(json.dumps(list_data, indent=4).encode("utf-8")), f"{list_description}.json") message = await ctx.send("⏳ Annihilation in progress, please hold...", file=file) # Unload the filter list. - filter_list.remove_list(list_type) + filter_list.pop(list_type) if not filter_list: # There's nothing left, remove from the cog. self.filter_lists.pop(filter_list.name) self.unsubscribe(filter_list) @@ -950,7 +950,7 @@ class Filtering(Cog): response = await bot.instance.api_client.patch( f'bot/filter/filter_lists/{list_id}', json=to_serializable(settings) ) - filter_list.remove_list(list_type) + filter_list.pop(list_type, None) filter_list.add_list(response) await msg.reply(f"✅ Edited filter list: {filter_list[list_type].label}") |