disagreement 0.0.1__py3-none-any.whl
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.
- disagreement/__init__.py +36 -0
- disagreement/cache.py +55 -0
- disagreement/client.py +1144 -0
- disagreement/components.py +166 -0
- disagreement/enums.py +357 -0
- disagreement/error_handler.py +33 -0
- disagreement/errors.py +112 -0
- disagreement/event_dispatcher.py +243 -0
- disagreement/gateway.py +490 -0
- disagreement/http.py +657 -0
- disagreement/hybrid_context.py +32 -0
- disagreement/i18n.py +22 -0
- disagreement/interactions.py +572 -0
- disagreement/logging_config.py +26 -0
- disagreement/models.py +1642 -0
- disagreement/oauth.py +109 -0
- disagreement/permissions.py +99 -0
- disagreement/rate_limiter.py +75 -0
- disagreement/shard_manager.py +65 -0
- disagreement/typing.py +42 -0
- disagreement/ui/__init__.py +17 -0
- disagreement/ui/button.py +99 -0
- disagreement/ui/item.py +38 -0
- disagreement/ui/modal.py +132 -0
- disagreement/ui/select.py +92 -0
- disagreement/ui/view.py +165 -0
- disagreement/voice_client.py +120 -0
- disagreement-0.0.1.dist-info/METADATA +163 -0
- disagreement-0.0.1.dist-info/RECORD +32 -0
- disagreement-0.0.1.dist-info/WHEEL +5 -0
- disagreement-0.0.1.dist-info/licenses/LICENSE +26 -0
- disagreement-0.0.1.dist-info/top_level.txt +1 -0
disagreement/i18n.py
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
import json
|
2
|
+
from typing import Dict, Optional
|
3
|
+
|
4
|
+
_translations: Dict[str, Dict[str, str]] = {}
|
5
|
+
|
6
|
+
|
7
|
+
def set_translations(locale: str, mapping: Dict[str, str]) -> None:
|
8
|
+
"""Set translations for a locale."""
|
9
|
+
_translations[locale] = mapping
|
10
|
+
|
11
|
+
|
12
|
+
def load_translations(locale: str, file_path: str) -> None:
|
13
|
+
"""Load translations for *locale* from a JSON file."""
|
14
|
+
with open(file_path, "r", encoding="utf-8") as handle:
|
15
|
+
_translations[locale] = json.load(handle)
|
16
|
+
|
17
|
+
|
18
|
+
def translate(key: str, locale: str, *, default: Optional[str] = None) -> str:
|
19
|
+
"""Return the translated string for *key* in *locale*."""
|
20
|
+
return _translations.get(locale, {}).get(
|
21
|
+
key, default if default is not None else key
|
22
|
+
)
|
@@ -0,0 +1,572 @@
|
|
1
|
+
# disagreement/interactions.py
|
2
|
+
|
3
|
+
"""
|
4
|
+
Data models for Discord Interaction objects.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from typing import Optional, List, Dict, Union, Any, TYPE_CHECKING
|
8
|
+
|
9
|
+
from .enums import (
|
10
|
+
ApplicationCommandType,
|
11
|
+
ApplicationCommandOptionType,
|
12
|
+
InteractionType,
|
13
|
+
InteractionCallbackType,
|
14
|
+
IntegrationType,
|
15
|
+
InteractionContextType,
|
16
|
+
ChannelType,
|
17
|
+
)
|
18
|
+
|
19
|
+
# Runtime imports for models used in this module
|
20
|
+
from .models import (
|
21
|
+
User,
|
22
|
+
Message,
|
23
|
+
Member,
|
24
|
+
Role,
|
25
|
+
Embed,
|
26
|
+
PartialChannel,
|
27
|
+
Attachment,
|
28
|
+
ActionRow,
|
29
|
+
Component,
|
30
|
+
AllowedMentions,
|
31
|
+
)
|
32
|
+
|
33
|
+
if TYPE_CHECKING:
|
34
|
+
# Import Client type only for type checking to avoid circular imports
|
35
|
+
from .client import Client
|
36
|
+
from .ui.modal import Modal
|
37
|
+
|
38
|
+
# MessageFlags, PartialAttachment can be added if/when defined
|
39
|
+
|
40
|
+
Snowflake = str
|
41
|
+
|
42
|
+
|
43
|
+
# Based on Application Command Option Choice Structure
|
44
|
+
class ApplicationCommandOptionChoice:
|
45
|
+
"""Represents a choice for an application command option."""
|
46
|
+
|
47
|
+
def __init__(self, data: dict):
|
48
|
+
self.name: str = data["name"]
|
49
|
+
self.value: Union[str, int, float] = data["value"]
|
50
|
+
self.name_localizations: Optional[Dict[str, str]] = data.get(
|
51
|
+
"name_localizations"
|
52
|
+
)
|
53
|
+
|
54
|
+
def __repr__(self) -> str:
|
55
|
+
return (
|
56
|
+
f"<ApplicationCommandOptionChoice name='{self.name}' value={self.value!r}>"
|
57
|
+
)
|
58
|
+
|
59
|
+
def to_dict(self) -> Dict[str, Any]:
|
60
|
+
payload: Dict[str, Any] = {"name": self.name, "value": self.value}
|
61
|
+
if self.name_localizations:
|
62
|
+
payload["name_localizations"] = self.name_localizations
|
63
|
+
return payload
|
64
|
+
|
65
|
+
|
66
|
+
# Based on Application Command Option Structure
|
67
|
+
class ApplicationCommandOption:
|
68
|
+
"""Represents an option for an application command."""
|
69
|
+
|
70
|
+
def __init__(self, data: dict):
|
71
|
+
self.type: ApplicationCommandOptionType = ApplicationCommandOptionType(
|
72
|
+
data["type"]
|
73
|
+
)
|
74
|
+
self.name: str = data["name"]
|
75
|
+
self.description: str = data["description"]
|
76
|
+
self.required: bool = data.get("required", False)
|
77
|
+
|
78
|
+
self.choices: Optional[List[ApplicationCommandOptionChoice]] = (
|
79
|
+
[ApplicationCommandOptionChoice(c) for c in data["choices"]]
|
80
|
+
if data.get("choices")
|
81
|
+
else None
|
82
|
+
)
|
83
|
+
|
84
|
+
self.options: Optional[List["ApplicationCommandOption"]] = (
|
85
|
+
[ApplicationCommandOption(o) for o in data["options"]]
|
86
|
+
if data.get("options")
|
87
|
+
else None
|
88
|
+
) # For subcommands/groups
|
89
|
+
|
90
|
+
self.channel_types: Optional[List[ChannelType]] = (
|
91
|
+
[ChannelType(ct) for ct in data.get("channel_types", [])]
|
92
|
+
if data.get("channel_types")
|
93
|
+
else None
|
94
|
+
)
|
95
|
+
self.min_value: Optional[Union[int, float]] = data.get("min_value")
|
96
|
+
self.max_value: Optional[Union[int, float]] = data.get("max_value")
|
97
|
+
self.min_length: Optional[int] = data.get("min_length")
|
98
|
+
self.max_length: Optional[int] = data.get("max_length")
|
99
|
+
self.autocomplete: bool = data.get("autocomplete", False)
|
100
|
+
self.name_localizations: Optional[Dict[str, str]] = data.get(
|
101
|
+
"name_localizations"
|
102
|
+
)
|
103
|
+
self.description_localizations: Optional[Dict[str, str]] = data.get(
|
104
|
+
"description_localizations"
|
105
|
+
)
|
106
|
+
|
107
|
+
def __repr__(self) -> str:
|
108
|
+
return f"<ApplicationCommandOption name='{self.name}' type={self.type!r}>"
|
109
|
+
|
110
|
+
def to_dict(self) -> Dict[str, Any]:
|
111
|
+
payload: Dict[str, Any] = {
|
112
|
+
"type": self.type.value,
|
113
|
+
"name": self.name,
|
114
|
+
"description": self.description,
|
115
|
+
}
|
116
|
+
if self.required: # Defaults to False, only include if True
|
117
|
+
payload["required"] = self.required
|
118
|
+
if self.choices:
|
119
|
+
payload["choices"] = [c.to_dict() for c in self.choices]
|
120
|
+
if self.options: # For subcommands/groups
|
121
|
+
payload["options"] = [o.to_dict() for o in self.options]
|
122
|
+
if self.channel_types:
|
123
|
+
payload["channel_types"] = [ct.value for ct in self.channel_types]
|
124
|
+
if self.min_value is not None:
|
125
|
+
payload["min_value"] = self.min_value
|
126
|
+
if self.max_value is not None:
|
127
|
+
payload["max_value"] = self.max_value
|
128
|
+
if self.min_length is not None:
|
129
|
+
payload["min_length"] = self.min_length
|
130
|
+
if self.max_length is not None:
|
131
|
+
payload["max_length"] = self.max_length
|
132
|
+
if self.autocomplete: # Defaults to False, only include if True
|
133
|
+
payload["autocomplete"] = self.autocomplete
|
134
|
+
if self.name_localizations:
|
135
|
+
payload["name_localizations"] = self.name_localizations
|
136
|
+
if self.description_localizations:
|
137
|
+
payload["description_localizations"] = self.description_localizations
|
138
|
+
return payload
|
139
|
+
|
140
|
+
|
141
|
+
# Based on Application Command Structure
|
142
|
+
class ApplicationCommand:
|
143
|
+
"""Represents an application command."""
|
144
|
+
|
145
|
+
def __init__(self, data: dict):
|
146
|
+
self.id: Optional[Snowflake] = data.get("id")
|
147
|
+
self.type: ApplicationCommandType = ApplicationCommandType(
|
148
|
+
data.get("type", 1)
|
149
|
+
) # Default to CHAT_INPUT
|
150
|
+
self.application_id: Optional[Snowflake] = data.get("application_id")
|
151
|
+
self.guild_id: Optional[Snowflake] = data.get("guild_id")
|
152
|
+
self.name: str = data["name"]
|
153
|
+
self.description: str = data.get(
|
154
|
+
"description", ""
|
155
|
+
) # Empty for USER/MESSAGE commands
|
156
|
+
|
157
|
+
self.options: Optional[List[ApplicationCommandOption]] = (
|
158
|
+
[ApplicationCommandOption(o) for o in data["options"]]
|
159
|
+
if data.get("options")
|
160
|
+
else None
|
161
|
+
)
|
162
|
+
|
163
|
+
self.default_member_permissions: Optional[str] = data.get(
|
164
|
+
"default_member_permissions"
|
165
|
+
)
|
166
|
+
self.dm_permission: Optional[bool] = data.get("dm_permission") # Deprecated
|
167
|
+
self.nsfw: bool = data.get("nsfw", False)
|
168
|
+
self.version: Optional[Snowflake] = data.get("version")
|
169
|
+
self.name_localizations: Optional[Dict[str, str]] = data.get(
|
170
|
+
"name_localizations"
|
171
|
+
)
|
172
|
+
self.description_localizations: Optional[Dict[str, str]] = data.get(
|
173
|
+
"description_localizations"
|
174
|
+
)
|
175
|
+
|
176
|
+
self.integration_types: Optional[List[IntegrationType]] = (
|
177
|
+
[IntegrationType(it) for it in data["integration_types"]]
|
178
|
+
if data.get("integration_types")
|
179
|
+
else None
|
180
|
+
)
|
181
|
+
|
182
|
+
self.contexts: Optional[List[InteractionContextType]] = (
|
183
|
+
[InteractionContextType(c) for c in data["contexts"]]
|
184
|
+
if data.get("contexts")
|
185
|
+
else None
|
186
|
+
)
|
187
|
+
|
188
|
+
def __repr__(self) -> str:
|
189
|
+
return (
|
190
|
+
f"<ApplicationCommand id='{self.id}' name='{self.name}' type={self.type!r}>"
|
191
|
+
)
|
192
|
+
|
193
|
+
|
194
|
+
# Based on Interaction Object's Resolved Data Structure
|
195
|
+
class ResolvedData:
|
196
|
+
"""Represents resolved data for an interaction."""
|
197
|
+
|
198
|
+
def __init__(
|
199
|
+
self, data: dict, client_instance: Optional["Client"] = None
|
200
|
+
): # client_instance for model hydration
|
201
|
+
# Models are now imported in TYPE_CHECKING block
|
202
|
+
|
203
|
+
users_data = data.get("users", {})
|
204
|
+
self.users: Dict[Snowflake, "User"] = {
|
205
|
+
uid: User(udata) for uid, udata in users_data.items()
|
206
|
+
}
|
207
|
+
|
208
|
+
self.members: Dict[Snowflake, "Member"] = {}
|
209
|
+
for mid, mdata in data.get("members", {}).items():
|
210
|
+
member_payload = dict(mdata)
|
211
|
+
member_payload.setdefault("id", mid)
|
212
|
+
if "user" not in member_payload and mid in users_data:
|
213
|
+
member_payload["user"] = users_data[mid]
|
214
|
+
self.members[mid] = Member(member_payload, client_instance=client_instance)
|
215
|
+
|
216
|
+
self.roles: Dict[Snowflake, "Role"] = {
|
217
|
+
rid: Role(rdata) for rid, rdata in data.get("roles", {}).items()
|
218
|
+
}
|
219
|
+
|
220
|
+
self.channels: Dict[Snowflake, "PartialChannel"] = {
|
221
|
+
cid: PartialChannel(cdata, client_instance=client_instance)
|
222
|
+
for cid, cdata in data.get("channels", {}).items()
|
223
|
+
}
|
224
|
+
|
225
|
+
self.messages: Dict[Snowflake, "Message"] = (
|
226
|
+
{
|
227
|
+
mid: Message(mdata, client_instance=client_instance) for mid, mdata in data.get("messages", {}).items() # type: ignore[misc]
|
228
|
+
}
|
229
|
+
if client_instance
|
230
|
+
else {}
|
231
|
+
) # Only hydrate if client is available
|
232
|
+
|
233
|
+
self.attachments: Dict[Snowflake, "Attachment"] = {
|
234
|
+
aid: Attachment(adata) for aid, adata in data.get("attachments", {}).items()
|
235
|
+
}
|
236
|
+
|
237
|
+
def __repr__(self) -> str:
|
238
|
+
return f"<ResolvedData users={len(self.users)} members={len(self.members)} roles={len(self.roles)} channels={len(self.channels)} messages={len(self.messages)} attachments={len(self.attachments)}>"
|
239
|
+
|
240
|
+
|
241
|
+
# Based on Interaction Object's Data Structure (for Application Commands)
|
242
|
+
class InteractionData:
|
243
|
+
"""Represents the data payload for an interaction."""
|
244
|
+
|
245
|
+
def __init__(self, data: dict, client_instance: Optional["Client"] = None):
|
246
|
+
self.id: Optional[Snowflake] = data.get("id") # Command ID
|
247
|
+
self.name: Optional[str] = data.get("name") # Command name
|
248
|
+
self.type: Optional[ApplicationCommandType] = (
|
249
|
+
ApplicationCommandType(data["type"]) if data.get("type") else None
|
250
|
+
)
|
251
|
+
|
252
|
+
self.resolved: Optional[ResolvedData] = (
|
253
|
+
ResolvedData(data["resolved"], client_instance=client_instance)
|
254
|
+
if data.get("resolved")
|
255
|
+
else None
|
256
|
+
)
|
257
|
+
|
258
|
+
# For CHAT_INPUT, this is List[ApplicationCommandInteractionDataOption]
|
259
|
+
# For USER/MESSAGE, this is not present or different.
|
260
|
+
# For now, storing as raw list of dicts. Parsing can happen in handler.
|
261
|
+
self.options: Optional[List[Dict[str, Any]]] = data.get("options")
|
262
|
+
|
263
|
+
# For message components
|
264
|
+
self.custom_id: Optional[str] = data.get("custom_id")
|
265
|
+
self.component_type: Optional[int] = data.get("component_type")
|
266
|
+
self.values: Optional[List[str]] = data.get("values")
|
267
|
+
|
268
|
+
self.guild_id: Optional[Snowflake] = data.get("guild_id")
|
269
|
+
self.target_id: Optional[Snowflake] = data.get(
|
270
|
+
"target_id"
|
271
|
+
) # For USER/MESSAGE commands
|
272
|
+
|
273
|
+
def __repr__(self) -> str:
|
274
|
+
return f"<InteractionData id='{self.id}' name='{self.name}' type={self.type!r}>"
|
275
|
+
|
276
|
+
|
277
|
+
# Based on Interaction Object Structure
|
278
|
+
class Interaction:
|
279
|
+
"""Represents an interaction from Discord."""
|
280
|
+
|
281
|
+
def __init__(self, data: dict, client_instance: "Client"):
|
282
|
+
self._client: "Client" = client_instance
|
283
|
+
|
284
|
+
self.id: Snowflake = data["id"]
|
285
|
+
self.application_id: Snowflake = data["application_id"]
|
286
|
+
self.type: InteractionType = InteractionType(data["type"])
|
287
|
+
|
288
|
+
self.data: Optional[InteractionData] = (
|
289
|
+
InteractionData(data["data"], client_instance=client_instance)
|
290
|
+
if data.get("data")
|
291
|
+
else None
|
292
|
+
)
|
293
|
+
|
294
|
+
self.guild_id: Optional[Snowflake] = data.get("guild_id")
|
295
|
+
self.channel_id: Optional[Snowflake] = data.get(
|
296
|
+
"channel_id"
|
297
|
+
) # Will be present on command invocations
|
298
|
+
|
299
|
+
member_data = data.get("member")
|
300
|
+
user_data_from_member = (
|
301
|
+
member_data.get("user") if isinstance(member_data, dict) else None
|
302
|
+
)
|
303
|
+
|
304
|
+
self.member: Optional["Member"] = (
|
305
|
+
Member(member_data, client_instance=self._client) if member_data else None
|
306
|
+
)
|
307
|
+
|
308
|
+
# User object is included within member if in guild, otherwise it's top-level
|
309
|
+
# If self.member was successfully hydrated, its .user attribute should be preferred if it exists.
|
310
|
+
# However, Member.__init__ handles setting User attributes.
|
311
|
+
# The primary source for User is data.get("user") or member_data.get("user").
|
312
|
+
|
313
|
+
if data.get("user"):
|
314
|
+
self.user: Optional["User"] = User(data["user"])
|
315
|
+
elif user_data_from_member:
|
316
|
+
self.user: Optional["User"] = User(user_data_from_member)
|
317
|
+
elif (
|
318
|
+
self.member
|
319
|
+
): # If member was hydrated and has user attributes (e.g. from Member(User) inheritance)
|
320
|
+
# This assumes Member correctly populates its User parts.
|
321
|
+
self.user: Optional["User"] = self.member # Member is a User subclass
|
322
|
+
else:
|
323
|
+
self.user: Optional["User"] = None
|
324
|
+
|
325
|
+
self.token: str = data["token"] # For responding to the interaction
|
326
|
+
self.version: int = data["version"]
|
327
|
+
|
328
|
+
self.message: Optional["Message"] = (
|
329
|
+
Message(data["message"], client_instance=client_instance)
|
330
|
+
if data.get("message")
|
331
|
+
else None
|
332
|
+
) # For component interactions
|
333
|
+
|
334
|
+
self.app_permissions: Optional[str] = data.get(
|
335
|
+
"app_permissions"
|
336
|
+
) # Bitwise set of permissions the app has in the source channel
|
337
|
+
self.locale: Optional[str] = data.get(
|
338
|
+
"locale"
|
339
|
+
) # Selected language of the invoking user
|
340
|
+
self.guild_locale: Optional[str] = data.get(
|
341
|
+
"guild_locale"
|
342
|
+
) # Guild's preferred language
|
343
|
+
|
344
|
+
self.response = InteractionResponse(self)
|
345
|
+
|
346
|
+
async def respond(
|
347
|
+
self,
|
348
|
+
content: Optional[str] = None,
|
349
|
+
*,
|
350
|
+
embed: Optional[Embed] = None,
|
351
|
+
embeds: Optional[List[Embed]] = None,
|
352
|
+
components: Optional[List[ActionRow]] = None,
|
353
|
+
ephemeral: bool = False,
|
354
|
+
tts: bool = False,
|
355
|
+
) -> None:
|
356
|
+
"""|coro|
|
357
|
+
|
358
|
+
Responds to this interaction.
|
359
|
+
|
360
|
+
Parameters:
|
361
|
+
content (Optional[str]): The content of the message.
|
362
|
+
embed (Optional[Embed]): A single embed to send.
|
363
|
+
embeds (Optional[List[Embed]]): A list of embeds to send.
|
364
|
+
components (Optional[List[ActionRow]]): A list of ActionRow components.
|
365
|
+
ephemeral (bool): Whether the response should be ephemeral (only visible to the user).
|
366
|
+
tts (bool): Whether the message should be sent with text-to-speech.
|
367
|
+
"""
|
368
|
+
if embed and embeds:
|
369
|
+
raise ValueError("Cannot provide both embed and embeds.")
|
370
|
+
|
371
|
+
data: Dict[str, Any] = {}
|
372
|
+
if tts:
|
373
|
+
data["tts"] = True
|
374
|
+
if content:
|
375
|
+
data["content"] = content
|
376
|
+
if embed:
|
377
|
+
data["embeds"] = [embed.to_dict()]
|
378
|
+
elif embeds:
|
379
|
+
data["embeds"] = [e.to_dict() for e in embeds]
|
380
|
+
if components:
|
381
|
+
data["components"] = [c.to_dict() for c in components]
|
382
|
+
if ephemeral:
|
383
|
+
data["flags"] = 1 << 6 # EPHEMERAL flag
|
384
|
+
|
385
|
+
payload = InteractionResponsePayload(
|
386
|
+
type=InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
|
387
|
+
data=InteractionCallbackData(data),
|
388
|
+
)
|
389
|
+
|
390
|
+
await self._client._http.create_interaction_response(
|
391
|
+
interaction_id=self.id,
|
392
|
+
interaction_token=self.token,
|
393
|
+
payload=payload,
|
394
|
+
)
|
395
|
+
|
396
|
+
async def respond_modal(self, modal: "Modal") -> None:
|
397
|
+
"""|coro| Send a modal in response to this interaction."""
|
398
|
+
|
399
|
+
from typing import Any, cast
|
400
|
+
|
401
|
+
payload = {
|
402
|
+
"type": InteractionCallbackType.MODAL.value,
|
403
|
+
"data": modal.to_dict(),
|
404
|
+
}
|
405
|
+
await self._client._http.create_interaction_response(
|
406
|
+
interaction_id=self.id,
|
407
|
+
interaction_token=self.token,
|
408
|
+
payload=cast(Any, payload),
|
409
|
+
)
|
410
|
+
|
411
|
+
async def edit(
|
412
|
+
self,
|
413
|
+
content: Optional[str] = None,
|
414
|
+
*,
|
415
|
+
embed: Optional[Embed] = None,
|
416
|
+
embeds: Optional[List[Embed]] = None,
|
417
|
+
components: Optional[List[ActionRow]] = None,
|
418
|
+
attachments: Optional[List[Any]] = None,
|
419
|
+
allowed_mentions: Optional[Dict[str, Any]] = None,
|
420
|
+
) -> None:
|
421
|
+
"""|coro|
|
422
|
+
|
423
|
+
Edits the original response to this interaction.
|
424
|
+
|
425
|
+
If the interaction is from a component, this will acknowledge the
|
426
|
+
interaction and update the message in one operation.
|
427
|
+
|
428
|
+
Parameters:
|
429
|
+
content (Optional[str]): The new message content.
|
430
|
+
embed (Optional[Embed]): A single embed to send. Ignored if
|
431
|
+
``embeds`` is provided.
|
432
|
+
embeds (Optional[List[Embed]]): A list of embeds to send.
|
433
|
+
components (Optional[List[ActionRow]]): Updated components for the
|
434
|
+
message.
|
435
|
+
attachments (Optional[List[Any]]): Attachments to include with the
|
436
|
+
message.
|
437
|
+
allowed_mentions (Optional[Dict[str, Any]]): Controls mentions in the
|
438
|
+
message.
|
439
|
+
"""
|
440
|
+
if embed and embeds:
|
441
|
+
raise ValueError("Cannot provide both embed and embeds.")
|
442
|
+
|
443
|
+
payload_data: Dict[str, Any] = {}
|
444
|
+
if content is not None:
|
445
|
+
payload_data["content"] = content
|
446
|
+
if embed:
|
447
|
+
payload_data["embeds"] = [embed.to_dict()]
|
448
|
+
elif embeds is not None:
|
449
|
+
payload_data["embeds"] = [e.to_dict() for e in embeds]
|
450
|
+
if components is not None:
|
451
|
+
payload_data["components"] = [c.to_dict() for c in components]
|
452
|
+
if attachments is not None:
|
453
|
+
payload_data["attachments"] = [
|
454
|
+
a.to_dict() if hasattr(a, "to_dict") else a for a in attachments
|
455
|
+
]
|
456
|
+
if allowed_mentions is not None:
|
457
|
+
payload_data["allowed_mentions"] = allowed_mentions
|
458
|
+
|
459
|
+
if self.type == InteractionType.MESSAGE_COMPONENT:
|
460
|
+
# For component interactions, we send an UPDATE_MESSAGE response
|
461
|
+
# to acknowledge the interaction and edit the message simultaneously.
|
462
|
+
payload = InteractionResponsePayload(
|
463
|
+
type=InteractionCallbackType.UPDATE_MESSAGE,
|
464
|
+
data=InteractionCallbackData(payload_data),
|
465
|
+
)
|
466
|
+
await self._client._http.create_interaction_response(
|
467
|
+
self.id, self.token, payload
|
468
|
+
)
|
469
|
+
else:
|
470
|
+
# For other interaction types (like an initial slash command response),
|
471
|
+
# we edit the original response via the webhook endpoint.
|
472
|
+
await self._client._http.edit_original_interaction_response(
|
473
|
+
application_id=self.application_id,
|
474
|
+
interaction_token=self.token,
|
475
|
+
payload=payload_data,
|
476
|
+
)
|
477
|
+
|
478
|
+
def __repr__(self) -> str:
|
479
|
+
return f"<Interaction id='{self.id}' type={self.type!r}>"
|
480
|
+
|
481
|
+
|
482
|
+
class InteractionResponse:
|
483
|
+
"""Helper for sending responses for an :class:`Interaction`."""
|
484
|
+
|
485
|
+
def __init__(self, interaction: "Interaction") -> None:
|
486
|
+
self._interaction = interaction
|
487
|
+
|
488
|
+
async def send_modal(self, modal: "Modal") -> None:
|
489
|
+
"""Sends a modal response."""
|
490
|
+
payload = InteractionResponsePayload(
|
491
|
+
type=InteractionCallbackType.MODAL,
|
492
|
+
data=InteractionCallbackData(modal.to_dict()),
|
493
|
+
)
|
494
|
+
await self._interaction._client._http.create_interaction_response(
|
495
|
+
self._interaction.id,
|
496
|
+
self._interaction.token,
|
497
|
+
payload,
|
498
|
+
)
|
499
|
+
|
500
|
+
|
501
|
+
# Based on Interaction Response Object's Data Structure
|
502
|
+
class InteractionCallbackData:
|
503
|
+
"""Data for an interaction response."""
|
504
|
+
|
505
|
+
def __init__(self, data: dict):
|
506
|
+
self.tts: Optional[bool] = data.get("tts")
|
507
|
+
self.content: Optional[str] = data.get("content")
|
508
|
+
self.embeds: Optional[List[Embed]] = (
|
509
|
+
[Embed(e) for e in data.get("embeds", [])] if data.get("embeds") else None
|
510
|
+
)
|
511
|
+
self.allowed_mentions: Optional[AllowedMentions] = (
|
512
|
+
AllowedMentions(data["allowed_mentions"])
|
513
|
+
if data.get("allowed_mentions")
|
514
|
+
else None
|
515
|
+
)
|
516
|
+
self.flags: Optional[int] = data.get("flags") # MessageFlags enum could be used
|
517
|
+
from .components import component_factory
|
518
|
+
|
519
|
+
self.components: Optional[List[Component]] = (
|
520
|
+
[component_factory(c) for c in data.get("components", [])]
|
521
|
+
if data.get("components")
|
522
|
+
else None
|
523
|
+
)
|
524
|
+
self.attachments: Optional[List[Attachment]] = (
|
525
|
+
[Attachment(a) for a in data.get("attachments", [])]
|
526
|
+
if data.get("attachments")
|
527
|
+
else None
|
528
|
+
)
|
529
|
+
|
530
|
+
def to_dict(self) -> dict:
|
531
|
+
# Helper to convert to dict for sending to Discord API
|
532
|
+
payload = {}
|
533
|
+
if self.tts is not None:
|
534
|
+
payload["tts"] = self.tts
|
535
|
+
if self.content is not None:
|
536
|
+
payload["content"] = self.content
|
537
|
+
if self.embeds is not None:
|
538
|
+
payload["embeds"] = [e.to_dict() for e in self.embeds]
|
539
|
+
if self.allowed_mentions is not None:
|
540
|
+
payload["allowed_mentions"] = self.allowed_mentions.to_dict()
|
541
|
+
if self.flags is not None:
|
542
|
+
payload["flags"] = self.flags
|
543
|
+
if self.components is not None:
|
544
|
+
payload["components"] = [c.to_dict() for c in self.components]
|
545
|
+
if self.attachments is not None:
|
546
|
+
payload["attachments"] = [a.to_dict() for a in self.attachments]
|
547
|
+
return payload
|
548
|
+
|
549
|
+
def __repr__(self) -> str:
|
550
|
+
return f"<InteractionCallbackData content='{self.content[:20] if self.content else None}'>"
|
551
|
+
|
552
|
+
|
553
|
+
# Based on Interaction Response Object Structure
|
554
|
+
class InteractionResponsePayload:
|
555
|
+
"""Payload for responding to an interaction."""
|
556
|
+
|
557
|
+
def __init__(
|
558
|
+
self,
|
559
|
+
type: InteractionCallbackType,
|
560
|
+
data: Optional[InteractionCallbackData] = None,
|
561
|
+
):
|
562
|
+
self.type: InteractionCallbackType = type
|
563
|
+
self.data: Optional[InteractionCallbackData] = data
|
564
|
+
|
565
|
+
def to_dict(self) -> Dict[str, Any]:
|
566
|
+
payload: Dict[str, Any] = {"type": self.type.value}
|
567
|
+
if self.data:
|
568
|
+
payload["data"] = self.data.to_dict()
|
569
|
+
return payload
|
570
|
+
|
571
|
+
def __repr__(self) -> str:
|
572
|
+
return f"<InteractionResponsePayload type={self.type!r}>"
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
|
5
|
+
def setup_logging(level: int, file: Optional[str] = None) -> None:
|
6
|
+
"""Configure logging for the library.
|
7
|
+
|
8
|
+
Parameters
|
9
|
+
----------
|
10
|
+
level:
|
11
|
+
Logging level from the :mod:`logging` module.
|
12
|
+
file:
|
13
|
+
Optional file path to write logs to. If ``None``, logs are sent to
|
14
|
+
standard output.
|
15
|
+
"""
|
16
|
+
handlers: list[logging.Handler] = []
|
17
|
+
if file is None:
|
18
|
+
handlers.append(logging.StreamHandler())
|
19
|
+
else:
|
20
|
+
handlers.append(logging.FileHandler(file))
|
21
|
+
|
22
|
+
logging.basicConfig(
|
23
|
+
level=level,
|
24
|
+
handlers=handlers,
|
25
|
+
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
26
|
+
)
|