aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar mbaruh <[email protected]>2022-10-10 22:40:05 +0300
committerGravatar mbaruh <[email protected]>2022-10-10 22:40:05 +0300
commit02fedb2304b85c734dce8efc08d54106acdc8986 (patch)
tree06aa9504c9cae5bbde980e44a93c1d68a74ccb50
parentRearrange UI into several modules (diff)
Add filter list edit command
The UI is a simplified version of the filter UI. In fact the two views now use the same base class. Also fixes a bug in filters with displaying the correct value in the embed for filter settings.
-rw-r--r--bot/exts/filtering/_filter_lists/domain.py2
-rw-r--r--bot/exts/filtering/_ui/filter.py102
-rw-r--r--bot/exts/filtering/_ui/filter_list.py166
-rw-r--r--bot/exts/filtering/_ui/ui.py97
-rw-r--r--bot/exts/filtering/filtering.py170
5 files changed, 393 insertions, 144 deletions
diff --git a/bot/exts/filtering/_filter_lists/domain.py b/bot/exts/filtering/_filter_lists/domain.py
index c407108ca..cae2eb878 100644
--- a/bot/exts/filtering/_filter_lists/domain.py
+++ b/bot/exts/filtering/_filter_lists/domain.py
@@ -27,7 +27,7 @@ class DomainsList(FilterList):
individual filters.
Domains are found by looking for a URL schema (http or https).
- Filters will also trigger for subdomains unless set otherwise.
+ Filters will also trigger for subdomains.
"""
name = "domain"
diff --git a/bot/exts/filtering/_ui/filter.py b/bot/exts/filtering/_ui/filter.py
index a26f5a841..4da8fe001 100644
--- a/bot/exts/filtering/_ui/filter.py
+++ b/bot/exts/filtering/_ui/filter.py
@@ -1,14 +1,10 @@
from __future__ import annotations
-import re
-from enum import EnumMeta
-from functools import partial
from typing import Any, Callable
import discord
import discord.ui
from botcore.site_api import ResponseCodeError
-from botcore.utils import scheduling
from discord import Embed, Interaction, User
from discord.ext.commands import BadArgument
from discord.ui.select import SelectOption
@@ -16,23 +12,14 @@ from discord.ui.select import SelectOption
from bot.exts.filtering._filter_lists.filter_list import FilterList, ListType
from bot.exts.filtering._filters.filter import Filter
from bot.exts.filtering._ui.ui import (
- BooleanSelectView, COMPONENT_TIMEOUT, CustomCallbackSelect, EnumSelectView, FreeInputModal, SequenceEditView,
- format_response_error, parse_value, remove_optional
+ COMPONENT_TIMEOUT, CustomCallbackSelect, EditBaseView, MISSING, SETTINGS_DELIMITER, SINGLE_SETTING_PATTERN,
+ format_response_error, parse_value, populate_embed_from_dict
)
from bot.exts.filtering._utils import repr_equals, to_serializable
from bot.log import get_logger
log = get_logger(__name__)
-# Max number of characters in a Discord embed field value, minus 6 characters for a placeholder.
-MAX_FIELD_SIZE = 1018
-# Max number of characters for an embed field's value before it should take its own line.
-MAX_INLINE_SIZE = 50
-# Number of seconds before a settings editing view timeout.
-EDIT_TIMEOUT = 600
-# Max length of modal text component label
-MAX_MODAL_LABEL_LENGTH = 45
-
def build_filter_repr_dict(
filter_list: FilterList,
@@ -63,30 +50,17 @@ def build_filter_repr_dict(
if name not in extra_fields_overrides or repr_equals(extra_fields_overrides[name], value):
total_values[f"{filter_type.name}/{name}"] = value
else:
- total_values[f"{filter_type.name}/{name}*"] = value
+ total_values[f"{filter_type.name}/{name}*"] = extra_fields_overrides[name]
return total_values
-def populate_embed_from_dict(embed: Embed, data: dict) -> None:
- """Populate a Discord embed by populating fields from the given dict."""
- for setting, value in data.items():
- if setting.startswith("_"):
- continue
- if type(value) in (set, tuple):
- value = list(value)
- value = str(value) if value not in ("", None) else "-"
- if len(value) > MAX_FIELD_SIZE:
- value = value[:MAX_FIELD_SIZE] + " [...]"
- embed.add_field(name=setting, value=value, inline=len(value) < MAX_INLINE_SIZE)
-
-
class EditContentModal(discord.ui.Modal, title="Edit Content"):
"""A modal to input a filter's content."""
content = discord.ui.TextInput(label="Content")
- def __init__(self, embed_view: SettingsEditView, message: discord.Message):
+ def __init__(self, embed_view: FilterEditView, message: discord.Message):
super().__init__(timeout=COMPONENT_TIMEOUT)
self.embed_view = embed_view
self.message = message
@@ -102,7 +76,7 @@ class EditDescriptionModal(discord.ui.Modal, title="Edit Description"):
description = discord.ui.TextInput(label="Description")
- def __init__(self, embed_view: SettingsEditView, message: discord.Message):
+ def __init__(self, embed_view: FilterEditView, message: discord.Message):
super().__init__(timeout=COMPONENT_TIMEOUT)
self.embed_view = embed_view
self.message = message
@@ -118,7 +92,7 @@ class TemplateModal(discord.ui.Modal, title="Template"):
template = discord.ui.TextInput(label="Template Filter ID")
- def __init__(self, embed_view: SettingsEditView, message: discord.Message):
+ def __init__(self, embed_view: FilterEditView, message: discord.Message):
super().__init__(timeout=COMPONENT_TIMEOUT)
self.embed_view = embed_view
self.message = message
@@ -128,7 +102,7 @@ class TemplateModal(discord.ui.Modal, title="Template"):
await self.embed_view.apply_template(self.template.value, self.message, interaction)
-class SettingsEditView(discord.ui.View):
+class FilterEditView(EditBaseView):
"""A view used to edit a filter's settings before updating the database."""
class _REMOVE:
@@ -149,7 +123,7 @@ class SettingsEditView(discord.ui.View):
embed: Embed,
confirm_callback: Callable
):
- super().__init__(timeout=EDIT_TIMEOUT)
+ super().__init__(author)
self.filter_list = filter_list
self.list_type = list_type
self.filter_type = filter_type
@@ -159,7 +133,6 @@ class SettingsEditView(discord.ui.View):
self.filter_settings_overrides = filter_settings_overrides
self.loaded_settings = loaded_settings
self.loaded_filter_settings = loaded_filter_settings
- self.author = author
self.embed = embed
self.confirm_callback = confirm_callback
@@ -175,7 +148,7 @@ class SettingsEditView(discord.ui.View):
})
add_select = CustomCallbackSelect(
- self._prompt_new_override,
+ self._prompt_new_value,
placeholder="Select a setting to edit",
options=[SelectOption(label=name) for name in sorted(self.type_per_setting_name)],
row=1
@@ -194,10 +167,6 @@ class SettingsEditView(discord.ui.View):
if remove_select.options:
self.add_item(remove_select)
- async def interaction_check(self, interaction: Interaction) -> bool:
- """Only allow interactions from the command invoker."""
- return interaction.user.id == self.author.id
-
@discord.ui.button(label="Edit Content", row=3)
async def edit_content(self, interaction: Interaction, button: discord.ui.Button) -> None:
"""A button to edit the filter's content. Pressing the button invokes a modal."""
@@ -259,44 +228,24 @@ class SettingsEditView(discord.ui.View):
await interaction.response.edit_message(content="🚫 Operation canceled.", embed=None, view=None)
self.stop()
- async def _prompt_new_override(self, interaction: Interaction, select: discord.ui.Select) -> None:
- """Prompt the user to give an override value for the setting they selected, and respond to the interaction."""
- setting_name = select.values[0]
- type_ = self.type_per_setting_name[setting_name]
- is_optional, type_ = remove_optional(type_)
- if hasattr(type_, "__origin__"): # In case this is a types.GenericAlias or a typing._GenericAlias
- type_ = type_.__origin__
- new_view = self.copy()
- # This is in order to not block the interaction response. There's a potential race condition here, since
- # a view's method is used without guaranteeing the task completed, but since it depends on user input
- # realistically it shouldn't happen.
- scheduling.create_task(interaction.message.edit(view=new_view))
- update_callback = partial(new_view.update_embed, interaction_or_msg=interaction.message)
- if type_ is bool:
- 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_value = self.settings_overrides.get(setting_name, [])
- await interaction.response.send_message(
- f"Current list: {current_value}",
- view=SequenceEditView(setting_name, current_value, type_, update_callback),
- ephemeral=True
- )
- elif isinstance(type_, EnumMeta):
- view = EnumSelectView(setting_name, type_, update_callback)
- await interaction.response.send_message(f"Choose a value for `{setting_name}`:", view=view, ephemeral=True)
- else:
- await interaction.response.send_modal(FreeInputModal(setting_name, not is_optional, type_, update_callback))
- self.stop()
+ def current_value(self, setting_name: str) -> Any:
+ """Get the current value stored for the setting or MISSING if none found."""
+ if setting_name in self.settings_overrides:
+ return self.settings_overrides[setting_name]
+ if "/" in setting_name:
+ _, setting_name = setting_name.split("/", maxsplit=1)
+ if setting_name in self.filter_settings_overrides:
+ return self.filter_settings_overrides[setting_name]
+ return MISSING
async def update_embed(
self,
interaction_or_msg: discord.Interaction | discord.Message,
*,
content: str | None = None,
- description: str | type[SettingsEditView._REMOVE] | None = None,
+ description: str | type[FilterEditView._REMOVE] | None = None,
setting_name: str | None = None,
- setting_value: str | type[SettingsEditView._REMOVE] | None = None,
+ setting_value: str | type[FilterEditView._REMOVE] | None = None,
) -> None:
"""
Update the embed with the new information.
@@ -386,9 +335,9 @@ class SettingsEditView(discord.ui.View):
"""
await self.update_embed(interaction, setting_name=select.values[0], setting_value=self._REMOVE)
- def copy(self) -> SettingsEditView:
+ def copy(self) -> FilterEditView:
"""Create a copy of this view."""
- return SettingsEditView(
+ return FilterEditView(
self.filter_list,
self.list_type,
self.filter_type,
@@ -416,15 +365,12 @@ def description_and_settings_converter(
if not input_data:
return "", {}, {}
- settings_pattern = re.compile(r"\s+(?=\S+=\S+)")
- single_setting_pattern = re.compile(r"\w+=.+")
-
- parsed = settings_pattern.split(input_data)
+ parsed = SETTINGS_DELIMITER.split(input_data)
if not parsed:
return "", {}, {}
description = ""
- if not single_setting_pattern.match(parsed[0]):
+ if not SINGLE_SETTING_PATTERN.match(parsed[0]):
description, *parsed = parsed
settings = {setting: value for setting, value in [part.split("=", maxsplit=1) for part in parsed]}
diff --git a/bot/exts/filtering/_ui/filter_list.py b/bot/exts/filtering/_ui/filter_list.py
index 26852f13b..051521f1e 100644
--- a/bot/exts/filtering/_ui/filter_list.py
+++ b/bot/exts/filtering/_ui/filter_list.py
@@ -1,12 +1,174 @@
-from typing import Callable
+from __future__ import annotations
+
+from typing import Any, Callable
import discord
-from discord import Interaction, Member, User
+from botcore.site_api import ResponseCodeError
+from discord import Embed, Interaction, Member, SelectOption, User
+from discord.ext.commands import BadArgument
+
+from bot.exts.filtering._filter_lists import FilterList, ListType
+from bot.exts.filtering._ui.ui import (
+ CustomCallbackSelect, EditBaseView, MISSING, SETTINGS_DELIMITER, format_response_error, parse_value,
+ populate_embed_from_dict
+)
+from bot.exts.filtering._utils import repr_equals, to_serializable
# Amount of seconds to confirm the operation.
DELETION_TIMEOUT = 60
+def settings_converter(loaded_settings: dict, input_data: str) -> dict[str, Any]:
+ """Parse a string representing settings, and validate the setting names."""
+ if not input_data:
+ return {}
+
+ parsed = SETTINGS_DELIMITER.split(input_data)
+ if not parsed:
+ return {}
+
+ settings = {setting: value for setting, value in [part.split("=", maxsplit=1) for part in parsed]}
+ for setting in settings:
+ if setting not in loaded_settings:
+ raise BadArgument(f"{setting!r} is not a recognized setting.")
+ else:
+ type_ = loaded_settings[setting][2]
+ try:
+ parsed_value = parse_value(settings.pop(setting), type_)
+ settings[setting] = parsed_value
+ except (TypeError, ValueError) as e:
+ raise BadArgument(e)
+
+ return settings
+
+
+def build_filterlist_repr_dict(filter_list: FilterList, list_type: ListType, new_settings: dict) -> 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():
+ 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.
+ total_values = {}
+ for name, value in default_setting_values.items():
+ if name not in new_settings or repr_equals(new_settings[name], value):
+ total_values[name] = value
+ else:
+ total_values[f"{name}~"] = new_settings[name]
+
+ return total_values
+
+
+class FilterListEditView(EditBaseView):
+ """A view used to edit a filter's settings before updating the database."""
+
+ def __init__(
+ self,
+ filter_list: FilterList,
+ list_type: ListType,
+ new_settings: dict,
+ loaded_settings: dict,
+ author: User,
+ embed: Embed,
+ confirm_callback: Callable
+ ):
+ super().__init__(author)
+ self.filter_list = filter_list
+ self.list_type = list_type
+ self.settings = new_settings
+ self.loaded_settings = loaded_settings
+ self.embed = embed
+ self.confirm_callback = confirm_callback
+
+ self.settings_repr_dict = build_filterlist_repr_dict(filter_list, list_type, new_settings)
+ populate_embed_from_dict(embed, self.settings_repr_dict)
+
+ self.type_per_setting_name = {setting: info[2] for setting, info in loaded_settings.items()}
+
+ edit_select = CustomCallbackSelect(
+ self._prompt_new_value,
+ placeholder="Select a setting to edit",
+ options=[SelectOption(label=name) for name in sorted(self.type_per_setting_name)],
+ row=0
+ )
+ self.add_item(edit_select)
+
+ @discord.ui.button(label="✅ Confirm", style=discord.ButtonStyle.green, row=1)
+ async def confirm(self, interaction: Interaction, button: discord.ui.Button) -> None:
+ """Confirm the content, description, and settings, and update the filters database."""
+ await interaction.response.edit_message(view=None) # Make sure the interaction succeeds first.
+ try:
+ await self.confirm_callback(interaction.message, self.filter_list, self.list_type, self.settings)
+ except ResponseCodeError as e:
+ await interaction.message.reply(embed=format_response_error(e))
+ await interaction.message.edit(view=self)
+ else:
+ self.stop()
+
+ @discord.ui.button(label="🚫 Cancel", style=discord.ButtonStyle.red, row=1)
+ async def cancel(self, interaction: Interaction, button: discord.ui.Button) -> None:
+ """Cancel the operation."""
+ await interaction.response.edit_message(content="🚫 Operation canceled.", embed=None, view=None)
+ self.stop()
+
+ def current_value(self, setting_name: str) -> Any:
+ """Get the current value stored for the setting or MISSING if none found."""
+ if setting_name in self.settings:
+ return self.settings[setting_name]
+ if setting_name in self.settings_repr_dict:
+ return self.settings_repr_dict[setting_name]
+ return MISSING
+
+ async def update_embed(
+ self,
+ interaction_or_msg: discord.Interaction | discord.Message,
+ *,
+ setting_name: str | None = None,
+ setting_value: str | None = None,
+ ) -> None:
+ """
+ Update the embed with the new information.
+
+ If `interaction_or_msg` is a Message, the invoking Interaction must be deferred before calling this function.
+ """
+ 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)
+ 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.
+ elif setting_name in self.settings:
+ self.settings.pop(setting_name)
+
+ self.embed.clear_fields()
+ new_view = self.copy()
+
+ try:
+ if isinstance(interaction_or_msg, discord.Interaction):
+ 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 errors such as embed description being too long.
+ pass
+ else:
+ self.stop()
+
+ def copy(self) -> FilterListEditView:
+ """Create a copy of this view."""
+ return FilterListEditView(
+ self.filter_list,
+ self.list_type,
+ self.settings,
+ self.loaded_settings,
+ self.author,
+ self.embed,
+ self.confirm_callback
+ )
+
+
class DeleteConfirmationView(discord.ui.View):
"""A view to confirm the deletion of a filter list."""
diff --git a/bot/exts/filtering/_ui/ui.py b/bot/exts/filtering/_ui/ui.py
index b31094b25..dc3bd01c9 100644
--- a/bot/exts/filtering/_ui/ui.py
+++ b/bot/exts/filtering/_ui/ui.py
@@ -1,17 +1,28 @@
from __future__ import annotations
+import re
+from abc import ABC, abstractmethod
from enum import EnumMeta
+from functools import partial
from typing import Any, Callable, Coroutine, Optional, TypeVar, Union
import discord
from botcore.site_api import ResponseCodeError
+from botcore.utils import scheduling
from botcore.utils.logging import get_logger
from discord import Embed, Interaction
from discord.ext.commands import Context
-from discord.ui.select import MISSING, SelectOption
+from discord.ui.select import MISSING as SELECT_MISSING, SelectOption
log = get_logger(__name__)
+
+# Max number of characters in a Discord embed field value, minus 6 characters for a placeholder.
+MAX_FIELD_SIZE = 1018
+# Max number of characters for an embed field's value before it should take its own line.
+MAX_INLINE_SIZE = 50
+# Number of seconds before a settings editing view timeout.
+EDIT_TIMEOUT = 600
# Number of seconds before timeout of an editing component.
COMPONENT_TIMEOUT = 180
# Max length of modal title
@@ -20,9 +31,28 @@ MAX_MODAL_TITLE_LENGTH = 45
MAX_SELECT_ITEMS = 25
MAX_EMBED_DESCRIPTION = 4000
+SETTINGS_DELIMITER = re.compile(r"\s+(?=\S+=\S+)")
+SINGLE_SETTING_PATTERN = re.compile(r"\w+=.+")
+
+# Sentinel value to denote that a value is missing
+MISSING = object()
+
T = TypeVar('T')
+def populate_embed_from_dict(embed: Embed, data: dict) -> None:
+ """Populate a Discord embed by populating fields from the given dict."""
+ for setting, value in data.items():
+ if setting.startswith("_"):
+ continue
+ if type(value) in (set, tuple):
+ value = list(value)
+ value = str(value) if value not in ("", None) else "-"
+ if len(value) > MAX_FIELD_SIZE:
+ value = value[:MAX_FIELD_SIZE] + " [...]"
+ embed.add_field(name=setting, value=value, inline=len(value) < MAX_INLINE_SIZE)
+
+
def remove_optional(type_: type) -> tuple[bool, type]:
"""Return whether the type is Optional, and the Union of types which aren't None."""
if not hasattr(type_, "__args__"):
@@ -133,11 +163,11 @@ class CustomCallbackSelect(discord.ui.Select):
self,
callback: Callable[[Interaction, discord.ui.Select], Coroutine[None]],
*,
- custom_id: str = MISSING,
+ custom_id: str = SELECT_MISSING,
placeholder: str | None = None,
min_values: int = 1,
max_values: int = 1,
- options: list[SelectOption] = MISSING,
+ options: list[SelectOption] = SELECT_MISSING,
disabled: bool = False,
row: int | None = None,
):
@@ -330,3 +360,64 @@ class EnumSelectView(discord.ui.View):
def __init__(self, setting_name: str, enum_cls: EnumMeta, update_callback: Callable):
super().__init__(timeout=COMPONENT_TIMEOUT)
self.add_item(self.EnumSelect(setting_name, enum_cls, update_callback))
+
+
+class EditBaseView(ABC, discord.ui.View):
+ """A view used to edit embed fields based on a provided type."""
+
+ def __init__(self, author: discord.User):
+ super().__init__(timeout=EDIT_TIMEOUT)
+ self.author = author
+ self.type_per_setting_name = {}
+
+ async def interaction_check(self, interaction: Interaction) -> bool:
+ """Only allow interactions from the command invoker."""
+ return interaction.user.id == self.author.id
+
+ async def _prompt_new_value(self, interaction: Interaction, select: discord.ui.Select) -> None:
+ """Prompt the user to give an override value for the setting they selected, and respond to the interaction."""
+ setting_name = select.values[0]
+ type_ = self.type_per_setting_name[setting_name]
+ is_optional, type_ = remove_optional(type_)
+ if hasattr(type_, "__origin__"): # In case this is a types.GenericAlias or a typing._GenericAlias
+ type_ = type_.__origin__
+ new_view = self.copy()
+ # This is in order to not block the interaction response. There's a potential race condition here, since
+ # a view's method is used without guaranteeing the task completed, but since it depends on user input
+ # realistically it shouldn't happen.
+ scheduling.create_task(interaction.message.edit(view=new_view))
+ update_callback = partial(new_view.update_embed, interaction_or_msg=interaction.message)
+ if type_ is bool:
+ 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 = self.current_value(setting_name)
+ if current_list is MISSING:
+ current_list = []
+ await interaction.response.send_message(
+ f"Current list: {current_list}",
+ view=SequenceEditView(setting_name, current_list, type_, update_callback),
+ ephemeral=True
+ )
+ elif isinstance(type_, EnumMeta):
+ view = EnumSelectView(setting_name, type_, update_callback)
+ await interaction.response.send_message(f"Choose a value for `{setting_name}`:", view=view, ephemeral=True)
+ else:
+ await interaction.response.send_modal(FreeInputModal(setting_name, not is_optional, type_, update_callback))
+ self.stop()
+
+ @abstractmethod
+ def current_value(self, setting_name: str) -> Any:
+ """Get the current value stored for the setting or MISSING if none found."""
+
+ @abstractmethod
+ async def update_embed(self, interaction_or_msg: Interaction | discord.Message) -> None:
+ """
+ Update the embed with the new information.
+
+ If `interaction_or_msg` is a Message, the invoking Interaction must be deferred before calling this function.
+ """
+
+ @abstractmethod
+ def copy(self) -> EditBaseView:
+ """Create a copy of this view."""
diff --git a/bot/exts/filtering/filtering.py b/bot/exts/filtering/filtering.py
index 0bcf485c0..a906b6a41 100644
--- a/bot/exts/filtering/filtering.py
+++ b/bot/exts/filtering/filtering.py
@@ -15,7 +15,7 @@ from discord.utils import escape_markdown
import bot
import bot.exts.filtering._ui.filter as filters_ui
from bot.bot import Bot
-from bot.constants import Colours, MODERATION_ROLES, Webhooks
+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._filters.filter import Filter
@@ -23,7 +23,7 @@ from bot.exts.filtering._settings import ActionSettings
from bot.exts.filtering._ui.filter import (
build_filter_repr_dict, description_and_settings_converter, filter_overrides, populate_embed_from_dict
)
-from bot.exts.filtering._ui.filter_list import DeleteConfirmationView
+from bot.exts.filtering._ui.filter_list import DeleteConfirmationView, FilterListEditView, settings_converter
from bot.exts.filtering._ui.ui import ArgumentCompletionView
from bot.exts.filtering._utils import past_tense, to_serializable
from bot.log import get_logger
@@ -409,34 +409,34 @@ class Filtering(Cog):
await patch_func(
ctx.message, filter_list, list_type, filter_type, content, description, settings, filter_settings
)
+ return
- else:
- embed = Embed(colour=Colour.blue())
- embed.description = f"`{filter_.content}`"
- if description:
- embed.description += f" - {description}"
- embed.set_author(
- name=f"Filter #{filter_id} - {past_tense(list_type.name.lower())} {filter_list.name}".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}`."
- ))
-
- view = filters_ui.SettingsEditView(
- filter_list,
- list_type,
- filter_type,
- content,
- description,
- settings,
- filter_settings,
- self.loaded_settings,
- self.loaded_filter_settings,
- ctx.author,
- embed,
- patch_func
- )
- await ctx.send(embed=embed, reference=ctx.message, view=view)
+ embed = Embed(colour=Colour.blue())
+ embed.description = f"`{filter_.content}`"
+ if description:
+ embed.description += f" - {description}"
+ embed.set_author(
+ name=f"Filter #{filter_id} - {past_tense(list_type.name.lower())} {filter_list.name}".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}`."
+ ))
+
+ view = filters_ui.FilterEditView(
+ filter_list,
+ list_type,
+ filter_type,
+ content,
+ description,
+ settings,
+ filter_settings,
+ self.loaded_settings,
+ self.loaded_filter_settings,
+ ctx.author,
+ embed,
+ patch_func
+ )
+ await ctx.send(embed=embed, reference=ctx.message, view=view)
@filter.command(name="delete", aliases=("d", "remove"))
async def f_delete(self, ctx: Context, filter_id: int) -> None:
@@ -524,7 +524,50 @@ class Filtering(Cog):
)
await ctx.send(embed=embed)
+ @filterlist.command(name="edit", aliases=("e",))
+ @has_any_role(Roles.admins)
+ async def fl_edit(
+ self,
+ ctx: Context,
+ noui: Optional[Literal["noui"]],
+ list_type: Optional[list_type_converter] = None,
+ list_name: Optional[str] = None,
+ *,
+ settings: str | None
+ ) -> None:
+ """
+ Edit the filter list.
+
+ Unless `noui` is specified, a UI will be provided to edit the settings before confirmation.
+
+ 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.
+ """
+ result = await self._resolve_list_type_and_name(ctx, list_type, list_name)
+ if result is None:
+ return
+ list_type, filter_list = result
+ settings = settings_converter(self.loaded_settings, settings)
+ if noui:
+ 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_footer(text="Field names with a ~ have values which change the existing value in the filter list.")
+
+ view = FilterListEditView(
+ filter_list,
+ list_type,
+ settings,
+ self.loaded_settings,
+ ctx.author,
+ embed,
+ self._patch_filter_list
+ )
+ await ctx.send(embed=embed, reference=ctx.message, view=view)
+
@filterlist.command(name="delete", aliases=("remove",))
+ @has_any_role(Roles.admins)
async def fl_delete(
self, ctx: Context, list_type: Optional[list_type_converter] = None, list_name: Optional[str] = None
) -> None:
@@ -699,34 +742,34 @@ class Filtering(Cog):
)
except ValueError as e:
raise BadArgument(str(e))
+ return
- else:
- embed = Embed(colour=Colour.blue())
- embed.description = f"`{content}`" if content else "*No content*"
- if description:
- embed.description += f" - {description}"
- embed.set_author(
- name=f"New Filter - {past_tense(list_type.name.lower())} {filter_list.name}".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}`."
- ))
-
- view = filters_ui.SettingsEditView(
- filter_list,
- list_type,
- filter_type,
- content,
- description,
- settings,
- filter_settings,
- self.loaded_settings,
- self.loaded_filter_settings,
- ctx.author,
- embed,
- self._post_new_filter
- )
- await ctx.send(embed=embed, reference=ctx.message, view=view)
+ embed = Embed(colour=Colour.blue())
+ embed.description = f"`{content}`" if content else "*No content*"
+ if description:
+ embed.description += f" - {description}"
+ embed.set_author(
+ name=f"New Filter - {past_tense(list_type.name.lower())} {filter_list.name}".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}`."
+ ))
+
+ view = filters_ui.FilterEditView(
+ filter_list,
+ list_type,
+ filter_type,
+ content,
+ description,
+ settings,
+ filter_settings,
+ self.loaded_settings,
+ self.loaded_filter_settings,
+ ctx.author,
+ embed,
+ self._post_new_filter
+ )
+ await ctx.send(embed=embed, reference=ctx.message, view=view)
@staticmethod
def _identical_filters_message(content: str, filter_list: FilterList, list_type: ListType, filter_: Filter) -> str:
@@ -794,20 +837,27 @@ class Filtering(Cog):
# If the setting is not in `settings`, the override was either removed, or there wasn't one in the first place.
for current_settings in (filter_.actions, filter_.validations):
if current_settings:
- for _, setting_entry in current_settings.items():
+ for setting_entry in current_settings.values():
settings.update({setting: None for setting in setting_entry.dict() if setting not in settings})
- list_id = filter_list.list_ids[list_type]
description = description or None
payload = {
- "filter_list": list_id, "content": content, "description": description,
- "additional_field": json.dumps(filter_settings), **settings
+ "content": content, "description": description, "additional_field": json.dumps(filter_settings), **settings
}
response = await bot.instance.api_client.patch(f'bot/filter/filters/{filter_.id}', json=payload)
edited_filter = filter_list.add_filter(response, list_type)
extra_msg = Filtering._identical_filters_message(content, filter_list, list_type, edited_filter)
await msg.reply(f"✅ Edited filter: {edited_filter}" + extra_msg)
+ @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]
+ response = await bot.instance.api_client.patch(f'bot/filter/filter_lists/{list_id}', json=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}")
+
# endregion