Skip to content

Commit 2d7e061

Browse files
SoheabRapptz
authored andcommitted
Add support for File Upload component
1 parent 944ffe9 commit 2d7e061

File tree

8 files changed

+461
-4
lines changed

8 files changed

+461
-4
lines changed

discord/components.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
ContainerComponent as ContainerComponentPayload,
7373
UnfurledMediaItem as UnfurledMediaItemPayload,
7474
LabelComponent as LabelComponentPayload,
75+
FileUploadComponent as FileUploadComponentPayload,
7576
)
7677

7778
from .emoji import Emoji
@@ -112,6 +113,7 @@
112113
'TextDisplay',
113114
'SeparatorComponent',
114115
'LabelComponent',
116+
'FileUploadComponent',
115117
)
116118

117119

@@ -131,6 +133,7 @@ class Component:
131133
- :class:`FileComponent`
132134
- :class:`SeparatorComponent`
133135
- :class:`Container`
136+
- :class:`FileUploadComponent`
134137
135138
This class is abstract and cannot be instantiated.
136139
@@ -1384,6 +1387,71 @@ def to_dict(self) -> LabelComponentPayload:
13841387
return payload
13851388

13861389

1390+
class FileUploadComponent(Component):
1391+
"""Represents a file upload component from the Discord Bot UI Kit.
1392+
1393+
This inherits from :class:`Component`.
1394+
1395+
.. note::
1396+
1397+
The user constructible and usable type for creating a file upload is
1398+
:class:`discord.ui.FileUpload` not this one.
1399+
1400+
.. versionadded:: 2.7
1401+
1402+
Attributes
1403+
------------
1404+
custom_id: Optional[:class:`str`]
1405+
The ID of the component that gets received during an interaction.
1406+
min_values: :class:`int`
1407+
The minimum number of files that must be uploaded for this component.
1408+
Defaults to 1 and must be between 0 and 10.
1409+
max_values: :class:`int`
1410+
The maximum number of files that must be uploaded for this component.
1411+
Defaults to 1 and must be between 1 and 10.
1412+
id: Optional[:class:`int`]
1413+
The ID of this component.
1414+
required: :class:`bool`
1415+
Whether the component is required.
1416+
Defaults to ``True``.
1417+
"""
1418+
1419+
__slots__: Tuple[str, ...] = (
1420+
'custom_id',
1421+
'min_values',
1422+
'max_values',
1423+
'required',
1424+
'id',
1425+
)
1426+
1427+
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
1428+
1429+
def __init__(self, data: FileUploadComponentPayload, /) -> None:
1430+
self.custom_id: str = data['custom_id']
1431+
self.min_values: int = data.get('min_values', 1)
1432+
self.max_values: int = data.get('max_values', 1)
1433+
self.required: bool = data.get('required', True)
1434+
self.id: Optional[int] = data.get('id')
1435+
1436+
@property
1437+
def type(self) -> Literal[ComponentType.file_upload]:
1438+
""":class:`ComponentType`: The type of component."""
1439+
return ComponentType.file_upload
1440+
1441+
def to_dict(self) -> FileUploadComponentPayload:
1442+
payload: FileUploadComponentPayload = {
1443+
'type': self.type.value,
1444+
'custom_id': self.custom_id,
1445+
'min_values': self.min_values,
1446+
'max_values': self.max_values,
1447+
'required': self.required,
1448+
}
1449+
if self.id is not None:
1450+
payload['id'] = self.id
1451+
1452+
return payload
1453+
1454+
13871455
def _component_factory(data: ComponentPayload, state: Optional[ConnectionState] = None) -> Optional[Component]:
13881456
if data['type'] == 1:
13891457
return ActionRow(data)
@@ -1409,3 +1477,5 @@ def _component_factory(data: ComponentPayload, state: Optional[ConnectionState]
14091477
return Container(data, state)
14101478
elif data['type'] == 18:
14111479
return LabelComponent(data, state)
1480+
elif data['type'] == 19:
1481+
return FileUploadComponent(data)

discord/enums.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,7 @@ class ComponentType(Enum):
693693
separator = 14
694694
container = 17
695695
label = 18
696+
file_upload = 19
696697

697698
def __int__(self) -> int:
698699
return self.value

discord/types/components.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from .emoji import PartialEmoji
3131
from .channel import ChannelType
3232

33-
ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17, 18]
33+
ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17, 18, 19]
3434
ButtonStyle = Literal[1, 2, 3, 4, 5, 6]
3535
TextStyle = Literal[1, 2]
3636
DefaultValueType = Literal['user', 'role', 'channel']
@@ -192,7 +192,15 @@ class LabelComponent(ComponentBase):
192192
type: Literal[18]
193193
label: str
194194
description: NotRequired[str]
195-
component: Union[StringSelectComponent, TextInput]
195+
component: Union[SelectMenu, TextInput, FileUploadComponent]
196+
197+
198+
class FileUploadComponent(ComponentBase):
199+
type: Literal[19]
200+
custom_id: str
201+
max_values: NotRequired[int]
202+
min_values: NotRequired[int]
203+
required: NotRequired[bool]
196204

