-
Notifications
You must be signed in to change notification settings - Fork 135
/
modal.py
256 lines (211 loc) · 8.42 KB
/
modal.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# SPDX-License-Identifier: MIT
from __future__ import annotations
import asyncio
import os
import sys
import traceback
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
from ..enums import TextInputStyle
from ..utils import MISSING
from .action_row import ActionRow, components_to_rows
from .text_input import TextInput
if TYPE_CHECKING:
from ..interactions.modal import ModalInteraction
from ..state import ConnectionState
from ..types.components import Modal as ModalPayload
from .action_row import Components, ModalUIComponent
__all__ = ("Modal",)
class Modal:
"""Represents a UI Modal.
.. versionadded:: 2.4
Parameters
----------
title: :class:`str`
The title of the modal.
components: |components_type|
The components to display in the modal. Up to 5 action rows.
custom_id: :class:`str`
The custom ID of the modal.
timeout: :class:`float`
The time to wait until the modal is removed from cache, if no interaction is made.
Modals without timeouts are not supported, since there's no event for when a modal is closed.
Defaults to 600 seconds.
"""
__slots__ = ("title", "custom_id", "components", "timeout")
def __init__(
self,
*,
title: str,
components: Components[ModalUIComponent],
custom_id: str = MISSING,
timeout: float = 600,
) -> None:
if timeout is None:
raise ValueError("Timeout may not be None")
rows = components_to_rows(components)
if len(rows) > 5:
raise ValueError("Maximum number of components exceeded.")
self.title: str = title
self.custom_id: str = os.urandom(16).hex() if custom_id is MISSING else custom_id
self.components: List[ActionRow] = rows
self.timeout: float = timeout
def __repr__(self) -> str:
return (
f"<Modal custom_id={self.custom_id!r} title={self.title!r} "
f"components={self.components!r}>"
)
def append_component(self, component: Union[TextInput, List[TextInput]]) -> None:
"""Adds one or multiple component(s) to the modal.
Parameters
----------
component: Union[:class:`~.ui.TextInput`, List[:class:`~.ui.TextInput`]]
The component(s) to add to the modal.
This can be a single component or a list of components.
Raises
------
ValueError
Maximum number of components (5) exceeded.
TypeError
An object of type :class:`TextInput` was not passed.
"""
if len(self.components) >= 5:
raise ValueError("Maximum number of components exceeded.")
if not isinstance(component, list):
component = [component]
for c in component:
if not isinstance(c, TextInput):
raise TypeError(
f"component must be of type 'TextInput' or a list of 'TextInput' objects, not {type(c).__name__}."
)
try:
self.components[-1].append_item(c)
except (ValueError, IndexError):
self.components.append(ActionRow(c))
def add_text_input(
self,
*,
label: str,
custom_id: str,
style: TextInputStyle = TextInputStyle.short,
placeholder: Optional[str] = None,
value: Optional[str] = None,
required: bool = True,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
) -> None:
"""Creates and adds a text input component to the modal.
To append a pre-existing instance of :class:`~disnake.ui.TextInput` use the
:meth:`append_component` method.
Parameters
----------
label: :class:`str`
The label of the text input.
custom_id: :class:`str`
The ID of the text input that gets received during an interaction.
style: :class:`.TextInputStyle`
The style of the text input.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is entered.
value: Optional[:class:`str`]
The pre-filled value of the text input.
required: :class:`bool`
Whether the text input is required. Defaults to ``True``.
min_length: Optional[:class:`int`]
The minimum length of the text input.
max_length: Optional[:class:`int`]
The maximum length of the text input.
Raises
------
ValueError
Maximum number of components (5) exceeded.
"""
self.append_component(
TextInput(
label=label,
custom_id=custom_id,
style=style,
placeholder=placeholder,
value=value,
required=required,
min_length=min_length,
max_length=max_length,
)
)
async def callback(self, interaction: ModalInteraction, /) -> None:
"""|coro|
The callback associated with this modal.
This can be overriden by subclasses.
Parameters
----------
interaction: :class:`.ModalInteraction`
The interaction that triggered this modal.
"""
pass
async def on_error(self, error: Exception, interaction: ModalInteraction) -> None:
"""|coro|
A callback that is called when an error occurs.
The default implementation prints the traceback to stderr.
Parameters
----------
error: :class:`Exception`
The exception that was raised.
interaction: :class:`.ModalInteraction`
The interaction that triggered this modal.
"""
traceback.print_exception(error.__class__, error, error.__traceback__, file=sys.stderr)
async def on_timeout(self) -> None:
"""|coro|
A callback that is called when the modal is removed from the cache
without an interaction being made.
"""
pass
def to_components(self) -> ModalPayload:
payload: ModalPayload = {
"title": self.title,
"custom_id": self.custom_id,
"components": [component.to_component_dict() for component in self.components],
}
return payload
async def _scheduled_task(self, interaction: ModalInteraction) -> None:
try:
await self.callback(interaction)
except Exception as e:
await self.on_error(e, interaction)
finally:
# if the interaction was responded to (no matter if in the callback or error handler),
# the modal closed for the user and therefore can be removed from the store
if interaction.response._response_type is not None:
interaction._state._modal_store.remove_modal(
interaction.author.id, interaction.custom_id
)
def dispatch(self, interaction: ModalInteraction) -> None:
asyncio.create_task(
self._scheduled_task(interaction), name=f"disnake-ui-modal-dispatch-{self.custom_id}"
)
class ModalStore:
def __init__(self, state: ConnectionState) -> None:
self._state = state
# (user_id, Modal.custom_id): Modal
self._modals: Dict[Tuple[int, str], Modal] = {}
def add_modal(self, user_id: int, modal: Modal) -> None:
loop = asyncio.get_running_loop()
self._modals[(user_id, modal.custom_id)] = modal
loop.create_task(self.handle_timeout(user_id, modal.custom_id, modal.timeout))
def remove_modal(self, user_id: int, modal_custom_id: str) -> Modal:
return self._modals.pop((user_id, modal_custom_id))
async def handle_timeout(self, user_id: int, modal_custom_id: str, timeout: float) -> None:
# Waits for the timeout and then removes the modal from cache, this is done just in case
# the user closed the modal, as there isn't an event for that.
await asyncio.sleep(timeout)
try:
modal = self.remove_modal(user_id, modal_custom_id)
except KeyError:
# The modal has already been removed.
pass
else:
await modal.on_timeout()
def dispatch(self, interaction: ModalInteraction) -> None:
key = (interaction.author.id, interaction.custom_id)
modal = self._modals.get(key)
if modal is not None:
modal.dispatch(interaction)