diff options
| -rw-r--r-- | bot/exts/filtering/_filter_lists/domain.py | 6 | ||||
| -rw-r--r-- | bot/exts/filtering/_filter_lists/extension.py | 10 | ||||
| -rw-r--r-- | bot/exts/filtering/_filter_lists/filter_list.py | 104 | ||||
| -rw-r--r-- | bot/exts/filtering/_filter_lists/invite.py | 12 | ||||
| -rw-r--r-- | bot/exts/filtering/_filter_lists/token.py | 6 | ||||
| -rw-r--r-- | bot/exts/filtering/_ui/filter.py | 4 | ||||
| -rw-r--r-- | bot/exts/filtering/_ui/filter_list.py | 4 | ||||
| -rw-r--r-- | bot/exts/filtering/_ui/ui.py | 5 | ||||
| -rw-r--r-- | bot/exts/filtering/filtering.py | 101 |
9 files changed, 153 insertions, 99 deletions
diff --git a/bot/exts/filtering/_filter_lists/domain.py b/bot/exts/filtering/_filter_lists/domain.py index cae2eb878..ec43e92df 100644 --- a/bot/exts/filtering/_filter_lists/domain.py +++ b/bot/exts/filtering/_filter_lists/domain.py @@ -33,7 +33,7 @@ class DomainsList(FilterList): name = "domain" def __init__(self, filtering_cog: Filtering): - super().__init__(DomainFilter) + super().__init__() filtering_cog.subscribe(self, Event.MESSAGE, Event.MESSAGE_EDIT) def get_filter_type(self, content: str) -> Type[Filter]: @@ -56,13 +56,13 @@ class DomainsList(FilterList): new_ctx = ctx.replace(content=urls) triggers = self.filter_list_result( - new_ctx, self.filter_lists[ListType.DENY], self.defaults[ListType.DENY]["validations"] + new_ctx, self[ListType.DENY].filters, self[ListType.DENY].defaults.validations ) ctx.notification_domain = new_ctx.notification_domain actions = None message = "" if triggers: - action_defaults = self.defaults[ListType.DENY]["actions"] + action_defaults = self[ListType.DENY].defaults.actions actions = reduce( or_, (filter_.actions.fallback_to(action_defaults) if filter_.actions else action_defaults diff --git a/bot/exts/filtering/_filter_lists/extension.py b/bot/exts/filtering/_filter_lists/extension.py index e34ead393..ce1a46e4a 100644 --- a/bot/exts/filtering/_filter_lists/extension.py +++ b/bot/exts/filtering/_filter_lists/extension.py @@ -49,7 +49,7 @@ class ExtensionsList(FilterList): name = "extension" def __init__(self, filtering_cog: Filtering): - super().__init__(ExtensionFilter) + super().__init__() filtering_cog.subscribe(self, Event.MESSAGE) self._whitelisted_description = None @@ -68,7 +68,7 @@ class ExtensionsList(FilterList): if not ctx.message.attachments: return None, "" - _, failed = self.defaults[ListType.ALLOW]["validations"].evaluate(ctx) + _, failed = self[ListType.ALLOW].defaults.validations.evaluate(ctx) if failed: # There's no extension filtering in this context. return None, "" @@ -77,7 +77,7 @@ class ExtensionsList(FilterList): (splitext(attachment.filename.lower())[1], attachment.filename) for attachment in ctx.message.attachments } new_ctx = ctx.replace(content={ext for ext, _ in all_ext}) # And prepare the context for the filters to read. - triggered = [filter_ for filter_ in self.filter_lists[ListType.ALLOW].values() if filter_.triggered_on(new_ctx)] + triggered = [filter_ for filter_ in self[ListType.ALLOW].filters.values() if filter_.triggered_on(new_ctx)] allowed_ext = {filter_.content for filter_ in triggered} # Get the extensions in the message that are allowed. # See if there are any extensions left which aren't allowed. @@ -101,7 +101,7 @@ class ExtensionsList(FilterList): meta_channel = bot.instance.get_channel(Channels.meta) if not self._whitelisted_description: self._whitelisted_description = ', '.join( - filter_.content for filter_ in self.filter_lists[ListType.ALLOW].values() + filter_.content for filter_ in self[ListType.ALLOW].filters.values() ) ctx.dm_embed = DISALLOWED_EMBED_DESCRIPTION.format( joined_whitelist=self._whitelisted_description, @@ -110,4 +110,4 @@ class ExtensionsList(FilterList): ) ctx.matches += not_allowed.values() - return self.defaults[ListType.ALLOW]["actions"], ", ".join(f"`{ext}`" for ext in not_allowed) + return self[ListType.ALLOW].defaults.actions, ", ".join(f"`{ext}`" for ext in not_allowed) diff --git a/bot/exts/filtering/_filter_lists/filter_list.py b/bot/exts/filtering/_filter_lists/filter_list.py index a4f22aed4..ecbcb8f09 100644 --- a/bot/exts/filtering/_filter_lists/filter_list.py +++ b/bot/exts/filtering/_filter_lists/filter_list.py @@ -1,6 +1,7 @@ from abc import abstractmethod +from collections.abc import Iterator from enum import Enum -from typing import Any, Optional, Type +from typing import Any, ItemsView, NamedTuple, Optional, Type from discord.ext.commands import BadArgument @@ -36,6 +37,32 @@ def list_type_converter(argument: str) -> ListType: raise BadArgument(f"No matching list type found for {argument!r}.") +class Defaults(NamedTuple): + """Represents an atomic list's default settings.""" + + actions: ActionSettings + validations: ValidationSettings + + +class AtomicList(NamedTuple): + """ + Represents the atomic structure of a single filter list as it appears in the database. + + This is as opposed to the FilterList class which is a combination of several list types. + """ + + id: int + name: str + list_type: ListType + defaults: Defaults + filters: dict[int, Filter] + + @property + def label(self) -> str: + """Provide a short description identifying the list with its name and type.""" + return f"{past_tense(self.list_type.name.lower())} {self.name.lower()}" + + class FilterList(FieldRequiring): """Dispatches events to lists of _filters, and aggregates the responses into a single list of actions to take.""" @@ -43,52 +70,63 @@ class FilterList(FieldRequiring): # Names must be unique across all filter lists. name = FieldRequiring.MUST_SET_UNIQUE - def __init__(self, filter_type: Type[Filter]): - self.list_ids = {} - self.filter_lists: dict[ListType, dict[int, Filter]] = {} - self.defaults = {} + def __init__(self): + self._filter_lists: dict[ListType, AtomicList] = {} + + def __iter__(self) -> Iterator[ListType]: + return iter(self._filter_lists) + + def __getitem__(self, list_type: ListType) -> AtomicList: + return self._filter_lists[list_type] + + def __contains__(self, list_type: ListType) -> bool: + return list_type in self._filter_lists - self.filter_type = filter_type + def __bool__(self) -> bool: + return bool(self._filter_lists) - def add_list(self, list_data: dict) -> None: + def __len__(self) -> int: + return len(self._filter_lists) + + def items(self) -> ItemsView[ListType, AtomicList]: + """Return an iterator for the lists' types and values.""" + return self._filter_lists.items() + + def add_list(self, list_data: dict) -> AtomicList: """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"]) - self.defaults[list_type] = {"actions": actions, "validations": validations} - self.list_ids[list_type] = list_data["id"] + defaults = Defaults(actions, validations) - self.filter_lists[list_type] = {} + filters = {} for filter_data in list_data["filters"]: - self.add_filter(filter_data, list_type) + 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: + if list_type not in self._filter_lists: return - self.filter_lists.pop(list_type) - self.defaults.pop(list_type) - self.list_ids.pop(list_type) - - def add_filter(self, filter_data: dict, list_type: ListType) -> Filter: - """Add a filter to the list of the specified type.""" - try: - new_filter = self.filter_type(filter_data) - self.filter_lists[list_type][filter_data["id"]] = new_filter - except TypeError as e: - log.warning(e) - else: - return new_filter + 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.defaults[list_type]["actions"].get_setting(setting, missing) + value = self._filter_lists[list_type].defaults.actions.get_setting(setting, missing) if value is missing: - value = self.defaults[list_type]["validations"].get_setting(setting, 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 + def add_filter(self, list_type: ListType, filter_data: dict) -> Filter: + """Add a filter to the list of the specified type.""" + new_filter = self._create_filter(filter_data) + self[list_type].filters[filter_data["id"]] = new_filter + return new_filter + @abstractmethod def get_filter_type(self, content: str) -> Type[Filter]: """Get a subclass of filter matching the filter list and the filter's content.""" @@ -104,7 +142,7 @@ class FilterList(FieldRequiring): @staticmethod def filter_list_result( - ctx: FilterContext, filters: dict[int, Filter], defaults: ValidationSettings + 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. @@ -134,3 +172,13 @@ class FilterList(FieldRequiring): relevant_filters.append(filter_) return relevant_filters + + def _create_filter(self, filter_data: dict) -> Filter: + """Create a filter from the given data.""" + try: + filter_type = self.get_filter_type(filter_data["content"]) + new_filter = filter_type(filter_data) + except TypeError as e: + log.warning(e) + else: + return new_filter diff --git a/bot/exts/filtering/_filter_lists/invite.py b/bot/exts/filtering/_filter_lists/invite.py index 095699597..30884a2ab 100644 --- a/bot/exts/filtering/_filter_lists/invite.py +++ b/bot/exts/filtering/_filter_lists/invite.py @@ -39,7 +39,7 @@ class InviteList(FilterList): name = "invite" def __init__(self, filtering_cog: Filtering): - super().__init__(InviteFilter) + super().__init__() filtering_cog.subscribe(self, Event.MESSAGE) def get_filter_type(self, content: str) -> Type[Filter]: @@ -53,7 +53,7 @@ class InviteList(FilterList): async def actions_for(self, ctx: FilterContext) -> tuple[Optional[ActionSettings], Optional[str]]: """Dispatch the given event to the list's filters, and return actions to take and a message to relay to mods.""" - _, failed = self.defaults[ListType.ALLOW]["validations"].evaluate(ctx) + _, failed = self[ListType.ALLOW].defaults.validations.evaluate(ctx) if failed: # There's no invite filtering in this context. return None, "" @@ -89,7 +89,7 @@ class InviteList(FilterList): 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.filter_lists[ListType.ALLOW].values() if filter_.triggered_on(new_ctx) + filter_.content for filter_ in self[ListType.ALLOW].filters.values() if filter_.triggered_on(new_ctx) } disallowed_invites.update({ invite_code: invite for invite_code, invite in denied_by_default.items() if invite.guild.id not in allowed @@ -99,7 +99,7 @@ class InviteList(FilterList): 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.filter_lists[ListType.ALLOW], self.defaults[ListType.DENY]["validations"] + new_ctx, self[ListType.ALLOW].filters, self[ListType.DENY].defaults.validations ) disallowed_invites.update({ invite_code: invite for invite_code, invite in allowed_by_default.items() @@ -111,14 +111,14 @@ class InviteList(FilterList): actions = None if len(disallowed_invites) > len(triggered): # There are invites which weren't allowed but aren't blacklisted. - deny_defaults = self.defaults[ListType.DENY]["actions"] + 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.defaults[ListType.ALLOW]["actions"] + self[ListType.ALLOW].defaults.actions ) elif triggered: actions = reduce(or_, (filter_.actions for filter_ in triggered)) diff --git a/bot/exts/filtering/_filter_lists/token.py b/bot/exts/filtering/_filter_lists/token.py index aca0fdedf..2abf94553 100644 --- a/bot/exts/filtering/_filter_lists/token.py +++ b/bot/exts/filtering/_filter_lists/token.py @@ -34,7 +34,7 @@ class TokensList(FilterList): name = "token" def __init__(self, filtering_cog: Filtering): - super().__init__(TokenFilter) + super().__init__() filtering_cog.subscribe(self, Event.MESSAGE, Event.MESSAGE_EDIT) def get_filter_type(self, content: str) -> Type[Filter]: @@ -57,12 +57,12 @@ class TokensList(FilterList): ctx = ctx.replace(content=text) triggers = self.filter_list_result( - ctx, self.filter_lists[ListType.DENY], self.defaults[ListType.DENY]["validations"] + ctx, self[ListType.DENY].filters, self[ListType.DENY].defaults.validations ) actions = None message = "" if triggers: - action_defaults = self.defaults[ListType.DENY]["actions"] + action_defaults = self[ListType.DENY].defaults.actions actions = reduce( or_, (filter_.actions.fallback_to(action_defaults) if filter_.actions else action_defaults diff --git a/bot/exts/filtering/_ui/filter.py b/bot/exts/filtering/_ui/filter.py index 4da8fe001..b372cac3e 100644 --- a/bot/exts/filtering/_ui/filter.py +++ b/bot/exts/filtering/_ui/filter.py @@ -31,8 +31,8 @@ def build_filter_repr_dict( """Build a dictionary of field names and values to pass to `_build_embed_from_dict`.""" # Get filter list settings default_setting_values = {} - for type_ in ("actions", "validations"): - for _, setting in filter_list.defaults[list_type][type_].items(): + for settings_group in filter_list[list_type].defaults: + for _, setting in settings_group.items(): default_setting_values.update(to_serializable(setting.dict())) # Add overrides. It's done in this way to preserve field order, since the filter won't have all settings. diff --git a/bot/exts/filtering/_ui/filter_list.py b/bot/exts/filtering/_ui/filter_list.py index cc58f5a62..55e03ce18 100644 --- a/bot/exts/filtering/_ui/filter_list.py +++ b/bot/exts/filtering/_ui/filter_list.py @@ -46,8 +46,8 @@ def build_filterlist_repr_dict(filter_list: FilterList, list_type: ListType, new """Build a dictionary of field names and values to pass to `_build_embed_from_dict`.""" # Get filter list settings default_setting_values = {} - for type_ in ("actions", "validations"): - for _, setting in filter_list.defaults[list_type][type_].items(): + for settings_group in filter_list[list_type].defaults: + for _, setting in settings_group.items(): default_setting_values.update(to_serializable(setting.dict())) # Add new values. It's done in this way to preserve field order, since the new_values won't have all settings. diff --git a/bot/exts/filtering/_ui/ui.py b/bot/exts/filtering/_ui/ui.py index 0ded6d4a4..cba151c70 100644 --- a/bot/exts/filtering/_ui/ui.py +++ b/bot/exts/filtering/_ui/ui.py @@ -390,8 +390,9 @@ class EditBaseView(ABC, discord.ui.View): view = BooleanSelectView(setting_name, update_callback) await interaction.response.send_message(f"Choose a value for `{setting_name}`:", view=view, ephemeral=True) elif type_ in (set, list, tuple): - current_list = list(self.current_value(setting_name)) - if current_list is MISSING: + if (current_value := self.current_value(setting_name)) is not MISSING: + current_list = list(current_value) + else: current_list = [] await interaction.response.send_message( f"Current list: {current_list}", diff --git a/bot/exts/filtering/filtering.py b/bot/exts/filtering/filtering.py index 09f458a59..607fbe8bf 100644 --- a/bot/exts/filtering/filtering.py +++ b/bot/exts/filtering/filtering.py @@ -18,6 +18,7 @@ from bot.bot import Bot from bot.constants import Colours, MODERATION_ROLES, Roles, Webhooks from bot.exts.filtering._filter_context import Event, FilterContext from bot.exts.filtering._filter_lists import FilterList, ListType, filter_list_types, list_type_converter +from bot.exts.filtering._filter_lists.filter_list import AtomicList from bot.exts.filtering._filters.filter import Filter from bot.exts.filtering._settings import ActionSettings from bot.exts.filtering._ui.filter import ( @@ -62,15 +63,18 @@ class Filtering(Cog): await self.bot.wait_until_guild_available() raw_filter_lists = await self.bot.api_client.get("bot/filter/filter_lists") + example_list = None for raw_filter_list in raw_filter_lists: - self._load_raw_filter_list(raw_filter_list) + loaded_list = self._load_raw_filter_list(raw_filter_list) + if not example_list and loaded_list: + example_list = loaded_list try: self.webhook = await self.bot.fetch_webhook(Webhooks.filters) except HTTPException: log.error(f"Failed to fetch filters webhook with ID `{Webhooks.filters}`.") - self.collect_loaded_types() + self.collect_loaded_types(example_list) def subscribe(self, filter_list: FilterList, *events: Event) -> None: """ @@ -98,12 +102,14 @@ class Filtering(Cog): if filter_list in self._subscriptions.get(event, []): self._subscriptions[event].remove(filter_list) - def collect_loaded_types(self) -> None: + def collect_loaded_types(self, example_list: AtomicList) -> None: """ Go over the classes used in initialization and collect them to dictionaries. The information that is collected is about the types actually used to load the API response, not all types available in the filtering extension. + + Any filter list has the fields for all settings in the DB schema, so picking any one of them is enough. """ # Get the filter types used by each filter list. for filter_list in self.filter_lists.values(): @@ -111,26 +117,25 @@ class Filtering(Cog): # Get the setting types used by each filter list. if self.filter_lists: - # Any filter list has the fields for all settings in the DB schema, so picking any one of them is enough. - list_defaults = list(list(self.filter_lists.values())[0].defaults.values())[0] - settings_types = set() + settings_entries = set() # The settings are split between actions and validations. - settings_types.update(type(setting) for _, setting in list_defaults["actions"].items()) - settings_types.update(type(setting) for _, setting in list_defaults["validations"].items()) - for setting_type in settings_types: - type_hints = get_type_hints(setting_type) + for settings_group in example_list.defaults: + settings_entries.update(type(setting) for _, setting in settings_group.items()) + + for setting_entry in settings_entries: + type_hints = get_type_hints(setting_entry) # The description should be either a string or a dictionary. - if isinstance(setting_type.description, str): - # If it's a string, then the setting matches a single field in the DB, + if isinstance(setting_entry.description, str): + # If it's a string, then the settings entry matches a single field in the DB, # and its name is the setting type's name attribute. - self.loaded_settings[setting_type.name] = ( - setting_type.description, setting_type, type_hints[setting_type.name] + self.loaded_settings[setting_entry.name] = ( + setting_entry.description, setting_entry, type_hints[setting_entry.name] ) else: - # Otherwise, the setting type works with compound settings. + # Otherwise, the setting entry works with compound settings. self.loaded_settings.update({ - subsetting: (description, setting_type, type_hints[subsetting]) - for subsetting, description in setting_type.description.items() + subsetting: (description, setting_entry, type_hints[subsetting]) + for subsetting, description in setting_entry.description.items() }) # Get the settings per filter as well. @@ -286,7 +291,7 @@ class Filtering(Cog): embed.description = f"`{filter_.content}`" if filter_.description: embed.description += f" - {filter_.description}" - embed.set_author(name=f"Filter #{id_} - " + f"{past_tense(list_type.name.lower())} {filter_list.name}".title()) + embed.set_author(name=f"Filter #{id_} - " + f"{filter_list[list_type].label}".title()) embed.set_footer(text=( "Field names with an asterisk have values which override the defaults of the containing filter list. " f"To view all defaults of the list, run `!filterlist describe {list_type.name} {filter_list.name}`." @@ -410,7 +415,7 @@ class Filtering(Cog): if description: embed.description += f" - {description}" embed.set_author( - name=f"Filter #{filter_id} - {past_tense(list_type.name.lower())} {filter_list.name}".title()) + name=f"Filter #{filter_id} - {filter_list[list_type].label}".title()) embed.set_footer(text=( "Field names with an asterisk have values which override the defaults of the containing filter list. " f"To view all defaults of the list, run `!filterlist describe {list_type.name} {filter_list.name}`." @@ -441,7 +446,7 @@ class Filtering(Cog): return filter_, filter_list, list_type = result await bot.instance.api_client.delete(f'bot/filter/filters/{filter_id}') - filter_list.filter_lists[list_type].pop(filter_id) + filter_list[list_type].filters.pop(filter_id) await ctx.reply(f"✅ Deleted filter: {filter_}") @filter.group(aliases=("settings",)) @@ -503,10 +508,9 @@ class Filtering(Cog): return list_type, filter_list = result - list_defaults = filter_list.defaults[list_type] setting_values = {} - for type_ in ("actions", "validations"): - for _, setting in list_defaults[type_].items(): + for settings_group in filter_list[list_type].defaults: + for _, setting in settings_group.items(): setting_values.update(to_serializable(setting.dict())) embed = Embed(colour=Colour.blue()) @@ -514,7 +518,7 @@ class Filtering(Cog): # Use the class's docstring, and ignore single newlines. embed.description = re.sub(r"(?<!\n)\n(?!\n)", " ", filter_list.__doc__) embed.set_author( - name=f"Description of the {past_tense(list_type.name.lower())} {list_name.lower()} filter list" + name=f"Description of the {filter_list[list_type].label} filter list" ) await ctx.send(embed=embed) @@ -525,7 +529,7 @@ class Filtering(Cog): list_description = f"{past_tense(list_type.name.lower())} {list_name.lower()}" if list_name in self.filter_lists: filter_list = self.filter_lists[list_name] - if list_type in filter_list.filter_lists: + if list_type in filter_list: await ctx.reply(f":x: The {list_description} filter list already exists.") return @@ -572,7 +576,7 @@ class Filtering(Cog): await self._patch_filter_list(ctx.message, filter_list, list_type, settings) embed = Embed(colour=Colour.blue()) - embed.set_author(name=f"{past_tense(list_type.name.lower())} {filter_list.name} Filter List".title()) + embed.set_author(name=f"{filter_list[list_type].label.title()} Filter List") embed.set_footer(text="Field names with a ~ have values which change the existing value in the filter list.") view = FilterListEditView( @@ -599,7 +603,7 @@ class Filtering(Cog): message = await ctx.send("⏳ Annihilation in progress, please hold...", file=file) # Unload the filter list. filter_list.remove_list(list_type) - if not filter_list.filter_lists: # There's nothing left, remove from the cog. + if not filter_list: # There's nothing left, remove from the cog. self.filter_lists.pop(filter_list.name) self.unsubscribe(filter_list) @@ -610,8 +614,8 @@ class Filtering(Cog): if result is None: return list_type, filter_list = result - list_id = filter_list.list_ids[list_type] - list_description = f"{past_tense(list_type.name.lower())} {filter_list.name}" + list_id = filter_list[list_type].id + list_description = filter_list[list_type].label await ctx.reply( f"Are you sure you want to delete the {list_description} list?", view=DeleteConfirmationView(ctx.author, delete_list) @@ -620,7 +624,7 @@ class Filtering(Cog): # endregion # region: helper functions - def _load_raw_filter_list(self, list_data: dict) -> None: + def _load_raw_filter_list(self, list_data: dict) -> AtomicList | None: """Load the raw list data to the cog.""" list_name = list_data["name"] if list_name not in self.filter_lists: @@ -630,9 +634,9 @@ class Filtering(Cog): f"A filter list named {list_name} was loaded from the database, but no matching class." ) self.already_warned.add(list_name) - return + return None self.filter_lists[list_name] = filter_list_types[list_name](self) - self.filter_lists[list_name].add_list(list_data) + return self.filter_lists[list_name].add_list(list_data) async def _resolve_action(self, ctx: FilterContext) -> tuple[Optional[ActionSettings], dict[FilterList, str]]: """ @@ -703,7 +707,7 @@ class Filtering(Cog): filter_list = self._get_list_by_name(list_name) if list_type is None: - if len(filter_list.filter_lists) > 1: + if len(filter_list) > 1: await ctx.send( "The **list_type** argument is unspecified. Please pick a value from the options below:", view=ArgumentCompletionView( @@ -711,7 +715,7 @@ class Filtering(Cog): ) ) return None - list_type = list(filter_list.filter_lists)[0] + list_type = list(filter_list)[0] return list_type, filter_list def _get_list_by_name(self, list_name: str) -> FilterList: @@ -729,25 +733,24 @@ class Filtering(Cog): @staticmethod async def _send_list(ctx: Context, filter_list: FilterList, list_type: ListType) -> None: """Show the list of filters identified by the list name and type.""" - type_filters = filter_list.filter_lists.get(list_type) - if type_filters is None: + if list_type not in filter_list: await ctx.send(f":x: There is no list of {past_tense(list_type.name.lower())} {filter_list.name}s.") return - lines = list(map(str, type_filters.values())) + lines = list(map(str, filter_list[list_type].filters.values())) log.trace(f"Sending a list of {len(lines)} filters.") embed = Embed(colour=Colour.blue()) - embed.set_author(name=f"List of {past_tense(list_type.name.lower())} {filter_list.name}s ({len(lines)} total)") + embed.set_author(name=f"List of {filter_list[list_type].label}s ({len(lines)} total)") await LinePaginator.paginate(lines, ctx, embed, max_lines=15, empty=False) def _get_filter_by_id(self, id_: int) -> Optional[tuple[Filter, FilterList, ListType]]: """Get the filter object corresponding to the provided ID, along with its containing list and list type.""" for filter_list in self.filter_lists.values(): - for list_type, sublist in filter_list.filter_lists.items(): - if id_ in sublist: - return sublist[id_], filter_list, list_type + for list_type, sublist in filter_list.items(): + if id_ in sublist.filters: + return sublist.filters[id_], filter_list, list_type async def _add_filter( self, @@ -783,7 +786,7 @@ class Filtering(Cog): if description: embed.description += f" - {description}" embed.set_author( - name=f"New Filter - {past_tense(list_type.name.lower())} {filter_list.name}".title()) + name=f"New Filter - {filter_list[list_type].label}".title()) embed.set_footer(text=( "Field names with an asterisk have values which override the defaults of the containing filter list. " f"To view all defaults of the list, run `!filterlist describe {list_type.name} {filter_list.name}`." @@ -808,8 +811,10 @@ class Filtering(Cog): @staticmethod def _identical_filters_message(content: str, filter_list: FilterList, list_type: ListType, filter_: Filter) -> str: """Returns all the filters in the list with content identical to the content supplied.""" + if list_type not in filter_list: + return "" duplicates = [ - f for f in filter_list.filter_lists.get(list_type, {}).values() + f for f in filter_list[list_type].filters.values() if f.content == content and f.id != filter_.id ] msg = "" @@ -837,14 +842,14 @@ class Filtering(Cog): content = await filter_type.process_content(content) - list_id = filter_list.list_ids[list_type] + list_id = filter_list[list_type].id description = description or None payload = { "filter_list": list_id, "content": content, "description": description, "additional_field": json.dumps(filter_settings), **settings } response = await bot.instance.api_client.post('bot/filter/filters', json=to_serializable(payload)) - new_filter = filter_list.add_filter(response, list_type) + 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) @@ -881,7 +886,7 @@ class Filtering(Cog): response = await bot.instance.api_client.patch( f'bot/filter/filters/{filter_.id}', json=to_serializable(payload) ) - edited_filter = filter_list.add_filter(response, list_type) + 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) @@ -895,13 +900,13 @@ class Filtering(Cog): @staticmethod async def _patch_filter_list(msg: Message, filter_list: FilterList, list_type: ListType, settings: dict) -> None: """PATCH the new data of the filter list to the site API.""" - list_id = filter_list.list_ids[list_type] + list_id = filter_list[list_type].id 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.add_list(response) - await msg.reply(f"✅ Edited filter list: {past_tense(list_type.name.lower())} {filter_list.name}") + await msg.reply(f"✅ Edited filter list: {filter_list[list_type].label}") # endregion |