197205

198206
ActionRowChildComponent = Union[ButtonComponent, SelectMenu, TextInput]
@@ -207,4 +215,4 @@ class LabelComponent(ComponentBase):
207215
SeparatorComponent,
208216
ThumbnailComponent,
209217
]
210-
Component = Union[ActionRowChildComponent, LabelComponent, ContainerChildComponent]
218+
Component = Union[ActionRowChildComponent, LabelComponent, FileUploadComponent, ContainerChildComponent]

discord/types/interactions.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,15 @@ class ModalSubmitSelectInteractionData(ComponentBase):
217217
values: List[str]
218218

219219

220-
ModalSubmitComponentItemInteractionData = Union[ModalSubmitSelectInteractionData, ModalSubmitTextInputInteractionData]
220+
class ModalSubmitFileUploadInteractionData(ComponentBase):
221+
type: Literal[19]
222+
custom_id: str
223+
values: List[str]
224+
225+
226+
ModalSubmitComponentItemInteractionData = Union[
227+
ModalSubmitSelectInteractionData, ModalSubmitTextInputInteractionData, ModalSubmitFileUploadInteractionData
228+
]
221229

222230

223231
class ModalSubmitActionRowInteractionData(TypedDict):

discord/ui/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@
2525
from .thumbnail import *
2626
from .action_row import *
2727
from .label import *
28+
from .file_upload import *

