diff options
| -rw-r--r-- | bot/exts/filtering/_ui.py | 88 | ||||
| -rw-r--r-- | bot/exts/filtering/filtering.py | 38 |
2 files changed, 100 insertions, 26 deletions
diff --git a/bot/exts/filtering/_ui.py b/bot/exts/filtering/_ui.py index 8bfcded77..a6bc1addd 100644 --- a/bot/exts/filtering/_ui.py +++ b/bot/exts/filtering/_ui.py @@ -383,6 +383,21 @@ class EnumSelectView(discord.ui.View): self.add_item(self.EnumSelect(setting_name, enum_cls, update_callback)) +class TemplateModal(discord.ui.Modal, title="Template"): + """A modal to enter a filter ID to copy its overrides over.""" + + template = discord.ui.TextInput(label="Template Filter ID") + + def __init__(self, embed_view: SettingsEditView, message: discord.Message): + super().__init__(timeout=COMPONENT_TIMEOUT) + self.embed_view = embed_view + self.message = message + + async def on_submit(self, interaction: Interaction) -> None: + """Update the embed with the new description.""" + await self.embed_view.apply_template(self.template.value, self.message, interaction) + + class SettingsEditView(discord.ui.View): """A view used to edit a filter's settings before updating the database.""" @@ -470,6 +485,12 @@ class SettingsEditView(discord.ui.View): """A button to empty the filter's description.""" await self.update_embed(interaction, description=self._REMOVE) + @discord.ui.button(label="Template", row=3) + async def enter_template(self, interaction: Interaction, button: discord.ui.Button) -> None: + """A button to enter a filter template ID and copy its overrides over.""" + modal = TemplateModal(self, interaction.message) + await interaction.response.send_modal(modal) + @discord.ui.button(label="✅ Confirm", style=discord.ButtonStyle.green, row=4) async def confirm(self, interaction: Interaction, button: discord.ui.Button) -> None: """Confirm the content, description, and settings, and update the filters database.""" @@ -599,7 +620,7 @@ class SettingsEditView(discord.ui.View): await interaction_or_msg.response.edit_message(embed=self.embed, view=new_view) else: await interaction_or_msg.edit(embed=self.embed, view=new_view) - except discord.errors.HTTPException: # Various error such as embed description being too long. + except discord.errors.HTTPException: # Various errors such as embed description being too long. pass else: self.stop() @@ -612,6 +633,21 @@ class SettingsEditView(discord.ui.View): """ await self.update_embed(interaction, setting_name=setting_name, setting_value=override_value) + async def apply_template(self, template_id: str, embed_message: discord.Message, interaction: Interaction) -> None: + """Replace any non-overridden settings with overrides from the given filter.""" + try: + settings, filter_settings = template_settings(template_id, self.filter_list, self.list_type) + except ValueError as e: # The interaction is necessary to send an ephemeral message. + await interaction.response.send_message(f":x: {e}", ephemeral=True) + return + else: + await interaction.response.defer() + + self.settings_overrides = settings | self.settings_overrides + self.filter_settings_overrides = filter_settings | self.filter_settings_overrides + self.embed.clear_fields() + await embed_message.edit(embed=self.embed, view=self.copy()) + async def _remove_override(self, interaction: Interaction, select: discord.ui.Select) -> None: """ Remove the override for the setting the user selected, and edit the embed. @@ -690,6 +726,9 @@ def description_and_settings_converter( description, *parsed = parsed settings = {setting: value for setting, value in [part.split("=", maxsplit=1) for part in parsed]} + template = None + if "--template" in settings: + template = settings.pop("--template") filter_settings = {} for setting, _ in list(settings.items()): @@ -723,9 +762,56 @@ def description_and_settings_converter( except (TypeError, ValueError) as e: raise BadArgument(e) + # Pull templates settings and apply them. + if template is not None: + try: + t_settings, t_filter_settings = template_settings(template, filter_list, list_type) + except ValueError as e: + raise BadArgument(str(e)) + else: + # The specified settings go on top of the template + settings = t_settings | settings + filter_settings = t_filter_settings | filter_settings + return description, settings, filter_settings +def filter_overrides(filter_: Filter, filter_list: FilterList, list_type: ListType) -> tuple[dict, dict]: + """Get the filter's overrides to the filter list settings and the extra fields settings.""" + overrides_values = {} + for settings in (filter_.actions, filter_.validations): + 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)): + overrides_values[setting_name] = value + + if filter_.extra_fields_type: + # The values here can be safely used since overrides equal to the defaults won't be saved. + extra_fields_overrides = filter_.extra_fields.dict(exclude_unset=True) + else: + extra_fields_overrides = {} + + return overrides_values, extra_fields_overrides + + +def template_settings(filter_id: str, filter_list: FilterList, list_type: ListType) -> tuple[dict, dict]: + """Find the filter with specified ID, and return its settings.""" + try: + filter_id = int(filter_id) + if filter_id < 0: + raise ValueError() + except ValueError: + raise ValueError("Template value must be a non-negative integer.") + + if filter_id not in filter_list.filter_lists[list_type]: + raise ValueError( + f"Could not find filter with ID `{filter_id}` in the {list_type.name} {filter_list.name} list." + ) + filter_ = filter_list.filter_lists[list_type][filter_id] + return filter_overrides(filter_, filter_list, list_type) + + def format_response_error(e: ResponseCodeError) -> Embed: """Format the response error into an embed.""" description = "" diff --git a/bot/exts/filtering/filtering.py b/bot/exts/filtering/filtering.py index aa90d1600..427735add 100644 --- a/bot/exts/filtering/filtering.py +++ b/bot/exts/filtering/filtering.py @@ -19,9 +19,10 @@ from bot.exts.filtering._filter_lists import FilterList, ListType, filter_list_t from bot.exts.filtering._filters.filter import Filter from bot.exts.filtering._settings import ActionSettings from bot.exts.filtering._ui import ( - ArgumentCompletionView, build_filter_repr_dict, description_and_settings_converter, populate_embed_from_dict + ArgumentCompletionView, build_filter_repr_dict, description_and_settings_converter, filter_overrides, + populate_embed_from_dict ) -from bot.exts.filtering._utils import past_tense, repr_equals, to_serializable +from bot.exts.filtering._utils import past_tense, to_serializable from bot.log import get_logger from bot.pagination import LinePaginator from bot.utils.messages import format_channel, format_user @@ -269,7 +270,7 @@ class Filtering(Cog): return filter_, filter_list, list_type = result - overrides_values, extra_fields_overrides = self._filter_overrides(filter_, filter_list, list_type) + overrides_values, extra_fields_overrides = filter_overrides(filter_, filter_list, list_type) all_settings_repr_dict = build_filter_repr_dict( filter_list, list_type, type(filter_), overrides_values, extra_fields_overrides @@ -337,7 +338,10 @@ class Filtering(Cog): The settings can be provided in the command itself, in the format of `setting_name=value` (no spaces around the equal sign). The value doesn't need to (shouldn't) be surrounded in quotes even if it contains spaces. - Example: `!filter add denied token "Scaleios is great" delete_messages=True send_alert=False` + A template filter can be specified in the settings area to copy overrides from. The setting name is "--template" + and the value is the filter ID. The template will be used before applying any other override. + + Example: `!filter add denied token "Scaleios is great" delete_messages=True send_alert=False --template=100` """ result = await self._resolve_list_type_and_name(ctx, list_type, list_name) if result is None: @@ -363,6 +367,9 @@ class Filtering(Cog): The settings can be provided in the command itself, in the format of `setting_name=value` (no spaces around the equal sign). The value doesn't need to (shouldn't) be surrounded in quotes even if it contains spaces. + A template filter can be specified in the settings area to copy overrides from. The setting name is "--template" + and the value is the filter ID. The template will be used before applying any other override. + To edit the filter's content, use the UI. """ result = self._get_filter_by_id(filter_id) @@ -371,7 +378,7 @@ class Filtering(Cog): return filter_, filter_list, list_type = result filter_type = type(filter_) - settings, filter_settings = self._filter_overrides(filter_, filter_list, list_type) + settings, filter_settings = filter_overrides(filter_, filter_list, list_type) description, new_settings, new_filter_settings = description_and_settings_converter( filter_list, list_type, filter_type, @@ -623,25 +630,6 @@ class Filtering(Cog): if id_ in sublist: return sublist[id_], filter_list, list_type - @staticmethod - def _filter_overrides(filter_: Filter, filter_list: FilterList, list_type: ListType) -> tuple[dict, dict]: - """Get the filter's overrides to the filter list settings and the extra fields settings.""" - overrides_values = {} - for settings in (filter_.actions, filter_.validations): - 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)): - overrides_values[setting_name] = value - - if filter_.extra_fields_type: - # The values here can be safely used since overrides equal to the defaults won't be saved. - extra_fields_overrides = filter_.extra_fields.dict(exclude_unset=True) - else: - extra_fields_overrides = {} - - return overrides_values, extra_fields_overrides - async def _add_filter( self, ctx: Context, @@ -736,7 +724,7 @@ class Filtering(Cog): "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=payload) + response = await bot.instance.api_client.post('bot/filter/filters', json=to_serializable(payload)) new_filter = filter_list.add_filter(response, list_type) extra_msg = Filtering._identical_filters_message(content, filter_list, list_type, new_filter) await msg.reply(f"✅ Added filter: {new_filter}" + extra_msg) |