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/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
+ )