aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/exts/filtering/_ui/filter.py334
-rw-r--r--bot/exts/filtering/_ui/ui.py332
-rw-r--r--bot/exts/filtering/filtering.py4
3 files changed, 344 insertions, 326 deletions
diff --git a/bot/exts/filtering/_ui/filter.py b/bot/exts/filtering/_ui/filter.py
index a6bc1addd..a26f5a841 100644
--- a/bot/exts/filtering/_ui/filter.py
+++ b/bot/exts/filtering/_ui/filter.py
@@ -3,18 +3,22 @@ from __future__ import annotations
import re
from enum import EnumMeta
from functools import partial
-from typing import Any, Callable, Coroutine, Optional, TypeVar, Union
+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, Context
-from discord.ui.select import MISSING, SelectOption
+from discord.ext.commands import BadArgument
+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
+)
from bot.exts.filtering._utils import repr_equals, to_serializable
from bot.log import get_logger
@@ -26,76 +30,8 @@ MAX_FIELD_SIZE = 1018
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
-MAX_MODAL_TITLE_LENGTH = 45
# Max length of modal text component label
MAX_MODAL_LABEL_LENGTH = 45
-# Max number of items in a select
-MAX_SELECT_ITEMS = 25
-MAX_EMBED_DESCRIPTION = 4000
-
-T = TypeVar('T')
-
-
-class ArgumentCompletionSelect(discord.ui.Select):
- """A select detailing the options that can be picked to assign to a missing argument."""
-
- def __init__(
- self,
- ctx: Context,
- args: list,
- arg_name: str,
- options: list[str],
- position: int,
- converter: Optional[Callable] = None
- ):
- super().__init__(
- placeholder=f"Select a value for {arg_name!r}",
- options=[discord.SelectOption(label=option) for option in options]
- )
- self.ctx = ctx
- self.args = args
- self.position = position
- self.converter = converter
-
- async def callback(self, interaction: discord.Interaction) -> None:
- """re-invoke the context command with the completed argument value."""
- await interaction.response.defer()
- value = interaction.data["values"][0]
- if self.converter:
- value = self.converter(value)
- args = self.args.copy() # This makes the view reusable.
- args.insert(self.position, value)
- log.trace(f"Argument filled with the value {value}. Re-invoking command")
- await self.ctx.invoke(self.ctx.command, *args)
-
-
-class ArgumentCompletionView(discord.ui.View):
- """A view used to complete a missing argument in an in invoked command."""
-
- def __init__(
- self,
- ctx: Context,
- args: list,
- arg_name: str,
- options: list[str],
- position: int,
- converter: Optional[Callable] = None
- ):
- super().__init__()
- log.trace(f"The {arg_name} argument was designated missing in the invocation {ctx.view.buffer!r}")
- self.add_item(ArgumentCompletionSelect(ctx, args, arg_name, options, position, converter))
- self.ctx = ctx
-
- async def interaction_check(self, interaction: discord.Interaction) -> bool:
- """Check to ensure that the interacting user is the user who invoked the command."""
- if interaction.user != self.ctx.author:
- embed = discord.Embed(description="Sorry, but this dropdown menu can only be used by the original author.")
- await interaction.response.send_message(embed=embed, ephemeral=True)
- return False
- return True
def build_filter_repr_dict(
@@ -145,37 +81,6 @@ def populate_embed_from_dict(embed: Embed, data: dict) -> None:
embed.add_field(name=setting, value=value, inline=len(value) < MAX_INLINE_SIZE)
-class CustomCallbackSelect(discord.ui.Select):
- """A selection which calls the provided callback on interaction."""
-
- def __init__(
- self,
- callback: Callable[[Interaction, discord.ui.Select], Coroutine[None]],
- *,
- custom_id: str = MISSING,
- placeholder: str | None = None,
- min_values: int = 1,
- max_values: int = 1,
- options: list[SelectOption] = MISSING,
- disabled: bool = False,
- row: int | None = None,
- ):
- super().__init__(
- custom_id=custom_id,
- placeholder=placeholder,
- min_values=min_values,
- max_values=max_values,
- options=options,
- disabled=disabled,
- row=row
- )
- self.custom_callback = callback
-
- async def callback(self, interaction: Interaction) -> Any:
- """Invoke the provided callback."""
- await self.custom_callback(interaction, self)
-
-
class EditContentModal(discord.ui.Modal, title="Edit Content"):
"""A modal to input a filter's content."""
@@ -208,181 +113,6 @@ class EditDescriptionModal(discord.ui.Modal, title="Edit Description"):
await self.embed_view.update_embed(self.message, description=self.description.value)
-class BooleanSelectView(discord.ui.View):
- """A view containing an instance of BooleanSelect."""
-
- class BooleanSelect(discord.ui.Select):
- """Select a true or false value and send it to the supplied callback."""
-
- def __init__(self, setting_name: str, update_callback: Callable):
- super().__init__(options=[SelectOption(label="True"), SelectOption(label="False")])
- self.setting_name = setting_name
- self.update_callback = update_callback
-
- async def callback(self, interaction: Interaction) -> Any:
- """Respond to the interaction by sending the boolean value to the update callback."""
- await interaction.response.edit_message(content="✅ Edit confirmed", view=None)
- value = self.values[0] == "True"
- await self.update_callback(setting_name=self.setting_name, setting_value=value)
-
- def __init__(self, setting_name: str, update_callback: Callable):
- super().__init__(timeout=COMPONENT_TIMEOUT)
- self.add_item(self.BooleanSelect(setting_name, update_callback))
-
-
-class FreeInputModal(discord.ui.Modal):
- """A modal to freely enter a value for a setting."""
-
- def __init__(self, setting_name: str, required: bool, type_: type, update_callback: Callable):
- title = f"{setting_name} Input" if len(setting_name) < MAX_MODAL_TITLE_LENGTH - 6 else "Setting Input"
- super().__init__(timeout=COMPONENT_TIMEOUT, title=title)
-
- self.setting_name = setting_name
- self.type_ = type_
- self.update_callback = update_callback
-
- label = setting_name if len(setting_name) < MAX_MODAL_TITLE_LENGTH else "Value"
- self.setting_input = discord.ui.TextInput(label=label, style=discord.TextStyle.paragraph, required=required)
- self.add_item(self.setting_input)
-
- async def on_submit(self, interaction: Interaction) -> None:
- """Update the setting with the new value in the embed."""
- try:
- value = self.type_(self.setting_input.value)
- except (ValueError, TypeError):
- await interaction.response.send_message(
- f"Could not process the input value for `{self.setting_name}`.", ephemeral=True
- )
- else:
- await interaction.response.defer()
- await self.update_callback(setting_name=self.setting_name, setting_value=value)
-
-
-class SequenceEditView(discord.ui.View):
- """A view to modify the contents of a sequence of values."""
-
- class SingleItemModal(discord.ui.Modal):
- """A modal to enter a single list item."""
-
- new_item = discord.ui.TextInput(label="New Item")
-
- def __init__(self, view: SequenceEditView):
- super().__init__(title="Item Addition", timeout=COMPONENT_TIMEOUT)
- self.view = view
-
- async def on_submit(self, interaction: Interaction) -> None:
- """Send the submitted value to be added to the list."""
- await self.view.apply_addition(interaction, self.new_item.value)
-
- class NewListModal(discord.ui.Modal):
- """A modal to enter new contents for the list."""
-
- new_value = discord.ui.TextInput(label="Enter comma separated values", style=discord.TextStyle.paragraph)
-
- def __init__(self, view: SequenceEditView):
- super().__init__(title="New List", timeout=COMPONENT_TIMEOUT)
- self.view = view
-
- async def on_submit(self, interaction: Interaction) -> None:
- """Send the submitted value to be added to the list."""
- await self.view.apply_edit(interaction, self.new_value.value)
-
- def __init__(self, setting_name: str, starting_value: list, type_: type, update_callback: Callable):
- super().__init__(timeout=COMPONENT_TIMEOUT)
- self.setting_name = setting_name
- self.stored_value = starting_value
- self.type_ = type_
- self.update_callback = update_callback
-
- options = [SelectOption(label=item) for item in starting_value[:MAX_SELECT_ITEMS]]
- self.removal_select = CustomCallbackSelect(
- self.apply_removal, placeholder="Enter an item to remove", options=options, row=1
- )
- if starting_value:
- self.add_item(self.removal_select)
-
- async def apply_removal(self, interaction: Interaction, select: discord.ui.Select) -> None:
- """Remove an item from the list."""
- # The value might not be stored as a string.
- _i = len(self.stored_value)
- for _i, element in enumerate(self.stored_value):
- if str(element) == select.values[0]:
- break
- if _i != len(self.stored_value):
- self.stored_value.pop(_i)
-
- select.options = [SelectOption(label=item) for item in self.stored_value[:MAX_SELECT_ITEMS]]
- if not self.stored_value:
- self.remove_item(self.removal_select)
- await interaction.response.edit_message(content=f"Current list: {self.stored_value}", view=self)
-
- async def apply_addition(self, interaction: Interaction, item: str) -> None:
- """Add an item to the list."""
- if item in self.stored_value: # Ignore duplicates
- await interaction.response.defer()
- return
-
- self.stored_value.append(item)
- self.removal_select.options = [SelectOption(label=item) for item in self.stored_value[:MAX_SELECT_ITEMS]]
- if len(self.stored_value) == 1:
- self.add_item(self.removal_select)
- await interaction.response.edit_message(content=f"Current list: {self.stored_value}", view=self)
-
- async def apply_edit(self, interaction: Interaction, new_list: str) -> None:
- """Change the contents of the list."""
- self.stored_value = list(set(new_list.split(",")))
- self.removal_select.options = [SelectOption(label=item) for item in self.stored_value[:MAX_SELECT_ITEMS]]
- if len(self.stored_value) == 1:
- self.add_item(self.removal_select)
- await interaction.response.edit_message(content=f"Current list: {self.stored_value}", view=self)
-
- @discord.ui.button(label="Add Value")
- async def add_value(self, interaction: Interaction, button: discord.ui.Button) -> None:
- """A button to add an item to the list."""
- await interaction.response.send_modal(self.SingleItemModal(self))
-
- @discord.ui.button(label="Free Input")
- async def free_input(self, interaction: Interaction, button: discord.ui.Button) -> None:
- """A button to change the entire list."""
- await interaction.response.send_modal(self.NewListModal(self))
-
- @discord.ui.button(label="✅ Confirm", style=discord.ButtonStyle.green)
- async def confirm(self, interaction: Interaction, button: discord.ui.Button) -> None:
- """Send the final value to the embed editor."""
- # Edit first, it might time out otherwise.
- await interaction.response.edit_message(content="✅ Edit confirmed", view=None)
- await self.update_callback(setting_name=self.setting_name, setting_value=self.stored_value)
- self.stop()
-
- @discord.ui.button(label="🚫 Cancel", style=discord.ButtonStyle.red)
- async def cancel(self, interaction: Interaction, button: discord.ui.Button) -> None:
- """Cancel the list editing."""
- await interaction.response.edit_message(content="🚫 Canceled", view=None)
- self.stop()
-
-
-class EnumSelectView(discord.ui.View):
- """A view containing an instance of EnumSelect."""
-
- class EnumSelect(discord.ui.Select):
- """Select an enum value and send it to the supplied callback."""
-
- def __init__(self, setting_name: str, enum_cls: EnumMeta, update_callback: Callable):
- super().__init__(options=[SelectOption(label=elem.name) for elem in enum_cls])
- self.setting_name = setting_name
- self.enum_cls = enum_cls
- self.update_callback = update_callback
-
- async def callback(self, interaction: Interaction) -> Any:
- """Respond to the interaction by sending the enum value to the update callback."""
- await interaction.response.edit_message(content="✅ Edit confirmed", view=None)
- await self.update_callback(setting_name=self.setting_name, setting_value=self.values[0])
-
- 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 TemplateModal(discord.ui.Modal, title="Template"):
"""A modal to enter a filter ID to copy its overrides over."""
@@ -533,7 +263,7 @@ class SettingsEditView(discord.ui.View):
"""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_)
+ 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()
@@ -674,34 +404,6 @@ class SettingsEditView(discord.ui.View):
)
-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__"):
- return False, type_
- args = list(type_.__args__)
- if type(None) not in args:
- return False, type_
- args.remove(type(None))
- return True, Union[tuple(args)]
-
-
-def _parse_value(value: str, type_: type[T]) -> T:
- """Parse the value and attempt to convert it to the provided type."""
- is_optional, type_ = _remove_optional(type_)
- if is_optional and value == '""':
- return None
- if hasattr(type_, "__origin__"): # In case this is a types.GenericAlias or a typing._GenericAlias
- type_ = type_.__origin__
- if type_ in (tuple, list, set):
- return type_(value.split(","))
- if type_ is bool:
- return value == "True"
- if isinstance(type_, EnumMeta):
- return type_[value.upper()]
-
- return type_(value)
-
-
def description_and_settings_converter(
filter_list: FilterList,
list_type: ListType,
@@ -745,7 +447,7 @@ def description_and_settings_converter(
raise BadArgument(f"{setting!r} is not a recognized setting.")
type_ = loaded_filter_settings[filter_list.name][filter_setting_name][2]
try:
- parsed_value = _parse_value(settings.pop(setting), type_)
+ parsed_value = parse_value(settings.pop(setting), type_)
if not repr_equals(parsed_value, getattr(filter_type.extra_fields_type(), filter_setting_name)):
filter_settings[filter_setting_name] = parsed_value
except (TypeError, ValueError) as e:
@@ -756,7 +458,7 @@ def description_and_settings_converter(
else:
type_ = loaded_settings[setting][2]
try:
- parsed_value = _parse_value(settings.pop(setting), type_)
+ parsed_value = parse_value(settings.pop(setting), type_)
if not repr_equals(parsed_value, filter_list.default(list_type, setting)):
settings[setting] = parsed_value
except (TypeError, ValueError) as e:
@@ -810,19 +512,3 @@ def template_settings(filter_id: str, filter_list: FilterList, list_type: ListTy
)
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 = ""
- if "non_field_errors" in e.response_json:
- non_field_errors = e.response_json.pop("non_field_errors")
- description += "\n".join(f"• {error}" for error in non_field_errors) + "\n"
- for field, errors in e.response_json.items():
- description += "\n".join(f"• {field} - {error}" for error in errors) + "\n"
- description = description.strip()
- if len(description) > MAX_EMBED_DESCRIPTION:
- description = description[:MAX_EMBED_DESCRIPTION] + "[...]"
-
- embed = Embed(colour=discord.Colour.red(), title="Oops...", description=description)
- return embed
diff --git a/bot/exts/filtering/_ui/ui.py b/bot/exts/filtering/_ui/ui.py
new file mode 100644
index 000000000..b31094b25
--- /dev/null
+++ b/bot/exts/filtering/_ui/ui.py
@@ -0,0 +1,332 @@
+from __future__ import annotations
+
+from enum import EnumMeta
+from typing import Any, Callable, Coroutine, Optional, TypeVar, Union
+
+import discord
+from botcore.site_api import ResponseCodeError
+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
+
+log = get_logger(__name__)
+
+# Number of seconds before timeout of an editing component.
+COMPONENT_TIMEOUT = 180
+# Max length of modal title
+MAX_MODAL_TITLE_LENGTH = 45
+# Max number of items in a select
+MAX_SELECT_ITEMS = 25
+MAX_EMBED_DESCRIPTION = 4000
+
+T = TypeVar('T')
+
+
+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__"):
+ return False, type_
+ args = list(type_.__args__)
+ if type(None) not in args:
+ return False, type_
+ args.remove(type(None))
+ return True, Union[tuple(args)]
+
+
+def parse_value(value: str, type_: type[T]) -> T:
+ """Parse the value and attempt to convert it to the provided type."""
+ is_optional, type_ = remove_optional(type_)
+ if is_optional and value == '""':
+ return None
+ if hasattr(type_, "__origin__"): # In case this is a types.GenericAlias or a typing._GenericAlias
+ type_ = type_.__origin__
+ if type_ in (tuple, list, set):
+ return type_(value.split(","))
+ if type_ is bool:
+ return value == "True"
+ if isinstance(type_, EnumMeta):
+ return type_[value.upper()]
+
+ return type_(value)
+
+
+def format_response_error(e: ResponseCodeError) -> Embed:
+ """Format the response error into an embed."""
+ description = ""
+ if "non_field_errors" in e.response_json:
+ non_field_errors = e.response_json.pop("non_field_errors")
+ description += "\n".join(f"• {error}" for error in non_field_errors) + "\n"
+ for field, errors in e.response_json.items():
+ description += "\n".join(f"• {field} - {error}" for error in errors) + "\n"
+ description = description.strip()
+ if len(description) > MAX_EMBED_DESCRIPTION:
+ description = description[:MAX_EMBED_DESCRIPTION] + "[...]"
+
+ embed = Embed(colour=discord.Colour.red(), title="Oops...", description=description)
+ return embed
+
+
+class ArgumentCompletionSelect(discord.ui.Select):
+ """A select detailing the options that can be picked to assign to a missing argument."""
+
+ def __init__(
+ self,
+ ctx: Context,
+ args: list,
+ arg_name: str,
+ options: list[str],
+ position: int,
+ converter: Optional[Callable] = None
+ ):
+ super().__init__(
+ placeholder=f"Select a value for {arg_name!r}",
+ options=[discord.SelectOption(label=option) for option in options]
+ )
+ self.ctx = ctx
+ self.args = args
+ self.position = position
+ self.converter = converter
+
+ async def callback(self, interaction: discord.Interaction) -> None:
+ """re-invoke the context command with the completed argument value."""
+ await interaction.response.defer()
+ value = interaction.data["values"][0]
+ if self.converter:
+ value = self.converter(value)
+ args = self.args.copy() # This makes the view reusable.
+ args.insert(self.position, value)
+ log.trace(f"Argument filled with the value {value}. Re-invoking command")
+ await self.ctx.invoke(self.ctx.command, *args)
+
+
+class ArgumentCompletionView(discord.ui.View):
+ """A view used to complete a missing argument in an in invoked command."""
+
+ def __init__(
+ self,
+ ctx: Context,
+ args: list,
+ arg_name: str,
+ options: list[str],
+ position: int,
+ converter: Optional[Callable] = None
+ ):
+ super().__init__()
+ log.trace(f"The {arg_name} argument was designated missing in the invocation {ctx.view.buffer!r}")
+ self.add_item(ArgumentCompletionSelect(ctx, args, arg_name, options, position, converter))
+ self.ctx = ctx
+
+ async def interaction_check(self, interaction: discord.Interaction) -> bool:
+ """Check to ensure that the interacting user is the user who invoked the command."""
+ if interaction.user != self.ctx.author:
+ embed = discord.Embed(description="Sorry, but this dropdown menu can only be used by the original author.")
+ await interaction.response.send_message(embed=embed, ephemeral=True)
+ return False
+ return True
+
+
+class CustomCallbackSelect(discord.ui.Select):
+ """A selection which calls the provided callback on interaction."""
+
+ def __init__(
+ self,
+ callback: Callable[[Interaction, discord.ui.Select], Coroutine[None]],
+ *,
+ custom_id: str = MISSING,
+ placeholder: str | None = None,
+ min_values: int = 1,
+ max_values: int = 1,
+ options: list[SelectOption] = MISSING,
+ disabled: bool = False,
+ row: int | None = None,
+ ):
+ super().__init__(
+ custom_id=custom_id,
+ placeholder=placeholder,
+ min_values=min_values,
+ max_values=max_values,
+ options=options,
+ disabled=disabled,
+ row=row
+ )
+ self.custom_callback = callback
+
+ async def callback(self, interaction: Interaction) -> Any:
+ """Invoke the provided callback."""
+ await self.custom_callback(interaction, self)
+
+
+class BooleanSelectView(discord.ui.View):
+ """A view containing an instance of BooleanSelect."""
+
+ class BooleanSelect(discord.ui.Select):
+ """Select a true or false value and send it to the supplied callback."""
+
+ def __init__(self, setting_name: str, update_callback: Callable):
+ super().__init__(options=[SelectOption(label="True"), SelectOption(label="False")])
+ self.setting_name = setting_name
+ self.update_callback = update_callback
+
+ async def callback(self, interaction: Interaction) -> Any:
+ """Respond to the interaction by sending the boolean value to the update callback."""
+ await interaction.response.edit_message(content="✅ Edit confirmed", view=None)
+ value = self.values[0] == "True"
+ await self.update_callback(setting_name=self.setting_name, setting_value=value)
+
+ def __init__(self, setting_name: str, update_callback: Callable):
+ super().__init__(timeout=COMPONENT_TIMEOUT)
+ self.add_item(self.BooleanSelect(setting_name, update_callback))
+
+
+class FreeInputModal(discord.ui.Modal):
+ """A modal to freely enter a value for a setting."""
+
+ def __init__(self, setting_name: str, required: bool, type_: type, update_callback: Callable):
+ title = f"{setting_name} Input" if len(setting_name) < MAX_MODAL_TITLE_LENGTH - 6 else "Setting Input"
+ super().__init__(timeout=COMPONENT_TIMEOUT, title=title)
+
+ self.setting_name = setting_name
+ self.type_ = type_
+ self.update_callback = update_callback
+
+ label = setting_name if len(setting_name) < MAX_MODAL_TITLE_LENGTH else "Value"
+ self.setting_input = discord.ui.TextInput(label=label, style=discord.TextStyle.paragraph, required=required)
+ self.add_item(self.setting_input)
+
+ async def on_submit(self, interaction: Interaction) -> None:
+ """Update the setting with the new value in the embed."""
+ try:
+ value = self.type_(self.setting_input.value)
+ except (ValueError, TypeError):
+ await interaction.response.send_message(
+ f"Could not process the input value for `{self.setting_name}`.", ephemeral=True
+ )
+ else:
+ await interaction.response.defer()
+ await self.update_callback(setting_name=self.setting_name, setting_value=value)
+
+
+class SequenceEditView(discord.ui.View):
+ """A view to modify the contents of a sequence of values."""
+
+ class SingleItemModal(discord.ui.Modal):
+ """A modal to enter a single list item."""
+
+ new_item = discord.ui.TextInput(label="New Item")
+
+ def __init__(self, view: SequenceEditView):
+ super().__init__(title="Item Addition", timeout=COMPONENT_TIMEOUT)
+ self.view = view
+
+ async def on_submit(self, interaction: Interaction) -> None:
+ """Send the submitted value to be added to the list."""
+ await self.view.apply_addition(interaction, self.new_item.value)
+
+ class NewListModal(discord.ui.Modal):
+ """A modal to enter new contents for the list."""
+
+ new_value = discord.ui.TextInput(label="Enter comma separated values", style=discord.TextStyle.paragraph)
+
+ def __init__(self, view: SequenceEditView):
+ super().__init__(title="New List", timeout=COMPONENT_TIMEOUT)
+ self.view = view
+
+ async def on_submit(self, interaction: Interaction) -> None:
+ """Send the submitted value to be added to the list."""
+ await self.view.apply_edit(interaction, self.new_value.value)
+
+ def __init__(self, setting_name: str, starting_value: list, type_: type, update_callback: Callable):
+ super().__init__(timeout=COMPONENT_TIMEOUT)
+ self.setting_name = setting_name
+ self.stored_value = starting_value
+ self.type_ = type_
+ self.update_callback = update_callback
+
+ options = [SelectOption(label=item) for item in starting_value[:MAX_SELECT_ITEMS]]
+ self.removal_select = CustomCallbackSelect(
+ self.apply_removal, placeholder="Enter an item to remove", options=options, row=1
+ )
+ if starting_value:
+ self.add_item(self.removal_select)
+
+ async def apply_removal(self, interaction: Interaction, select: discord.ui.Select) -> None:
+ """Remove an item from the list."""
+ # The value might not be stored as a string.
+ _i = len(self.stored_value)
+ for _i, element in enumerate(self.stored_value):
+ if str(element) == select.values[0]:
+ break
+ if _i != len(self.stored_value):
+ self.stored_value.pop(_i)
+
+ select.options = [SelectOption(label=item) for item in self.stored_value[:MAX_SELECT_ITEMS]]
+ if not self.stored_value:
+ self.remove_item(self.removal_select)
+ await interaction.response.edit_message(content=f"Current list: {self.stored_value}", view=self)
+
+ async def apply_addition(self, interaction: Interaction, item: str) -> None:
+ """Add an item to the list."""
+ if item in self.stored_value: # Ignore duplicates
+ await interaction.response.defer()
+ return
+
+ self.stored_value.append(item)
+ self.removal_select.options = [SelectOption(label=item) for item in self.stored_value[:MAX_SELECT_ITEMS]]
+ if len(self.stored_value) == 1:
+ self.add_item(self.removal_select)
+ await interaction.response.edit_message(content=f"Current list: {self.stored_value}", view=self)
+
+ async def apply_edit(self, interaction: Interaction, new_list: str) -> None:
+ """Change the contents of the list."""
+ self.stored_value = list(set(new_list.split(",")))
+ self.removal_select.options = [SelectOption(label=item) for item in self.stored_value[:MAX_SELECT_ITEMS]]
+ if len(self.stored_value) == 1:
+ self.add_item(self.removal_select)
+ await interaction.response.edit_message(content=f"Current list: {self.stored_value}", view=self)
+
+ @discord.ui.button(label="Add Value")
+ async def add_value(self, interaction: Interaction, button: discord.ui.Button) -> None:
+ """A button to add an item to the list."""
+ await interaction.response.send_modal(self.SingleItemModal(self))
+
+ @discord.ui.button(label="Free Input")
+ async def free_input(self, interaction: Interaction, button: discord.ui.Button) -> None:
+ """A button to change the entire list."""
+ await interaction.response.send_modal(self.NewListModal(self))
+
+ @discord.ui.button(label="✅ Confirm", style=discord.ButtonStyle.green)
+ async def confirm(self, interaction: Interaction, button: discord.ui.Button) -> None:
+ """Send the final value to the embed editor."""
+ # Edit first, it might time out otherwise.
+ await interaction.response.edit_message(content="✅ Edit confirmed", view=None)
+ await self.update_callback(setting_name=self.setting_name, setting_value=self.stored_value)
+ self.stop()
+
+ @discord.ui.button(label="🚫 Cancel", style=discord.ButtonStyle.red)
+ async def cancel(self, interaction: Interaction, button: discord.ui.Button) -> None:
+ """Cancel the list editing."""
+ await interaction.response.edit_message(content="🚫 Canceled", view=None)
+ self.stop()
+
+
+class EnumSelectView(discord.ui.View):
+ """A view containing an instance of EnumSelect."""
+
+ class EnumSelect(discord.ui.Select):
+ """Select an enum value and send it to the supplied callback."""
+
+ def __init__(self, setting_name: str, enum_cls: EnumMeta, update_callback: Callable):
+ super().__init__(options=[SelectOption(label=elem.name) for elem in enum_cls])
+ self.setting_name = setting_name
+ self.enum_cls = enum_cls
+ self.update_callback = update_callback
+
+ async def callback(self, interaction: Interaction) -> Any:
+ """Respond to the interaction by sending the enum value to the update callback."""
+ await interaction.response.edit_message(content="✅ Edit confirmed", view=None)
+ await self.update_callback(setting_name=self.setting_name, setting_value=self.values[0])
+
+ 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))
diff --git a/bot/exts/filtering/filtering.py b/bot/exts/filtering/filtering.py
index 2b37f1ee5..0bcf485c0 100644
--- a/bot/exts/filtering/filtering.py
+++ b/bot/exts/filtering/filtering.py
@@ -21,10 +21,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.filter import (
- ArgumentCompletionView, build_filter_repr_dict, description_and_settings_converter, filter_overrides,
- populate_embed_from_dict
+ 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.ui import ArgumentCompletionView
from bot.exts.filtering._utils import past_tense, to_serializable
from bot.log import get_logger
from bot.pagination import LinePaginator