Browse Source

Merge eefc8008f0 into 1add8a0b40

pull/10494/merge
Soheab 4 days ago
committed by GitHub
parent
commit
a7fa654e4e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 98
      discord/app_commands/commands.py
  2. 7
      discord/app_commands/models.py
  3. 6
      discord/app_commands/transformers.py
  4. 11
      discord/components.py
  5. 24
      discord/enums.py
  6. 6
      discord/types/command.py
  7. 1
      discord/types/components.py
  8. 49
      discord/ui/file_upload.py
  9. 28
      discord/utils.py
  10. 17
      docs/api.rst

98
discord/app_commands/commands.py

@ -36,6 +36,7 @@ from typing import (
List,
MutableMapping,
Optional,
Sequence,
Set,
TYPE_CHECKING,
Tuple,
@ -48,7 +49,7 @@ from typing import (
import re
from copy import copy as shallow_copy
from ..enums import AppCommandOptionType, AppCommandType, ChannelType, Locale
from ..enums import AppCommandOptionType, AppCommandType, ChannelType, Locale, FileType
from .installs import AppCommandContext, AppInstallationType
from .models import Choice
from .transformers import annotation_to_parameter, CommandParameter, NoneType
@ -67,6 +68,7 @@ from ..utils import (
_iscoroutinefunction,
_shorten,
_to_kebab_case,
_validate_discord_file_types,
)
if TYPE_CHECKING:
@ -107,6 +109,7 @@ __all__ = (
'user_install',
'allowed_installs',
'default_permissions',
'set_file_types',
)
if TYPE_CHECKING:
@ -373,6 +376,26 @@ def _populate_autocomplete(params: Dict[str, CommandParameter], autocomplete: Di
raise TypeError(f'unknown parameter given: {first}')
def _populate_file_types(params: Dict[str, CommandParameter], file_types: Dict[str, Sequence[Union[str, FileType]]]) -> None:
for name, param in params.items():
types = file_types.pop(name, MISSING)
if types is MISSING:
continue
if param.type is not AppCommandOptionType.attachment:
raise TypeError('file_types is only supported for attachment option types')
if not isinstance(types, (list, tuple)) or not all(isinstance(ft, (str, FileType)) for ft in types):
raise TypeError('file_types must be a list of strings and FileType enums')
_validate_discord_file_types(types)
param.file_types = types
if file_types:
first = next(iter(file_types))
raise TypeError(f'unknown parameter given: {first}')
def _extract_parameters_from_callback(func: Callable[..., Any], globalns: Dict[str, Any]) -> Dict[str, CommandParameter]:
params = inspect.signature(func).parameters
cache = {}
@ -428,6 +451,13 @@ def _extract_parameters_from_callback(func: Callable[..., Any], globalns: Dict[s
else:
_populate_autocomplete(result, autocomplete.copy())
try:
file_types = func.__discord_app_commands_param_file_types__
except AttributeError:
pass
else:
_populate_file_types(result, file_types.copy())
return result
@ -497,6 +527,12 @@ class Parameter:
The minimum supported value for this parameter.
max_value: Optional[Union[:class:`int`, :class:`float`]]
The maximum supported value for this parameter.
file_types: Optional[Sequence[Union[:class:`str`, :class:`.FileType`]]]
A list of file types that are allowed to be uploaded for this parameter.
Only applicable for :class:`~discord.AppCommandOptionType.attachment` parameters.
.. versionadded:: 2.8
default: Any
The default value of the parameter, if given.
If not given then this is :data:`~discord.utils.MISSING`.
@ -574,6 +610,10 @@ class Parameter:
def max_value(self) -> Optional[Union[int, float]]:
return self.__parent.max_value
@property
def file_types(self) -> Optional[Sequence[Union[str, FileType]]]:
return self.__parent.file_types
class Command(Generic[GroupT, P, T]):
"""A class that implements an application command.
@ -2905,3 +2945,59 @@ def default_permissions(perms_obj: Optional[Permissions] = None, /, **perms: Unp
return func
return decorator
def set_file_types(**parameters: Sequence[Union[str, FileType]]) -> Callable[[T], T]:
r"""Sets the file types for the given parameters by their name using the key of the keyword argument
as the name.
.. versionadded:: 2.8
.. warning::
The actual file is not guaranteed to be of the specified type. The client only
checks the file extension, so users can easily bypass this check by renaming the file.
Example:
.. code-block:: python3
@app_commands.command(description='Uploads a file')
@app_commands.set_file_types(file=['.png', discord.FileType.video])
async def upload(interaction: discord.Interaction, file: discord.Attachment):
await interaction.response.send_message(f'Uploaded {file.filename}')
Parameters
-----------
\*\*parameters: Sequence[Union[:class:`str`, :class:`.FileType`]]
A list of up to 10 file types that are allowed to be uploaded for each attachment parameter.
The type of the parameter must be :class:`discord.Attachment`.
You can mix and match strings and :class:`.FileType` enums in the list.
If a string is provided, make sure to prefix it with a period (``.``) (e.g. ``.png``).
This is required.
You may provide any string you want, but (if you are specifying only extensions) you must
include ``.jpg`` for image uploads, and both ``.mp4`` and ``.mov`` for video uploads.
Must be between 0 and 10. Defaults to allowing all file types.
Raises
--------
TypeError
The parameter name is not found or the parameter type is not :class:`discord.Attachment`.
"""
def decorator(inner: T) -> T:
unwrapped = getattr(inner, '__discord_app_commands_unwrap__', inner) or inner
if isinstance(unwrapped, Command):
_populate_file_types(unwrapped._params, parameters)
else:
try:
inner.__discord_app_commands_param_file_types__.update(parameters) # type: ignore # Runtime attribute access
except AttributeError:
inner.__discord_app_commands_param_file_types__ = parameters # type: ignore # Runtime attribute assignment
return inner
return decorator

7
discord/app_commands/models.py

@ -1018,6 +1018,10 @@ class Argument:
The maximum allowed length for this parameter.
autocomplete: :class:`bool`
Whether the argument has autocomplete.
file_types: Sequence[Union[:class:`str`, :class:`.FileType`]]
A list of file types that are allowed to be uploaded for this argument.
.. versionadded:: 2.8
"""
__slots__ = (
@ -1036,6 +1040,7 @@ class Argument:
'autocomplete',
'parent',
'_state',
'file_types',
)
def __init__(
@ -1062,6 +1067,7 @@ class Argument:
self.choices: List[Choice[Union[int, float, str]]] = [Choice.from_dict(d) for d in data.get('choices', [])]
self.name_localizations: Dict[Locale, str] = _to_locale_dict(data.get('name_localizations') or {})
self.description_localizations: Dict[Locale, str] = _to_locale_dict(data.get('description_localizations') or {})
self.file_types: List[str] = data.get('file_types', [])
def to_dict(self) -> ApplicationCommandOption:
return {
@ -1079,6 +1085,7 @@ class Argument:
'options': [],
'name_localizations': {str(k): v for k, v in self.name_localizations.items()},
'description_localizations': {str(k): v for k, v in self.description_localizations.items()},
'file_types': self.file_types,
} # type: ignore # Type checker does not understand this literal.

6
discord/app_commands/transformers.py

@ -39,6 +39,7 @@ from typing import (
List,
Literal,
Optional,
Sequence,
Set,
Tuple,
Type,
@ -52,7 +53,7 @@ from .translator import TranslationContextLocation, TranslationContext, Translat
from ..channel import StageChannel, VoiceChannel, TextChannel, CategoryChannel, ForumChannel
from ..abc import GuildChannel
from ..threads import Thread
from ..enums import Enum as InternalEnum, AppCommandOptionType, ChannelType, Locale
from ..enums import Enum as InternalEnum, AppCommandOptionType, ChannelType, Locale, FileType
from ..utils import MISSING, maybe_coroutine, _human_join, _iscoroutinefunction, TIMESTAMP_PATTERN
from ..user import User
from ..role import Role
@ -91,6 +92,7 @@ class CommandParameter:
min_value: Optional[Union[int, float]] = None
max_value: Optional[Union[int, float]] = None
autocomplete: Optional[Callable[..., Coroutine[Any, Any, Any]]] = None
file_types: Optional[Sequence[Union[str, FileType]]] = MISSING
_rename: Union[str, locale_str] = MISSING
_annotation: Any = MISSING
@ -143,6 +145,8 @@ class CommandParameter:
base['channel_types'] = [t.value for t in self.channel_types]
if self.autocomplete:
base['autocomplete'] = True
if self.file_types:
base['file_types'] = [ft.value if isinstance(ft, FileType) else ft for ft in self.file_types]
min_key, max_key = (
('min_value', 'max_value') if self.type is not AppCommandOptionType.string else ('min_length', 'max_length')

11
discord/components.py

@ -32,6 +32,7 @@ from typing import (
TYPE_CHECKING,
Tuple,
Union,
Sequence,
)
from .asset import AssetMixin
@ -44,6 +45,7 @@ from .enums import (
SelectDefaultValueType,
SeparatorSpacing,
MediaItemLoadingState,
FileType,
)
from .flags import AttachmentFlags
from .colour import Colour
@ -1467,6 +1469,11 @@ class FileUploadComponent(Component):
required: :class:`bool`
Whether the component is required.
Defaults to ``True``.
file_types: List[:class:`str`]
A list of file types that are allowed to be uploaded for this component.
Defaults to allowing all file types.
.. versionadded:: 2.8
"""
__slots__: Tuple[str, ...] = (
@ -1475,6 +1482,7 @@ class FileUploadComponent(Component):
'max_values',
'required',
'id',
'file_types',
)
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
@ -1485,6 +1493,7 @@ class FileUploadComponent(Component):
self.max_values: int = data.get('max_values', 1)
self.required: bool = data.get('required', True)
self.id: Optional[int] = data.get('id')
self.file_types: Sequence[Union[str, FileType]] = data.get('file_types', [])
@property
def type(self) -> Literal[ComponentType.file_upload]:
@ -1501,6 +1510,8 @@ class FileUploadComponent(Component):
}
if self.id is not None:
payload['id'] = self.id
if self.file_types:
payload['file_types'] = [ft.value if isinstance(ft, FileType) else ft for ft in self.file_types]
return payload

24
discord/enums.py

@ -87,6 +87,7 @@ __all__ = (
'MediaItemLoadingState',
'CollectibleType',
'NameplatePalette',
'FileType',
)
@ -1006,6 +1007,29 @@ class NameplatePalette(Enum):
white = 'white'
class FileType(Enum):
audio = 'audio'
video = 'video'
image = 'image'
@property
def file_extensions(self) -> Tuple[str, ...]:
""":class:`tuple[str]`: Returns a tuple of file extensions that belong to this file type.
.. warning::
These are subject to change at anytime and should not be relied upon for validation.
"""
# fmt: off
lookup: Dict[FileType, Tuple[str, ...]] = {
FileType.image: ('png', 'gif', 'jpg', 'jpeg', 'jfif', 'webp', 'avif'),
FileType.video: ('mp4', 'mov', 'qt', 'webm'),
FileType.audio: ('mp3', 'm4a', 'wav', 'ogg', 'opus', 'flac'),
}
# fmt: on
return lookup.get(self, ())
def create_unknown_value(cls: Type[E], val: Any) -> E:
value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below
name = f'unknown_{val}'

6
discord/types/command.py

@ -118,12 +118,18 @@ class _NumberApplicationCommandOption(_BaseValueApplicationCommandOption, total=
autocomplete: bool
class _AttachmentApplicationCommandOption(_BaseValueApplicationCommandOption):
type: Literal[11]
file_types: NotRequired[List[str]]
_ValueApplicationCommandOption = Union[
_StringApplicationCommandOption,
_IntegerApplicationCommandOption,
_BooleanApplicationCommandOption,
_SnowflakeApplicationCommandOptionChoice,
_NumberApplicationCommandOption,
_AttachmentApplicationCommandOption,
]
ApplicationCommandOption = Union[

1
discord/types/components.py

@ -204,6 +204,7 @@ class FileUploadComponent(ComponentBase):
max_values: NotRequired[int]
min_values: NotRequired[int]
required: NotRequired[bool]
file_types: NotRequired[List[str]]
class RadioGroupComponent(ComponentBase):

49
discord/ui/file_upload.py

@ -23,13 +23,13 @@ DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, List, Literal, Optional, Tuple, TypeVar, Dict
from typing import TYPE_CHECKING, Any, List, Literal, Optional, Sequence, Tuple, TypeVar, Dict, Union
import os
from ..utils import MISSING
from ..utils import MISSING, _validate_discord_file_types
from ..components import FileUploadComponent
from ..enums import ComponentType
from ..enums import ComponentType, FileType
from .item import Item
if TYPE_CHECKING:
@ -72,6 +72,24 @@ class FileUpload(Item[V]):
required: :class:`bool`
Whether this component is required to be filled before submitting the modal.
Defaults to ``True``.
file_types: List[Union[:class:`str`, :class:`.FileType`]]
A list of file types that are allowed to be uploaded for this component.
You can mix and match strings and :class:`.FileType` enums in the list.
If a string is provided, make sure to prefix it with a period (``.``) (e.g. ``.png``).
This is required.
You may provide any string you want, but (if you are specifying only extensions) you must
include ``.jpg`` for image uploads, and both ``.mp4`` and ``.mov`` for video uploads.
Must be between 0 and 10. Defaults to allowing all file types.
.. warning::
The actual file is not guaranteed to be of the specified type. The client only
checks the file extension, so users can easily bypass this check by renaming the file.
.. versionadded:: 2.8
"""
__item_repr_attributes__: Tuple[str, ...] = (
@ -90,6 +108,7 @@ class FileUpload(Item[V]):
min_values: Optional[int] = None,
max_values: Optional[int] = None,
id: Optional[int] = None,
file_types: Optional[Sequence[Union[str, FileType]]] = None,
) -> None:
super().__init__()
self._provided_custom_id = custom_id is not MISSING
@ -97,12 +116,16 @@ class FileUpload(Item[V]):
if not isinstance(custom_id, str):
raise TypeError(f'expected custom_id to be str not {custom_id.__class__.__name__}')
if file_types:
_validate_discord_file_types(file_types)
self._underlying: FileUploadComponent = FileUploadComponent._raw_construct(
id=id,
custom_id=custom_id,
max_values=max_values,
min_values=min_values,
required=required,
file_types=file_types if file_types else [],
)
self.id = id
self._values: List[Attachment] = []
@ -165,6 +188,25 @@ class FileUpload(Item[V]):
def required(self, value: bool) -> None:
self._underlying.required = bool(value)
@property
def file_types(self) -> List[Union[str, FileType]]:
"""List[:class:`str`]: A list of file types that are allowed to be uploaded for this component.
When setting this property, see the documentation for this parameter in the :class:`.FileUpload`
constructor for more information.
.. versionadded:: 2.8
"""
return list(self._underlying.file_types)
@file_types.setter
def file_types(self, value: Sequence[Union[str, FileType]]) -> None:
if not isinstance(value, (list, tuple)) or not all(isinstance(ft, (str, FileType)) for ft in value):
raise TypeError('file_types must be a list of str or FileType')
_validate_discord_file_types(value)
self._underlying.file_types = list(value)
@property
def width(self) -> int:
return 5
@ -188,6 +230,7 @@ class FileUpload(Item[V]):
max_values=component.max_values,
min_values=component.min_values,
required=component.required,
file_types=component.file_types,
)
return self

28
discord/utils.py

@ -1537,6 +1537,34 @@ def _format_call_duration(duration: datetime.timedelta) -> str:
return formatted
if TYPE_CHECKING:
from .enums import FileType
DISCORD_FILE_TYPES_RE = re.compile(r'^\.[\w\-\.]+$', re.IGNORECASE)
def _validate_discord_file_types(exts: Sequence[Union[FileType, str]], /) -> None:
if len(exts) > 10:
raise ValueError(
f'Too many file extensions provided. Must be 10 or less, got {len(exts)}.',
)
for ext in exts:
# don't need to validate the presets (FileType enum) since they are guaranteed to be valid
if not isinstance(ext, str):
continue
if len(ext) > 16:
raise ValueError(
f'File extension {ext!r} is too long. Must be 16 characters or less.',
)
if not DISCORD_FILE_TYPES_RE.match(ext):
raise ValueError(
f'File extension {ext!r} is invalid. It must start with a dot and contain only alphanumeric characters, hyphens, or underscores.',
)
class _RawReprMixin:
__slots__: Tuple[str, ...] = ()

17
docs/api.rst

@ -4154,6 +4154,23 @@ of :class:`enum.Enum`.
The collectible nameplate palette is white.
.. class:: FileType
.. versionadded:: 2.8
.. attribute:: image
Preset to represent image files.
.. attribute:: video
Preset to represent video files.
.. attribute:: audio
Preset to represent audio files.
.. _discord-api-audit-logs:
Audit Log Data

Loading…
Cancel
Save