disagreement 0.2.0rc1__py3-none-any.whl → 0.3.0b1__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/models.py CHANGED
@@ -1,2263 +1,2588 @@
1
- # disagreement/models.py
2
-
3
- """
4
- Data models for Discord objects.
5
- """
6
-
7
- import asyncio
8
- import json
9
- from dataclasses import dataclass
10
- from typing import Any, AsyncIterator, Dict, List, Optional, TYPE_CHECKING, Union
11
-
12
- import aiohttp # pylint: disable=import-error
13
- from .color import Color
14
- from .errors import DisagreementException, HTTPException
15
- from .enums import ( # These enums will need to be defined in disagreement/enums.py
16
- VerificationLevel,
17
- MessageNotificationLevel,
18
- ExplicitContentFilterLevel,
19
- MFALevel,
20
- GuildNSFWLevel,
21
- PremiumTier,
22
- GuildFeature,
23
- ChannelType,
24
- ComponentType,
25
- ButtonStyle, # Added for Button
26
- GuildScheduledEventPrivacyLevel,
27
- GuildScheduledEventStatus,
28
- GuildScheduledEventEntityType,
29
- # SelectMenuType will be part of ComponentType or a new enum if needed
30
- )
31
- from .permissions import Permissions
32
-
33
-
34
- if TYPE_CHECKING:
35
- from .client import Client # For type hinting to avoid circular imports
36
- from .enums import OverwriteType # For PermissionOverwrite model
37
- from .ui.view import View
38
- from .interactions import Snowflake
39
-
40
- # Forward reference Message if it were used in type hints before its definition
41
- # from .models import Message # Not needed as Message is defined before its use in TextChannel.send etc.
42
- from .components import component_factory
43
-
44
-
45
- class User:
46
- """Represents a Discord User.
47
-
48
- Attributes:
49
- id (str): The user's unique ID.
50
- username (str): The user's username.
51
- discriminator (str): The user's 4-digit discord-tag.
52
- bot (bool): Whether the user belongs to an OAuth2 application. Defaults to False.
53
- avatar (Optional[str]): The user's avatar hash, if any.
54
- """
55
-
56
- def __init__(self, data: dict):
57
- self.id: str = data["id"]
58
- self.username: str = data["username"]
59
- self.discriminator: str = data["discriminator"]
60
- self.bot: bool = data.get("bot", False)
61
- self.avatar: Optional[str] = data.get("avatar")
62
-
63
- @property
64
- def mention(self) -> str:
65
- """str: Returns a string that allows you to mention the user."""
66
- return f"<@{self.id}>"
67
-
68
- def __repr__(self) -> str:
69
- return f"<User id='{self.id}' username='{self.username}' discriminator='{self.discriminator}'>"
70
-
71
-
72
- class Message:
73
- """Represents a message sent in a channel on Discord.
74
-
75
- Attributes:
76
- id (str): The message's unique ID.
77
- channel_id (str): The ID of the channel the message was sent in.
78
- guild_id (Optional[str]): The ID of the guild the message was sent in, if applicable.
79
- author (User): The user who sent the message.
80
- content (str): The actual content of the message.
81
- timestamp (str): When this message was sent (ISO8601 timestamp).
82
- components (Optional[List[ActionRow]]): Structured components attached
83
- to the message if present.
84
- attachments (List[Attachment]): Attachments included with the message.
85
- """
86
-
87
- def __init__(self, data: dict, client_instance: "Client"):
88
- self._client: "Client" = (
89
- client_instance # Store reference to client for methods like reply
90
- )
91
-
92
- self.id: str = data["id"]
93
- self.channel_id: str = data["channel_id"]
94
- self.guild_id: Optional[str] = data.get("guild_id")
95
- self.author: User = User(data["author"])
96
- self.content: str = data["content"]
97
- self.timestamp: str = data["timestamp"]
98
- if data.get("components"):
99
- self.components: Optional[List[ActionRow]] = [
100
- ActionRow.from_dict(c, client_instance)
101
- for c in data.get("components", [])
102
- ]
103
- else:
104
- self.components = None
105
- self.attachments: List[Attachment] = [
106
- Attachment(a) for a in data.get("attachments", [])
107
- ]
108
- # Add other fields as needed, e.g., attachments, embeds, reactions, etc.
109
- # self.mentions: List[User] = [User(u) for u in data.get("mentions", [])]
110
- # self.mention_roles: List[str] = data.get("mention_roles", [])
111
- # self.mention_everyone: bool = data.get("mention_everyone", False)
112
-
113
- async def reply(
114
- self,
115
- content: Optional[str] = None,
116
- *, # Make additional params keyword-only
117
- tts: bool = False,
118
- embed: Optional["Embed"] = None,
119
- embeds: Optional[List["Embed"]] = None,
120
- components: Optional[List["ActionRow"]] = None,
121
- allowed_mentions: Optional[Dict[str, Any]] = None,
122
- mention_author: Optional[bool] = None,
123
- flags: Optional[int] = None,
124
- view: Optional["View"] = None,
125
- ) -> "Message":
126
- """|coro|
127
-
128
- Sends a reply to the message.
129
- This is a shorthand for `Client.send_message` in the message's channel.
130
-
131
- Parameters:
132
- content (Optional[str]): The content of the message.
133
- tts (bool): Whether the message should be sent with text-to-speech.
134
- embed (Optional[Embed]): A single embed to send. Cannot be used with `embeds`.
135
- embeds (Optional[List[Embed]]): A list of embeds to send.
136
- components (Optional[List[ActionRow]]): A list of ActionRow components.
137
- allowed_mentions (Optional[Dict[str, Any]]): Allowed mentions for the message.
138
- mention_author (Optional[bool]): Whether to mention the author in the reply. If ``None`` the
139
- client's :attr:`mention_replies` setting is used.
140
- flags (Optional[int]): Message flags.
141
- view (Optional[View]): A view to send with the message.
142
-
143
- Returns:
144
- Message: The message that was sent.
145
-
146
- Raises:
147
- HTTPException: Sending the message failed.
148
- ValueError: If both `embed` and `embeds` are provided.
149
- """
150
- # Determine allowed mentions for the reply
151
- if mention_author is None:
152
- mention_author = getattr(self._client, "mention_replies", False)
153
-
154
- if allowed_mentions is None:
155
- allowed_mentions = {"replied_user": mention_author}
156
- else:
157
- allowed_mentions = dict(allowed_mentions)
158
- allowed_mentions.setdefault("replied_user", mention_author)
159
-
160
- # Client.send_message is already updated to handle these parameters
161
- return await self._client.send_message(
162
- channel_id=self.channel_id,
163
- content=content,
164
- tts=tts,
165
- embed=embed,
166
- embeds=embeds,
167
- components=components,
168
- allowed_mentions=allowed_mentions,
169
- message_reference={
170
- "message_id": self.id,
171
- "channel_id": self.channel_id,
172
- "guild_id": self.guild_id,
173
- },
174
- flags=flags,
175
- view=view,
176
- )
177
-
178
- async def edit(
179
- self,
180
- *,
181
- content: Optional[str] = None,
182
- embed: Optional["Embed"] = None,
183
- embeds: Optional[List["Embed"]] = None,
184
- components: Optional[List["ActionRow"]] = None,
185
- allowed_mentions: Optional[Dict[str, Any]] = None,
186
- flags: Optional[int] = None,
187
- view: Optional["View"] = None,
188
- ) -> "Message":
189
- """|coro|
190
-
191
- Edits this message.
192
-
193
- Parameters are the same as :meth:`Client.edit_message`.
194
- """
195
-
196
- return await self._client.edit_message(
197
- channel_id=self.channel_id,
198
- message_id=self.id,
199
- content=content,
200
- embed=embed,
201
- embeds=embeds,
202
- components=components,
203
- allowed_mentions=allowed_mentions,
204
- flags=flags,
205
- view=view,
206
- )
207
-
208
- async def add_reaction(self, emoji: str) -> None:
209
- """|coro| Add a reaction to this message."""
210
-
211
- await self._client.add_reaction(self.channel_id, self.id, emoji)
212
-
213
- async def remove_reaction(self, emoji: str) -> None:
214
- """|coro| Remove the bot's reaction from this message."""
215
-
216
- await self._client.remove_reaction(self.channel_id, self.id, emoji)
217
-
218
- async def clear_reactions(self) -> None:
219
- """|coro| Remove all reactions from this message."""
220
-
221
- await self._client.clear_reactions(self.channel_id, self.id)
222
-
223
- async def delete(self, delay: Optional[float] = None) -> None:
224
- """|coro|
225
-
226
- Deletes this message.
227
-
228
- Parameters
229
- ----------
230
- delay:
231
- If provided, wait this many seconds before deleting.
232
- """
233
-
234
- if delay is not None:
235
- await asyncio.sleep(delay)
236
-
237
- await self._client._http.delete_message(self.channel_id, self.id)
238
-
239
- def __repr__(self) -> str:
240
- return f"<Message id='{self.id}' channel_id='{self.channel_id}' author='{self.author!r}'>"
241
-
242
-
243
- class EmbedFooter:
244
- """Represents an embed footer."""
245
-
246
- def __init__(self, data: Dict[str, Any]):
247
- self.text: str = data["text"]
248
- self.icon_url: Optional[str] = data.get("icon_url")
249
- self.proxy_icon_url: Optional[str] = data.get("proxy_icon_url")
250
-
251
- def to_dict(self) -> Dict[str, Any]:
252
- payload = {"text": self.text}
253
- if self.icon_url:
254
- payload["icon_url"] = self.icon_url
255
- if self.proxy_icon_url:
256
- payload["proxy_icon_url"] = self.proxy_icon_url
257
- return payload
258
-
259
-
260
- class EmbedImage:
261
- """Represents an embed image."""
262
-
263
- def __init__(self, data: Dict[str, Any]):
264
- self.url: str = data["url"]
265
- self.proxy_url: Optional[str] = data.get("proxy_url")
266
- self.height: Optional[int] = data.get("height")
267
- self.width: Optional[int] = data.get("width")
268
-
269
- def to_dict(self) -> Dict[str, Any]:
270
- payload: Dict[str, Any] = {"url": self.url}
271
- if self.proxy_url:
272
- payload["proxy_url"] = self.proxy_url
273
- if self.height:
274
- payload["height"] = self.height
275
- if self.width:
276
- payload["width"] = self.width
277
- return payload
278
-
279
- def __repr__(self) -> str:
280
- return f"<EmbedImage url='{self.url}'>"
281
-
282
-
283
- class EmbedThumbnail(EmbedImage): # Similar structure to EmbedImage
284
- """Represents an embed thumbnail."""
285
-
286
- pass
287
-
288
-
289
- class EmbedAuthor:
290
- """Represents an embed author."""
291
-
292
- def __init__(self, data: Dict[str, Any]):
293
- self.name: str = data["name"]
294
- self.url: Optional[str] = data.get("url")
295
- self.icon_url: Optional[str] = data.get("icon_url")
296
- self.proxy_icon_url: Optional[str] = data.get("proxy_icon_url")
297
-
298
- def to_dict(self) -> Dict[str, Any]:
299
- payload = {"name": self.name}
300
- if self.url:
301
- payload["url"] = self.url
302
- if self.icon_url:
303
- payload["icon_url"] = self.icon_url
304
- if self.proxy_icon_url:
305
- payload["proxy_icon_url"] = self.proxy_icon_url
306
- return payload
307
-
308
-
309
- class EmbedField:
310
- """Represents an embed field."""
311
-
312
- def __init__(self, data: Dict[str, Any]):
313
- self.name: str = data["name"]
314
- self.value: str = data["value"]
315
- self.inline: bool = data.get("inline", False)
316
-
317
- def to_dict(self) -> Dict[str, Any]:
318
- return {"name": self.name, "value": self.value, "inline": self.inline}
319
-
320
-
321
- class Embed:
322
- """Represents a Discord embed.
323
-
324
- Attributes can be set directly or via methods like `set_author`, `add_field`.
325
- """
326
-
327
- def __init__(self, data: Optional[Dict[str, Any]] = None):
328
- data = data or {}
329
- self.title: Optional[str] = data.get("title")
330
- self.type: str = data.get("type", "rich") # Default to "rich" for sending
331
- self.description: Optional[str] = data.get("description")
332
- self.url: Optional[str] = data.get("url")
333
- self.timestamp: Optional[str] = data.get("timestamp") # ISO8601 timestamp
334
- self.color = Color.parse(data.get("color"))
335
-
336
- self.footer: Optional[EmbedFooter] = (
337
- EmbedFooter(data["footer"]) if data.get("footer") else None
338
- )
339
- self.image: Optional[EmbedImage] = (
340
- EmbedImage(data["image"]) if data.get("image") else None
341
- )
342
- self.thumbnail: Optional[EmbedThumbnail] = (
343
- EmbedThumbnail(data["thumbnail"]) if data.get("thumbnail") else None
344
- )
345
- # Video and Provider are less common for bot-sent embeds, can be added if needed.
346
- self.author: Optional[EmbedAuthor] = (
347
- EmbedAuthor(data["author"]) if data.get("author") else None
348
- )
349
- self.fields: List[EmbedField] = (
350
- [EmbedField(f) for f in data["fields"]] if data.get("fields") else []
351
- )
352
-
353
- def to_dict(self) -> Dict[str, Any]:
354
- payload: Dict[str, Any] = {"type": self.type}
355
- if self.title:
356
- payload["title"] = self.title
357
- if self.description:
358
- payload["description"] = self.description
359
- if self.url:
360
- payload["url"] = self.url
361
- if self.timestamp:
362
- payload["timestamp"] = self.timestamp
363
- if self.color is not None:
364
- payload["color"] = self.color.value
365
- if self.footer:
366
- payload["footer"] = self.footer.to_dict()
367
- if self.image:
368
- payload["image"] = self.image.to_dict()
369
- if self.thumbnail:
370
- payload["thumbnail"] = self.thumbnail.to_dict()
371
- if self.author:
372
- payload["author"] = self.author.to_dict()
373
- if self.fields:
374
- payload["fields"] = [f.to_dict() for f in self.fields]
375
- return payload
376
-
377
- # Convenience methods for building embeds can be added here
378
- # e.g., set_author, add_field, set_footer, set_image, etc.
379
-
380
-
381
- class Attachment:
382
- """Represents a message attachment."""
383
-
384
- def __init__(self, data: Dict[str, Any]):
385
- self.id: str = data["id"]
386
- self.filename: str = data["filename"]
387
- self.description: Optional[str] = data.get("description")
388
- self.content_type: Optional[str] = data.get("content_type")
389
- self.size: Optional[int] = data.get("size")
390
- self.url: Optional[str] = data.get("url")
391
- self.proxy_url: Optional[str] = data.get("proxy_url")
392
- self.height: Optional[int] = data.get("height") # If image
393
- self.width: Optional[int] = data.get("width") # If image
394
- self.ephemeral: bool = data.get("ephemeral", False)
395
-
396
- def __repr__(self) -> str:
397
- return f"<Attachment id='{self.id}' filename='{self.filename}'>"
398
-
399
- def to_dict(self) -> Dict[str, Any]:
400
- payload: Dict[str, Any] = {"id": self.id, "filename": self.filename}
401
- if self.description is not None:
402
- payload["description"] = self.description
403
- if self.content_type is not None:
404
- payload["content_type"] = self.content_type
405
- if self.size is not None:
406
- payload["size"] = self.size
407
- if self.url is not None:
408
- payload["url"] = self.url
409
- if self.proxy_url is not None:
410
- payload["proxy_url"] = self.proxy_url
411
- if self.height is not None:
412
- payload["height"] = self.height
413
- if self.width is not None:
414
- payload["width"] = self.width
415
- if self.ephemeral:
416
- payload["ephemeral"] = self.ephemeral
417
- return payload
418
-
419
-
420
- class File:
421
- """Represents a file to be uploaded."""
422
-
423
- def __init__(self, filename: str, data: bytes):
424
- self.filename = filename
425
- self.data = data
426
-
427
-
428
- class AllowedMentions:
429
- """Represents allowed mentions for a message or interaction response."""
430
-
431
- def __init__(self, data: Dict[str, Any]):
432
- self.parse: List[str] = data.get("parse", [])
433
- self.roles: List[str] = data.get("roles", [])
434
- self.users: List[str] = data.get("users", [])
435
- self.replied_user: bool = data.get("replied_user", False)
436
-
437
- def to_dict(self) -> Dict[str, Any]:
438
- payload: Dict[str, Any] = {"parse": self.parse}
439
- if self.roles:
440
- payload["roles"] = self.roles
441
- if self.users:
442
- payload["users"] = self.users
443
- if self.replied_user:
444
- payload["replied_user"] = self.replied_user
445
- return payload
446
-
447
-
448
- class RoleTags:
449
- """Represents tags for a role."""
450
-
451
- def __init__(self, data: Dict[str, Any]):
452
- self.bot_id: Optional[str] = data.get("bot_id")
453
- self.integration_id: Optional[str] = data.get("integration_id")
454
- self.premium_subscriber: Optional[bool] = (
455
- data.get("premium_subscriber") is None
456
- ) # presence of null value means true
457
-
458
- def to_dict(self) -> Dict[str, Any]:
459
- payload = {}
460
- if self.bot_id:
461
- payload["bot_id"] = self.bot_id
462
- if self.integration_id:
463
- payload["integration_id"] = self.integration_id
464
- if self.premium_subscriber:
465
- payload["premium_subscriber"] = None # Explicitly null
466
- return payload
467
-
468
-
469
- class Role:
470
- """Represents a Discord Role."""
471
-
472
- def __init__(self, data: Dict[str, Any]):
473
- self.id: str = data["id"]
474
- self.name: str = data["name"]
475
- self.color: int = data["color"]
476
- self.hoist: bool = data["hoist"]
477
- self.icon: Optional[str] = data.get("icon")
478
- self.unicode_emoji: Optional[str] = data.get("unicode_emoji")
479
- self.position: int = data["position"]
480
- self.permissions: str = data["permissions"] # String of bitwise permissions
481
- self.managed: bool = data["managed"]
482
- self.mentionable: bool = data["mentionable"]
483
- self.tags: Optional[RoleTags] = (
484
- RoleTags(data["tags"]) if data.get("tags") else None
485
- )
486
-
487
- @property
488
- def mention(self) -> str:
489
- """str: Returns a string that allows you to mention the role."""
490
- return f"<@&{self.id}>"
491
-
492
- def __repr__(self) -> str:
493
- return f"<Role id='{self.id}' name='{self.name}'>"
494
-
495
-
496
- class Member(User): # Member inherits from User
497
- """Represents a Guild Member.
498
- This class combines User attributes with guild-specific Member attributes.
499
- """
500
-
501
- def __init__(
502
- self, data: Dict[str, Any], client_instance: Optional["Client"] = None
503
- ):
504
- self._client: Optional["Client"] = client_instance
505
- self.guild_id: Optional[str] = None
506
- # User part is nested under 'user' key in member data from gateway/API
507
- user_data = data.get("user", {})
508
- # If 'id' is not in user_data but is top-level (e.g. from interaction resolved member without user object)
509
- if "id" not in user_data and "id" in data:
510
- # This case is less common for full member objects but can happen.
511
- # We'd need to construct a partial user from top-level member fields if 'user' is missing.
512
- # For now, assume 'user' object is present for full Member hydration.
513
- # If 'user' is missing, the User part might be incomplete.
514
- pass # User fields will be missing or default if 'user' not in data.
515
-
516
- super().__init__(
517
- user_data if user_data else data
518
- ) # Pass user_data or data if user_data is empty
519
-
520
- self.nick: Optional[str] = data.get("nick")
521
- self.avatar: Optional[str] = data.get("avatar") # Guild-specific avatar hash
522
- self.roles: List[str] = data.get("roles", []) # List of role IDs
523
- self.joined_at: str = data["joined_at"] # ISO8601 timestamp
524
- self.premium_since: Optional[str] = data.get(
525
- "premium_since"
526
- ) # ISO8601 timestamp
527
- self.deaf: bool = data.get("deaf", False)
528
- self.mute: bool = data.get("mute", False)
529
- self.pending: bool = data.get("pending", False)
530
- self.permissions: Optional[str] = data.get(
531
- "permissions"
532
- ) # Permissions in the channel, if applicable
533
- self.communication_disabled_until: Optional[str] = data.get(
534
- "communication_disabled_until"
535
- ) # ISO8601 timestamp
536
-
537
- # If 'user' object was present, ensure User attributes are from there
538
- if user_data:
539
- self.id = user_data.get("id", self.id) # Prefer user.id if available
540
- self.username = user_data.get("username", self.username)
541
- self.discriminator = user_data.get("discriminator", self.discriminator)
542
- self.bot = user_data.get("bot", self.bot)
543
- # User's global avatar is User.avatar, Member.avatar is guild-specific
544
- # super() already set self.avatar from user_data if present.
545
- # The self.avatar = data.get("avatar") line above overwrites it with guild avatar. This is correct.
546
-
547
- def __repr__(self) -> str:
548
- return f"<Member id='{self.id}' username='{self.username}' nick='{self.nick}'>"
549
-
550
- @property
551
- def display_name(self) -> str:
552
- """Return the nickname if set, otherwise the username."""
553
-
554
- return self.nick or self.username
555
-
556
- async def kick(self, *, reason: Optional[str] = None) -> None:
557
- if not self.guild_id or not self._client:
558
- raise DisagreementException("Member.kick requires guild_id and client")
559
- await self._client._http.kick_member(self.guild_id, self.id, reason=reason)
560
-
561
- async def ban(
562
- self,
563
- *,
564
- delete_message_seconds: int = 0,
565
- reason: Optional[str] = None,
566
- ) -> None:
567
- if not self.guild_id or not self._client:
568
- raise DisagreementException("Member.ban requires guild_id and client")
569
- await self._client._http.ban_member(
570
- self.guild_id,
571
- self.id,
572
- delete_message_seconds=delete_message_seconds,
573
- reason=reason,
574
- )
575
-
576
- async def timeout(
577
- self, until: Optional[str], *, reason: Optional[str] = None
578
- ) -> None:
579
- if not self.guild_id or not self._client:
580
- raise DisagreementException("Member.timeout requires guild_id and client")
581
- await self._client._http.timeout_member(
582
- self.guild_id,
583
- self.id,
584
- until=until,
585
- reason=reason,
586
- )
587
-
588
- @property
589
- def top_role(self) -> Optional["Role"]:
590
- """Return the member's highest role from the guild cache."""
591
-
592
- if not self.guild_id or not self._client:
593
- return None
594
-
595
- guild = self._client.get_guild(self.guild_id)
596
- if not guild:
597
- return None
598
-
599
- if not guild.roles and hasattr(self._client, "fetch_roles"):
600
- try:
601
- self._client.loop.run_until_complete(
602
- self._client.fetch_roles(self.guild_id)
603
- )
604
- except RuntimeError:
605
- future = asyncio.run_coroutine_threadsafe(
606
- self._client.fetch_roles(self.guild_id), self._client.loop
607
- )
608
- future.result()
609
-
610
- role_objects = [r for r in guild.roles if r.id in self.roles]
611
- if not role_objects:
612
- return None
613
-
614
- return max(role_objects, key=lambda r: r.position)
615
-
616
-
617
- class PartialEmoji:
618
- """Represents a partial emoji, often used in components or reactions.
619
-
620
- This typically means only id, name, and animated are known.
621
- For unicode emojis, id will be None and name will be the unicode character.
622
- """
623
-
624
- def __init__(self, data: Dict[str, Any]):
625
- self.id: Optional[str] = data.get("id")
626
- self.name: Optional[str] = data.get(
627
- "name"
628
- ) # Can be None for unknown custom emoji, or unicode char
629
- self.animated: bool = data.get("animated", False)
630
-
631
- def to_dict(self) -> Dict[str, Any]:
632
- payload: Dict[str, Any] = {}
633
- if self.id:
634
- payload["id"] = self.id
635
- if self.name:
636
- payload["name"] = self.name
637
- if self.animated: # Only include if true, as per some Discord patterns
638
- payload["animated"] = self.animated
639
- return payload
640
-
641
- def __str__(self) -> str:
642
- if self.id:
643
- return f"<{'a' if self.animated else ''}:{self.name}:{self.id}>"
644
- return self.name or "" # For unicode emoji
645
-
646
- def __repr__(self) -> str:
647
- return (
648
- f"<PartialEmoji id='{self.id}' name='{self.name}' animated={self.animated}>"
649
- )
650
-
651
-
652
- def to_partial_emoji(
653
- value: Union[str, "PartialEmoji", None],
654
- ) -> Optional["PartialEmoji"]:
655
- """Convert a string or PartialEmoji to a PartialEmoji instance.
656
-
657
- Args:
658
- value: Either a unicode emoji string, a :class:`PartialEmoji`, or ``None``.
659
-
660
- Returns:
661
- A :class:`PartialEmoji` or ``None`` if ``value`` was ``None``.
662
-
663
- Raises:
664
- TypeError: If ``value`` is not ``str`` or :class:`PartialEmoji`.
665
- """
666
-
667
- if value is None or isinstance(value, PartialEmoji):
668
- return value
669
- if isinstance(value, str):
670
- return PartialEmoji({"name": value, "id": None})
671
- raise TypeError("emoji must be a str or PartialEmoji")
672
-
673
-
674
- class Emoji(PartialEmoji):
675
- """Represents a custom guild emoji.
676
-
677
- Inherits id, name, animated from PartialEmoji.
678
- """
679
-
680
- def __init__(
681
- self, data: Dict[str, Any], client_instance: Optional["Client"] = None
682
- ):
683
- super().__init__(data)
684
- self._client: Optional["Client"] = (
685
- client_instance # For potential future methods
686
- )
687
-
688
- # Roles this emoji is whitelisted to
689
- self.roles: List[str] = data.get("roles", []) # List of role IDs
690
-
691
- # User object for the user that created this emoji (optional, only for GUILD_EMOJIS_AND_STICKERS intent)
692
- self.user: Optional[User] = User(data["user"]) if data.get("user") else None
693
-
694
- self.require_colons: bool = data.get("require_colons", False)
695
- self.managed: bool = data.get(
696
- "managed", False
697
- ) # If this emoji is managed by an integration
698
- self.available: bool = data.get(
699
- "available", True
700
- ) # Whether this emoji can be used
701
-
702
- def __repr__(self) -> str:
703
- return f"<Emoji id='{self.id}' name='{self.name}' animated={self.animated} available={self.available}>"
704
-
705
-
706
- class StickerItem:
707
- """Represents a sticker item, a basic representation of a sticker.
708
-
709
- Used in sticker packs and sometimes in message data.
710
- """
711
-
712
- def __init__(self, data: Dict[str, Any]):
713
- self.id: str = data["id"]
714
- self.name: str = data["name"]
715
- self.format_type: int = data["format_type"] # StickerFormatType enum
716
-
717
- def __repr__(self) -> str:
718
- return f"<StickerItem id='{self.id}' name='{self.name}'>"
719
-
720
-
721
- class Sticker(StickerItem):
722
- """Represents a Discord sticker.
723
-
724
- Inherits id, name, format_type from StickerItem.
725
- """
726
-
727
- def __init__(
728
- self, data: Dict[str, Any], client_instance: Optional["Client"] = None
729
- ):
730
- super().__init__(data)
731
- self._client: Optional["Client"] = client_instance
732
-
733
- self.pack_id: Optional[str] = data.get(
734
- "pack_id"
735
- ) # For standard stickers, ID of the pack
736
- self.description: Optional[str] = data.get("description")
737
- self.tags: str = data.get(
738
- "tags", ""
739
- ) # Comma-separated list of tags for guild stickers
740
- # type is StickerType enum (STANDARD or GUILD)
741
- # For guild stickers, this is 2. For standard stickers, this is 1.
742
- self.type: int = data["type"]
743
- self.available: bool = data.get(
744
- "available", True
745
- ) # Whether this sticker can be used
746
- self.guild_id: Optional[str] = data.get(
747
- "guild_id"
748
- ) # ID of the guild that owns this sticker
749
-
750
- # User object of the user that uploaded the guild sticker
751
- self.user: Optional[User] = User(data["user"]) if data.get("user") else None
752
-
753
- self.sort_value: Optional[int] = data.get(
754
- "sort_value"
755
- ) # The standard sticker's sort order within its pack
756
-
757
- def __repr__(self) -> str:
758
- return f"<Sticker id='{self.id}' name='{self.name}' guild_id='{self.guild_id}'>"
759
-
760
-
761
- class StickerPack:
762
- """Represents a pack of standard stickers."""
763
-
764
- def __init__(
765
- self, data: Dict[str, Any], client_instance: Optional["Client"] = None
766
- ):
767
- self._client: Optional["Client"] = client_instance
768
- self.id: str = data["id"]
769
- self.stickers: List[Sticker] = [
770
- Sticker(s_data, client_instance) for s_data in data.get("stickers", [])
771
- ]
772
- self.name: str = data["name"]
773
- self.sku_id: str = data["sku_id"]
774
- self.cover_sticker_id: Optional[str] = data.get("cover_sticker_id")
775
- self.description: str = data["description"]
776
- self.banner_asset_id: Optional[str] = data.get(
777
- "banner_asset_id"
778
- ) # ID of the pack's banner image
779
-
780
- def __repr__(self) -> str:
781
- return f"<StickerPack id='{self.id}' name='{self.name}' stickers={len(self.stickers)}>"
782
-
783
-
784
- class PermissionOverwrite:
785
- """Represents a permission overwrite for a role or member in a channel."""
786
-
787
- def __init__(self, data: Dict[str, Any]):
788
- self.id: str = data["id"] # Role or user ID
789
- self._type_val: int = int(data["type"]) # Store raw type for enum property
790
- self.allow: str = data["allow"] # Bitwise value of allowed permissions
791
- self.deny: str = data["deny"] # Bitwise value of denied permissions
792
-
793
- @property
794
- def type(self) -> "OverwriteType":
795
- from .enums import (
796
- OverwriteType,
797
- ) # Local import to avoid circularity at module level
798
-
799
- return OverwriteType(self._type_val)
800
-
801
- def to_dict(self) -> Dict[str, Any]:
802
- return {
803
- "id": self.id,
804
- "type": self.type.value,
805
- "allow": self.allow,
806
- "deny": self.deny,
807
- }
808
-
809
- def __repr__(self) -> str:
810
- return f"<PermissionOverwrite id='{self.id}' type='{self.type.name if hasattr(self.type, 'name') else self._type_val}' allow='{self.allow}' deny='{self.deny}'>"
811
-
812
-
813
- class Guild:
814
- """Represents a Discord Guild (Server).
815
-
816
- Attributes:
817
- id (str): Guild ID.
818
- name (str): Guild name (2-100 characters, excluding @, #, :, ```).
819
- icon (Optional[str]): Icon hash.
820
- splash (Optional[str]): Splash hash.
821
- discovery_splash (Optional[str]): Discovery splash hash; only present for discoverable guilds.
822
- owner (Optional[bool]): True if the user is the owner of the guild. (Only for /users/@me/guilds endpoint)
823
- owner_id (str): ID of owner.
824
- permissions (Optional[str]): Total permissions for the user in the guild (excludes overwrites). (Only for /users/@me/guilds endpoint)
825
- afk_channel_id (Optional[str]): ID of afk channel.
826
- afk_timeout (int): AFK timeout in seconds.
827
- widget_enabled (Optional[bool]): True if the server widget is enabled.
828
- widget_channel_id (Optional[str]): The channel id that the widget will generate an invite to, or null if set to no invite.
829
- verification_level (VerificationLevel): Verification level required for the guild.
830
- default_message_notifications (MessageNotificationLevel): Default message notifications level.
831
- explicit_content_filter (ExplicitContentFilterLevel): Explicit content filter level.
832
- roles (List[Role]): Roles in the guild.
833
- emojis (List[Dict]): Custom emojis. (Consider creating an Emoji model)
834
- features (List[GuildFeature]): Enabled guild features.
835
- mfa_level (MFALevel): Required MFA level for the guild.
836
- application_id (Optional[str]): Application ID of the guild creator if it is bot-created.
837
- system_channel_id (Optional[str]): The id of the channel where guild notices such as welcome messages and boost events are posted.
838
- system_channel_flags (int): System channel flags.
839
- rules_channel_id (Optional[str]): The id of the channel where Community guilds can display rules.
840
- max_members (Optional[int]): The maximum number of members for the guild.
841
- vanity_url_code (Optional[str]): The vanity url code for the guild.
842
- description (Optional[str]): The description of a Community guild.
843
- banner (Optional[str]): Banner hash.
844
- premium_tier (PremiumTier): Premium tier (Server Boost level).
845
- premium_subscription_count (Optional[int]): The number of boosts this guild currently has.
846
- preferred_locale (str): The preferred locale of a Community guild. Defaults to "en-US".
847
- public_updates_channel_id (Optional[str]): The id of the channel where admins and moderators of Community guilds receive notices from Discord.
848
- max_video_channel_users (Optional[int]): The maximum number of users in a video channel.
849
- welcome_screen (Optional[Dict]): The welcome screen of a Community guild. (Consider a WelcomeScreen model)
850
- nsfw_level (GuildNSFWLevel): Guild NSFW level.
851
- stickers (Optional[List[Dict]]): Custom stickers in the guild. (Consider a Sticker model)
852
- premium_progress_bar_enabled (bool): Whether the guild has the premium progress bar enabled.
853
- """
854
-
855
- def __init__(self, data: Dict[str, Any], client_instance: "Client"):
856
- self._client: "Client" = client_instance
857
- self.id: str = data["id"]
858
- self.name: str = data["name"]
859
- self.icon: Optional[str] = data.get("icon")
860
- self.splash: Optional[str] = data.get("splash")
861
- self.discovery_splash: Optional[str] = data.get("discovery_splash")
862
- self.owner: Optional[bool] = data.get("owner")
863
- self.owner_id: str = data["owner_id"]
864
- self.permissions: Optional[str] = data.get("permissions")
865
- self.afk_channel_id: Optional[str] = data.get("afk_channel_id")
866
- self.afk_timeout: int = data["afk_timeout"]
867
- self.widget_enabled: Optional[bool] = data.get("widget_enabled")
868
- self.widget_channel_id: Optional[str] = data.get("widget_channel_id")
869
- self.verification_level: VerificationLevel = VerificationLevel(
870
- data["verification_level"]
871
- )
872
- self.default_message_notifications: MessageNotificationLevel = (
873
- MessageNotificationLevel(data["default_message_notifications"])
874
- )
875
- self.explicit_content_filter: ExplicitContentFilterLevel = (
876
- ExplicitContentFilterLevel(data["explicit_content_filter"])
877
- )
878
-
879
- self.roles: List[Role] = [Role(r) for r in data.get("roles", [])]
880
- self.emojis: List[Emoji] = [
881
- Emoji(e_data, client_instance) for e_data in data.get("emojis", [])
882
- ]
883
-
884
- # Assuming GuildFeature can be constructed from string feature names or their values
885
- self.features: List[GuildFeature] = [
886
- GuildFeature(f) if not isinstance(f, GuildFeature) else f
887
- for f in data.get("features", [])
888
- ]
889
-
890
- self.mfa_level: MFALevel = MFALevel(data["mfa_level"])
891
- self.application_id: Optional[str] = data.get("application_id")
892
- self.system_channel_id: Optional[str] = data.get("system_channel_id")
893
- self.system_channel_flags: int = data["system_channel_flags"]
894
- self.rules_channel_id: Optional[str] = data.get("rules_channel_id")
895
- self.max_members: Optional[int] = data.get("max_members")
896
- self.vanity_url_code: Optional[str] = data.get("vanity_url_code")
897
- self.description: Optional[str] = data.get("description")
898
- self.banner: Optional[str] = data.get("banner")
899
- self.premium_tier: PremiumTier = PremiumTier(data["premium_tier"])
900
- self.premium_subscription_count: Optional[int] = data.get(
901
- "premium_subscription_count"
902
- )
903
- self.preferred_locale: str = data.get("preferred_locale", "en-US")
904
- self.public_updates_channel_id: Optional[str] = data.get(
905
- "public_updates_channel_id"
906
- )
907
- self.max_video_channel_users: Optional[int] = data.get(
908
- "max_video_channel_users"
909
- )
910
- self.approximate_member_count: Optional[int] = data.get(
911
- "approximate_member_count"
912
- )
913
- self.approximate_presence_count: Optional[int] = data.get(
914
- "approximate_presence_count"
915
- )
916
- self.welcome_screen: Optional["WelcomeScreen"] = (
917
- WelcomeScreen(data["welcome_screen"], client_instance)
918
- if data.get("welcome_screen")
919
- else None
920
- )
921
- self.nsfw_level: GuildNSFWLevel = GuildNSFWLevel(data["nsfw_level"])
922
- self.stickers: Optional[List[Sticker]] = (
923
- [Sticker(s_data, client_instance) for s_data in data.get("stickers", [])]
924
- if data.get("stickers")
925
- else None
926
- )
927
- self.premium_progress_bar_enabled: bool = data.get(
928
- "premium_progress_bar_enabled", False
929
- )
930
-
931
- # Internal caches, populated by events or specific fetches
932
- self._channels: Dict[str, "Channel"] = {}
933
- self._members: Dict[str, Member] = {}
934
- self._threads: Dict[str, "Thread"] = {}
935
-
936
- def get_channel(self, channel_id: str) -> Optional["Channel"]:
937
- return self._channels.get(channel_id)
938
-
939
- def get_member(self, user_id: str) -> Optional[Member]:
940
- return self._members.get(user_id)
941
-
942
- def get_member_named(self, name: str) -> Optional[Member]:
943
- """Retrieve a cached member by username or nickname.
944
-
945
- The lookup is case-insensitive and searches both the username and
946
- guild nickname for a match.
947
-
948
- Parameters
949
- ----------
950
- name: str
951
- The username or nickname to search for.
952
-
953
- Returns
954
- -------
955
- Optional[Member]
956
- The matching member if found, otherwise ``None``.
957
- """
958
-
959
- lowered = name.lower()
960
- for member in self._members.values():
961
- if member.username.lower() == lowered:
962
- return member
963
- if member.nick and member.nick.lower() == lowered:
964
- return member
965
- return None
966
-
967
- def get_role(self, role_id: str) -> Optional[Role]:
968
- return next((role for role in self.roles if role.id == role_id), None)
969
-
970
- def __repr__(self) -> str:
971
- return f"<Guild id='{self.id}' name='{self.name}'>"
972
-
973
-
974
- class Channel:
975
- """Base class for Discord channels."""
976
-
977
- def __init__(self, data: Dict[str, Any], client_instance: "Client"):
978
- self._client: "Client" = client_instance
979
- self.id: str = data["id"]
980
- self._type_val: int = int(data["type"]) # Store raw type for enum property
981
-
982
- self.guild_id: Optional[str] = data.get("guild_id")
983
- self.name: Optional[str] = data.get("name")
984
- self.position: Optional[int] = data.get("position")
985
- self.permission_overwrites: List["PermissionOverwrite"] = [
986
- PermissionOverwrite(d) for d in data.get("permission_overwrites", [])
987
- ]
988
- self.nsfw: Optional[bool] = data.get("nsfw", False)
989
- self.parent_id: Optional[str] = data.get(
990
- "parent_id"
991
- ) # ID of the parent category channel or thread parent
992
-
993
- @property
994
- def type(self) -> ChannelType:
995
- return ChannelType(self._type_val)
996
-
997
- @property
998
- def mention(self) -> str:
999
- return f"<#{self.id}>"
1000
-
1001
- async def delete(self, reason: Optional[str] = None):
1002
- await self._client._http.delete_channel(self.id, reason=reason)
1003
-
1004
- def __repr__(self) -> str:
1005
- return f"<Channel id='{self.id}' name='{self.name}' type='{self.type.name if hasattr(self.type, 'name') else self._type_val}'>"
1006
-
1007
- def permission_overwrite_for(
1008
- self, target: Union["Role", "Member", str]
1009
- ) -> Optional["PermissionOverwrite"]:
1010
- """Return the :class:`PermissionOverwrite` for ``target`` if present."""
1011
-
1012
- if isinstance(target, str):
1013
- target_id = target
1014
- else:
1015
- target_id = target.id
1016
- for overwrite in self.permission_overwrites:
1017
- if overwrite.id == target_id:
1018
- return overwrite
1019
- return None
1020
-
1021
- @staticmethod
1022
- def _apply_overwrite(
1023
- perms: Permissions, overwrite: Optional["PermissionOverwrite"]
1024
- ) -> Permissions:
1025
- if overwrite is None:
1026
- return perms
1027
-
1028
- perms &= ~Permissions(int(overwrite.deny))
1029
- perms |= Permissions(int(overwrite.allow))
1030
- return perms
1031
-
1032
- def permissions_for(self, member: "Member") -> Permissions:
1033
- """Resolve channel permissions for ``member``."""
1034
-
1035
- if self.guild_id is None:
1036
- return Permissions(~0)
1037
-
1038
- if not hasattr(self._client, "get_guild"):
1039
- return Permissions(0)
1040
-
1041
- guild = self._client.get_guild(self.guild_id)
1042
- if guild is None:
1043
- return Permissions(0)
1044
-
1045
- base = Permissions(0)
1046
-
1047
- everyone = guild.get_role(guild.id)
1048
- if everyone is not None:
1049
- base |= Permissions(int(everyone.permissions))
1050
-
1051
- for rid in member.roles:
1052
- role = guild.get_role(rid)
1053
- if role is not None:
1054
- base |= Permissions(int(role.permissions))
1055
-
1056
- if base & Permissions.ADMINISTRATOR:
1057
- return Permissions(~0)
1058
-
1059
- # Apply @everyone overwrite
1060
- base = self._apply_overwrite(base, self.permission_overwrite_for(guild.id))
1061
-
1062
- # Role overwrites
1063
- role_allow = Permissions(0)
1064
- role_deny = Permissions(0)
1065
- for rid in member.roles:
1066
- ow = self.permission_overwrite_for(rid)
1067
- if ow is not None:
1068
- role_allow |= Permissions(int(ow.allow))
1069
- role_deny |= Permissions(int(ow.deny))
1070
-
1071
- base &= ~role_deny
1072
- base |= role_allow
1073
-
1074
- # Member overwrite
1075
- base = self._apply_overwrite(base, self.permission_overwrite_for(member.id))
1076
-
1077
- return base
1078
-
1079
-
1080
- class TextChannel(Channel):
1081
- """Represents a guild text channel or announcement channel."""
1082
-
1083
- def __init__(self, data: Dict[str, Any], client_instance: "Client"):
1084
- super().__init__(data, client_instance)
1085
- self.topic: Optional[str] = data.get("topic")
1086
- self.last_message_id: Optional[str] = data.get("last_message_id")
1087
- self.rate_limit_per_user: Optional[int] = data.get("rate_limit_per_user", 0)
1088
- self.default_auto_archive_duration: Optional[int] = data.get(
1089
- "default_auto_archive_duration"
1090
- )
1091
- self.last_pin_timestamp: Optional[str] = data.get("last_pin_timestamp")
1092
-
1093
- def history(
1094
- self,
1095
- *,
1096
- limit: Optional[int] = None,
1097
- before: Optional[str] = None,
1098
- after: Optional[str] = None,
1099
- ) -> AsyncIterator["Message"]:
1100
- """Return an async iterator over this channel's messages."""
1101
-
1102
- from .utils import message_pager
1103
-
1104
- return message_pager(self, limit=limit, before=before, after=after)
1105
-
1106
- async def send(
1107
- self,
1108
- content: Optional[str] = None,
1109
- *,
1110
- embed: Optional[Embed] = None,
1111
- embeds: Optional[List[Embed]] = None,
1112
- components: Optional[List["ActionRow"]] = None, # Added components
1113
- ) -> "Message": # Forward reference Message
1114
- if not hasattr(self._client, "send_message"):
1115
- raise NotImplementedError(
1116
- "Client.send_message is required for TextChannel.send"
1117
- )
1118
-
1119
- return await self._client.send_message(
1120
- channel_id=self.id,
1121
- content=content,
1122
- embed=embed,
1123
- embeds=embeds,
1124
- components=components,
1125
- )
1126
-
1127
- async def purge(
1128
- self, limit: int, *, before: "Snowflake | None" = None
1129
- ) -> List["Snowflake"]:
1130
- """Bulk delete messages from this channel."""
1131
-
1132
- params: Dict[str, Union[int, str]] = {"limit": limit}
1133
- if before is not None:
1134
- params["before"] = before
1135
-
1136
- messages = await self._client._http.request(
1137
- "GET", f"/channels/{self.id}/messages", params=params
1138
- )
1139
- ids = [m["id"] for m in messages]
1140
- if not ids:
1141
- return []
1142
-
1143
- await self._client._http.bulk_delete_messages(self.id, ids)
1144
- for mid in ids:
1145
- self._client._messages.pop(mid, None)
1146
- return ids
1147
-
1148
- def __repr__(self) -> str:
1149
- return f"<TextChannel id='{self.id}' name='{self.name}' guild_id='{self.guild_id}'>"
1150
-
1151
-
1152
- class VoiceChannel(Channel):
1153
- """Represents a guild voice channel or stage voice channel."""
1154
-
1155
- def __init__(self, data: Dict[str, Any], client_instance: "Client"):
1156
- super().__init__(data, client_instance)
1157
- self.bitrate: int = data.get("bitrate", 64000)
1158
- self.user_limit: int = data.get("user_limit", 0)
1159
- self.rtc_region: Optional[str] = data.get("rtc_region")
1160
- self.video_quality_mode: Optional[int] = data.get("video_quality_mode")
1161
-
1162
- def __repr__(self) -> str:
1163
- return f"<VoiceChannel id='{self.id}' name='{self.name}' guild_id='{self.guild_id}'>"
1164
-
1165
-
1166
- class StageChannel(VoiceChannel):
1167
- """Represents a guild stage channel."""
1168
-
1169
- def __repr__(self) -> str:
1170
- return f"<StageChannel id='{self.id}' name='{self.name}' guild_id='{self.guild_id}'>"
1171
-
1172
- async def start_stage_instance(
1173
- self,
1174
- topic: str,
1175
- *,
1176
- privacy_level: int = 2,
1177
- reason: Optional[str] = None,
1178
- guild_scheduled_event_id: Optional[str] = None,
1179
- ) -> "StageInstance":
1180
- if not hasattr(self._client, "_http"):
1181
- raise DisagreementException("Client missing HTTP for stage instance")
1182
-
1183
- payload: Dict[str, Any] = {
1184
- "channel_id": self.id,
1185
- "topic": topic,
1186
- "privacy_level": privacy_level,
1187
- }
1188
- if guild_scheduled_event_id is not None:
1189
- payload["guild_scheduled_event_id"] = guild_scheduled_event_id
1190
-
1191
- instance = await self._client._http.start_stage_instance(payload, reason=reason)
1192
- instance._client = self._client
1193
- return instance
1194
-
1195
- async def edit_stage_instance(
1196
- self,
1197
- *,
1198
- topic: Optional[str] = None,
1199
- privacy_level: Optional[int] = None,
1200
- reason: Optional[str] = None,
1201
- ) -> "StageInstance":
1202
- if not hasattr(self._client, "_http"):
1203
- raise DisagreementException("Client missing HTTP for stage instance")
1204
-
1205
- payload: Dict[str, Any] = {}
1206
- if topic is not None:
1207
- payload["topic"] = topic
1208
- if privacy_level is not None:
1209
- payload["privacy_level"] = privacy_level
1210
-
1211
- instance = await self._client._http.edit_stage_instance(
1212
- self.id, payload, reason=reason
1213
- )
1214
- instance._client = self._client
1215
- return instance
1216
-
1217
- async def end_stage_instance(self, *, reason: Optional[str] = None) -> None:
1218
- if not hasattr(self._client, "_http"):
1219
- raise DisagreementException("Client missing HTTP for stage instance")
1220
-
1221
- await self._client._http.end_stage_instance(self.id, reason=reason)
1222
-
1223
-
1224
- class StageInstance:
1225
- """Represents a stage instance."""
1226
-
1227
- def __init__(
1228
- self, data: Dict[str, Any], client_instance: Optional["Client"] = None
1229
- ) -> None:
1230
- self._client = client_instance
1231
- self.id: str = data["id"]
1232
- self.guild_id: Optional[str] = data.get("guild_id")
1233
- self.channel_id: str = data["channel_id"]
1234
- self.topic: str = data["topic"]
1235
- self.privacy_level: int = data.get("privacy_level", 2)
1236
- self.discoverable_disabled: bool = data.get("discoverable_disabled", False)
1237
- self.guild_scheduled_event_id: Optional[str] = data.get(
1238
- "guild_scheduled_event_id"
1239
- )
1240
-
1241
- def __repr__(self) -> str:
1242
- return f"<StageInstance id='{self.id}' channel_id='{self.channel_id}'>"
1243
-
1244
-
1245
- class CategoryChannel(Channel):
1246
- """Represents a guild category channel."""
1247
-
1248
- def __init__(self, data: Dict[str, Any], client_instance: "Client"):
1249
- super().__init__(data, client_instance)
1250
-
1251
- @property
1252
- def channels(self) -> List[Channel]:
1253
- if not self.guild_id or not hasattr(self._client, "get_guild"):
1254
- return []
1255
- guild = self._client.get_guild(self.guild_id)
1256
- if not guild or not hasattr(
1257
- guild, "_channels"
1258
- ): # Ensure guild and _channels exist
1259
- return []
1260
-
1261
- categorized_channels = [
1262
- ch
1263
- for ch in guild._channels.values()
1264
- if getattr(ch, "parent_id", None) == self.id
1265
- ]
1266
- return sorted(
1267
- categorized_channels,
1268
- key=lambda c: c.position if c.position is not None else -1,
1269
- )
1270
-
1271
- def __repr__(self) -> str:
1272
- return f"<CategoryChannel id='{self.id}' name='{self.name}' guild_id='{self.guild_id}'>"
1273
-
1274
-
1275
- class ThreadMetadata:
1276
- """Represents the metadata of a thread."""
1277
-
1278
- def __init__(self, data: Dict[str, Any]):
1279
- self.archived: bool = data["archived"]
1280
- self.auto_archive_duration: int = data["auto_archive_duration"]
1281
- self.archive_timestamp: str = data["archive_timestamp"]
1282
- self.locked: bool = data["locked"]
1283
- self.invitable: Optional[bool] = data.get("invitable")
1284
- self.create_timestamp: Optional[str] = data.get("create_timestamp")
1285
-
1286
-
1287
- class Thread(TextChannel): # Threads are a specialized TextChannel
1288
- """Represents a Discord Thread."""
1289
-
1290
- def __init__(self, data: Dict[str, Any], client_instance: "Client"):
1291
- super().__init__(data, client_instance) # Handles common text channel fields
1292
- self.owner_id: Optional[str] = data.get("owner_id")
1293
- # parent_id is already handled by base Channel init if present in data
1294
- self.message_count: Optional[int] = data.get("message_count")
1295
- self.member_count: Optional[int] = data.get("member_count")
1296
- self.thread_metadata: ThreadMetadata = ThreadMetadata(data["thread_metadata"])
1297
- self.member: Optional["ThreadMember"] = (
1298
- ThreadMember(data["member"], client_instance)
1299
- if data.get("member")
1300
- else None
1301
- )
1302
-
1303
- def __repr__(self) -> str:
1304
- return (
1305
- f"<Thread id='{self.id}' name='{self.name}' parent_id='{self.parent_id}'>"
1306
- )
1307
-
1308
-
1309
- class DMChannel(Channel):
1310
- """Represents a Direct Message channel."""
1311
-
1312
- def __init__(self, data: Dict[str, Any], client_instance: "Client"):
1313
- super().__init__(data, client_instance)
1314
- self.last_message_id: Optional[str] = data.get("last_message_id")
1315
- self.recipients: List[User] = [
1316
- User(u_data) for u_data in data.get("recipients", [])
1317
- ]
1318
-
1319
- @property
1320
- def recipient(self) -> Optional[User]:
1321
- return self.recipients[0] if self.recipients else None
1322
-
1323
- async def send(
1324
- self,
1325
- content: Optional[str] = None,
1326
- *,
1327
- embed: Optional[Embed] = None,
1328
- embeds: Optional[List[Embed]] = None,
1329
- components: Optional[List["ActionRow"]] = None, # Added components
1330
- ) -> "Message":
1331
- if not hasattr(self._client, "send_message"):
1332
- raise NotImplementedError(
1333
- "Client.send_message is required for DMChannel.send"
1334
- )
1335
-
1336
- return await self._client.send_message(
1337
- channel_id=self.id,
1338
- content=content,
1339
- embed=embed,
1340
- embeds=embeds,
1341
- components=components,
1342
- )
1343
-
1344
- async def history(
1345
- self,
1346
- *,
1347
- limit: Optional[int] = 100,
1348
- before: "Snowflake | None" = None,
1349
- ):
1350
- """An async iterator over messages in this DM."""
1351
-
1352
- params: Dict[str, Union[int, str]] = {}
1353
- if before is not None:
1354
- params["before"] = before
1355
-
1356
- fetched = 0
1357
- while True:
1358
- to_fetch = 100 if limit is None else min(100, limit - fetched)
1359
- if to_fetch <= 0:
1360
- break
1361
- params["limit"] = to_fetch
1362
- messages = await self._client._http.request(
1363
- "GET", f"/channels/{self.id}/messages", params=params.copy()
1364
- )
1365
- if not messages:
1366
- break
1367
- params["before"] = messages[-1]["id"]
1368
- for msg in messages:
1369
- yield Message(msg, self._client)
1370
- fetched += 1
1371
- if limit is not None and fetched >= limit:
1372
- return
1373
-
1374
- def __repr__(self) -> str:
1375
- recipient_repr = self.recipient.username if self.recipient else "Unknown"
1376
- return f"<DMChannel id='{self.id}' recipient='{recipient_repr}'>"
1377
-
1378
-
1379
- class PartialChannel:
1380
- """Represents a partial channel object, often from interactions."""
1381
-
1382
- def __init__(
1383
- self, data: Dict[str, Any], client_instance: Optional["Client"] = None
1384
- ):
1385
- self._client: Optional["Client"] = client_instance
1386
- self.id: str = data["id"]
1387
- self.name: Optional[str] = data.get("name")
1388
- self._type_val: int = int(data["type"])
1389
- self.permissions: Optional[str] = data.get("permissions")
1390
-
1391
- @property
1392
- def type(self) -> ChannelType:
1393
- return ChannelType(self._type_val)
1394
-
1395
- @property
1396
- def mention(self) -> str:
1397
- return f"<#{self.id}>"
1398
-
1399
- async def fetch_full_channel(self) -> Optional[Channel]:
1400
- if not self._client or not hasattr(self._client, "fetch_channel"):
1401
- # Log or raise if fetching is not possible
1402
- return None
1403
- try:
1404
- # This assumes Client.fetch_channel exists and returns a full Channel object
1405
- return await self._client.fetch_channel(self.id)
1406
- except HTTPException as exc:
1407
- print(f"HTTP error while fetching channel {self.id}: {exc}")
1408
- except (json.JSONDecodeError, KeyError, ValueError) as exc:
1409
- print(f"Failed to parse channel {self.id}: {exc}")
1410
- except DisagreementException as exc:
1411
- print(f"Error fetching channel {self.id}: {exc}")
1412
- return None
1413
-
1414
- def __repr__(self) -> str:
1415
- type_name = self.type.name if hasattr(self.type, "name") else self._type_val
1416
- return f"<PartialChannel id='{self.id}' name='{self.name}' type='{type_name}'>"
1417
-
1418
-
1419
- class Webhook:
1420
- """Represents a Discord Webhook."""
1421
-
1422
- def __init__(
1423
- self, data: Dict[str, Any], client_instance: Optional["Client"] = None
1424
- ):
1425
- self._client: Optional["Client"] = client_instance
1426
- self.id: str = data["id"]
1427
- self.type: int = int(data.get("type", 1))
1428
- self.guild_id: Optional[str] = data.get("guild_id")
1429
- self.channel_id: Optional[str] = data.get("channel_id")
1430
- self.name: Optional[str] = data.get("name")
1431
- self.avatar: Optional[str] = data.get("avatar")
1432
- self.token: Optional[str] = data.get("token")
1433
- self.application_id: Optional[str] = data.get("application_id")
1434
- self.url: Optional[str] = data.get("url")
1435
- self.user: Optional[User] = User(data["user"]) if data.get("user") else None
1436
-
1437
- def __repr__(self) -> str:
1438
- return f"<Webhook id='{self.id}' name='{self.name}'>"
1439
-
1440
- @classmethod
1441
- def from_url(
1442
- cls, url: str, session: Optional[aiohttp.ClientSession] = None
1443
- ) -> "Webhook":
1444
- """Create a minimal :class:`Webhook` from a webhook URL.
1445
-
1446
- Parameters
1447
- ----------
1448
- url:
1449
- The full Discord webhook URL.
1450
- session:
1451
- Unused for now. Present for API compatibility.
1452
-
1453
- Returns
1454
- -------
1455
- Webhook
1456
- A webhook instance containing only the ``id``, ``token`` and ``url``.
1457
- """
1458
-
1459
- parts = url.rstrip("/").split("/")
1460
- if len(parts) < 2:
1461
- raise ValueError("Invalid webhook URL")
1462
- token = parts[-1]
1463
- webhook_id = parts[-2]
1464
-
1465
- return cls({"id": webhook_id, "token": token, "url": url})
1466
-
1467
- async def send(
1468
- self,
1469
- content: Optional[str] = None,
1470
- *,
1471
- username: Optional[str] = None,
1472
- avatar_url: Optional[str] = None,
1473
- tts: bool = False,
1474
- embed: Optional["Embed"] = None,
1475
- embeds: Optional[List["Embed"]] = None,
1476
- components: Optional[List["ActionRow"]] = None,
1477
- allowed_mentions: Optional[Dict[str, Any]] = None,
1478
- attachments: Optional[List[Any]] = None,
1479
- files: Optional[List[Any]] = None,
1480
- flags: Optional[int] = None,
1481
- ) -> "Message":
1482
- """Send a message using this webhook."""
1483
-
1484
- if not self._client:
1485
- raise DisagreementException("Webhook is not bound to a Client")
1486
- assert self.token is not None, "Webhook token missing"
1487
-
1488
- if embed and embeds:
1489
- raise ValueError("Cannot provide both embed and embeds.")
1490
-
1491
- final_embeds_payload: Optional[List[Dict[str, Any]]] = None
1492
- if embed:
1493
- final_embeds_payload = [embed.to_dict()]
1494
- elif embeds:
1495
- final_embeds_payload = [e.to_dict() for e in embeds]
1496
-
1497
- components_payload: Optional[List[Dict[str, Any]]] = None
1498
- if components:
1499
- components_payload = [c.to_dict() for c in components]
1500
-
1501
- message_data = await self._client._http.execute_webhook(
1502
- self.id,
1503
- self.token,
1504
- content=content,
1505
- tts=tts,
1506
- embeds=final_embeds_payload,
1507
- components=components_payload,
1508
- allowed_mentions=allowed_mentions,
1509
- attachments=attachments,
1510
- files=files,
1511
- flags=flags,
1512
- username=username,
1513
- avatar_url=avatar_url,
1514
- )
1515
-
1516
- return self._client.parse_message(message_data)
1517
-
1518
-
1519
- class GuildTemplate:
1520
- """Represents a guild template."""
1521
-
1522
- def __init__(
1523
- self, data: Dict[str, Any], client_instance: Optional["Client"] = None
1524
- ):
1525
- self._client = client_instance
1526
- self.code: str = data["code"]
1527
- self.name: str = data["name"]
1528
- self.description: Optional[str] = data.get("description")
1529
- self.usage_count: int = data.get("usage_count", 0)
1530
- self.creator_id: str = data.get("creator_id", "")
1531
- self.creator: Optional[User] = (
1532
- User(data["creator"]) if data.get("creator") else None
1533
- )
1534
- self.created_at: Optional[str] = data.get("created_at")
1535
- self.updated_at: Optional[str] = data.get("updated_at")
1536
- self.source_guild_id: Optional[str] = data.get("source_guild_id")
1537
- self.serialized_source_guild: Dict[str, Any] = data.get(
1538
- "serialized_source_guild", {}
1539
- )
1540
- self.is_dirty: Optional[bool] = data.get("is_dirty")
1541
-
1542
- def __repr__(self) -> str:
1543
- return f"<GuildTemplate code='{self.code}' name='{self.name}'>"
1544
-
1545
-
1546
- # --- Message Components ---
1547
-
1548
-
1549
- class Component:
1550
- """Base class for message components."""
1551
-
1552
- def __init__(self, type: ComponentType):
1553
- self.type: ComponentType = type
1554
- self.custom_id: Optional[str] = None
1555
-
1556
- def to_dict(self) -> Dict[str, Any]:
1557
- payload: Dict[str, Any] = {"type": self.type.value}
1558
- if self.custom_id:
1559
- payload["custom_id"] = self.custom_id
1560
- return payload
1561
-
1562
-
1563
- class ActionRow(Component):
1564
- """Represents an Action Row, a container for other components."""
1565
-
1566
- def __init__(self, components: Optional[List[Component]] = None):
1567
- super().__init__(ComponentType.ACTION_ROW)
1568
- self.components: List[Component] = components or []
1569
-
1570
- def add_component(self, component: Component):
1571
- if isinstance(component, ActionRow):
1572
- raise ValueError("Cannot nest ActionRows inside another ActionRow.")
1573
-
1574
- select_types = {
1575
- ComponentType.STRING_SELECT,
1576
- ComponentType.USER_SELECT,
1577
- ComponentType.ROLE_SELECT,
1578
- ComponentType.MENTIONABLE_SELECT,
1579
- ComponentType.CHANNEL_SELECT,
1580
- }
1581
-
1582
- if component.type in select_types:
1583
- if self.components:
1584
- raise ValueError(
1585
- "Select menu components must be the only component in an ActionRow."
1586
- )
1587
- self.components.append(component)
1588
- return self
1589
-
1590
- if any(c.type in select_types for c in self.components):
1591
- raise ValueError(
1592
- "Cannot add components to an ActionRow that already contains a select menu."
1593
- )
1594
-
1595
- if len(self.components) >= 5:
1596
- raise ValueError("ActionRow cannot have more than 5 components.")
1597
-
1598
- self.components.append(component)
1599
- return self
1600
-
1601
- def to_dict(self) -> Dict[str, Any]:
1602
- payload = super().to_dict()
1603
- payload["components"] = [c.to_dict() for c in self.components]
1604
- return payload
1605
-
1606
- @classmethod
1607
- def from_dict(
1608
- cls, data: Dict[str, Any], client: Optional["Client"] = None
1609
- ) -> "ActionRow":
1610
- """Deserialize an action row payload."""
1611
- from .components import component_factory
1612
-
1613
- row = cls()
1614
- for comp_data in data.get("components", []):
1615
- try:
1616
- row.add_component(component_factory(comp_data, client))
1617
- except Exception:
1618
- # Skip components that fail to parse for now
1619
- continue
1620
- return row
1621
-
1622
-
1623
- class Button(Component):
1624
- """Represents a button component."""
1625
-
1626
- def __init__(
1627
- self,
1628
- *, # Make parameters keyword-only for clarity
1629
- style: ButtonStyle,
1630
- label: Optional[str] = None,
1631
- emoji: Optional["PartialEmoji"] = None, # Changed to PartialEmoji type
1632
- custom_id: Optional[str] = None,
1633
- url: Optional[str] = None,
1634
- disabled: bool = False,
1635
- ):
1636
- super().__init__(ComponentType.BUTTON)
1637
-
1638
- if style == ButtonStyle.LINK and url is None:
1639
- raise ValueError("Link buttons must have a URL.")
1640
- if style != ButtonStyle.LINK and custom_id is None:
1641
- raise ValueError("Non-link buttons must have a custom_id.")
1642
- if label is None and emoji is None:
1643
- raise ValueError("Button must have a label or an emoji.")
1644
-
1645
- self.style: ButtonStyle = style
1646
- self.label: Optional[str] = label
1647
- self.emoji: Optional[PartialEmoji] = emoji
1648
- self.custom_id = custom_id
1649
- self.url: Optional[str] = url
1650
- self.disabled: bool = disabled
1651
-
1652
- def to_dict(self) -> Dict[str, Any]:
1653
- payload = super().to_dict()
1654
- payload["style"] = self.style.value
1655
- if self.label:
1656
- payload["label"] = self.label
1657
- if self.emoji:
1658
- payload["emoji"] = self.emoji.to_dict() # Call to_dict()
1659
- if self.custom_id:
1660
- payload["custom_id"] = self.custom_id
1661
- if self.url:
1662
- payload["url"] = self.url
1663
- if self.disabled:
1664
- payload["disabled"] = self.disabled
1665
- return payload
1666
-
1667
-
1668
- class SelectOption:
1669
- """Represents an option in a select menu."""
1670
-
1671
- def __init__(
1672
- self,
1673
- *, # Make parameters keyword-only
1674
- label: str,
1675
- value: str,
1676
- description: Optional[str] = None,
1677
- emoji: Optional["PartialEmoji"] = None, # Changed to PartialEmoji type
1678
- default: bool = False,
1679
- ):
1680
- self.label: str = label
1681
- self.value: str = value
1682
- self.description: Optional[str] = description
1683
- self.emoji: Optional["PartialEmoji"] = emoji
1684
- self.default: bool = default
1685
-
1686
- def to_dict(self) -> Dict[str, Any]:
1687
- payload: Dict[str, Any] = {
1688
- "label": self.label,
1689
- "value": self.value,
1690
- }
1691
- if self.description:
1692
- payload["description"] = self.description
1693
- if self.emoji:
1694
- payload["emoji"] = self.emoji.to_dict() # Call to_dict()
1695
- if self.default:
1696
- payload["default"] = self.default
1697
- return payload
1698
-
1699
-
1700
- class SelectMenu(Component):
1701
- """Represents a select menu component.
1702
-
1703
- Currently supports STRING_SELECT (type 3).
1704
- User (5), Role (6), Mentionable (7), Channel (8) selects are not yet fully modeled.
1705
- """
1706
-
1707
- def __init__(
1708
- self,
1709
- *, # Make parameters keyword-only
1710
- custom_id: str,
1711
- options: List[SelectOption],
1712
- placeholder: Optional[str] = None,
1713
- min_values: int = 1,
1714
- max_values: int = 1,
1715
- disabled: bool = False,
1716
- channel_types: Optional[List[ChannelType]] = None,
1717
- # For other select types, specific fields would be needed.
1718
- # This constructor primarily targets STRING_SELECT (type 3).
1719
- type: ComponentType = ComponentType.STRING_SELECT, # Default to string select
1720
- ):
1721
- super().__init__(type) # Pass the specific select menu type
1722
-
1723
- if not (1 <= len(options) <= 25):
1724
- raise ValueError("Select menu must have between 1 and 25 options.")
1725
- if not (
1726
- 0 <= min_values <= 25
1727
- ): # Discord docs say min_values can be 0 for some types
1728
- raise ValueError("min_values must be between 0 and 25.")
1729
- if not (1 <= max_values <= 25):
1730
- raise ValueError("max_values must be between 1 and 25.")
1731
- if min_values > max_values:
1732
- raise ValueError("min_values cannot be greater than max_values.")
1733
-
1734
- self.custom_id = custom_id
1735
- self.options: List[SelectOption] = options
1736
- self.placeholder: Optional[str] = placeholder
1737
- self.min_values: int = min_values
1738
- self.max_values: int = max_values
1739
- self.disabled: bool = disabled
1740
- self.channel_types: Optional[List[ChannelType]] = channel_types
1741
-
1742
- def to_dict(self) -> Dict[str, Any]:
1743
- payload = super().to_dict() # Gets {"type": self.type.value}
1744
- payload["custom_id"] = self.custom_id
1745
- payload["options"] = [opt.to_dict() for opt in self.options]
1746
- if self.placeholder:
1747
- payload["placeholder"] = self.placeholder
1748
- payload["min_values"] = self.min_values
1749
- payload["max_values"] = self.max_values
1750
- if self.disabled:
1751
- payload["disabled"] = self.disabled
1752
- if self.type == ComponentType.CHANNEL_SELECT and self.channel_types:
1753
- payload["channel_types"] = [ct.value for ct in self.channel_types]
1754
- return payload
1755
-
1756
-
1757
- class UnfurledMediaItem:
1758
- """Represents an unfurled media item."""
1759
-
1760
- def __init__(
1761
- self,
1762
- url: str,
1763
- proxy_url: Optional[str] = None,
1764
- height: Optional[int] = None,
1765
- width: Optional[int] = None,
1766
- content_type: Optional[str] = None,
1767
- ):
1768
- self.url = url
1769
- self.proxy_url = proxy_url
1770
- self.height = height
1771
- self.width = width
1772
- self.content_type = content_type
1773
-
1774
- def to_dict(self) -> Dict[str, Any]:
1775
- return {
1776
- "url": self.url,
1777
- "proxy_url": self.proxy_url,
1778
- "height": self.height,
1779
- "width": self.width,
1780
- "content_type": self.content_type,
1781
- }
1782
-
1783
-
1784
- class MediaGalleryItem:
1785
- """Represents an item in a media gallery."""
1786
-
1787
- def __init__(
1788
- self,
1789
- media: UnfurledMediaItem,
1790
- description: Optional[str] = None,
1791
- spoiler: bool = False,
1792
- ):
1793
- self.media = media
1794
- self.description = description
1795
- self.spoiler = spoiler
1796
-
1797
- def to_dict(self) -> Dict[str, Any]:
1798
- return {
1799
- "media": self.media.to_dict(),
1800
- "description": self.description,
1801
- "spoiler": self.spoiler,
1802
- }
1803
-
1804
-
1805
- class TextDisplay(Component):
1806
- """Represents a text display component."""
1807
-
1808
- def __init__(self, content: str, id: Optional[int] = None):
1809
- super().__init__(ComponentType.TEXT_DISPLAY)
1810
- self.content = content
1811
- self.id = id
1812
-
1813
- def to_dict(self) -> Dict[str, Any]:
1814
- payload = super().to_dict()
1815
- payload["content"] = self.content
1816
- if self.id is not None:
1817
- payload["id"] = self.id
1818
- return payload
1819
-
1820
-
1821
- class Thumbnail(Component):
1822
- """Represents a thumbnail component."""
1823
-
1824
- def __init__(
1825
- self,
1826
- media: UnfurledMediaItem,
1827
- description: Optional[str] = None,
1828
- spoiler: bool = False,
1829
- id: Optional[int] = None,
1830
- ):
1831
- super().__init__(ComponentType.THUMBNAIL)
1832
- self.media = media
1833
- self.description = description
1834
- self.spoiler = spoiler
1835
- self.id = id
1836
-
1837
- def to_dict(self) -> Dict[str, Any]:
1838
- payload = super().to_dict()
1839
- payload["media"] = self.media.to_dict()
1840
- if self.description:
1841
- payload["description"] = self.description
1842
- if self.spoiler:
1843
- payload["spoiler"] = self.spoiler
1844
- if self.id is not None:
1845
- payload["id"] = self.id
1846
- return payload
1847
-
1848
-
1849
- class Section(Component):
1850
- """Represents a section component."""
1851
-
1852
- def __init__(
1853
- self,
1854
- components: List[TextDisplay],
1855
- accessory: Optional[Union[Thumbnail, Button]] = None,
1856
- id: Optional[int] = None,
1857
- ):
1858
- super().__init__(ComponentType.SECTION)
1859
- self.components = components
1860
- self.accessory = accessory
1861
- self.id = id
1862
-
1863
- def to_dict(self) -> Dict[str, Any]:
1864
- payload = super().to_dict()
1865
- payload["components"] = [c.to_dict() for c in self.components]
1866
- if self.accessory:
1867
- payload["accessory"] = self.accessory.to_dict()
1868
- if self.id is not None:
1869
- payload["id"] = self.id
1870
- return payload
1871
-
1872
-
1873
- class MediaGallery(Component):
1874
- """Represents a media gallery component."""
1875
-
1876
- def __init__(self, items: List[MediaGalleryItem], id: Optional[int] = None):
1877
- super().__init__(ComponentType.MEDIA_GALLERY)
1878
- self.items = items
1879
- self.id = id
1880
-
1881
- def to_dict(self) -> Dict[str, Any]:
1882
- payload = super().to_dict()
1883
- payload["items"] = [i.to_dict() for i in self.items]
1884
- if self.id is not None:
1885
- payload["id"] = self.id
1886
- return payload
1887
-
1888
-
1889
- class FileComponent(Component):
1890
- """Represents a file component."""
1891
-
1892
- def __init__(
1893
- self, file: UnfurledMediaItem, spoiler: bool = False, id: Optional[int] = None
1894
- ):
1895
- super().__init__(ComponentType.FILE)
1896
- self.file = file
1897
- self.spoiler = spoiler
1898
- self.id = id
1899
-
1900
- def to_dict(self) -> Dict[str, Any]:
1901
- payload = super().to_dict()
1902
- payload["file"] = self.file.to_dict()
1903
- if self.spoiler:
1904
- payload["spoiler"] = self.spoiler
1905
- if self.id is not None:
1906
- payload["id"] = self.id
1907
- return payload
1908
-
1909
-
1910
- class Separator(Component):
1911
- """Represents a separator component."""
1912
-
1913
- def __init__(
1914
- self, divider: bool = True, spacing: int = 1, id: Optional[int] = None
1915
- ):
1916
- super().__init__(ComponentType.SEPARATOR)
1917
- self.divider = divider
1918
- self.spacing = spacing
1919
- self.id = id
1920
-
1921
- def to_dict(self) -> Dict[str, Any]:
1922
- payload = super().to_dict()
1923
- payload["divider"] = self.divider
1924
- payload["spacing"] = self.spacing
1925
- if self.id is not None:
1926
- payload["id"] = self.id
1927
- return payload
1928
-
1929
-
1930
- class Container(Component):
1931
- """Represents a container component."""
1932
-
1933
- def __init__(
1934
- self,
1935
- components: List[Component],
1936
- accent_color: Color | int | str | None = None,
1937
- spoiler: bool = False,
1938
- id: Optional[int] = None,
1939
- ):
1940
- super().__init__(ComponentType.CONTAINER)
1941
- self.components = components
1942
- self.accent_color = Color.parse(accent_color)
1943
- self.spoiler = spoiler
1944
- self.id = id
1945
-
1946
- def to_dict(self) -> Dict[str, Any]:
1947
- payload = super().to_dict()
1948
- payload["components"] = [c.to_dict() for c in self.components]
1949
- if self.accent_color:
1950
- payload["accent_color"] = self.accent_color.value
1951
- if self.spoiler:
1952
- payload["spoiler"] = self.spoiler
1953
- if self.id is not None:
1954
- payload["id"] = self.id
1955
- return payload
1956
-
1957
-
1958
- class WelcomeChannel:
1959
- """Represents a channel shown in the server's welcome screen.
1960
-
1961
- Attributes:
1962
- channel_id (str): The ID of the channel.
1963
- description (str): The description shown for the channel.
1964
- emoji_id (Optional[str]): The ID of the emoji, if custom.
1965
- emoji_name (Optional[str]): The name of the emoji if custom, or the unicode character if standard.
1966
- """
1967
-
1968
- def __init__(self, data: Dict[str, Any]):
1969
- self.channel_id: str = data["channel_id"]
1970
- self.description: str = data["description"]
1971
- self.emoji_id: Optional[str] = data.get("emoji_id")
1972
- self.emoji_name: Optional[str] = data.get("emoji_name")
1973
-
1974
- def __repr__(self) -> str:
1975
- return (
1976
- f"<WelcomeChannel id='{self.channel_id}' description='{self.description}'>"
1977
- )
1978
-
1979
-
1980
- class WelcomeScreen:
1981
- """Represents the welcome screen of a Community guild.
1982
-
1983
- Attributes:
1984
- description (Optional[str]): The server description shown in the welcome screen.
1985
- welcome_channels (List[WelcomeChannel]): The channels shown in the welcome screen.
1986
- """
1987
-
1988
- def __init__(self, data: Dict[str, Any], client_instance: "Client"):
1989
- self._client: "Client" = (
1990
- client_instance # May be useful for fetching channel objects
1991
- )
1992
- self.description: Optional[str] = data.get("description")
1993
- self.welcome_channels: List[WelcomeChannel] = [
1994
- WelcomeChannel(wc_data) for wc_data in data.get("welcome_channels", [])
1995
- ]
1996
-
1997
- def __repr__(self) -> str:
1998
- return f"<WelcomeScreen description='{self.description}' channels={len(self.welcome_channels)}>"
1999
-
2000
-
2001
- class ThreadMember:
2002
- """Represents a member of a thread.
2003
-
2004
- Attributes:
2005
- id (Optional[str]): The ID of the thread. Not always present.
2006
- user_id (Optional[str]): The ID of the user. Not always present.
2007
- join_timestamp (str): When the user joined the thread (ISO8601 timestamp).
2008
- flags (int): User-specific flags for thread settings.
2009
- member (Optional[Member]): The guild member object for this user, if resolved.
2010
- Only available from GUILD_MEMBERS intent and if fetched.
2011
- """
2012
-
2013
- def __init__(
2014
- self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2015
- ): # client_instance for member resolution
2016
- self._client: Optional["Client"] = client_instance
2017
- self.id: Optional[str] = data.get("id") # Thread ID
2018
- self.user_id: Optional[str] = data.get("user_id")
2019
- self.join_timestamp: str = data["join_timestamp"]
2020
- self.flags: int = data["flags"]
2021
-
2022
- # The 'member' field in ThreadMember payload is a full guild member object.
2023
- # This is present in some contexts like when listing thread members.
2024
- self.member: Optional[Member] = (
2025
- Member(data["member"], client_instance) if data.get("member") else None
2026
- )
2027
-
2028
- # Note: The 'presence' field is not included as it's often unavailable or too dynamic for a simple model.
2029
-
2030
- def __repr__(self) -> str:
2031
- return f"<ThreadMember user_id='{self.user_id}' thread_id='{self.id}'>"
2032
-
2033
-
2034
- class PresenceUpdate:
2035
- """Represents a PRESENCE_UPDATE event."""
2036
-
2037
- def __init__(
2038
- self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2039
- ):
2040
- self._client = client_instance
2041
- self.user = User(data["user"])
2042
- self.guild_id: Optional[str] = data.get("guild_id")
2043
- self.status: Optional[str] = data.get("status")
2044
- self.activities: List[Dict[str, Any]] = data.get("activities", [])
2045
- self.client_status: Dict[str, Any] = data.get("client_status", {})
2046
-
2047
- def __repr__(self) -> str:
2048
- return f"<PresenceUpdate user_id='{self.user.id}' guild_id='{self.guild_id}' status='{self.status}'>"
2049
-
2050
-
2051
- class TypingStart:
2052
- """Represents a TYPING_START event."""
2053
-
2054
- def __init__(
2055
- self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2056
- ):
2057
- self._client = client_instance
2058
- self.channel_id: str = data["channel_id"]
2059
- self.guild_id: Optional[str] = data.get("guild_id")
2060
- self.user_id: str = data["user_id"]
2061
- self.timestamp: int = data["timestamp"]
2062
- self.member: Optional[Member] = (
2063
- Member(data["member"], client_instance) if data.get("member") else None
2064
- )
2065
-
2066
- def __repr__(self) -> str:
2067
- return f"<TypingStart channel_id='{self.channel_id}' user_id='{self.user_id}'>"
2068
-
2069
-
2070
- class Reaction:
2071
- """Represents a message reaction event."""
2072
-
2073
- def __init__(
2074
- self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2075
- ):
2076
- self._client = client_instance
2077
- self.user_id: str = data["user_id"]
2078
- self.channel_id: str = data["channel_id"]
2079
- self.message_id: str = data["message_id"]
2080
- self.guild_id: Optional[str] = data.get("guild_id")
2081
- self.member: Optional[Member] = (
2082
- Member(data["member"], client_instance) if data.get("member") else None
2083
- )
2084
- self.emoji: Dict[str, Any] = data.get("emoji", {})
2085
-
2086
- def __repr__(self) -> str:
2087
- emoji_value = self.emoji.get("name") or self.emoji.get("id")
2088
- return f"<Reaction message_id='{self.message_id}' user_id='{self.user_id}' emoji='{emoji_value}'>"
2089
-
2090
-
2091
- class ScheduledEvent:
2092
- """Represents a guild scheduled event."""
2093
-
2094
- def __init__(
2095
- self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2096
- ):
2097
- self._client = client_instance
2098
- self.id: str = data["id"]
2099
- self.guild_id: str = data["guild_id"]
2100
- self.channel_id: Optional[str] = data.get("channel_id")
2101
- self.creator_id: Optional[str] = data.get("creator_id")
2102
- self.name: str = data["name"]
2103
- self.description: Optional[str] = data.get("description")
2104
- self.scheduled_start_time: str = data["scheduled_start_time"]
2105
- self.scheduled_end_time: Optional[str] = data.get("scheduled_end_time")
2106
- self.privacy_level: GuildScheduledEventPrivacyLevel = (
2107
- GuildScheduledEventPrivacyLevel(data["privacy_level"])
2108
- )
2109
- self.status: GuildScheduledEventStatus = GuildScheduledEventStatus(
2110
- data["status"]
2111
- )
2112
- self.entity_type: GuildScheduledEventEntityType = GuildScheduledEventEntityType(
2113
- data["entity_type"]
2114
- )
2115
- self.entity_id: Optional[str] = data.get("entity_id")
2116
- self.entity_metadata: Optional[Dict[str, Any]] = data.get("entity_metadata")
2117
- self.creator: Optional[User] = (
2118
- User(data["creator"]) if data.get("creator") else None
2119
- )
2120
- self.user_count: Optional[int] = data.get("user_count")
2121
- self.image: Optional[str] = data.get("image")
2122
-
2123
- def __repr__(self) -> str:
2124
- return f"<ScheduledEvent id='{self.id}' name='{self.name}'>"
2125
-
2126
-
2127
- @dataclass
2128
- class Invite:
2129
- """Represents a Discord invite."""
2130
-
2131
- code: str
2132
- channel_id: Optional[str]
2133
- guild_id: Optional[str]
2134
- inviter_id: Optional[str]
2135
- uses: Optional[int]
2136
- max_uses: Optional[int]
2137
- max_age: Optional[int]
2138
- temporary: Optional[bool]
2139
- created_at: Optional[str]
2140
-
2141
- @classmethod
2142
- def from_dict(cls, data: Dict[str, Any]) -> "Invite":
2143
- channel = data.get("channel")
2144
- guild = data.get("guild")
2145
- inviter = data.get("inviter")
2146
- return cls(
2147
- code=data["code"],
2148
- channel_id=(channel or {}).get("id") if channel else data.get("channel_id"),
2149
- guild_id=(guild or {}).get("id") if guild else data.get("guild_id"),
2150
- inviter_id=(inviter or {}).get("id"),
2151
- uses=data.get("uses"),
2152
- max_uses=data.get("max_uses"),
2153
- max_age=data.get("max_age"),
2154
- temporary=data.get("temporary"),
2155
- created_at=data.get("created_at"),
2156
- )
2157
-
2158
- def __repr__(self) -> str:
2159
- return f"<Invite code='{self.code}' guild_id='{self.guild_id}' channel_id='{self.channel_id}'>"
2160
-
2161
-
2162
- class GuildMemberRemove:
2163
- """Represents a GUILD_MEMBER_REMOVE event."""
2164
-
2165
- def __init__(
2166
- self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2167
- ):
2168
- self._client = client_instance
2169
- self.guild_id: str = data["guild_id"]
2170
- self.user: User = User(data["user"])
2171
-
2172
- def __repr__(self) -> str:
2173
- return (
2174
- f"<GuildMemberRemove guild_id='{self.guild_id}' user_id='{self.user.id}'>"
2175
- )
2176
-
2177
-
2178
- class GuildBanAdd:
2179
- """Represents a GUILD_BAN_ADD event."""
2180
-
2181
- def __init__(
2182
- self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2183
- ):
2184
- self._client = client_instance
2185
- self.guild_id: str = data["guild_id"]
2186
- self.user: User = User(data["user"])
2187
-
2188
- def __repr__(self) -> str:
2189
- return f"<GuildBanAdd guild_id='{self.guild_id}' user_id='{self.user.id}'>"
2190
-
2191
-
2192
- class GuildBanRemove:
2193
- """Represents a GUILD_BAN_REMOVE event."""
2194
-
2195
- def __init__(
2196
- self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2197
- ):
2198
- self._client = client_instance
2199
- self.guild_id: str = data["guild_id"]
2200
- self.user: User = User(data["user"])
2201
-
2202
- def __repr__(self) -> str:
2203
- return f"<GuildBanRemove guild_id='{self.guild_id}' user_id='{self.user.id}'>"
2204
-
2205
-
2206
- class GuildRoleUpdate:
2207
- """Represents a GUILD_ROLE_UPDATE event."""
2208
-
2209
- def __init__(
2210
- self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2211
- ):
2212
- self._client = client_instance
2213
- self.guild_id: str = data["guild_id"]
2214
- self.role: Role = Role(data["role"])
2215
-
2216
- def __repr__(self) -> str:
2217
- return f"<GuildRoleUpdate guild_id='{self.guild_id}' role_id='{self.role.id}'>"
2218
-
2219
-
2220
- class AuditLogEntry:
2221
- """Represents a single entry in a guild's audit log."""
2222
-
2223
- def __init__(
2224
- self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2225
- ) -> None:
2226
- self._client = client_instance
2227
- self.id: str = data["id"]
2228
- self.user_id: Optional[str] = data.get("user_id")
2229
- self.target_id: Optional[str] = data.get("target_id")
2230
- self.action_type: int = data["action_type"]
2231
- self.reason: Optional[str] = data.get("reason")
2232
- self.changes: List[Dict[str, Any]] = data.get("changes", [])
2233
- self.options: Optional[Dict[str, Any]] = data.get("options")
2234
-
2235
- def __repr__(self) -> str:
2236
- return f"<AuditLogEntry id='{self.id}' action_type={self.action_type} user_id='{self.user_id}'>"
2237
-
2238
-
2239
- def channel_factory(data: Dict[str, Any], client: "Client") -> Channel:
2240
- """Create a channel object from raw API data."""
2241
- channel_type = data.get("type")
2242
-
2243
- if channel_type in (
2244
- ChannelType.GUILD_TEXT.value,
2245
- ChannelType.GUILD_ANNOUNCEMENT.value,
2246
- ):
2247
- return TextChannel(data, client)
2248
- if channel_type == ChannelType.GUILD_VOICE.value:
2249
- return VoiceChannel(data, client)
2250
- if channel_type == ChannelType.GUILD_STAGE_VOICE.value:
2251
- return StageChannel(data, client)
2252
- if channel_type == ChannelType.GUILD_CATEGORY.value:
2253
- return CategoryChannel(data, client)
2254
- if channel_type in (
2255
- ChannelType.ANNOUNCEMENT_THREAD.value,
2256
- ChannelType.PUBLIC_THREAD.value,
2257
- ChannelType.PRIVATE_THREAD.value,
2258
- ):
2259
- return Thread(data, client)
2260
- if channel_type in (ChannelType.DM.value, ChannelType.GROUP_DM.value):
2261
- return DMChannel(data, client)
2262
-
2263
- return Channel(data, client)
1
+ # disagreement/models.py
2
+
3
+ """
4
+ Data models for Discord objects.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ from dataclasses import dataclass
10
+ from typing import Any, AsyncIterator, Dict, List, Optional, TYPE_CHECKING, Union, cast
11
+
12
+ from .cache import ChannelCache, MemberCache
13
+ from .caching import MemberCacheFlags
14
+
15
+ import aiohttp # pylint: disable=import-error
16
+ from .color import Color
17
+ from .errors import DisagreementException, HTTPException
18
+ from .enums import ( # These enums will need to be defined in disagreement/enums.py
19
+ VerificationLevel,
20
+ MessageNotificationLevel,
21
+ ExplicitContentFilterLevel,
22
+ MFALevel,
23
+ GuildNSFWLevel,
24
+ PremiumTier,
25
+ GuildFeature,
26
+ ChannelType,
27
+ ComponentType,
28
+ ButtonStyle, # Added for Button
29
+ GuildScheduledEventPrivacyLevel,
30
+ GuildScheduledEventStatus,
31
+ GuildScheduledEventEntityType,
32
+ # SelectMenuType will be part of ComponentType or a new enum if needed
33
+ )
34
+ from .permissions import Permissions
35
+
36
+
37
+ if TYPE_CHECKING:
38
+ from .client import Client # For type hinting to avoid circular imports
39
+ from .enums import OverwriteType # For PermissionOverwrite model
40
+ from .ui.view import View
41
+ from .interactions import Snowflake
42
+
43
+ # Forward reference Message if it were used in type hints before its definition
44
+ # from .models import Message # Not needed as Message is defined before its use in TextChannel.send etc.
45
+ from .components import component_factory
46
+
47
+
48
+ class User:
49
+ """Represents a Discord User.
50
+
51
+ Attributes:
52
+ id (str): The user's unique ID.
53
+ username (str): The user's username.
54
+ discriminator (str): The user's 4-digit discord-tag.
55
+ bot (bool): Whether the user belongs to an OAuth2 application. Defaults to False.
56
+ avatar (Optional[str]): The user's avatar hash, if any.
57
+ """
58
+
59
+ def __init__(self, data: dict):
60
+ self.id: str = data["id"]
61
+ self.username: str = data["username"]
62
+ self.discriminator: str = data["discriminator"]
63
+ self.bot: bool = data.get("bot", False)
64
+ self.avatar: Optional[str] = data.get("avatar")
65
+
66
+ @property
67
+ def mention(self) -> str:
68
+ """str: Returns a string that allows you to mention the user."""
69
+ return f"<@{self.id}>"
70
+
71
+ def __repr__(self) -> str:
72
+ return f"<User id='{self.id}' username='{self.username}' discriminator='{self.discriminator}'>"
73
+
74
+
75
+ class Message:
76
+ """Represents a message sent in a channel on Discord.
77
+
78
+ Attributes:
79
+ id (str): The message's unique ID.
80
+ channel_id (str): The ID of the channel the message was sent in.
81
+ guild_id (Optional[str]): The ID of the guild the message was sent in, if applicable.
82
+ author (User): The user who sent the message.
83
+ content (str): The actual content of the message.
84
+ timestamp (str): When this message was sent (ISO8601 timestamp).
85
+ components (Optional[List[ActionRow]]): Structured components attached
86
+ to the message if present.
87
+ attachments (List[Attachment]): Attachments included with the message.
88
+ """
89
+
90
+ def __init__(self, data: dict, client_instance: "Client"):
91
+ self._client: "Client" = (
92
+ client_instance # Store reference to client for methods like reply
93
+ )
94
+
95
+ self.id: str = data["id"]
96
+ self.channel_id: str = data["channel_id"]
97
+ self.guild_id: Optional[str] = data.get("guild_id")
98
+ self.author: User = User(data["author"])
99
+ self.content: str = data["content"]
100
+ self.timestamp: str = data["timestamp"]
101
+ if data.get("components"):
102
+ self.components: Optional[List[ActionRow]] = [
103
+ ActionRow.from_dict(c, client_instance)
104
+ for c in data.get("components", [])
105
+ ]
106
+ else:
107
+ self.components = None
108
+ self.attachments: List[Attachment] = [
109
+ Attachment(a) for a in data.get("attachments", [])
110
+ ]
111
+ self.pinned: bool = data.get("pinned", False)
112
+ # Add other fields as needed, e.g., attachments, embeds, reactions, etc.
113
+ # self.mentions: List[User] = [User(u) for u in data.get("mentions", [])]
114
+ # self.mention_roles: List[str] = data.get("mention_roles", [])
115
+ # self.mention_everyone: bool = data.get("mention_everyone", False)
116
+
117
+ async def pin(self) -> None:
118
+ """|coro|
119
+
120
+ Pins this message to its channel.
121
+
122
+ Raises
123
+ ------
124
+ HTTPException
125
+ Pinning the message failed.
126
+ """
127
+ await self._client._http.pin_message(self.channel_id, self.id)
128
+ self.pinned = True
129
+
130
+ async def unpin(self) -> None:
131
+ """|coro|
132
+
133
+ Unpins this message from its channel.
134
+
135
+ Raises
136
+ ------
137
+ HTTPException
138
+ Unpinning the message failed.
139
+ """
140
+ await self._client._http.unpin_message(self.channel_id, self.id)
141
+ self.pinned = False
142
+
143
+ async def reply(
144
+ self,
145
+ content: Optional[str] = None,
146
+ *, # Make additional params keyword-only
147
+ tts: bool = False,
148
+ embed: Optional["Embed"] = None,
149
+ embeds: Optional[List["Embed"]] = None,
150
+ components: Optional[List["ActionRow"]] = None,
151
+ allowed_mentions: Optional[Dict[str, Any]] = None,
152
+ mention_author: Optional[bool] = None,
153
+ flags: Optional[int] = None,
154
+ view: Optional["View"] = None,
155
+ ) -> "Message":
156
+ """|coro|
157
+
158
+ Sends a reply to the message.
159
+ This is a shorthand for `Client.send_message` in the message's channel.
160
+
161
+ Parameters:
162
+ content (Optional[str]): The content of the message.
163
+ tts (bool): Whether the message should be sent with text-to-speech.
164
+ embed (Optional[Embed]): A single embed to send. Cannot be used with `embeds`.
165
+ embeds (Optional[List[Embed]]): A list of embeds to send.
166
+ components (Optional[List[ActionRow]]): A list of ActionRow components.
167
+ allowed_mentions (Optional[Dict[str, Any]]): Allowed mentions for the message.
168
+ mention_author (Optional[bool]): Whether to mention the author in the reply. If ``None`` the
169
+ client's :attr:`mention_replies` setting is used.
170
+ flags (Optional[int]): Message flags.
171
+ view (Optional[View]): A view to send with the message.
172
+
173
+ Returns:
174
+ Message: The message that was sent.
175
+
176
+ Raises:
177
+ HTTPException: Sending the message failed.
178
+ ValueError: If both `embed` and `embeds` are provided.
179
+ """
180
+ # Determine allowed mentions for the reply
181
+ if mention_author is None:
182
+ mention_author = getattr(self._client, "mention_replies", False)
183
+
184
+ if allowed_mentions is None:
185
+ allowed_mentions = {"replied_user": mention_author}
186
+ else:
187
+ allowed_mentions = dict(allowed_mentions)
188
+ allowed_mentions.setdefault("replied_user", mention_author)
189
+
190
+ # Client.send_message is already updated to handle these parameters
191
+ return await self._client.send_message(
192
+ channel_id=self.channel_id,
193
+ content=content,
194
+ tts=tts,
195
+ embed=embed,
196
+ embeds=embeds,
197
+ components=components,
198
+ allowed_mentions=allowed_mentions,
199
+ message_reference={
200
+ "message_id": self.id,
201
+ "channel_id": self.channel_id,
202
+ "guild_id": self.guild_id,
203
+ },
204
+ flags=flags,
205
+ view=view,
206
+ )
207
+
208
+ async def edit(
209
+ self,
210
+ *,
211
+ content: Optional[str] = None,
212
+ embed: Optional["Embed"] = None,
213
+ embeds: Optional[List["Embed"]] = None,
214
+ components: Optional[List["ActionRow"]] = None,
215
+ allowed_mentions: Optional[Dict[str, Any]] = None,
216
+ flags: Optional[int] = None,
217
+ view: Optional["View"] = None,
218
+ ) -> "Message":
219
+ """|coro|
220
+
221
+ Edits this message.
222
+
223
+ Parameters are the same as :meth:`Client.edit_message`.
224
+ """
225
+
226
+ return await self._client.edit_message(
227
+ channel_id=self.channel_id,
228
+ message_id=self.id,
229
+ content=content,
230
+ embed=embed,
231
+ embeds=embeds,
232
+ components=components,
233
+ allowed_mentions=allowed_mentions,
234
+ flags=flags,
235
+ view=view,
236
+ )
237
+
238
+ async def add_reaction(self, emoji: str) -> None:
239
+ """|coro| Add a reaction to this message."""
240
+
241
+ await self._client.add_reaction(self.channel_id, self.id, emoji)
242
+
243
+ async def remove_reaction(self, emoji: str, member: Optional[User] = None) -> None:
244
+ """|coro|
245
+ Removes a reaction from this message.
246
+ If no ``member`` is provided, removes the bot's own reaction.
247
+ """
248
+ if member:
249
+ await self._client._http.delete_user_reaction(
250
+ self.channel_id, self.id, emoji, member.id
251
+ )
252
+ else:
253
+ await self._client.remove_reaction(self.channel_id, self.id, emoji)
254
+
255
+ async def clear_reactions(self) -> None:
256
+ """|coro| Remove all reactions from this message."""
257
+
258
+ await self._client.clear_reactions(self.channel_id, self.id)
259
+
260
+ async def delete(self, delay: Optional[float] = None) -> None:
261
+ """|coro|
262
+
263
+ Deletes this message.
264
+
265
+ Parameters
266
+ ----------
267
+ delay:
268
+ If provided, wait this many seconds before deleting.
269
+ """
270
+
271
+ if delay is not None:
272
+ await asyncio.sleep(delay)
273
+
274
+ await self._client._http.delete_message(self.channel_id, self.id)
275
+
276
+ def __repr__(self) -> str:
277
+ return f"<Message id='{self.id}' channel_id='{self.channel_id}' author='{self.author!r}'>"
278
+
279
+ async def create_thread(
280
+ self,
281
+ name: str,
282
+ *,
283
+ auto_archive_duration: Optional[int] = None,
284
+ rate_limit_per_user: Optional[int] = None,
285
+ reason: Optional[str] = None,
286
+ ) -> "Thread":
287
+ """|coro|
288
+
289
+ Creates a new thread from this message.
290
+
291
+ Parameters
292
+ ----------
293
+ name: str
294
+ The name of the thread.
295
+ auto_archive_duration: Optional[int]
296
+ The duration in minutes to automatically archive the thread after recent activity.
297
+ Can be one of 60, 1440, 4320, 10080.
298
+ rate_limit_per_user: Optional[int]
299
+ The number of seconds a user has to wait before sending another message.
300
+ reason: Optional[str]
301
+ The reason for creating the thread.
302
+
303
+ Returns
304
+ -------
305
+ Thread
306
+ The created thread.
307
+ """
308
+ payload: Dict[str, Any] = {"name": name}
309
+ if auto_archive_duration is not None:
310
+ payload["auto_archive_duration"] = auto_archive_duration
311
+ if rate_limit_per_user is not None:
312
+ payload["rate_limit_per_user"] = rate_limit_per_user
313
+
314
+ data = await self._client._http.start_thread_from_message(
315
+ self.channel_id, self.id, payload
316
+ )
317
+ return cast("Thread", self._client.parse_channel(data))
318
+
319
+
320
+ class PartialMessage:
321
+ """Represents a partial message, identified by its ID and channel.
322
+
323
+ This model is used to perform actions on a message without having the
324
+ full message object in the cache.
325
+
326
+ Attributes:
327
+ id (str): The message's unique ID.
328
+ channel (TextChannel): The text channel this message belongs to.
329
+ """
330
+
331
+ def __init__(self, *, id: str, channel: "TextChannel"):
332
+ self.id = id
333
+ self.channel = channel
334
+ self._client = channel._client
335
+
336
+ async def fetch(self) -> "Message":
337
+ """|coro|
338
+
339
+ Fetches the full message data from Discord.
340
+
341
+ Returns
342
+ -------
343
+ Message
344
+ The complete message object.
345
+ """
346
+ data = await self._client._http.get_message(self.channel.id, self.id)
347
+ return self._client.parse_message(data)
348
+
349
+ async def delete(self, *, delay: Optional[float] = None) -> None:
350
+ """|coro|
351
+
352
+ Deletes this message.
353
+
354
+ Parameters
355
+ ----------
356
+ delay: Optional[float]
357
+ If provided, wait this many seconds before deleting.
358
+ """
359
+ if delay is not None:
360
+ await asyncio.sleep(delay)
361
+ await self._client._http.delete_message(self.channel.id, self.id)
362
+
363
+ async def pin(self) -> None:
364
+ """|coro|
365
+
366
+ Pins this message to its channel.
367
+ """
368
+ await self._client._http.pin_message(self.channel.id, self.id)
369
+
370
+ async def unpin(self) -> None:
371
+ """|coro|
372
+
373
+ Unpins this message from its channel.
374
+ """
375
+ await self._client._http.unpin_message(self.channel.id, self.id)
376
+
377
+ async def add_reaction(self, emoji: str) -> None:
378
+ """|coro|
379
+
380
+ Adds a reaction to this message.
381
+ """
382
+ await self._client._http.create_reaction(self.channel.id, self.id, emoji)
383
+
384
+ async def remove_reaction(self, emoji: str, member: Optional[User] = None) -> None:
385
+ """|coro|
386
+
387
+ Removes a reaction from this message.
388
+
389
+ If no ``member`` is provided, removes the bot's own reaction.
390
+ """
391
+ if member:
392
+ await self._client._http.delete_user_reaction(
393
+ self.channel.id, self.id, emoji, member.id
394
+ )
395
+ else:
396
+ await self._client._http.delete_reaction(self.channel.id, self.id, emoji)
397
+
398
+
399
+ class EmbedFooter:
400
+ """Represents an embed footer."""
401
+
402
+ def __init__(self, data: Dict[str, Any]):
403
+ self.text: str = data["text"]
404
+ self.icon_url: Optional[str] = data.get("icon_url")
405
+ self.proxy_icon_url: Optional[str] = data.get("proxy_icon_url")
406
+
407
+ def to_dict(self) -> Dict[str, Any]:
408
+ payload = {"text": self.text}
409
+ if self.icon_url:
410
+ payload["icon_url"] = self.icon_url
411
+ if self.proxy_icon_url:
412
+ payload["proxy_icon_url"] = self.proxy_icon_url
413
+ return payload
414
+
415
+
416
+ class EmbedImage:
417
+ """Represents an embed image."""
418
+
419
+ def __init__(self, data: Dict[str, Any]):
420
+ self.url: str = data["url"]
421
+ self.proxy_url: Optional[str] = data.get("proxy_url")
422
+ self.height: Optional[int] = data.get("height")
423
+ self.width: Optional[int] = data.get("width")
424
+
425
+ def to_dict(self) -> Dict[str, Any]:
426
+ payload: Dict[str, Any] = {"url": self.url}
427
+ if self.proxy_url:
428
+ payload["proxy_url"] = self.proxy_url
429
+ if self.height:
430
+ payload["height"] = self.height
431
+ if self.width:
432
+ payload["width"] = self.width
433
+ return payload
434
+
435
+ def __repr__(self) -> str:
436
+ return f"<EmbedImage url='{self.url}'>"
437
+
438
+
439
+ class EmbedThumbnail(EmbedImage): # Similar structure to EmbedImage
440
+ """Represents an embed thumbnail."""
441
+
442
+ pass
443
+
444
+
445
+ class EmbedAuthor:
446
+ """Represents an embed author."""
447
+
448
+ def __init__(self, data: Dict[str, Any]):
449
+ self.name: str = data["name"]
450
+ self.url: Optional[str] = data.get("url")
451
+ self.icon_url: Optional[str] = data.get("icon_url")
452
+ self.proxy_icon_url: Optional[str] = data.get("proxy_icon_url")
453
+
454
+ def to_dict(self) -> Dict[str, Any]:
455
+ payload = {"name": self.name}
456
+ if self.url:
457
+ payload["url"] = self.url
458
+ if self.icon_url:
459
+ payload["icon_url"] = self.icon_url
460
+ if self.proxy_icon_url:
461
+ payload["proxy_icon_url"] = self.proxy_icon_url
462
+ return payload
463
+
464
+
465
+ class EmbedField:
466
+ """Represents an embed field."""
467
+
468
+ def __init__(self, data: Dict[str, Any]):
469
+ self.name: str = data["name"]
470
+ self.value: str = data["value"]
471
+ self.inline: bool = data.get("inline", False)
472
+
473
+ def to_dict(self) -> Dict[str, Any]:
474
+ return {"name": self.name, "value": self.value, "inline": self.inline}
475
+
476
+
477
+ class Embed:
478
+ """Represents a Discord embed.
479
+
480
+ Attributes can be set directly or via methods like `set_author`, `add_field`.
481
+ """
482
+
483
+ def __init__(self, data: Optional[Dict[str, Any]] = None):
484
+ data = data or {}
485
+ self.title: Optional[str] = data.get("title")
486
+ self.type: str = data.get("type", "rich") # Default to "rich" for sending
487
+ self.description: Optional[str] = data.get("description")
488
+ self.url: Optional[str] = data.get("url")
489
+ self.timestamp: Optional[str] = data.get("timestamp") # ISO8601 timestamp
490
+ self.color = Color.parse(data.get("color"))
491
+
492
+ self.footer: Optional[EmbedFooter] = (
493
+ EmbedFooter(data["footer"]) if data.get("footer") else None
494
+ )
495
+ self.image: Optional[EmbedImage] = (
496
+ EmbedImage(data["image"]) if data.get("image") else None
497
+ )
498
+ self.thumbnail: Optional[EmbedThumbnail] = (
499
+ EmbedThumbnail(data["thumbnail"]) if data.get("thumbnail") else None
500
+ )
501
+ # Video and Provider are less common for bot-sent embeds, can be added if needed.
502
+ self.author: Optional[EmbedAuthor] = (
503
+ EmbedAuthor(data["author"]) if data.get("author") else None
504
+ )
505
+ self.fields: List[EmbedField] = (
506
+ [EmbedField(f) for f in data["fields"]] if data.get("fields") else []
507
+ )
508
+
509
+ def to_dict(self) -> Dict[str, Any]:
510
+ payload: Dict[str, Any] = {"type": self.type}
511
+ if self.title:
512
+ payload["title"] = self.title
513
+ if self.description:
514
+ payload["description"] = self.description
515
+ if self.url:
516
+ payload["url"] = self.url
517
+ if self.timestamp:
518
+ payload["timestamp"] = self.timestamp
519
+ if self.color is not None:
520
+ payload["color"] = self.color.value
521
+ if self.footer:
522
+ payload["footer"] = self.footer.to_dict()
523
+ if self.image:
524
+ payload["image"] = self.image.to_dict()
525
+ if self.thumbnail:
526
+ payload["thumbnail"] = self.thumbnail.to_dict()
527
+ if self.author:
528
+ payload["author"] = self.author.to_dict()
529
+ if self.fields:
530
+ payload["fields"] = [f.to_dict() for f in self.fields]
531
+ return payload
532
+
533
+ # Convenience methods for building embeds can be added here
534
+ # e.g., set_author, add_field, set_footer, set_image, etc.
535
+
536
+
537
+ class Attachment:
538
+ """Represents a message attachment."""
539
+
540
+ def __init__(self, data: Dict[str, Any]):
541
+ self.id: str = data["id"]
542
+ self.filename: str = data["filename"]
543
+ self.description: Optional[str] = data.get("description")
544
+ self.content_type: Optional[str] = data.get("content_type")
545
+ self.size: Optional[int] = data.get("size")
546
+ self.url: Optional[str] = data.get("url")
547
+ self.proxy_url: Optional[str] = data.get("proxy_url")
548
+ self.height: Optional[int] = data.get("height") # If image
549
+ self.width: Optional[int] = data.get("width") # If image
550
+ self.ephemeral: bool = data.get("ephemeral", False)
551
+
552
+ def __repr__(self) -> str:
553
+ return f"<Attachment id='{self.id}' filename='{self.filename}'>"
554
+
555
+ def to_dict(self) -> Dict[str, Any]:
556
+ payload: Dict[str, Any] = {"id": self.id, "filename": self.filename}
557
+ if self.description is not None:
558
+ payload["description"] = self.description
559
+ if self.content_type is not None:
560
+ payload["content_type"] = self.content_type
561
+ if self.size is not None:
562
+ payload["size"] = self.size
563
+ if self.url is not None:
564
+ payload["url"] = self.url
565
+ if self.proxy_url is not None:
566
+ payload["proxy_url"] = self.proxy_url
567
+ if self.height is not None:
568
+ payload["height"] = self.height
569
+ if self.width is not None:
570
+ payload["width"] = self.width
571
+ if self.ephemeral:
572
+ payload["ephemeral"] = self.ephemeral
573
+ return payload
574
+
575
+
576
+ class File:
577
+ """Represents a file to be uploaded."""
578
+
579
+ def __init__(self, filename: str, data: bytes):
580
+ self.filename = filename
581
+ self.data = data
582
+
583
+
584
+ class AllowedMentions:
585
+ """Represents allowed mentions for a message or interaction response."""
586
+
587
+ def __init__(self, data: Dict[str, Any]):
588
+ self.parse: List[str] = data.get("parse", [])
589
+ self.roles: List[str] = data.get("roles", [])
590
+ self.users: List[str] = data.get("users", [])
591
+ self.replied_user: bool = data.get("replied_user", False)
592
+
593
+ def to_dict(self) -> Dict[str, Any]:
594
+ payload: Dict[str, Any] = {"parse": self.parse}
595
+ if self.roles:
596
+ payload["roles"] = self.roles
597
+ if self.users:
598
+ payload["users"] = self.users
599
+ if self.replied_user:
600
+ payload["replied_user"] = self.replied_user
601
+ return payload
602
+
603
+
604
+ class RoleTags:
605
+ """Represents tags for a role."""
606
+
607
+ def __init__(self, data: Dict[str, Any]):
608
+ self.bot_id: Optional[str] = data.get("bot_id")
609
+ self.integration_id: Optional[str] = data.get("integration_id")
610
+ self.premium_subscriber: Optional[bool] = (
611
+ data.get("premium_subscriber") is None
612
+ ) # presence of null value means true
613
+
614
+ def to_dict(self) -> Dict[str, Any]:
615
+ payload = {}
616
+ if self.bot_id:
617
+ payload["bot_id"] = self.bot_id
618
+ if self.integration_id:
619
+ payload["integration_id"] = self.integration_id
620
+ if self.premium_subscriber:
621
+ payload["premium_subscriber"] = None # Explicitly null
622
+ return payload
623
+
624
+
625
+ class Role:
626
+ """Represents a Discord Role."""
627
+
628
+ def __init__(self, data: Dict[str, Any]):
629
+ self.id: str = data["id"]
630
+ self.name: str = data["name"]
631
+ self.color: int = data["color"]
632
+ self.hoist: bool = data["hoist"]
633
+ self.icon: Optional[str] = data.get("icon")
634
+ self.unicode_emoji: Optional[str] = data.get("unicode_emoji")
635
+ self.position: int = data["position"]
636
+ self.permissions: str = data["permissions"] # String of bitwise permissions
637
+ self.managed: bool = data["managed"]
638
+ self.mentionable: bool = data["mentionable"]
639
+ self.tags: Optional[RoleTags] = (
640
+ RoleTags(data["tags"]) if data.get("tags") else None
641
+ )
642
+
643
+ @property
644
+ def mention(self) -> str:
645
+ """str: Returns a string that allows you to mention the role."""
646
+ return f"<@&{self.id}>"
647
+
648
+ def __repr__(self) -> str:
649
+ return f"<Role id='{self.id}' name='{self.name}'>"
650
+
651
+
652
+ class Member(User): # Member inherits from User
653
+ """Represents a Guild Member.
654
+ This class combines User attributes with guild-specific Member attributes.
655
+ """
656
+
657
+ def __init__(
658
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
659
+ ):
660
+ self._client: Optional["Client"] = client_instance
661
+ self.guild_id: Optional[str] = None
662
+ self.status: Optional[str] = None
663
+ self.voice_state: Optional[Dict[str, Any]] = None
664
+ # User part is nested under 'user' key in member data from gateway/API
665
+ user_data = data.get("user", {})
666
+ # If 'id' is not in user_data but is top-level (e.g. from interaction resolved member without user object)
667
+ if "id" not in user_data and "id" in data:
668
+ # This case is less common for full member objects but can happen.
669
+ # We'd need to construct a partial user from top-level member fields if 'user' is missing.
670
+ # For now, assume 'user' object is present for full Member hydration.
671
+ # If 'user' is missing, the User part might be incomplete.
672
+ pass # User fields will be missing or default if 'user' not in data.
673
+
674
+ super().__init__(
675
+ user_data if user_data else data
676
+ ) # Pass user_data or data if user_data is empty
677
+
678
+ self.nick: Optional[str] = data.get("nick")
679
+ self.avatar: Optional[str] = data.get("avatar") # Guild-specific avatar hash
680
+ self.roles: List[str] = data.get("roles", []) # List of role IDs
681
+ self.joined_at: str = data["joined_at"] # ISO8601 timestamp
682
+ self.premium_since: Optional[str] = data.get(
683
+ "premium_since"
684
+ ) # ISO8601 timestamp
685
+ self.deaf: bool = data.get("deaf", False)
686
+ self.mute: bool = data.get("mute", False)
687
+ self.pending: bool = data.get("pending", False)
688
+ self.permissions: Optional[str] = data.get(
689
+ "permissions"
690
+ ) # Permissions in the channel, if applicable
691
+ self.communication_disabled_until: Optional[str] = data.get(
692
+ "communication_disabled_until"
693
+ ) # ISO8601 timestamp
694
+
695
+ # If 'user' object was present, ensure User attributes are from there
696
+ if user_data:
697
+ self.id = user_data.get("id", self.id) # Prefer user.id if available
698
+ self.username = user_data.get("username", self.username)
699
+ self.discriminator = user_data.get("discriminator", self.discriminator)
700
+ self.bot = user_data.get("bot", self.bot)
701
+ # User's global avatar is User.avatar, Member.avatar is guild-specific
702
+ # super() already set self.avatar from user_data if present.
703
+ # The self.avatar = data.get("avatar") line above overwrites it with guild avatar. This is correct.
704
+
705
+ def __repr__(self) -> str:
706
+ return f"<Member id='{self.id}' username='{self.username}' nick='{self.nick}'>"
707
+
708
+ @property
709
+ def display_name(self) -> str:
710
+ """Return the nickname if set, otherwise the username."""
711
+
712
+ return self.nick or self.username
713
+
714
+ async def kick(self, *, reason: Optional[str] = None) -> None:
715
+ if not self.guild_id or not self._client:
716
+ raise DisagreementException("Member.kick requires guild_id and client")
717
+ await self._client._http.kick_member(self.guild_id, self.id, reason=reason)
718
+
719
+ async def ban(
720
+ self,
721
+ *,
722
+ delete_message_seconds: int = 0,
723
+ reason: Optional[str] = None,
724
+ ) -> None:
725
+ if not self.guild_id or not self._client:
726
+ raise DisagreementException("Member.ban requires guild_id and client")
727
+ await self._client._http.ban_member(
728
+ self.guild_id,
729
+ self.id,
730
+ delete_message_seconds=delete_message_seconds,
731
+ reason=reason,
732
+ )
733
+
734
+ async def timeout(
735
+ self, until: Optional[str], *, reason: Optional[str] = None
736
+ ) -> None:
737
+ if not self.guild_id or not self._client:
738
+ raise DisagreementException("Member.timeout requires guild_id and client")
739
+ await self._client._http.timeout_member(
740
+ self.guild_id,
741
+ self.id,
742
+ until=until,
743
+ reason=reason,
744
+ )
745
+
746
+ @property
747
+ def top_role(self) -> Optional["Role"]:
748
+ """Return the member's highest role from the guild cache."""
749
+
750
+ if not self.guild_id or not self._client:
751
+ return None
752
+
753
+ guild = self._client.get_guild(self.guild_id)
754
+ if not guild:
755
+ return None
756
+
757
+ if not guild.roles and hasattr(self._client, "fetch_roles"):
758
+ try:
759
+ self._client.loop.run_until_complete(
760
+ self._client.fetch_roles(self.guild_id)
761
+ )
762
+ except RuntimeError:
763
+ future = asyncio.run_coroutine_threadsafe(
764
+ self._client.fetch_roles(self.guild_id), self._client.loop
765
+ )
766
+ future.result()
767
+
768
+ role_objects = [r for r in guild.roles if r.id in self.roles]
769
+ if not role_objects:
770
+ return None
771
+
772
+ return max(role_objects, key=lambda r: r.position)
773
+
774
+
775
+ class PartialEmoji:
776
+ """Represents a partial emoji, often used in components or reactions.
777
+
778
+ This typically means only id, name, and animated are known.
779
+ For unicode emojis, id will be None and name will be the unicode character.
780
+ """
781
+
782
+ def __init__(self, data: Dict[str, Any]):
783
+ self.id: Optional[str] = data.get("id")
784
+ self.name: Optional[str] = data.get(
785
+ "name"
786
+ ) # Can be None for unknown custom emoji, or unicode char
787
+ self.animated: bool = data.get("animated", False)
788
+
789
+ def to_dict(self) -> Dict[str, Any]:
790
+ payload: Dict[str, Any] = {}
791
+ if self.id:
792
+ payload["id"] = self.id
793
+ if self.name:
794
+ payload["name"] = self.name
795
+ if self.animated: # Only include if true, as per some Discord patterns
796
+ payload["animated"] = self.animated
797
+ return payload
798
+
799
+ def __str__(self) -> str:
800
+ if self.id:
801
+ return f"<{'a' if self.animated else ''}:{self.name}:{self.id}>"
802
+ return self.name or "" # For unicode emoji
803
+
804
+ def __repr__(self) -> str:
805
+ return (
806
+ f"<PartialEmoji id='{self.id}' name='{self.name}' animated={self.animated}>"
807
+ )
808
+
809
+
810
+ def to_partial_emoji(
811
+ value: Union[str, "PartialEmoji", None],
812
+ ) -> Optional["PartialEmoji"]:
813
+ """Convert a string or PartialEmoji to a PartialEmoji instance.
814
+
815
+ Args:
816
+ value: Either a unicode emoji string, a :class:`PartialEmoji`, or ``None``.
817
+
818
+ Returns:
819
+ A :class:`PartialEmoji` or ``None`` if ``value`` was ``None``.
820
+
821
+ Raises:
822
+ TypeError: If ``value`` is not ``str`` or :class:`PartialEmoji`.
823
+ """
824
+
825
+ if value is None or isinstance(value, PartialEmoji):
826
+ return value
827
+ if isinstance(value, str):
828
+ return PartialEmoji({"name": value, "id": None})
829
+ raise TypeError("emoji must be a str or PartialEmoji")
830
+
831
+
832
+ class Emoji(PartialEmoji):
833
+ """Represents a custom guild emoji.
834
+
835
+ Inherits id, name, animated from PartialEmoji.
836
+ """
837
+
838
+ def __init__(
839
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
840
+ ):
841
+ super().__init__(data)
842
+ self._client: Optional["Client"] = (
843
+ client_instance # For potential future methods
844
+ )
845
+
846
+ # Roles this emoji is whitelisted to
847
+ self.roles: List[str] = data.get("roles", []) # List of role IDs
848
+
849
+ # User object for the user that created this emoji (optional, only for GUILD_EMOJIS_AND_STICKERS intent)
850
+ self.user: Optional[User] = User(data["user"]) if data.get("user") else None
851
+
852
+ self.require_colons: bool = data.get("require_colons", False)
853
+ self.managed: bool = data.get(
854
+ "managed", False
855
+ ) # If this emoji is managed by an integration
856
+ self.available: bool = data.get(
857
+ "available", True
858
+ ) # Whether this emoji can be used
859
+
860
+ def __repr__(self) -> str:
861
+ return f"<Emoji id='{self.id}' name='{self.name}' animated={self.animated} available={self.available}>"
862
+
863
+
864
+ class StickerItem:
865
+ """Represents a sticker item, a basic representation of a sticker.
866
+
867
+ Used in sticker packs and sometimes in message data.
868
+ """
869
+
870
+ def __init__(self, data: Dict[str, Any]):
871
+ self.id: str = data["id"]
872
+ self.name: str = data["name"]
873
+ self.format_type: int = data["format_type"] # StickerFormatType enum
874
+
875
+ def __repr__(self) -> str:
876
+ return f"<StickerItem id='{self.id}' name='{self.name}'>"
877
+
878
+
879
+ class Sticker(StickerItem):
880
+ """Represents a Discord sticker.
881
+
882
+ Inherits id, name, format_type from StickerItem.
883
+ """
884
+
885
+ def __init__(
886
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
887
+ ):
888
+ super().__init__(data)
889
+ self._client: Optional["Client"] = client_instance
890
+
891
+ self.pack_id: Optional[str] = data.get(
892
+ "pack_id"
893
+ ) # For standard stickers, ID of the pack
894
+ self.description: Optional[str] = data.get("description")
895
+ self.tags: str = data.get(
896
+ "tags", ""
897
+ ) # Comma-separated list of tags for guild stickers
898
+ # type is StickerType enum (STANDARD or GUILD)
899
+ # For guild stickers, this is 2. For standard stickers, this is 1.
900
+ self.type: int = data["type"]
901
+ self.available: bool = data.get(
902
+ "available", True
903
+ ) # Whether this sticker can be used
904
+ self.guild_id: Optional[str] = data.get(
905
+ "guild_id"
906
+ ) # ID of the guild that owns this sticker
907
+
908
+ # User object of the user that uploaded the guild sticker
909
+ self.user: Optional[User] = User(data["user"]) if data.get("user") else None
910
+
911
+ self.sort_value: Optional[int] = data.get(
912
+ "sort_value"
913
+ ) # The standard sticker's sort order within its pack
914
+
915
+ def __repr__(self) -> str:
916
+ return f"<Sticker id='{self.id}' name='{self.name}' guild_id='{self.guild_id}'>"
917
+
918
+
919
+ class StickerPack:
920
+ """Represents a pack of standard stickers."""
921
+
922
+ def __init__(
923
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
924
+ ):
925
+ self._client: Optional["Client"] = client_instance
926
+ self.id: str = data["id"]
927
+ self.stickers: List[Sticker] = [
928
+ Sticker(s_data, client_instance) for s_data in data.get("stickers", [])
929
+ ]
930
+ self.name: str = data["name"]
931
+ self.sku_id: str = data["sku_id"]
932
+ self.cover_sticker_id: Optional[str] = data.get("cover_sticker_id")
933
+ self.description: str = data["description"]
934
+ self.banner_asset_id: Optional[str] = data.get(
935
+ "banner_asset_id"
936
+ ) # ID of the pack's banner image
937
+
938
+ def __repr__(self) -> str:
939
+ return f"<StickerPack id='{self.id}' name='{self.name}' stickers={len(self.stickers)}>"
940
+
941
+
942
+ class PermissionOverwrite:
943
+ """Represents a permission overwrite for a role or member in a channel."""
944
+
945
+ def __init__(self, data: Dict[str, Any]):
946
+ self.id: str = data["id"] # Role or user ID
947
+ self._type_val: int = int(data["type"]) # Store raw type for enum property
948
+ self.allow: str = data["allow"] # Bitwise value of allowed permissions
949
+ self.deny: str = data["deny"] # Bitwise value of denied permissions
950
+
951
+ @property
952
+ def type(self) -> "OverwriteType":
953
+ from .enums import (
954
+ OverwriteType,
955
+ ) # Local import to avoid circularity at module level
956
+
957
+ return OverwriteType(self._type_val)
958
+
959
+ def to_dict(self) -> Dict[str, Any]:
960
+ return {
961
+ "id": self.id,
962
+ "type": self.type.value,
963
+ "allow": self.allow,
964
+ "deny": self.deny,
965
+ }
966
+
967
+ def __repr__(self) -> str:
968
+ return f"<PermissionOverwrite id='{self.id}' type='{self.type.name if hasattr(self.type, 'name') else self._type_val}' allow='{self.allow}' deny='{self.deny}'>"
969
+
970
+
971
+ class Guild:
972
+ """Represents a Discord Guild (Server).
973
+
974
+ Attributes:
975
+ id (str): Guild ID.
976
+ name (str): Guild name (2-100 characters, excluding @, #, :, ```).
977
+ icon (Optional[str]): Icon hash.
978
+ splash (Optional[str]): Splash hash.
979
+ discovery_splash (Optional[str]): Discovery splash hash; only present for discoverable guilds.
980
+ owner (Optional[bool]): True if the user is the owner of the guild. (Only for /users/@me/guilds endpoint)
981
+ owner_id (str): ID of owner.
982
+ permissions (Optional[str]): Total permissions for the user in the guild (excludes overwrites). (Only for /users/@me/guilds endpoint)
983
+ afk_channel_id (Optional[str]): ID of afk channel.
984
+ afk_timeout (int): AFK timeout in seconds.
985
+ widget_enabled (Optional[bool]): True if the server widget is enabled.
986
+ widget_channel_id (Optional[str]): The channel id that the widget will generate an invite to, or null if set to no invite.
987
+ verification_level (VerificationLevel): Verification level required for the guild.
988
+ default_message_notifications (MessageNotificationLevel): Default message notifications level.
989
+ explicit_content_filter (ExplicitContentFilterLevel): Explicit content filter level.
990
+ roles (List[Role]): Roles in the guild.
991
+ emojis (List[Dict]): Custom emojis. (Consider creating an Emoji model)
992
+ features (List[GuildFeature]): Enabled guild features.
993
+ mfa_level (MFALevel): Required MFA level for the guild.
994
+ application_id (Optional[str]): Application ID of the guild creator if it is bot-created.
995
+ system_channel_id (Optional[str]): The id of the channel where guild notices such as welcome messages and boost events are posted.
996
+ system_channel_flags (int): System channel flags.
997
+ rules_channel_id (Optional[str]): The id of the channel where Community guilds can display rules.
998
+ max_members (Optional[int]): The maximum number of members for the guild.
999
+ vanity_url_code (Optional[str]): The vanity url code for the guild.
1000
+ description (Optional[str]): The description of a Community guild.
1001
+ banner (Optional[str]): Banner hash.
1002
+ premium_tier (PremiumTier): Premium tier (Server Boost level).
1003
+ premium_subscription_count (Optional[int]): The number of boosts this guild currently has.
1004
+ preferred_locale (str): The preferred locale of a Community guild. Defaults to "en-US".
1005
+ public_updates_channel_id (Optional[str]): The id of the channel where admins and moderators of Community guilds receive notices from Discord.
1006
+ max_video_channel_users (Optional[int]): The maximum number of users in a video channel.
1007
+ welcome_screen (Optional[Dict]): The welcome screen of a Community guild. (Consider a WelcomeScreen model)
1008
+ nsfw_level (GuildNSFWLevel): Guild NSFW level.
1009
+ stickers (Optional[List[Dict]]): Custom stickers in the guild. (Consider a Sticker model)
1010
+ premium_progress_bar_enabled (bool): Whether the guild has the premium progress bar enabled.
1011
+ """
1012
+
1013
+ def __init__(self, data: Dict[str, Any], client_instance: "Client"):
1014
+ self._client: "Client" = client_instance
1015
+ self.id: str = data["id"]
1016
+ self.name: str = data["name"]
1017
+ self.icon: Optional[str] = data.get("icon")
1018
+ self.splash: Optional[str] = data.get("splash")
1019
+ self.discovery_splash: Optional[str] = data.get("discovery_splash")
1020
+ self.owner: Optional[bool] = data.get("owner")
1021
+ self.owner_id: str = data["owner_id"]
1022
+ self.permissions: Optional[str] = data.get("permissions")
1023
+ self.afk_channel_id: Optional[str] = data.get("afk_channel_id")
1024
+ self.afk_timeout: int = data["afk_timeout"]
1025
+ self.widget_enabled: Optional[bool] = data.get("widget_enabled")
1026
+ self.widget_channel_id: Optional[str] = data.get("widget_channel_id")
1027
+ self.verification_level: VerificationLevel = VerificationLevel(
1028
+ data["verification_level"]
1029
+ )
1030
+ self.default_message_notifications: MessageNotificationLevel = (
1031
+ MessageNotificationLevel(data["default_message_notifications"])
1032
+ )
1033
+ self.explicit_content_filter: ExplicitContentFilterLevel = (
1034
+ ExplicitContentFilterLevel(data["explicit_content_filter"])
1035
+ )
1036
+
1037
+ self.roles: List[Role] = [Role(r) for r in data.get("roles", [])]
1038
+ self.emojis: List[Emoji] = [
1039
+ Emoji(e_data, client_instance) for e_data in data.get("emojis", [])
1040
+ ]
1041
+
1042
+ # Assuming GuildFeature can be constructed from string feature names or their values
1043
+ self.features: List[GuildFeature] = [
1044
+ GuildFeature(f) if not isinstance(f, GuildFeature) else f
1045
+ for f in data.get("features", [])
1046
+ ]
1047
+
1048
+ self.mfa_level: MFALevel = MFALevel(data["mfa_level"])
1049
+ self.application_id: Optional[str] = data.get("application_id")
1050
+ self.system_channel_id: Optional[str] = data.get("system_channel_id")
1051
+ self.system_channel_flags: int = data["system_channel_flags"]
1052
+ self.rules_channel_id: Optional[str] = data.get("rules_channel_id")
1053
+ self.max_members: Optional[int] = data.get("max_members")
1054
+ self.vanity_url_code: Optional[str] = data.get("vanity_url_code")
1055
+ self.description: Optional[str] = data.get("description")
1056
+ self.banner: Optional[str] = data.get("banner")
1057
+ self.premium_tier: PremiumTier = PremiumTier(data["premium_tier"])
1058
+ self.premium_subscription_count: Optional[int] = data.get(
1059
+ "premium_subscription_count"
1060
+ )
1061
+ self.preferred_locale: str = data.get("preferred_locale", "en-US")
1062
+ self.public_updates_channel_id: Optional[str] = data.get(
1063
+ "public_updates_channel_id"
1064
+ )
1065
+ self.max_video_channel_users: Optional[int] = data.get(
1066
+ "max_video_channel_users"
1067
+ )
1068
+ self.approximate_member_count: Optional[int] = data.get(
1069
+ "approximate_member_count"
1070
+ )
1071
+ self.approximate_presence_count: Optional[int] = data.get(
1072
+ "approximate_presence_count"
1073
+ )
1074
+ self.welcome_screen: Optional["WelcomeScreen"] = (
1075
+ WelcomeScreen(data["welcome_screen"], client_instance)
1076
+ if data.get("welcome_screen")
1077
+ else None
1078
+ )
1079
+ self.nsfw_level: GuildNSFWLevel = GuildNSFWLevel(data["nsfw_level"])
1080
+ self.stickers: Optional[List[Sticker]] = (
1081
+ [Sticker(s_data, client_instance) for s_data in data.get("stickers", [])]
1082
+ if data.get("stickers")
1083
+ else None
1084
+ )
1085
+ self.premium_progress_bar_enabled: bool = data.get(
1086
+ "premium_progress_bar_enabled", False
1087
+ )
1088
+
1089
+ # Internal caches, populated by events or specific fetches
1090
+ self._channels: ChannelCache = ChannelCache()
1091
+ self._members: MemberCache = MemberCache(getattr(client_instance, "member_cache_flags", MemberCacheFlags()))
1092
+ self._threads: Dict[str, "Thread"] = {}
1093
+
1094
+ def get_channel(self, channel_id: str) -> Optional["Channel"]:
1095
+ return self._channels.get(channel_id)
1096
+
1097
+ def get_member(self, user_id: str) -> Optional[Member]:
1098
+ return self._members.get(user_id)
1099
+
1100
+ def get_member_named(self, name: str) -> Optional[Member]:
1101
+ """Retrieve a cached member by username or nickname.
1102
+
1103
+ The lookup is case-insensitive and searches both the username and
1104
+ guild nickname for a match.
1105
+
1106
+ Parameters
1107
+ ----------
1108
+ name: str
1109
+ The username or nickname to search for.
1110
+
1111
+ Returns
1112
+ -------
1113
+ Optional[Member]
1114
+ The matching member if found, otherwise ``None``.
1115
+ """
1116
+
1117
+ lowered = name.lower()
1118
+ for member in self._members.values():
1119
+ if member.username.lower() == lowered:
1120
+ return member
1121
+ if member.nick and member.nick.lower() == lowered:
1122
+ return member
1123
+ return None
1124
+
1125
+ def get_role(self, role_id: str) -> Optional[Role]:
1126
+ return next((role for role in self.roles if role.id == role_id), None)
1127
+
1128
+ def __repr__(self) -> str:
1129
+ return f"<Guild id='{self.id}' name='{self.name}'>"
1130
+
1131
+ async def fetch_members(self, *, limit: Optional[int] = None) -> List["Member"]:
1132
+ """|coro|
1133
+
1134
+ Fetches all members for this guild.
1135
+
1136
+ This requires the ``GUILD_MEMBERS`` intent.
1137
+
1138
+ Parameters
1139
+ ----------
1140
+ limit: Optional[int]
1141
+ The maximum number of members to fetch. If ``None``, all members
1142
+ are fetched.
1143
+
1144
+ Returns
1145
+ -------
1146
+ List[Member]
1147
+ A list of all members in the guild.
1148
+
1149
+ Raises
1150
+ ------
1151
+ DisagreementException
1152
+ The gateway is not available to make the request.
1153
+ asyncio.TimeoutError
1154
+ The request timed out.
1155
+ """
1156
+ if not self._client._gateway:
1157
+ raise DisagreementException("Gateway not available for member fetching.")
1158
+
1159
+ nonce = str(asyncio.get_running_loop().time())
1160
+ future = self._client._gateway._loop.create_future()
1161
+ self._client._gateway._member_chunk_requests[nonce] = future
1162
+
1163
+ try:
1164
+ await self._client._gateway.request_guild_members(
1165
+ self.id, limit=limit or 0, nonce=nonce
1166
+ )
1167
+ member_data = await asyncio.wait_for(future, timeout=60.0)
1168
+ return [Member(m, self._client) for m in member_data]
1169
+ except asyncio.TimeoutError:
1170
+ if nonce in self._client._gateway._member_chunk_requests:
1171
+ del self._client._gateway._member_chunk_requests[nonce]
1172
+ raise
1173
+
1174
+
1175
+ class Channel:
1176
+ """Base class for Discord channels."""
1177
+
1178
+ def __init__(self, data: Dict[str, Any], client_instance: "Client"):
1179
+ self._client: "Client" = client_instance
1180
+ self.id: str = data["id"]
1181
+ self._type_val: int = int(data["type"]) # Store raw type for enum property
1182
+
1183
+ self.guild_id: Optional[str] = data.get("guild_id")
1184
+ self.name: Optional[str] = data.get("name")
1185
+ self.position: Optional[int] = data.get("position")
1186
+ self.permission_overwrites: List["PermissionOverwrite"] = [
1187
+ PermissionOverwrite(d) for d in data.get("permission_overwrites", [])
1188
+ ]
1189
+ self.nsfw: Optional[bool] = data.get("nsfw", False)
1190
+ self.parent_id: Optional[str] = data.get(
1191
+ "parent_id"
1192
+ ) # ID of the parent category channel or thread parent
1193
+
1194
+ @property
1195
+ def type(self) -> ChannelType:
1196
+ return ChannelType(self._type_val)
1197
+
1198
+ @property
1199
+ def mention(self) -> str:
1200
+ return f"<#{self.id}>"
1201
+
1202
+ async def delete(self, reason: Optional[str] = None):
1203
+ await self._client._http.delete_channel(self.id, reason=reason)
1204
+
1205
+ def __repr__(self) -> str:
1206
+ return f"<Channel id='{self.id}' name='{self.name}' type='{self.type.name if hasattr(self.type, 'name') else self._type_val}'>"
1207
+
1208
+ def permission_overwrite_for(
1209
+ self, target: Union["Role", "Member", str]
1210
+ ) -> Optional["PermissionOverwrite"]:
1211
+ """Return the :class:`PermissionOverwrite` for ``target`` if present."""
1212
+
1213
+ if isinstance(target, str):
1214
+ target_id = target
1215
+ else:
1216
+ target_id = target.id
1217
+ for overwrite in self.permission_overwrites:
1218
+ if overwrite.id == target_id:
1219
+ return overwrite
1220
+ return None
1221
+
1222
+ @staticmethod
1223
+ def _apply_overwrite(
1224
+ perms: Permissions, overwrite: Optional["PermissionOverwrite"]
1225
+ ) -> Permissions:
1226
+ if overwrite is None:
1227
+ return perms
1228
+
1229
+ perms &= ~Permissions(int(overwrite.deny))
1230
+ perms |= Permissions(int(overwrite.allow))
1231
+ return perms
1232
+
1233
+ def permissions_for(self, member: "Member") -> Permissions:
1234
+ """Resolve channel permissions for ``member``."""
1235
+
1236
+ if self.guild_id is None:
1237
+ return Permissions(~0)
1238
+
1239
+ if not hasattr(self._client, "get_guild"):
1240
+ return Permissions(0)
1241
+
1242
+ guild = self._client.get_guild(self.guild_id)
1243
+ if guild is None:
1244
+ return Permissions(0)
1245
+
1246
+ base = Permissions(0)
1247
+
1248
+ everyone = guild.get_role(guild.id)
1249
+ if everyone is not None:
1250
+ base |= Permissions(int(everyone.permissions))
1251
+
1252
+ for rid in member.roles:
1253
+ role = guild.get_role(rid)
1254
+ if role is not None:
1255
+ base |= Permissions(int(role.permissions))
1256
+
1257
+ if base & Permissions.ADMINISTRATOR:
1258
+ return Permissions(~0)
1259
+
1260
+ # Apply @everyone overwrite
1261
+ base = self._apply_overwrite(base, self.permission_overwrite_for(guild.id))
1262
+
1263
+ # Role overwrites
1264
+ role_allow = Permissions(0)
1265
+ role_deny = Permissions(0)
1266
+ for rid in member.roles:
1267
+ ow = self.permission_overwrite_for(rid)
1268
+ if ow is not None:
1269
+ role_allow |= Permissions(int(ow.allow))
1270
+ role_deny |= Permissions(int(ow.deny))
1271
+
1272
+ base &= ~role_deny
1273
+ base |= role_allow
1274
+
1275
+ # Member overwrite
1276
+ base = self._apply_overwrite(base, self.permission_overwrite_for(member.id))
1277
+
1278
+ return base
1279
+
1280
+
1281
+ class TextChannel(Channel):
1282
+ """Represents a guild text channel or announcement channel."""
1283
+
1284
+ def __init__(self, data: Dict[str, Any], client_instance: "Client"):
1285
+ super().__init__(data, client_instance)
1286
+ self.topic: Optional[str] = data.get("topic")
1287
+ self.last_message_id: Optional[str] = data.get("last_message_id")
1288
+ self.rate_limit_per_user: Optional[int] = data.get("rate_limit_per_user", 0)
1289
+ self.default_auto_archive_duration: Optional[int] = data.get(
1290
+ "default_auto_archive_duration"
1291
+ )
1292
+ self.last_pin_timestamp: Optional[str] = data.get("last_pin_timestamp")
1293
+
1294
+ def history(
1295
+ self,
1296
+ *,
1297
+ limit: Optional[int] = None,
1298
+ before: Optional[str] = None,
1299
+ after: Optional[str] = None,
1300
+ ) -> AsyncIterator["Message"]:
1301
+ """Return an async iterator over this channel's messages."""
1302
+
1303
+ from .utils import message_pager
1304
+
1305
+ return message_pager(self, limit=limit, before=before, after=after)
1306
+
1307
+ async def send(
1308
+ self,
1309
+ content: Optional[str] = None,
1310
+ *,
1311
+ embed: Optional[Embed] = None,
1312
+ embeds: Optional[List[Embed]] = None,
1313
+ components: Optional[List["ActionRow"]] = None, # Added components
1314
+ ) -> "Message": # Forward reference Message
1315
+ if not hasattr(self._client, "send_message"):
1316
+ raise NotImplementedError(
1317
+ "Client.send_message is required for TextChannel.send"
1318
+ )
1319
+
1320
+ return await self._client.send_message(
1321
+ channel_id=self.id,
1322
+ content=content,
1323
+ embed=embed,
1324
+ embeds=embeds,
1325
+ components=components,
1326
+ )
1327
+
1328
+ async def purge(
1329
+ self, limit: int, *, before: "Snowflake | None" = None
1330
+ ) -> List["Snowflake"]:
1331
+ """Bulk delete messages from this channel."""
1332
+
1333
+ params: Dict[str, Union[int, str]] = {"limit": limit}
1334
+ if before is not None:
1335
+ params["before"] = before
1336
+
1337
+ messages = await self._client._http.request(
1338
+ "GET", f"/channels/{self.id}/messages", params=params
1339
+ )
1340
+ ids = [m["id"] for m in messages]
1341
+ if not ids:
1342
+ return []
1343
+
1344
+ await self._client._http.bulk_delete_messages(self.id, ids)
1345
+ for mid in ids:
1346
+ self._client._messages.invalidate(mid)
1347
+ return ids
1348
+
1349
+ def get_partial_message(self, id: int) -> "PartialMessage":
1350
+ """Returns a :class:`PartialMessage` for the given ID.
1351
+
1352
+ This allows performing actions on a message without fetching it first.
1353
+
1354
+ Parameters
1355
+ ----------
1356
+ id: int
1357
+ The ID of the message to get a partial instance of.
1358
+
1359
+ Returns
1360
+ -------
1361
+ PartialMessage
1362
+ The partial message instance.
1363
+ """
1364
+ return PartialMessage(id=str(id), channel=self)
1365
+
1366
+ def __repr__(self) -> str:
1367
+ return f"<TextChannel id='{self.id}' name='{self.name}' guild_id='{self.guild_id}'>"
1368
+
1369
+ async def pins(self) -> List["Message"]:
1370
+ """|coro|
1371
+
1372
+ Fetches all pinned messages in this channel.
1373
+
1374
+ Returns
1375
+ -------
1376
+ List[Message]
1377
+ The pinned messages.
1378
+
1379
+ Raises
1380
+ ------
1381
+ HTTPException
1382
+ Fetching the pinned messages failed.
1383
+ """
1384
+
1385
+ messages_data = await self._client._http.get_pinned_messages(self.id)
1386
+ return [self._client.parse_message(m) for m in messages_data]
1387
+
1388
+ async def create_thread(
1389
+ self,
1390
+ name: str,
1391
+ *,
1392
+ type: ChannelType = ChannelType.PUBLIC_THREAD,
1393
+ auto_archive_duration: Optional[int] = None,
1394
+ invitable: Optional[bool] = None,
1395
+ rate_limit_per_user: Optional[int] = None,
1396
+ reason: Optional[str] = None,
1397
+ ) -> "Thread":
1398
+ """|coro|
1399
+
1400
+ Creates a new thread in this channel.
1401
+
1402
+ Parameters
1403
+ ----------
1404
+ name: str
1405
+ The name of the thread.
1406
+ type: ChannelType
1407
+ The type of thread to create. Defaults to PUBLIC_THREAD.
1408
+ Can be PUBLIC_THREAD, PRIVATE_THREAD, or ANNOUNCEMENT_THREAD.
1409
+ auto_archive_duration: Optional[int]
1410
+ The duration in minutes to automatically archive the thread after recent activity.
1411
+ invitable: Optional[bool]
1412
+ Whether non-moderators can invite other non-moderators to a private thread.
1413
+ Only applicable to private threads.
1414
+ rate_limit_per_user: Optional[int]
1415
+ The number of seconds a user has to wait before sending another message.
1416
+ reason: Optional[str]
1417
+ The reason for creating the thread.
1418
+
1419
+ Returns
1420
+ -------
1421
+ Thread
1422
+ The created thread.
1423
+ """
1424
+ payload: Dict[str, Any] = {
1425
+ "name": name,
1426
+ "type": type.value,
1427
+ }
1428
+ if auto_archive_duration is not None:
1429
+ payload["auto_archive_duration"] = auto_archive_duration
1430
+ if invitable is not None and type == ChannelType.PRIVATE_THREAD:
1431
+ payload["invitable"] = invitable
1432
+ if rate_limit_per_user is not None:
1433
+ payload["rate_limit_per_user"] = rate_limit_per_user
1434
+
1435
+ data = await self._client._http.start_thread_without_message(self.id, payload)
1436
+ return cast("Thread", self._client.parse_channel(data))
1437
+
1438
+
1439
+ class VoiceChannel(Channel):
1440
+ """Represents a guild voice channel or stage voice channel."""
1441
+
1442
+ def __init__(self, data: Dict[str, Any], client_instance: "Client"):
1443
+ super().__init__(data, client_instance)
1444
+ self.bitrate: int = data.get("bitrate", 64000)
1445
+ self.user_limit: int = data.get("user_limit", 0)
1446
+ self.rtc_region: Optional[str] = data.get("rtc_region")
1447
+ self.video_quality_mode: Optional[int] = data.get("video_quality_mode")
1448
+
1449
+ def __repr__(self) -> str:
1450
+ return f"<VoiceChannel id='{self.id}' name='{self.name}' guild_id='{self.guild_id}'>"
1451
+
1452
+
1453
+ class StageChannel(VoiceChannel):
1454
+ """Represents a guild stage channel."""
1455
+
1456
+ def __repr__(self) -> str:
1457
+ return f"<StageChannel id='{self.id}' name='{self.name}' guild_id='{self.guild_id}'>"
1458
+
1459
+ async def start_stage_instance(
1460
+ self,
1461
+ topic: str,
1462
+ *,
1463
+ privacy_level: int = 2,
1464
+ reason: Optional[str] = None,
1465
+ guild_scheduled_event_id: Optional[str] = None,
1466
+ ) -> "StageInstance":
1467
+ if not hasattr(self._client, "_http"):
1468
+ raise DisagreementException("Client missing HTTP for stage instance")
1469
+
1470
+ payload: Dict[str, Any] = {
1471
+ "channel_id": self.id,
1472
+ "topic": topic,
1473
+ "privacy_level": privacy_level,
1474
+ }
1475
+ if guild_scheduled_event_id is not None:
1476
+ payload["guild_scheduled_event_id"] = guild_scheduled_event_id
1477
+
1478
+ instance = await self._client._http.start_stage_instance(payload, reason=reason)
1479
+ instance._client = self._client
1480
+ return instance
1481
+
1482
+ async def edit_stage_instance(
1483
+ self,
1484
+ *,
1485
+ topic: Optional[str] = None,
1486
+ privacy_level: Optional[int] = None,
1487
+ reason: Optional[str] = None,
1488
+ ) -> "StageInstance":
1489
+ if not hasattr(self._client, "_http"):
1490
+ raise DisagreementException("Client missing HTTP for stage instance")
1491
+
1492
+ payload: Dict[str, Any] = {}
1493
+ if topic is not None:
1494
+ payload["topic"] = topic
1495
+ if privacy_level is not None:
1496
+ payload["privacy_level"] = privacy_level
1497
+
1498
+ instance = await self._client._http.edit_stage_instance(
1499
+ self.id, payload, reason=reason
1500
+ )
1501
+ instance._client = self._client
1502
+ return instance
1503
+
1504
+ async def end_stage_instance(self, *, reason: Optional[str] = None) -> None:
1505
+ if not hasattr(self._client, "_http"):
1506
+ raise DisagreementException("Client missing HTTP for stage instance")
1507
+
1508
+ await self._client._http.end_stage_instance(self.id, reason=reason)
1509
+
1510
+
1511
+ class StageInstance:
1512
+ """Represents a stage instance."""
1513
+
1514
+ def __init__(
1515
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
1516
+ ) -> None:
1517
+ self._client = client_instance
1518
+ self.id: str = data["id"]
1519
+ self.guild_id: Optional[str] = data.get("guild_id")
1520
+ self.channel_id: str = data["channel_id"]
1521
+ self.topic: str = data["topic"]
1522
+ self.privacy_level: int = data.get("privacy_level", 2)
1523
+ self.discoverable_disabled: bool = data.get("discoverable_disabled", False)
1524
+ self.guild_scheduled_event_id: Optional[str] = data.get(
1525
+ "guild_scheduled_event_id"
1526
+ )
1527
+
1528
+ def __repr__(self) -> str:
1529
+ return f"<StageInstance id='{self.id}' channel_id='{self.channel_id}'>"
1530
+
1531
+
1532
+ class CategoryChannel(Channel):
1533
+ """Represents a guild category channel."""
1534
+
1535
+ def __init__(self, data: Dict[str, Any], client_instance: "Client"):
1536
+ super().__init__(data, client_instance)
1537
+
1538
+ @property
1539
+ def channels(self) -> List[Channel]:
1540
+ if not self.guild_id or not hasattr(self._client, "get_guild"):
1541
+ return []
1542
+ guild = self._client.get_guild(self.guild_id)
1543
+ if not guild or not hasattr(
1544
+ guild, "_channels"
1545
+ ): # Ensure guild and _channels exist
1546
+ return []
1547
+
1548
+ categorized_channels = [
1549
+ ch
1550
+ for ch in guild._channels.values()
1551
+ if getattr(ch, "parent_id", None) == self.id
1552
+ ]
1553
+ return sorted(
1554
+ categorized_channels,
1555
+ key=lambda c: c.position if c.position is not None else -1,
1556
+ )
1557
+
1558
+ def __repr__(self) -> str:
1559
+ return f"<CategoryChannel id='{self.id}' name='{self.name}' guild_id='{self.guild_id}'>"
1560
+
1561
+
1562
+ class ThreadMetadata:
1563
+ """Represents the metadata of a thread."""
1564
+
1565
+ def __init__(self, data: Dict[str, Any]):
1566
+ self.archived: bool = data["archived"]
1567
+ self.auto_archive_duration: int = data["auto_archive_duration"]
1568
+ self.archive_timestamp: str = data["archive_timestamp"]
1569
+ self.locked: bool = data["locked"]
1570
+ self.invitable: Optional[bool] = data.get("invitable")
1571
+ self.create_timestamp: Optional[str] = data.get("create_timestamp")
1572
+
1573
+
1574
+ class Thread(TextChannel): # Threads are a specialized TextChannel
1575
+ """Represents a Discord Thread."""
1576
+
1577
+ def __init__(self, data: Dict[str, Any], client_instance: "Client"):
1578
+ super().__init__(data, client_instance) # Handles common text channel fields
1579
+ self.owner_id: Optional[str] = data.get("owner_id")
1580
+ # parent_id is already handled by base Channel init if present in data
1581
+ self.message_count: Optional[int] = data.get("message_count")
1582
+ self.member_count: Optional[int] = data.get("member_count")
1583
+ self.thread_metadata: ThreadMetadata = ThreadMetadata(data["thread_metadata"])
1584
+ self.member: Optional["ThreadMember"] = (
1585
+ ThreadMember(data["member"], client_instance)
1586
+ if data.get("member")
1587
+ else None
1588
+ )
1589
+
1590
+ def __repr__(self) -> str:
1591
+ return (
1592
+ f"<Thread id='{self.id}' name='{self.name}' parent_id='{self.parent_id}'>"
1593
+ )
1594
+
1595
+ async def join(self) -> None:
1596
+ """|coro|
1597
+
1598
+ Joins this thread.
1599
+ """
1600
+ await self._client._http.join_thread(self.id)
1601
+
1602
+ async def leave(self) -> None:
1603
+ """|coro|
1604
+
1605
+ Leaves this thread.
1606
+ """
1607
+ await self._client._http.leave_thread(self.id)
1608
+
1609
+ async def archive(self, locked: bool = False, *, reason: Optional[str] = None) -> "Thread":
1610
+ """|coro|
1611
+
1612
+ Archives this thread.
1613
+
1614
+ Parameters
1615
+ ----------
1616
+ locked: bool
1617
+ Whether to lock the thread.
1618
+ reason: Optional[str]
1619
+ The reason for archiving the thread.
1620
+
1621
+ Returns
1622
+ -------
1623
+ Thread
1624
+ The updated thread.
1625
+ """
1626
+ payload = {
1627
+ "archived": True,
1628
+ "locked": locked,
1629
+ }
1630
+ data = await self._client._http.edit_channel(self.id, payload, reason=reason)
1631
+ return cast("Thread", self._client.parse_channel(data))
1632
+
1633
+
1634
+ class DMChannel(Channel):
1635
+ """Represents a Direct Message channel."""
1636
+
1637
+ def __init__(self, data: Dict[str, Any], client_instance: "Client"):
1638
+ super().__init__(data, client_instance)
1639
+ self.last_message_id: Optional[str] = data.get("last_message_id")
1640
+ self.recipients: List[User] = [
1641
+ User(u_data) for u_data in data.get("recipients", [])
1642
+ ]
1643
+
1644
+ @property
1645
+ def recipient(self) -> Optional[User]:
1646
+ return self.recipients[0] if self.recipients else None
1647
+
1648
+ async def send(
1649
+ self,
1650
+ content: Optional[str] = None,
1651
+ *,
1652
+ embed: Optional[Embed] = None,
1653
+ embeds: Optional[List[Embed]] = None,
1654
+ components: Optional[List["ActionRow"]] = None, # Added components
1655
+ ) -> "Message":
1656
+ if not hasattr(self._client, "send_message"):
1657
+ raise NotImplementedError(
1658
+ "Client.send_message is required for DMChannel.send"
1659
+ )
1660
+
1661
+ return await self._client.send_message(
1662
+ channel_id=self.id,
1663
+ content=content,
1664
+ embed=embed,
1665
+ embeds=embeds,
1666
+ components=components,
1667
+ )
1668
+
1669
+ async def history(
1670
+ self,
1671
+ *,
1672
+ limit: Optional[int] = 100,
1673
+ before: "Snowflake | None" = None,
1674
+ ):
1675
+ """An async iterator over messages in this DM."""
1676
+
1677
+ params: Dict[str, Union[int, str]] = {}
1678
+ if before is not None:
1679
+ params["before"] = before
1680
+
1681
+ fetched = 0
1682
+ while True:
1683
+ to_fetch = 100 if limit is None else min(100, limit - fetched)
1684
+ if to_fetch <= 0:
1685
+ break
1686
+ params["limit"] = to_fetch
1687
+ messages = await self._client._http.request(
1688
+ "GET", f"/channels/{self.id}/messages", params=params.copy()
1689
+ )
1690
+ if not messages:
1691
+ break
1692
+ params["before"] = messages[-1]["id"]
1693
+ for msg in messages:
1694
+ yield Message(msg, self._client)
1695
+ fetched += 1
1696
+ if limit is not None and fetched >= limit:
1697
+ return
1698
+
1699
+ def __repr__(self) -> str:
1700
+ recipient_repr = self.recipient.username if self.recipient else "Unknown"
1701
+ return f"<DMChannel id='{self.id}' recipient='{recipient_repr}'>"
1702
+
1703
+
1704
+ class PartialChannel:
1705
+ """Represents a partial channel object, often from interactions."""
1706
+
1707
+ def __init__(
1708
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
1709
+ ):
1710
+ self._client: Optional["Client"] = client_instance
1711
+ self.id: str = data["id"]
1712
+ self.name: Optional[str] = data.get("name")
1713
+ self._type_val: int = int(data["type"])
1714
+ self.permissions: Optional[str] = data.get("permissions")
1715
+
1716
+ @property
1717
+ def type(self) -> ChannelType:
1718
+ return ChannelType(self._type_val)
1719
+
1720
+ @property
1721
+ def mention(self) -> str:
1722
+ return f"<#{self.id}>"
1723
+
1724
+ async def fetch_full_channel(self) -> Optional[Channel]:
1725
+ if not self._client or not hasattr(self._client, "fetch_channel"):
1726
+ # Log or raise if fetching is not possible
1727
+ return None
1728
+ try:
1729
+ # This assumes Client.fetch_channel exists and returns a full Channel object
1730
+ return await self._client.fetch_channel(self.id)
1731
+ except HTTPException as exc:
1732
+ print(f"HTTP error while fetching channel {self.id}: {exc}")
1733
+ except (json.JSONDecodeError, KeyError, ValueError) as exc:
1734
+ print(f"Failed to parse channel {self.id}: {exc}")
1735
+ except DisagreementException as exc:
1736
+ print(f"Error fetching channel {self.id}: {exc}")
1737
+ return None
1738
+
1739
+ def __repr__(self) -> str:
1740
+ type_name = self.type.name if hasattr(self.type, "name") else self._type_val
1741
+ return f"<PartialChannel id='{self.id}' name='{self.name}' type='{type_name}'>"
1742
+
1743
+
1744
+ class Webhook:
1745
+ """Represents a Discord Webhook."""
1746
+
1747
+ def __init__(
1748
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
1749
+ ):
1750
+ self._client: Optional["Client"] = client_instance
1751
+ self.id: str = data["id"]
1752
+ self.type: int = int(data.get("type", 1))
1753
+ self.guild_id: Optional[str] = data.get("guild_id")
1754
+ self.channel_id: Optional[str] = data.get("channel_id")
1755
+ self.name: Optional[str] = data.get("name")
1756
+ self.avatar: Optional[str] = data.get("avatar")
1757
+ self.token: Optional[str] = data.get("token")
1758
+ self.application_id: Optional[str] = data.get("application_id")
1759
+ self.url: Optional[str] = data.get("url")
1760
+ self.user: Optional[User] = User(data["user"]) if data.get("user") else None
1761
+
1762
+ def __repr__(self) -> str:
1763
+ return f"<Webhook id='{self.id}' name='{self.name}'>"
1764
+
1765
+ @classmethod
1766
+ def from_url(
1767
+ cls, url: str, session: Optional[aiohttp.ClientSession] = None
1768
+ ) -> "Webhook":
1769
+ """Create a minimal :class:`Webhook` from a webhook URL.
1770
+
1771
+ Parameters
1772
+ ----------
1773
+ url:
1774
+ The full Discord webhook URL.
1775
+ session:
1776
+ Unused for now. Present for API compatibility.
1777
+
1778
+ Returns
1779
+ -------
1780
+ Webhook
1781
+ A webhook instance containing only the ``id``, ``token`` and ``url``.
1782
+ """
1783
+
1784
+ parts = url.rstrip("/").split("/")
1785
+ if len(parts) < 2:
1786
+ raise ValueError("Invalid webhook URL")
1787
+ token = parts[-1]
1788
+ webhook_id = parts[-2]
1789
+
1790
+ return cls({"id": webhook_id, "token": token, "url": url})
1791
+
1792
+ async def send(
1793
+ self,
1794
+ content: Optional[str] = None,
1795
+ *,
1796
+ username: Optional[str] = None,
1797
+ avatar_url: Optional[str] = None,
1798
+ tts: bool = False,
1799
+ embed: Optional["Embed"] = None,
1800
+ embeds: Optional[List["Embed"]] = None,
1801
+ components: Optional[List["ActionRow"]] = None,
1802
+ allowed_mentions: Optional[Dict[str, Any]] = None,
1803
+ attachments: Optional[List[Any]] = None,
1804
+ files: Optional[List[Any]] = None,
1805
+ flags: Optional[int] = None,
1806
+ ) -> "Message":
1807
+ """Send a message using this webhook."""
1808
+
1809
+ if not self._client:
1810
+ raise DisagreementException("Webhook is not bound to a Client")
1811
+ assert self.token is not None, "Webhook token missing"
1812
+
1813
+ if embed and embeds:
1814
+ raise ValueError("Cannot provide both embed and embeds.")
1815
+
1816
+ final_embeds_payload: Optional[List[Dict[str, Any]]] = None
1817
+ if embed:
1818
+ final_embeds_payload = [embed.to_dict()]
1819
+ elif embeds:
1820
+ final_embeds_payload = [e.to_dict() for e in embeds]
1821
+
1822
+ components_payload: Optional[List[Dict[str, Any]]] = None
1823
+ if components:
1824
+ components_payload = [c.to_dict() for c in components]
1825
+
1826
+ message_data = await self._client._http.execute_webhook(
1827
+ self.id,
1828
+ self.token,
1829
+ content=content,
1830
+ tts=tts,
1831
+ embeds=final_embeds_payload,
1832
+ components=components_payload,
1833
+ allowed_mentions=allowed_mentions,
1834
+ attachments=attachments,
1835
+ files=files,
1836
+ flags=flags,
1837
+ username=username,
1838
+ avatar_url=avatar_url,
1839
+ )
1840
+
1841
+ return self._client.parse_message(message_data)
1842
+
1843
+
1844
+ class GuildTemplate:
1845
+ """Represents a guild template."""
1846
+
1847
+ def __init__(
1848
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
1849
+ ):
1850
+ self._client = client_instance
1851
+ self.code: str = data["code"]
1852
+ self.name: str = data["name"]
1853
+ self.description: Optional[str] = data.get("description")
1854
+ self.usage_count: int = data.get("usage_count", 0)
1855
+ self.creator_id: str = data.get("creator_id", "")
1856
+ self.creator: Optional[User] = (
1857
+ User(data["creator"]) if data.get("creator") else None
1858
+ )
1859
+ self.created_at: Optional[str] = data.get("created_at")
1860
+ self.updated_at: Optional[str] = data.get("updated_at")
1861
+ self.source_guild_id: Optional[str] = data.get("source_guild_id")
1862
+ self.serialized_source_guild: Dict[str, Any] = data.get(
1863
+ "serialized_source_guild", {}
1864
+ )
1865
+ self.is_dirty: Optional[bool] = data.get("is_dirty")
1866
+
1867
+ def __repr__(self) -> str:
1868
+ return f"<GuildTemplate code='{self.code}' name='{self.name}'>"
1869
+
1870
+
1871
+ # --- Message Components ---
1872
+
1873
+
1874
+ class Component:
1875
+ """Base class for message components."""
1876
+
1877
+ def __init__(self, type: ComponentType):
1878
+ self.type: ComponentType = type
1879
+ self.custom_id: Optional[str] = None
1880
+
1881
+ def to_dict(self) -> Dict[str, Any]:
1882
+ payload: Dict[str, Any] = {"type": self.type.value}
1883
+ if self.custom_id:
1884
+ payload["custom_id"] = self.custom_id
1885
+ return payload
1886
+
1887
+
1888
+ class ActionRow(Component):
1889
+ """Represents an Action Row, a container for other components."""
1890
+
1891
+ def __init__(self, components: Optional[List[Component]] = None):
1892
+ super().__init__(ComponentType.ACTION_ROW)
1893
+ self.components: List[Component] = components or []
1894
+
1895
+ def add_component(self, component: Component):
1896
+ if isinstance(component, ActionRow):
1897
+ raise ValueError("Cannot nest ActionRows inside another ActionRow.")
1898
+
1899
+ select_types = {
1900
+ ComponentType.STRING_SELECT,
1901
+ ComponentType.USER_SELECT,
1902
+ ComponentType.ROLE_SELECT,
1903
+ ComponentType.MENTIONABLE_SELECT,
1904
+ ComponentType.CHANNEL_SELECT,
1905
+ }
1906
+
1907
+ if component.type in select_types:
1908
+ if self.components:
1909
+ raise ValueError(
1910
+ "Select menu components must be the only component in an ActionRow."
1911
+ )
1912
+ self.components.append(component)
1913
+ return self
1914
+
1915
+ if any(c.type in select_types for c in self.components):
1916
+ raise ValueError(
1917
+ "Cannot add components to an ActionRow that already contains a select menu."
1918
+ )
1919
+
1920
+ if len(self.components) >= 5:
1921
+ raise ValueError("ActionRow cannot have more than 5 components.")
1922
+
1923
+ self.components.append(component)
1924
+ return self
1925
+
1926
+ def to_dict(self) -> Dict[str, Any]:
1927
+ payload = super().to_dict()
1928
+ payload["components"] = [c.to_dict() for c in self.components]
1929
+ return payload
1930
+
1931
+ @classmethod
1932
+ def from_dict(
1933
+ cls, data: Dict[str, Any], client: Optional["Client"] = None
1934
+ ) -> "ActionRow":
1935
+ """Deserialize an action row payload."""
1936
+ from .components import component_factory
1937
+
1938
+ row = cls()
1939
+ for comp_data in data.get("components", []):
1940
+ try:
1941
+ row.add_component(component_factory(comp_data, client))
1942
+ except Exception:
1943
+ # Skip components that fail to parse for now
1944
+ continue
1945
+ return row
1946
+
1947
+
1948
+ class Button(Component):
1949
+ """Represents a button component."""
1950
+
1951
+ def __init__(
1952
+ self,
1953
+ *, # Make parameters keyword-only for clarity
1954
+ style: ButtonStyle,
1955
+ label: Optional[str] = None,
1956
+ emoji: Optional["PartialEmoji"] = None, # Changed to PartialEmoji type
1957
+ custom_id: Optional[str] = None,
1958
+ url: Optional[str] = None,
1959
+ disabled: bool = False,
1960
+ ):
1961
+ super().__init__(ComponentType.BUTTON)
1962
+
1963
+ if style == ButtonStyle.LINK and url is None:
1964
+ raise ValueError("Link buttons must have a URL.")
1965
+ if style != ButtonStyle.LINK and custom_id is None:
1966
+ raise ValueError("Non-link buttons must have a custom_id.")
1967
+ if label is None and emoji is None:
1968
+ raise ValueError("Button must have a label or an emoji.")
1969
+
1970
+ self.style: ButtonStyle = style
1971
+ self.label: Optional[str] = label
1972
+ self.emoji: Optional[PartialEmoji] = emoji
1973
+ self.custom_id = custom_id
1974
+ self.url: Optional[str] = url
1975
+ self.disabled: bool = disabled
1976
+
1977
+ def to_dict(self) -> Dict[str, Any]:
1978
+ payload = super().to_dict()
1979
+ payload["style"] = self.style.value
1980
+ if self.label:
1981
+ payload["label"] = self.label
1982
+ if self.emoji:
1983
+ payload["emoji"] = self.emoji.to_dict() # Call to_dict()
1984
+ if self.custom_id:
1985
+ payload["custom_id"] = self.custom_id
1986
+ if self.url:
1987
+ payload["url"] = self.url
1988
+ if self.disabled:
1989
+ payload["disabled"] = self.disabled
1990
+ return payload
1991
+
1992
+
1993
+ class SelectOption:
1994
+ """Represents an option in a select menu."""
1995
+
1996
+ def __init__(
1997
+ self,
1998
+ *, # Make parameters keyword-only
1999
+ label: str,
2000
+ value: str,
2001
+ description: Optional[str] = None,
2002
+ emoji: Optional["PartialEmoji"] = None, # Changed to PartialEmoji type
2003
+ default: bool = False,
2004
+ ):
2005
+ self.label: str = label
2006
+ self.value: str = value
2007
+ self.description: Optional[str] = description
2008
+ self.emoji: Optional["PartialEmoji"] = emoji
2009
+ self.default: bool = default
2010
+
2011
+ def to_dict(self) -> Dict[str, Any]:
2012
+ payload: Dict[str, Any] = {
2013
+ "label": self.label,
2014
+ "value": self.value,
2015
+ }
2016
+ if self.description:
2017
+ payload["description"] = self.description
2018
+ if self.emoji:
2019
+ payload["emoji"] = self.emoji.to_dict() # Call to_dict()
2020
+ if self.default:
2021
+ payload["default"] = self.default
2022
+ return payload
2023
+
2024
+
2025
+ class SelectMenu(Component):
2026
+ """Represents a select menu component.
2027
+
2028
+ Currently supports STRING_SELECT (type 3).
2029
+ User (5), Role (6), Mentionable (7), Channel (8) selects are not yet fully modeled.
2030
+ """
2031
+
2032
+ def __init__(
2033
+ self,
2034
+ *, # Make parameters keyword-only
2035
+ custom_id: str,
2036
+ options: List[SelectOption],
2037
+ placeholder: Optional[str] = None,
2038
+ min_values: int = 1,
2039
+ max_values: int = 1,
2040
+ disabled: bool = False,
2041
+ channel_types: Optional[List[ChannelType]] = None,
2042
+ # For other select types, specific fields would be needed.
2043
+ # This constructor primarily targets STRING_SELECT (type 3).
2044
+ type: ComponentType = ComponentType.STRING_SELECT, # Default to string select
2045
+ ):
2046
+ super().__init__(type) # Pass the specific select menu type
2047
+
2048
+ if not (1 <= len(options) <= 25):
2049
+ raise ValueError("Select menu must have between 1 and 25 options.")
2050
+ if not (
2051
+ 0 <= min_values <= 25
2052
+ ): # Discord docs say min_values can be 0 for some types
2053
+ raise ValueError("min_values must be between 0 and 25.")
2054
+ if not (1 <= max_values <= 25):
2055
+ raise ValueError("max_values must be between 1 and 25.")
2056
+ if min_values > max_values:
2057
+ raise ValueError("min_values cannot be greater than max_values.")
2058
+
2059
+ self.custom_id = custom_id
2060
+ self.options: List[SelectOption] = options
2061
+ self.placeholder: Optional[str] = placeholder
2062
+ self.min_values: int = min_values
2063
+ self.max_values: int = max_values
2064
+ self.disabled: bool = disabled
2065
+ self.channel_types: Optional[List[ChannelType]] = channel_types
2066
+
2067
+ def to_dict(self) -> Dict[str, Any]:
2068
+ payload = super().to_dict() # Gets {"type": self.type.value}
2069
+ payload["custom_id"] = self.custom_id
2070
+ payload["options"] = [opt.to_dict() for opt in self.options]
2071
+ if self.placeholder:
2072
+ payload["placeholder"] = self.placeholder
2073
+ payload["min_values"] = self.min_values
2074
+ payload["max_values"] = self.max_values
2075
+ if self.disabled:
2076
+ payload["disabled"] = self.disabled
2077
+ if self.type == ComponentType.CHANNEL_SELECT and self.channel_types:
2078
+ payload["channel_types"] = [ct.value for ct in self.channel_types]
2079
+ return payload
2080
+
2081
+
2082
+ class UnfurledMediaItem:
2083
+ """Represents an unfurled media item."""
2084
+
2085
+ def __init__(
2086
+ self,
2087
+ url: str,
2088
+ proxy_url: Optional[str] = None,
2089
+ height: Optional[int] = None,
2090
+ width: Optional[int] = None,
2091
+ content_type: Optional[str] = None,
2092
+ ):
2093
+ self.url = url
2094
+ self.proxy_url = proxy_url
2095
+ self.height = height
2096
+ self.width = width
2097
+ self.content_type = content_type
2098
+
2099
+ def to_dict(self) -> Dict[str, Any]:
2100
+ return {
2101
+ "url": self.url,
2102
+ "proxy_url": self.proxy_url,
2103
+ "height": self.height,
2104
+ "width": self.width,
2105
+ "content_type": self.content_type,
2106
+ }
2107
+
2108
+
2109
+ class MediaGalleryItem:
2110
+ """Represents an item in a media gallery."""
2111
+
2112
+ def __init__(
2113
+ self,
2114
+ media: UnfurledMediaItem,
2115
+ description: Optional[str] = None,
2116
+ spoiler: bool = False,
2117
+ ):
2118
+ self.media = media
2119
+ self.description = description
2120
+ self.spoiler = spoiler
2121
+
2122
+ def to_dict(self) -> Dict[str, Any]:
2123
+ return {
2124
+ "media": self.media.to_dict(),
2125
+ "description": self.description,
2126
+ "spoiler": self.spoiler,
2127
+ }
2128
+
2129
+
2130
+ class TextDisplay(Component):
2131
+ """Represents a text display component."""
2132
+
2133
+ def __init__(self, content: str, id: Optional[int] = None):
2134
+ super().__init__(ComponentType.TEXT_DISPLAY)
2135
+ self.content = content
2136
+ self.id = id
2137
+
2138
+ def to_dict(self) -> Dict[str, Any]:
2139
+ payload = super().to_dict()
2140
+ payload["content"] = self.content
2141
+ if self.id is not None:
2142
+ payload["id"] = self.id
2143
+ return payload
2144
+
2145
+
2146
+ class Thumbnail(Component):
2147
+ """Represents a thumbnail component."""
2148
+
2149
+ def __init__(
2150
+ self,
2151
+ media: UnfurledMediaItem,
2152
+ description: Optional[str] = None,
2153
+ spoiler: bool = False,
2154
+ id: Optional[int] = None,
2155
+ ):
2156
+ super().__init__(ComponentType.THUMBNAIL)
2157
+ self.media = media
2158
+ self.description = description
2159
+ self.spoiler = spoiler
2160
+ self.id = id
2161
+
2162
+ def to_dict(self) -> Dict[str, Any]:
2163
+ payload = super().to_dict()
2164
+ payload["media"] = self.media.to_dict()
2165
+ if self.description:
2166
+ payload["description"] = self.description
2167
+ if self.spoiler:
2168
+ payload["spoiler"] = self.spoiler
2169
+ if self.id is not None:
2170
+ payload["id"] = self.id
2171
+ return payload
2172
+
2173
+
2174
+ class Section(Component):
2175
+ """Represents a section component."""
2176
+
2177
+ def __init__(
2178
+ self,
2179
+ components: List[TextDisplay],
2180
+ accessory: Optional[Union[Thumbnail, Button]] = None,
2181
+ id: Optional[int] = None,
2182
+ ):
2183
+ super().__init__(ComponentType.SECTION)
2184
+ self.components = components
2185
+ self.accessory = accessory
2186
+ self.id = id
2187
+
2188
+ def to_dict(self) -> Dict[str, Any]:
2189
+ payload = super().to_dict()
2190
+ payload["components"] = [c.to_dict() for c in self.components]
2191
+ if self.accessory:
2192
+ payload["accessory"] = self.accessory.to_dict()
2193
+ if self.id is not None:
2194
+ payload["id"] = self.id
2195
+ return payload
2196
+
2197
+
2198
+ class MediaGallery(Component):
2199
+ """Represents a media gallery component."""
2200
+
2201
+ def __init__(self, items: List[MediaGalleryItem], id: Optional[int] = None):
2202
+ super().__init__(ComponentType.MEDIA_GALLERY)
2203
+ self.items = items
2204
+ self.id = id
2205
+
2206
+ def to_dict(self) -> Dict[str, Any]:
2207
+ payload = super().to_dict()
2208
+ payload["items"] = [i.to_dict() for i in self.items]
2209
+ if self.id is not None:
2210
+ payload["id"] = self.id
2211
+ return payload
2212
+
2213
+
2214
+ class FileComponent(Component):
2215
+ """Represents a file component."""
2216
+
2217
+ def __init__(
2218
+ self, file: UnfurledMediaItem, spoiler: bool = False, id: Optional[int] = None
2219
+ ):
2220
+ super().__init__(ComponentType.FILE)
2221
+ self.file = file
2222
+ self.spoiler = spoiler
2223
+ self.id = id
2224
+
2225
+ def to_dict(self) -> Dict[str, Any]:
2226
+ payload = super().to_dict()
2227
+ payload["file"] = self.file.to_dict()
2228
+ if self.spoiler:
2229
+ payload["spoiler"] = self.spoiler
2230
+ if self.id is not None:
2231
+ payload["id"] = self.id
2232
+ return payload
2233
+
2234
+
2235
+ class Separator(Component):
2236
+ """Represents a separator component."""
2237
+
2238
+ def __init__(
2239
+ self, divider: bool = True, spacing: int = 1, id: Optional[int] = None
2240
+ ):
2241
+ super().__init__(ComponentType.SEPARATOR)
2242
+ self.divider = divider
2243
+ self.spacing = spacing
2244
+ self.id = id
2245
+
2246
+ def to_dict(self) -> Dict[str, Any]:
2247
+ payload = super().to_dict()
2248
+ payload["divider"] = self.divider
2249
+ payload["spacing"] = self.spacing
2250
+ if self.id is not None:
2251
+ payload["id"] = self.id
2252
+ return payload
2253
+
2254
+
2255
+ class Container(Component):
2256
+ """Represents a container component."""
2257
+
2258
+ def __init__(
2259
+ self,
2260
+ components: List[Component],
2261
+ accent_color: Color | int | str | None = None,
2262
+ spoiler: bool = False,
2263
+ id: Optional[int] = None,
2264
+ ):
2265
+ super().__init__(ComponentType.CONTAINER)
2266
+ self.components = components
2267
+ self.accent_color = Color.parse(accent_color)
2268
+ self.spoiler = spoiler
2269
+ self.id = id
2270
+
2271
+ def to_dict(self) -> Dict[str, Any]:
2272
+ payload = super().to_dict()
2273
+ payload["components"] = [c.to_dict() for c in self.components]
2274
+ if self.accent_color:
2275
+ payload["accent_color"] = self.accent_color.value
2276
+ if self.spoiler:
2277
+ payload["spoiler"] = self.spoiler
2278
+ if self.id is not None:
2279
+ payload["id"] = self.id
2280
+ return payload
2281
+
2282
+
2283
+ class WelcomeChannel:
2284
+ """Represents a channel shown in the server's welcome screen.
2285
+
2286
+ Attributes:
2287
+ channel_id (str): The ID of the channel.
2288
+ description (str): The description shown for the channel.
2289
+ emoji_id (Optional[str]): The ID of the emoji, if custom.
2290
+ emoji_name (Optional[str]): The name of the emoji if custom, or the unicode character if standard.
2291
+ """
2292
+
2293
+ def __init__(self, data: Dict[str, Any]):
2294
+ self.channel_id: str = data["channel_id"]
2295
+ self.description: str = data["description"]
2296
+ self.emoji_id: Optional[str] = data.get("emoji_id")
2297
+ self.emoji_name: Optional[str] = data.get("emoji_name")
2298
+
2299
+ def __repr__(self) -> str:
2300
+ return (
2301
+ f"<WelcomeChannel id='{self.channel_id}' description='{self.description}'>"
2302
+ )
2303
+
2304
+
2305
+ class WelcomeScreen:
2306
+ """Represents the welcome screen of a Community guild.
2307
+
2308
+ Attributes:
2309
+ description (Optional[str]): The server description shown in the welcome screen.
2310
+ welcome_channels (List[WelcomeChannel]): The channels shown in the welcome screen.
2311
+ """
2312
+
2313
+ def __init__(self, data: Dict[str, Any], client_instance: "Client"):
2314
+ self._client: "Client" = (
2315
+ client_instance # May be useful for fetching channel objects
2316
+ )
2317
+ self.description: Optional[str] = data.get("description")
2318
+ self.welcome_channels: List[WelcomeChannel] = [
2319
+ WelcomeChannel(wc_data) for wc_data in data.get("welcome_channels", [])
2320
+ ]
2321
+
2322
+ def __repr__(self) -> str:
2323
+ return f"<WelcomeScreen description='{self.description}' channels={len(self.welcome_channels)}>"
2324
+
2325
+
2326
+ class ThreadMember:
2327
+ """Represents a member of a thread.
2328
+
2329
+ Attributes:
2330
+ id (Optional[str]): The ID of the thread. Not always present.
2331
+ user_id (Optional[str]): The ID of the user. Not always present.
2332
+ join_timestamp (str): When the user joined the thread (ISO8601 timestamp).
2333
+ flags (int): User-specific flags for thread settings.
2334
+ member (Optional[Member]): The guild member object for this user, if resolved.
2335
+ Only available from GUILD_MEMBERS intent and if fetched.
2336
+ """
2337
+
2338
+ def __init__(
2339
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2340
+ ): # client_instance for member resolution
2341
+ self._client: Optional["Client"] = client_instance
2342
+ self.id: Optional[str] = data.get("id") # Thread ID
2343
+ self.user_id: Optional[str] = data.get("user_id")
2344
+ self.join_timestamp: str = data["join_timestamp"]
2345
+ self.flags: int = data["flags"]
2346
+
2347
+ # The 'member' field in ThreadMember payload is a full guild member object.
2348
+ # This is present in some contexts like when listing thread members.
2349
+ self.member: Optional[Member] = (
2350
+ Member(data["member"], client_instance) if data.get("member") else None
2351
+ )
2352
+
2353
+ # Note: The 'presence' field is not included as it's often unavailable or too dynamic for a simple model.
2354
+
2355
+ def __repr__(self) -> str:
2356
+ return f"<ThreadMember user_id='{self.user_id}' thread_id='{self.id}'>"
2357
+
2358
+
2359
+ class PresenceUpdate:
2360
+ """Represents a PRESENCE_UPDATE event."""
2361
+
2362
+ def __init__(
2363
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2364
+ ):
2365
+ self._client = client_instance
2366
+ self.user = User(data["user"])
2367
+ self.guild_id: Optional[str] = data.get("guild_id")
2368
+ self.status: Optional[str] = data.get("status")
2369
+ self.activities: List[Dict[str, Any]] = data.get("activities", [])
2370
+ self.client_status: Dict[str, Any] = data.get("client_status", {})
2371
+
2372
+ def __repr__(self) -> str:
2373
+ return f"<PresenceUpdate user_id='{self.user.id}' guild_id='{self.guild_id}' status='{self.status}'>"
2374
+
2375
+
2376
+ class TypingStart:
2377
+ """Represents a TYPING_START event."""
2378
+
2379
+ def __init__(
2380
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2381
+ ):
2382
+ self._client = client_instance
2383
+ self.channel_id: str = data["channel_id"]
2384
+ self.guild_id: Optional[str] = data.get("guild_id")
2385
+ self.user_id: str = data["user_id"]
2386
+ self.timestamp: int = data["timestamp"]
2387
+ self.member: Optional[Member] = (
2388
+ Member(data["member"], client_instance) if data.get("member") else None
2389
+ )
2390
+
2391
+ def __repr__(self) -> str:
2392
+ return f"<TypingStart channel_id='{self.channel_id}' user_id='{self.user_id}'>"
2393
+
2394
+
2395
+ class Reaction:
2396
+ """Represents a message reaction event."""
2397
+
2398
+ def __init__(
2399
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2400
+ ):
2401
+ self._client = client_instance
2402
+ self.user_id: str = data["user_id"]
2403
+ self.channel_id: str = data["channel_id"]
2404
+ self.message_id: str = data["message_id"]
2405
+ self.guild_id: Optional[str] = data.get("guild_id")
2406
+ self.member: Optional[Member] = (
2407
+ Member(data["member"], client_instance) if data.get("member") else None
2408
+ )
2409
+ self.emoji: Dict[str, Any] = data.get("emoji", {})
2410
+
2411
+ def __repr__(self) -> str:
2412
+ emoji_value = self.emoji.get("name") or self.emoji.get("id")
2413
+ return f"<Reaction message_id='{self.message_id}' user_id='{self.user_id}' emoji='{emoji_value}'>"
2414
+
2415
+
2416
+ class ScheduledEvent:
2417
+ """Represents a guild scheduled event."""
2418
+
2419
+ def __init__(
2420
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2421
+ ):
2422
+ self._client = client_instance
2423
+ self.id: str = data["id"]
2424
+ self.guild_id: str = data["guild_id"]
2425
+ self.channel_id: Optional[str] = data.get("channel_id")
2426
+ self.creator_id: Optional[str] = data.get("creator_id")
2427
+ self.name: str = data["name"]
2428
+ self.description: Optional[str] = data.get("description")
2429
+ self.scheduled_start_time: str = data["scheduled_start_time"]
2430
+ self.scheduled_end_time: Optional[str] = data.get("scheduled_end_time")
2431
+ self.privacy_level: GuildScheduledEventPrivacyLevel = (
2432
+ GuildScheduledEventPrivacyLevel(data["privacy_level"])
2433
+ )
2434
+ self.status: GuildScheduledEventStatus = GuildScheduledEventStatus(
2435
+ data["status"]
2436
+ )
2437
+ self.entity_type: GuildScheduledEventEntityType = GuildScheduledEventEntityType(
2438
+ data["entity_type"]
2439
+ )
2440
+ self.entity_id: Optional[str] = data.get("entity_id")
2441
+ self.entity_metadata: Optional[Dict[str, Any]] = data.get("entity_metadata")
2442
+ self.creator: Optional[User] = (
2443
+ User(data["creator"]) if data.get("creator") else None
2444
+ )
2445
+ self.user_count: Optional[int] = data.get("user_count")
2446
+ self.image: Optional[str] = data.get("image")
2447
+
2448
+ def __repr__(self) -> str:
2449
+ return f"<ScheduledEvent id='{self.id}' name='{self.name}'>"
2450
+
2451
+
2452
+ @dataclass
2453
+ class Invite:
2454
+ """Represents a Discord invite."""
2455
+
2456
+ code: str
2457
+ channel_id: Optional[str]
2458
+ guild_id: Optional[str]
2459
+ inviter_id: Optional[str]
2460
+ uses: Optional[int]
2461
+ max_uses: Optional[int]
2462
+ max_age: Optional[int]
2463
+ temporary: Optional[bool]
2464
+ created_at: Optional[str]
2465
+
2466
+ @classmethod
2467
+ def from_dict(cls, data: Dict[str, Any]) -> "Invite":
2468
+ channel = data.get("channel")
2469
+ guild = data.get("guild")
2470
+ inviter = data.get("inviter")
2471
+ return cls(
2472
+ code=data["code"],
2473
+ channel_id=(channel or {}).get("id") if channel else data.get("channel_id"),
2474
+ guild_id=(guild or {}).get("id") if guild else data.get("guild_id"),
2475
+ inviter_id=(inviter or {}).get("id"),
2476
+ uses=data.get("uses"),
2477
+ max_uses=data.get("max_uses"),
2478
+ max_age=data.get("max_age"),
2479
+ temporary=data.get("temporary"),
2480
+ created_at=data.get("created_at"),
2481
+ )
2482
+
2483
+ def __repr__(self) -> str:
2484
+ return f"<Invite code='{self.code}' guild_id='{self.guild_id}' channel_id='{self.channel_id}'>"
2485
+
2486
+
2487
+ class GuildMemberRemove:
2488
+ """Represents a GUILD_MEMBER_REMOVE event."""
2489
+
2490
+ def __init__(
2491
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2492
+ ):
2493
+ self._client = client_instance
2494
+ self.guild_id: str = data["guild_id"]
2495
+ self.user: User = User(data["user"])
2496
+
2497
+ def __repr__(self) -> str:
2498
+ return (
2499
+ f"<GuildMemberRemove guild_id='{self.guild_id}' user_id='{self.user.id}'>"
2500
+ )
2501
+
2502
+
2503
+ class GuildBanAdd:
2504
+ """Represents a GUILD_BAN_ADD event."""
2505
+
2506
+ def __init__(
2507
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2508
+ ):
2509
+ self._client = client_instance
2510
+ self.guild_id: str = data["guild_id"]
2511
+ self.user: User = User(data["user"])
2512
+
2513
+ def __repr__(self) -> str:
2514
+ return f"<GuildBanAdd guild_id='{self.guild_id}' user_id='{self.user.id}'>"
2515
+
2516
+
2517
+ class GuildBanRemove:
2518
+ """Represents a GUILD_BAN_REMOVE event."""
2519
+
2520
+ def __init__(
2521
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2522
+ ):
2523
+ self._client = client_instance
2524
+ self.guild_id: str = data["guild_id"]
2525
+ self.user: User = User(data["user"])
2526
+
2527
+ def __repr__(self) -> str:
2528
+ return f"<GuildBanRemove guild_id='{self.guild_id}' user_id='{self.user.id}'>"
2529
+
2530
+
2531
+ class GuildRoleUpdate:
2532
+ """Represents a GUILD_ROLE_UPDATE event."""
2533
+
2534
+ def __init__(
2535
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2536
+ ):
2537
+ self._client = client_instance
2538
+ self.guild_id: str = data["guild_id"]
2539
+ self.role: Role = Role(data["role"])
2540
+
2541
+ def __repr__(self) -> str:
2542
+ return f"<GuildRoleUpdate guild_id='{self.guild_id}' role_id='{self.role.id}'>"
2543
+
2544
+
2545
+ class AuditLogEntry:
2546
+ """Represents a single entry in a guild's audit log."""
2547
+
2548
+ def __init__(
2549
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2550
+ ) -> None:
2551
+ self._client = client_instance
2552
+ self.id: str = data["id"]
2553
+ self.user_id: Optional[str] = data.get("user_id")
2554
+ self.target_id: Optional[str] = data.get("target_id")
2555
+ self.action_type: int = data["action_type"]
2556
+ self.reason: Optional[str] = data.get("reason")
2557
+ self.changes: List[Dict[str, Any]] = data.get("changes", [])
2558
+ self.options: Optional[Dict[str, Any]] = data.get("options")
2559
+
2560
+ def __repr__(self) -> str:
2561
+ return f"<AuditLogEntry id='{self.id}' action_type={self.action_type} user_id='{self.user_id}'>"
2562
+
2563
+
2564
+ def channel_factory(data: Dict[str, Any], client: "Client") -> Channel:
2565
+ """Create a channel object from raw API data."""
2566
+ channel_type = data.get("type")
2567
+
2568
+ if channel_type in (
2569
+ ChannelType.GUILD_TEXT.value,
2570
+ ChannelType.GUILD_ANNOUNCEMENT.value,
2571
+ ):
2572
+ return TextChannel(data, client)
2573
+ if channel_type == ChannelType.GUILD_VOICE.value:
2574
+ return VoiceChannel(data, client)
2575
+ if channel_type == ChannelType.GUILD_STAGE_VOICE.value:
2576
+ return StageChannel(data, client)
2577
+ if channel_type == ChannelType.GUILD_CATEGORY.value:
2578
+ return CategoryChannel(data, client)
2579
+ if channel_type in (
2580
+ ChannelType.ANNOUNCEMENT_THREAD.value,
2581
+ ChannelType.PUBLIC_THREAD.value,
2582
+ ChannelType.PRIVATE_THREAD.value,
2583
+ ):
2584
+ return Thread(data, client)
2585
+ if channel_type in (ChannelType.DM.value, ChannelType.GROUP_DM.value):
2586
+ return DMChannel(data, client)
2587
+
2588
+ return Channel(data, client)