discord/ui/file_upload.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
"""
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2015-present Rapptz
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a
7+
copy of this software and associated documentation files (the "Software"),
8+
to deal in the Software without restriction, including without limitation
9+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
10+
and/or sell copies of the Software, and to permit persons to whom the
11+
Software is furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22+
DEALINGS IN THE SOFTWARE.
23+
"""
24+
25+
from __future__ import annotations
26+
from typing import TYPE_CHECKING, Any, List, Literal, Optional, Tuple, TypeVar, Dict
27+
28+
import os
29+
30+
from ..utils import MISSING
31+
from ..components import FileUploadComponent
32+
from ..enums import ComponentType
33+
from .item import Item
34+
35+
if TYPE_CHECKING:
36+
from typing_extensions import Self
37+
38+
from ..message import Attachment
39+
from ..interactions import Interaction
40+
from ..types.interactions import ModalSubmitTextInputInteractionData as ModalSubmitFileUploadInteractionDataPayload
41+
from ..types.components import FileUploadComponent as FileUploadComponentPayload
42+
from .view import BaseView
43+
from ..app_commands.namespace import ResolveKey
44+
45+
46+
# fmt: off
47+
__all__ = (
48+
'FileUpload',
49+
)
50+
# fmt: on
51+
52+
V = TypeVar('V', bound='BaseView', covariant=True)
53+
54+
55+
class FileUpload(Item[V]):
56+
"""Represents a file upload component within a modal.
57+
58+
.. versionadded:: 2.7
59+
60+
Parameters
61+
------------
62+
id: Optional[:class:`int`]
63+
The ID of the component. This must be unique across the view.
64+
custom_id: Optional[:class:`str`]
65+
The custom ID of the file upload component.
66+
max_values: Optional[:class:`int`]
67+
The maximum number of files that can be uploaded in this component.
68+
Must be between 1 and 10. Defaults to 1.
69+
min_values: Optional[:class:`int`]
70+
The minimum number of files that must be uploaded in this component.
71+
Must be between 0 and 10. Defaults to 0.
72+
required: :class:`bool`
73+
Whether this component is required to be filled before submitting the modal.
74+
Defaults to ``True``.
75+
"""
76+
77+
__item_repr_attributes__: Tuple[str, ...] = (
78+
'id',
79+
'custom_id',
80+
'max_values',
81+
'min_values',
82+
'required',
83+
)
84+
85+
def __init__(
86+
self,
87+
*,
88+
custom_id: str = MISSING,
89+
required: bool = True,
90+
min_values: Optional[int] = None,
91+
max_values: Optional[int] = None,
92+
id: Optional[int] = None,
93+
) -> None:
94+
super().__init__()
95+
self._provided_custom_id = custom_id is not MISSING
96+
custom_id = os.urandom(16).hex() if custom_id is MISSING else custom_id
97+
if not isinstance(custom_id, str):
98+
raise TypeError(f'expected custom_id to be str not {custom_id.__class__.__name__}')
99+
100+
self._underlying: FileUploadComponent = FileUploadComponent._raw_construct(
101+
id=id,
102+
custom_id=custom_id,
103+
max_values=max_values,
104+
min_values=min_values,
105+
required=required,
106+
)
107+
self.id = id
108+
self._values: List[Attachment] = []
109+
110+
@property
111+
def id(self) -> Optional[int]:
112+
"""Optional[:class:`int`]: The ID of this component."""
113+
return self._underlying.id
114+
115+
@id.setter
116+
def id(self, value: Optional[int]) -> None:
117+
self._underlying.id = value
118+
119+
@property
120+
def values(self) -> List[Attachment]:
121+
"""List[:class:`discord.Attachment`]: The list of attachments uploaded by the user.
122+
123+
You can call :meth:`~discord.Attachment.to_file` on each attachment
124+
to get a :class:`~discord.File` for sending.
125+
"""
126+
return self._values
127+
128+
@property
129+
def custom_id(self) -> str:
130+
""":class:`str`: The ID of the component that gets received during an interaction."""
131+
return self._underlying.custom_id
132+
133+
@custom_id.setter
134+
def custom_id(self, value: str) -> None:
135+
if not isinstance(value, str):
136+
raise TypeError('custom_id must be a str')
137+
138+
self._underlying.custom_id = value
139+
self._provided_custom_id = True
140+
141+
@property
142+
def min_values(self) -> int:
143+
""":class:`int`: The minimum number of files that must be user upload before submitting the modal."""
144+
return self._underlying.min_values
145+
146+
@min_values.setter
147+
def min_values(self, value: int) -> None:
148+
self._underlying.min_values = int(value)
149+
150+
@property
151+
def max_values(self) -> int:
152+
""":class:`int`: The maximum number of files that the user must upload before submitting the modal."""
153+
return self._underlying.max_values
154+
155+
@max_values.setter
156+
def max_values(self, value: int) -> None:
157+
self._underlying.max_values = int(value)
158+
159+
@property
160+
def required(self) -> bool:
161+
""":class:`bool`: Whether the component is required or not."""
162+
return self._underlying.required
163+
164+
@required.setter
165+
def required(self, value: bool) -> None:
166+
self._underlying.required = bool(value)
167+
168+
@property
169+
def width(self) -> int:
170+
return 5
171+
172+
def to_component_dict(self) -> FileUploadComponentPayload:
173+
return self._underlying.to_dict()
174+
175+
def _refresh_component(self, component: FileUploadComponent) -> None:
176+
self._underlying = component
177+
178+
def _handle_submit(
179+
self, interaction: Interaction, data: ModalSubmitFileUploadInteractionDataPayload, resolved: Dict[ResolveKey, Any]
180+
) -> None:
181+
self._values = [v for k, v in resolved.items() if k.id in data.get('values', [])]
182+
183+
@classmethod
184+
def from_component(cls, component: FileUploadComponent) -> Self:
185+
self = cls(
186+
id=component.id,
187+
custom_id=component.custom_id,
188+
max_values=component.max_values,
189+
min_values=component.min_values,
190+
required=component.required,
191+
)
192+
return self
193+
194+
@property
195+
def type(self) -> Literal[ComponentType.file_upload]:
196+
return self._underlying.type
197+
198+
def is_dispatchable(self) -> bool:
199+
return False

0 commit comments

Comments
 (0)