DiscordPYComponents 1.3.0__tar.gz → 1.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- discordpycomponents-1.3.2/DiscordPYComponents.egg-info/PKG-INFO +694 -0
- discordpycomponents-1.3.2/PKG-INFO +694 -0
- discordpycomponents-1.3.2/README.md +682 -0
- {discordpycomponents-1.3.0 → discordpycomponents-1.3.2}/py_components/__init__.py +1 -1
- {discordpycomponents-1.3.0 → discordpycomponents-1.3.2}/py_components/ui.py +103 -53
- {discordpycomponents-1.3.0 → discordpycomponents-1.3.2}/pyproject.toml +1 -1
- discordpycomponents-1.3.0/DiscordPYComponents.egg-info/PKG-INFO +0 -248
- discordpycomponents-1.3.0/PKG-INFO +0 -248
- discordpycomponents-1.3.0/README.md +0 -236
- {discordpycomponents-1.3.0 → discordpycomponents-1.3.2}/DiscordPYComponents.egg-info/SOURCES.txt +0 -0
- {discordpycomponents-1.3.0 → discordpycomponents-1.3.2}/DiscordPYComponents.egg-info/dependency_links.txt +0 -0
- {discordpycomponents-1.3.0 → discordpycomponents-1.3.2}/DiscordPYComponents.egg-info/requires.txt +0 -0
- {discordpycomponents-1.3.0 → discordpycomponents-1.3.2}/DiscordPYComponents.egg-info/top_level.txt +0 -0
- {discordpycomponents-1.3.0 → discordpycomponents-1.3.2}/py_components/components.py +0 -0
- {discordpycomponents-1.3.0 → discordpycomponents-1.3.2}/py_components/enums.py +0 -0
- {discordpycomponents-1.3.0 → discordpycomponents-1.3.2}/py_components/http.py +0 -0
- {discordpycomponents-1.3.0 → discordpycomponents-1.3.2}/py_components/ids.py +0 -0
- {discordpycomponents-1.3.0 → discordpycomponents-1.3.2}/py_components/modal.py +0 -0
- {discordpycomponents-1.3.0 → discordpycomponents-1.3.2}/py_components/registry.py +0 -0
- {discordpycomponents-1.3.0 → discordpycomponents-1.3.2}/py_components/view.py +0 -0
- {discordpycomponents-1.3.0 → discordpycomponents-1.3.2}/setup.cfg +0 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: DiscordPYComponents
|
|
3
|
+
Version: 1.3.2
|
|
4
|
+
Summary: Discord Components V2 for discord.py
|
|
5
|
+
Author: 2gm49
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://pypi.org/project/DiscordPYComponents/
|
|
8
|
+
Project-URL: Documentation, https://docs.discord.com/developers/components/overview
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: aiohttp
|
|
12
|
+
|
|
13
|
+
# DiscordPYComponents
|
|
14
|
+
|
|
15
|
+
Discord Components V2 for discord.py.
|
|
16
|
+
|
|
17
|
+
PyPI: https://pypi.org/project/DiscordPYComponents/
|
|
18
|
+
|
|
19
|
+
Discord docs:
|
|
20
|
+
- https://docs.discord.com/developers/components/overview
|
|
21
|
+
- https://docs.discord.com/developers/components/reference
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install DiscordPYComponents
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Needs:
|
|
30
|
+
- Python 3.10+
|
|
31
|
+
- discord.py (or anything that gives you a token / interaction)
|
|
32
|
+
- aiohttp
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from py_components import (
|
|
36
|
+
UI, TextDisplay, Button, ButtonStyle,
|
|
37
|
+
Container, Section, Separator, ActionRow,
|
|
38
|
+
StringSelect, SelectOption, Modal, Label, TextInput,
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
If your package folder is named differently, import from that name instead.
|
|
43
|
+
|
|
44
|
+
## How V2 works
|
|
45
|
+
|
|
46
|
+
Messages with Components V2 set `flags` to `IS_COMPONENTS_V2` (`1 << 15` / `32768`).
|
|
47
|
+
|
|
48
|
+
That means:
|
|
49
|
+
- no top-level `content` or `embeds`
|
|
50
|
+
- no stickers / poll on that message
|
|
51
|
+
- everything is components
|
|
52
|
+
- attachments only show if a `File` / media component points at them
|
|
53
|
+
- max 40 components per message
|
|
54
|
+
- once the flag is on a message, you can't turn it off
|
|
55
|
+
|
|
56
|
+
This library builds those payloads and can send them through discord.py or raw HTTP.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## UI
|
|
61
|
+
|
|
62
|
+
Main way to build and send a V2 message.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
ui = UI(
|
|
66
|
+
TextDisplay("hello"),
|
|
67
|
+
Button("ok", custom_id="ok", style=ButtonStyle.SUCCESS),
|
|
68
|
+
)
|
|
69
|
+
await ui.send(ctx)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Methods
|
|
73
|
+
|
|
74
|
+
| method | what |
|
|
75
|
+
|--------|------|
|
|
76
|
+
| `UI(*components)` | start a builder |
|
|
77
|
+
| `ui.add(*components)` | append more |
|
|
78
|
+
| `ui.to_payload()` | `{"flags": 32768, "components": [...]}` |
|
|
79
|
+
| `ui.to_components()` | just the components list |
|
|
80
|
+
| `ui.get(id)` | find a component dict by numeric id |
|
|
81
|
+
| `ui.replace(id, component)` | payload with that id swapped |
|
|
82
|
+
| `await ui.send(target, ephemeral=False, files=None)` | channel or interaction |
|
|
83
|
+
| `await ui.reply(interaction, ...)` | same as send on an interaction |
|
|
84
|
+
| `await ui.edit(target, message=None)` | edit interaction original / a Message |
|
|
85
|
+
|
|
86
|
+
`send` / `reply` try discord.py first (`ctx.send`, `interaction.response.send_message`, `followup.send`). Falls back to raw HTTP if that fails.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
await ui.send(ctx)
|
|
90
|
+
await ui.reply(interaction)
|
|
91
|
+
await ui.reply(interaction, ephemeral=True)
|
|
92
|
+
await ui.edit(interaction)
|
|
93
|
+
await ui.edit(message)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Also: `await send_ui(target, ui)` — same as `ui.send(target)`.
|
|
97
|
+
|
|
98
|
+
Loose `Button`s are auto-wrapped into `ActionRow`s (5 per row). Put selects in an `ActionRow` yourself.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Content components
|
|
103
|
+
|
|
104
|
+
### TextDisplay / Text
|
|
105
|
+
|
|
106
|
+
Markdown text. Alias: `Text`.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
TextDisplay("**bold** and normal", id=1)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| arg | notes |
|
|
113
|
+
|-----|--------|
|
|
114
|
+
| `content` | required, 1–4000 chars |
|
|
115
|
+
| `id` | optional numeric id |
|
|
116
|
+
|
|
117
|
+
### Thumbnail
|
|
118
|
+
|
|
119
|
+
Small image for a Section accessory. Images only (incl gif/webp).
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
Thumbnail("https://cdn.discordapp.com/...", description="icon", spoiler=False)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
| arg | notes |
|
|
126
|
+
|-----|--------|
|
|
127
|
+
| `url` | required |
|
|
128
|
+
| `description` | alt text, max 1024 |
|
|
129
|
+
| `spoiler` | blur it |
|
|
130
|
+
| `id` | optional |
|
|
131
|
+
|
|
132
|
+
### MediaGallery / MediaGalleryItem
|
|
133
|
+
|
|
134
|
+
1–10 media items.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
MediaGallery(
|
|
138
|
+
"https://example.com/a.png",
|
|
139
|
+
MediaGalleryItem("https://example.com/b.png", description="chart", spoiler=True),
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
| arg | notes |
|
|
144
|
+
|-----|--------|
|
|
145
|
+
| `*items` | urls and/or `MediaGalleryItem` |
|
|
146
|
+
| `id` | optional |
|
|
147
|
+
|
|
148
|
+
`MediaGalleryItem(url, description=None, spoiler=False)`
|
|
149
|
+
|
|
150
|
+
### File
|
|
151
|
+
|
|
152
|
+
Shows an uploaded attachment. Upload the file with the message; reference it here.
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
File("report.pdf") # -> attachment://report.pdf
|
|
156
|
+
File("attachment://report.pdf", spoiler=True)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
| arg | notes |
|
|
160
|
+
|-----|--------|
|
|
161
|
+
| `filename` | name or `attachment://name` |
|
|
162
|
+
| `spoiler` | blur it |
|
|
163
|
+
| `id` | optional |
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Layout components
|
|
168
|
+
|
|
169
|
+
### ActionRow
|
|
170
|
+
|
|
171
|
+
Holds up to 5 buttons **or** one select.
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
ActionRow(
|
|
175
|
+
Button("a", custom_id="a"),
|
|
176
|
+
Button("b", custom_id="b"),
|
|
177
|
+
)
|
|
178
|
+
ActionRow(StringSelect("pick", options=[...]))
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Section
|
|
182
|
+
|
|
183
|
+
1–3 TextDisplays + optional accessory (Button or Thumbnail).
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
Section(
|
|
187
|
+
TextDisplay("**title**"),
|
|
188
|
+
TextDisplay("body text"),
|
|
189
|
+
accessory=Thumbnail("https://..."),
|
|
190
|
+
)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Separator
|
|
194
|
+
|
|
195
|
+
Spacing between components.
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
Separator() # divider on, small spacing
|
|
199
|
+
Separator(divider=False, spacing=SeparatorSpacing.LARGE)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
| arg | notes |
|
|
203
|
+
|-----|--------|
|
|
204
|
+
| `divider` | show a line, default True |
|
|
205
|
+
| `spacing` | `SeparatorSpacing.SMALL` (1) or `LARGE` (2) |
|
|
206
|
+
| `id` | optional |
|
|
207
|
+
|
|
208
|
+
### Container
|
|
209
|
+
|
|
210
|
+
Groups children. Optional left accent colour (like embeds) and spoiler.
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
Container(
|
|
214
|
+
TextDisplay("# header"),
|
|
215
|
+
Separator(),
|
|
216
|
+
TextDisplay("body"),
|
|
217
|
+
accent_color=0x5865F2,
|
|
218
|
+
spoiler=False,
|
|
219
|
+
)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Children can be ActionRow, TextDisplay, Section, MediaGallery, Separator, File.
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
c = Container(TextDisplay("x"))
|
|
226
|
+
c.add(Separator())
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Buttons
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
Button(
|
|
235
|
+
"label",
|
|
236
|
+
custom_id="id",
|
|
237
|
+
style=ButtonStyle.PRIMARY,
|
|
238
|
+
emoji="🔥", # or make_emoji(...)
|
|
239
|
+
disabled=False,
|
|
240
|
+
id=None,
|
|
241
|
+
)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
| style | value | needs |
|
|
245
|
+
|-------|-------|--------|
|
|
246
|
+
| `PRIMARY` | 1 | custom_id |
|
|
247
|
+
| `SECONDARY` | 2 | custom_id |
|
|
248
|
+
| `SUCCESS` | 3 | custom_id |
|
|
249
|
+
| `DANGER` | 4 | custom_id |
|
|
250
|
+
| `LINK` | 5 | url= |
|
|
251
|
+
| `PREMIUM` | 6 | sku_id= |
|
|
252
|
+
|
|
253
|
+
Lowercase aliases work too: `ButtonStyle.primary`, etc.
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
Button("docs", style=ButtonStyle.LINK, url="https://discord.com/developers/docs")
|
|
257
|
+
Button("buy", style=ButtonStyle.PREMIUM, sku_id="1234567890")
|
|
258
|
+
Button("nope", custom_id="x", disabled=True)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Must sit in an ActionRow or as a Section accessory. UI auto-wraps loose buttons.
|
|
262
|
+
|
|
263
|
+
Label max 80 chars. custom_id 1–100 chars.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Selects
|
|
268
|
+
|
|
269
|
+
All selects need a `custom_id`. In messages they go inside an ActionRow (one select per row). In modals they go inside a Label.
|
|
270
|
+
|
|
271
|
+
### SelectOption (StringSelect only)
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
SelectOption(
|
|
275
|
+
"Red",
|
|
276
|
+
"red",
|
|
277
|
+
description="the red one",
|
|
278
|
+
emoji="🔴",
|
|
279
|
+
default=False,
|
|
280
|
+
)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### StringSelect
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
StringSelect(
|
|
287
|
+
"colour",
|
|
288
|
+
options=[
|
|
289
|
+
SelectOption("red", "red"),
|
|
290
|
+
SelectOption("blue", "blue"),
|
|
291
|
+
],
|
|
292
|
+
placeholder="pick one",
|
|
293
|
+
min_values=1,
|
|
294
|
+
max_values=1,
|
|
295
|
+
disabled=False,
|
|
296
|
+
required=None, # modals only
|
|
297
|
+
)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
1–25 options.
|
|
301
|
+
|
|
302
|
+
### UserSelect / RoleSelect / MentionableSelect / ChannelSelect
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
UserSelect(
|
|
306
|
+
"mods",
|
|
307
|
+
placeholder="pick mods",
|
|
308
|
+
min_values=1,
|
|
309
|
+
max_values=3,
|
|
310
|
+
default_values=[
|
|
311
|
+
{"id": "111111111111111111", "type": "user"},
|
|
312
|
+
],
|
|
313
|
+
required=None,
|
|
314
|
+
disabled=False,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
RoleSelect("roles", min_values=0, max_values=5)
|
|
318
|
+
MentionableSelect("who")
|
|
319
|
+
ChannelSelect(
|
|
320
|
+
"channel",
|
|
321
|
+
channel_types=[0, 2, 5], # text, voice, announce — discord channel type ints
|
|
322
|
+
)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
`default_values` entries: `{"id": "<snowflake>", "type": "user"|"role"|"channel"}`.
|
|
326
|
+
|
|
327
|
+
`required` only matters in modals. `disabled` only in messages (modals error if disabled).
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Emoji helper
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
from py_components import make_emoji
|
|
335
|
+
|
|
336
|
+
make_emoji("🔥")
|
|
337
|
+
make_emoji("pepe", id=123456789012345678, animated=True)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Buttons and SelectOptions also take a plain unicode string:
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
Button("ok", custom_id="ok", emoji="✅")
|
|
344
|
+
SelectOption("fire", "fire", emoji="🔥")
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Component ids
|
|
350
|
+
|
|
351
|
+
Every component can take `id=` (int). If you don't set one, payloads from UI / View / Modal get sequential ids filled in automatically.
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
TextDisplay("hi", id=10)
|
|
355
|
+
Button("x", custom_id="x", id=11)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
```python
|
|
359
|
+
from py_components import assign_ids, find_by_id, replace_by_id
|
|
360
|
+
|
|
361
|
+
payload = ui.to_payload()
|
|
362
|
+
node = ui.get(10)
|
|
363
|
+
payload = ui.replace(10, TextDisplay("updated", id=10))
|
|
364
|
+
|
|
365
|
+
# or on raw dict trees
|
|
366
|
+
assign_ids(payload["components"], start=1)
|
|
367
|
+
found = find_by_id(payload["components"], 10)
|
|
368
|
+
replace_by_id(payload["components"], 10, TextDisplay("x", id=10).to_dict())
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Use this when editing a message and only changing one piece.
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Modals
|
|
376
|
+
|
|
377
|
+
```python
|
|
378
|
+
from py_components import (
|
|
379
|
+
Modal, Label, TextInput, TextInputStyle,
|
|
380
|
+
RadioGroup, RadioGroupOption, CheckboxGroup, Checkbox,
|
|
381
|
+
FileUpload, StringSelect, SelectOption,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
modal = Modal(
|
|
385
|
+
"report_modal",
|
|
386
|
+
"bug report",
|
|
387
|
+
Label(
|
|
388
|
+
"what happened",
|
|
389
|
+
TextInput("desc", style=TextInputStyle.PARAGRAPH, max_length=1000),
|
|
390
|
+
description="as much detail as you can",
|
|
391
|
+
),
|
|
392
|
+
Label(
|
|
393
|
+
"severity",
|
|
394
|
+
RadioGroup("sev", [
|
|
395
|
+
RadioGroupOption("low", "low"),
|
|
396
|
+
RadioGroupOption("high", "high", default=True),
|
|
397
|
+
]),
|
|
398
|
+
),
|
|
399
|
+
Label(
|
|
400
|
+
"areas",
|
|
401
|
+
CheckboxGroup("areas", [
|
|
402
|
+
GroupOption("ui", "ui"),
|
|
403
|
+
GroupOption("api", "api"),
|
|
404
|
+
], min_values=1, max_values=2),
|
|
405
|
+
),
|
|
406
|
+
Label("i can reproduce this", Checkbox("repro")),
|
|
407
|
+
Label(
|
|
408
|
+
"screenshots",
|
|
409
|
+
FileUpload("shots", min_values=0, max_values=3, required=False, file_types=["image"]),
|
|
410
|
+
),
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
@bot.tree.command(name="report")
|
|
414
|
+
async def report(interaction):
|
|
415
|
+
await modal.open(interaction)
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Max **5** top-level components. Top-level should be `Label` (or `TextDisplay`). Interactive controls go **inside** Label.
|
|
419
|
+
|
|
420
|
+
### Modal
|
|
421
|
+
|
|
422
|
+
```python
|
|
423
|
+
Modal(custom_id, title, *components)
|
|
424
|
+
modal.add(*components)
|
|
425
|
+
modal.to_dict() # data object
|
|
426
|
+
modal.to_response() # {"type": 9, "data": ...}
|
|
427
|
+
await modal.open(interaction)
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Title max 45 chars. custom_id 1–100.
|
|
431
|
+
|
|
432
|
+
### Label
|
|
433
|
+
|
|
434
|
+
```python
|
|
435
|
+
Label("visible label", child_component, description="optional helper text")
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Label text max 45. Description max 100. One child.
|
|
439
|
+
|
|
440
|
+
### TextInput
|
|
441
|
+
|
|
442
|
+
```python
|
|
443
|
+
TextInput(
|
|
444
|
+
"field_id",
|
|
445
|
+
style=TextInputStyle.SHORT, # or PARAGRAPH (2)
|
|
446
|
+
min_length=0,
|
|
447
|
+
max_length=4000,
|
|
448
|
+
required=True,
|
|
449
|
+
value="prefill",
|
|
450
|
+
placeholder="type here",
|
|
451
|
+
)
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### FileUpload
|
|
455
|
+
|
|
456
|
+
```python
|
|
457
|
+
FileUpload(
|
|
458
|
+
"files",
|
|
459
|
+
min_values=0,
|
|
460
|
+
max_values=10,
|
|
461
|
+
required=True,
|
|
462
|
+
file_types=["image", "video", "audio", ".pdf"], # optional filter
|
|
463
|
+
)
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### RadioGroup / CheckboxGroup / Checkbox
|
|
467
|
+
|
|
468
|
+
```python
|
|
469
|
+
# one choice, 2–10 options
|
|
470
|
+
RadioGroup("sev", [RadioGroupOption("low", "low"), RadioGroupOption("high", "high")], required=True)
|
|
471
|
+
|
|
472
|
+
# multi, 1–10 options
|
|
473
|
+
CheckboxGroup(
|
|
474
|
+
"tags",
|
|
475
|
+
[GroupOption("a", "a"), GroupOption("b", "b")],
|
|
476
|
+
min_values=1,
|
|
477
|
+
max_values=2,
|
|
478
|
+
required=True,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
# single yes/no — label comes from the Label wrapper
|
|
482
|
+
Checkbox("agree", default=False)
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
`GroupOption` / `RadioGroupOption` / `CheckboxGroupOption` are the same thing:
|
|
486
|
+
|
|
487
|
+
```python
|
|
488
|
+
GroupOption("label", "value", description="...", default=False)
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Selects also work inside Label in modals (StringSelect, UserSelect, etc.).
|
|
492
|
+
|
|
493
|
+
On submit you get a `MODAL_SUBMIT` interaction. Values are under `interaction.data["components"]` (each Label wraps its filled child).
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
## View + callbacks
|
|
498
|
+
|
|
499
|
+
```python
|
|
500
|
+
from py_components import View, ButtonStyle
|
|
501
|
+
|
|
502
|
+
view = View()
|
|
503
|
+
|
|
504
|
+
async def on_yes(interaction):
|
|
505
|
+
await interaction.response.send_message("ok", ephemeral=True)
|
|
506
|
+
|
|
507
|
+
view.add_button("yes", "yes", style=ButtonStyle.SUCCESS, callback=on_yes)
|
|
508
|
+
view.add_button("no", "no", style=ButtonStyle.SECONDARY)
|
|
509
|
+
|
|
510
|
+
payload = view.to_payload()
|
|
511
|
+
# send payload yourself or merge into a UI
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
```python
|
|
515
|
+
view.add_component(some_component, callback=coro) # registers by custom_id
|
|
516
|
+
await view.dispatch(custom_id, interaction)
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### @button decorator
|
|
520
|
+
|
|
521
|
+
```python
|
|
522
|
+
from py_components import button
|
|
523
|
+
|
|
524
|
+
@button("click me", custom_id="my_btn", style=ButtonStyle.PRIMARY)
|
|
525
|
+
async def on_click(interaction):
|
|
526
|
+
await interaction.response.send_message("clicked")
|
|
527
|
+
|
|
528
|
+
# sets on_click._button_label / _button_custom_id / _button_style
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Registry
|
|
532
|
+
|
|
533
|
+
Global custom_id → callback map.
|
|
534
|
+
|
|
535
|
+
```python
|
|
536
|
+
from py_components import Registry, default_registry
|
|
537
|
+
|
|
538
|
+
@default_registry.route("yes")
|
|
539
|
+
async def handle_yes(interaction):
|
|
540
|
+
await interaction.response.send_message("ok")
|
|
541
|
+
|
|
542
|
+
# or
|
|
543
|
+
default_registry.register("no", handle_no)
|
|
544
|
+
|
|
545
|
+
await default_registry.dispatch(interaction.data["custom_id"], interaction)
|
|
546
|
+
default_registry.clear()
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
You can make your own `Registry()` instance too.
|
|
550
|
+
|
|
551
|
+
Wire it up in your bot:
|
|
552
|
+
|
|
553
|
+
```python
|
|
554
|
+
@bot.event
|
|
555
|
+
async def on_interaction(interaction):
|
|
556
|
+
if interaction.type.name == "component":
|
|
557
|
+
cid = interaction.data.get("custom_id")
|
|
558
|
+
await default_registry.dispatch(cid, interaction)
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
## Low-level HTTP
|
|
564
|
+
|
|
565
|
+
Used internally. Available if you're not on discord.py send paths.
|
|
566
|
+
|
|
567
|
+
```python
|
|
568
|
+
from py_components import send_message, edit_message, respond_interaction, open_modal
|
|
569
|
+
|
|
570
|
+
await send_message(channel_id, bot_token, payload, files=None)
|
|
571
|
+
await edit_message(channel_id, message_id, bot_token, payload)
|
|
572
|
+
await respond_interaction(application_id, interaction_token, bot_token, payload, type=4)
|
|
573
|
+
await open_modal(interaction_id, interaction_token, modal_data)
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
`files` for `send_message` is a list of dicts: `{"fp": fileobj, "filename": "x.png", "content_type": "..."}`.
|
|
577
|
+
|
|
578
|
+
API base: `https://discord.com/api/v10`.
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
582
|
+
## Enums / constants
|
|
583
|
+
|
|
584
|
+
```python
|
|
585
|
+
from py_components import (
|
|
586
|
+
ComponentType, # ACTION_ROW=1, BUTTON=2, ... CHECKBOX=23
|
|
587
|
+
ButtonStyle, # PRIMARY..PREMIUM
|
|
588
|
+
TextInputStyle, # SHORT=1, PARAGRAPH=2
|
|
589
|
+
SeparatorSpacing, # SMALL=1, LARGE=2
|
|
590
|
+
IS_COMPONENTS_V2, # 1 << 15
|
|
591
|
+
)
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## Full message example
|
|
597
|
+
|
|
598
|
+
```python
|
|
599
|
+
from py_components import (
|
|
600
|
+
UI, Container, TextDisplay, Section, Thumbnail, Separator,
|
|
601
|
+
SeparatorSpacing, MediaGallery, ActionRow, Button, ButtonStyle,
|
|
602
|
+
StringSelect, SelectOption, File,
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
ui = UI(
|
|
606
|
+
Container(
|
|
607
|
+
TextDisplay("# server status"),
|
|
608
|
+
Separator(spacing=SeparatorSpacing.SMALL),
|
|
609
|
+
Section(
|
|
610
|
+
TextDisplay("**online** — 1204 members"),
|
|
611
|
+
TextDisplay("peak today: 312"),
|
|
612
|
+
accessory=Thumbnail("https://cdn.discordapp.com/icons/ID/icon.png"),
|
|
613
|
+
),
|
|
614
|
+
Separator(divider=True),
|
|
615
|
+
MediaGallery(
|
|
616
|
+
"https://example.com/chart1.png",
|
|
617
|
+
"https://example.com/chart2.png",
|
|
618
|
+
),
|
|
619
|
+
accent_color=0x5865F2,
|
|
620
|
+
),
|
|
621
|
+
ActionRow(
|
|
622
|
+
StringSelect(
|
|
623
|
+
"range",
|
|
624
|
+
options=[
|
|
625
|
+
SelectOption("24h", "24h"),
|
|
626
|
+
SelectOption("7d", "7d"),
|
|
627
|
+
SelectOption("30d", "30d"),
|
|
628
|
+
],
|
|
629
|
+
placeholder="range",
|
|
630
|
+
)
|
|
631
|
+
),
|
|
632
|
+
Button("refresh", custom_id="refresh", style=ButtonStyle.PRIMARY, emoji="🔄"),
|
|
633
|
+
Button("docs", style=ButtonStyle.LINK, url="https://example.com/docs"),
|
|
634
|
+
File("report.pdf"),
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
await ui.send(ctx)
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
## Full modal example
|
|
643
|
+
|
|
644
|
+
```python
|
|
645
|
+
from py_components import (
|
|
646
|
+
Modal, Label, TextInput, TextInputStyle,
|
|
647
|
+
RadioGroup, RadioGroupOption, CheckboxGroup, GroupOption,
|
|
648
|
+
Checkbox, FileUpload, UserSelect,
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
modal = Modal(
|
|
652
|
+
"apply",
|
|
653
|
+
"staff application",
|
|
654
|
+
Label("why us", TextInput("why", style=TextInputStyle.PARAGRAPH, min_length=50)),
|
|
655
|
+
Label(
|
|
656
|
+
"experience",
|
|
657
|
+
RadioGroup("exp", [
|
|
658
|
+
RadioGroupOption("none", "none"),
|
|
659
|
+
RadioGroupOption("some", "some"),
|
|
660
|
+
RadioGroupOption("lots", "lots"),
|
|
661
|
+
]),
|
|
662
|
+
),
|
|
663
|
+
Label(
|
|
664
|
+
"teams",
|
|
665
|
+
CheckboxGroup("teams", [
|
|
666
|
+
GroupOption("mod", "mod"),
|
|
667
|
+
GroupOption("events", "events"),
|
|
668
|
+
GroupOption("tech", "tech"),
|
|
669
|
+
], min_values=1, max_values=3),
|
|
670
|
+
),
|
|
671
|
+
Label("referral", UserSelect("ref", min_values=0, max_values=1, required=False)),
|
|
672
|
+
Label("i read the rules", Checkbox("rules")),
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
await modal.open(interaction)
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
680
|
+
## Limits (discord)
|
|
681
|
+
|
|
682
|
+
- V2 flag is permanent on that message
|
|
683
|
+
- no content / embeds / stickers / poll with V2
|
|
684
|
+
- attachments need a File or media component
|
|
685
|
+
- 40 components max per message
|
|
686
|
+
- buttons: ActionRow or Section accessory
|
|
687
|
+
- one select per ActionRow
|
|
688
|
+
- modals: max 5 top-level components
|
|
689
|
+
- Label label ≤45, description ≤100
|
|
690
|
+
- Button label ≤80, custom_id ≤100
|
|
691
|
+
|
|
692
|
+
## License
|
|
693
|
+
|
|
694
|
+
MIT
|