disagreement 0.0.2__py3-none-any.whl → 0.1.0rc2__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.
@@ -395,17 +395,14 @@ class Interaction:
395
395
 
396
396
  async def respond_modal(self, modal: "Modal") -> None:
397
397
  """|coro| Send a modal in response to this interaction."""
398
-
399
- from typing import Any, cast
400
-
401
- payload = {
402
- "type": InteractionCallbackType.MODAL.value,
403
- "data": modal.to_dict(),
404
- }
398
+ payload = InteractionResponsePayload(
399
+ type=InteractionCallbackType.MODAL,
400
+ data=modal.to_dict(),
401
+ )
405
402
  await self._client._http.create_interaction_response(
406
403
  interaction_id=self.id,
407
404
  interaction_token=self.token,
408
- payload=cast(Any, payload),
405
+ payload=payload,
409
406
  )
410
407
 
411
408
  async def edit(
@@ -489,7 +486,7 @@ class InteractionResponse:
489
486
  """Sends a modal response."""
490
487
  payload = InteractionResponsePayload(
491
488
  type=InteractionCallbackType.MODAL,
492
- data=InteractionCallbackData(modal.to_dict()),
489
+ data=modal.to_dict(),
493
490
  )
494
491
  await self._interaction._client._http.create_interaction_response(
495
492
  self._interaction.id,
@@ -510,7 +507,7 @@ class InteractionCallbackData:
510
507
  )
511
508
  self.allowed_mentions: Optional[AllowedMentions] = (
512
509
  AllowedMentions(data["allowed_mentions"])
513
- if data.get("allowed_mentions")
510
+ if "allowed_mentions" in data
514
511
  else None
515
512
  )
516
513
  self.flags: Optional[int] = data.get("flags") # MessageFlags enum could be used
@@ -557,16 +554,22 @@ class InteractionResponsePayload:
557
554
  def __init__(
558
555
  self,
559
556
  type: InteractionCallbackType,
560
- data: Optional[InteractionCallbackData] = None,
557
+ data: Optional[Union[InteractionCallbackData, Dict[str, Any]]] = None,
561
558
  ):
562
- self.type: InteractionCallbackType = type
563
- self.data: Optional[InteractionCallbackData] = data
559
+ self.type = type
560
+ self.data = data
564
561
 
565
562
  def to_dict(self) -> Dict[str, Any]:
566
563
  payload: Dict[str, Any] = {"type": self.type.value}
567
564
  if self.data:
568
- payload["data"] = self.data.to_dict()
565
+ if isinstance(self.data, dict):
566
+ payload["data"] = self.data
567
+ else:
568
+ payload["data"] = self.data.to_dict()
569
569
  return payload
570
570
 
571
571
  def __repr__(self) -> str:
572
572
  return f"<InteractionResponsePayload type={self.type!r}>"
573
+
574
+ def __getitem__(self, key: str) -> Any:
575
+ return self.to_dict()[key]
disagreement/models.py CHANGED
@@ -5,7 +5,10 @@ Data models for Discord objects.
5
5
  """
6
6
 
7
7
  import json
8
- from typing import Optional, TYPE_CHECKING, List, Dict, Any, Union
8
+ import asyncio
9
+ import aiohttp # pylint: disable=import-error
10
+ import asyncio
11
+ from typing import Any, AsyncIterator, Dict, List, Optional, TYPE_CHECKING, Union
9
12
 
10
13
  from .errors import DisagreementException, HTTPException
11
14
  from .enums import ( # These enums will need to be defined in disagreement/enums.py
@@ -21,12 +24,15 @@ from .enums import ( # These enums will need to be defined in disagreement/enum
21
24
  ButtonStyle, # Added for Button
22
25
  # SelectMenuType will be part of ComponentType or a new enum if needed
23
26
  )
27
+ from .permissions import Permissions
28
+ from .color import Color
24
29
 
25
30
 
26
31
  if TYPE_CHECKING:
27
32
  from .client import Client # For type hinting to avoid circular imports
28
33
  from .enums import OverwriteType # For PermissionOverwrite model
29
34
  from .ui.view import View
35
+ from .interactions import Snowflake
30
36
 
31
37
  # Forward reference Message if it were used in type hints before its definition
32
38
  # from .models import Message # Not needed as Message is defined before its use in TextChannel.send etc.
@@ -72,6 +78,7 @@ class Message:
72
78
  timestamp (str): When this message was sent (ISO8601 timestamp).
73
79
  components (Optional[List[ActionRow]]): Structured components attached
74
80
  to the message if present.
81
+ attachments (List[Attachment]): Attachments included with the message.
75
82
  """
76
83
 
77
84
  def __init__(self, data: dict, client_instance: "Client"):
@@ -92,6 +99,9 @@ class Message:
92
99
  ]
93
100
  else:
94
101
  self.components = None
102
+ self.attachments: List[Attachment] = [
103
+ Attachment(a) for a in data.get("attachments", [])
104
+ ]
95
105
  # Add other fields as needed, e.g., attachments, embeds, reactions, etc.
96
106
  # self.mentions: List[User] = [User(u) for u in data.get("mentions", [])]
97
107
  # self.mention_roles: List[str] = data.get("mention_roles", [])
@@ -192,6 +202,37 @@ class Message:
192
202
  view=view,
193
203
  )
194
204
 
205
+ async def add_reaction(self, emoji: str) -> None:
206
+ """|coro| Add a reaction to this message."""
207
+
208
+ await self._client.add_reaction(self.channel_id, self.id, emoji)
209
+
210
+ async def remove_reaction(self, emoji: str) -> None:
211
+ """|coro| Remove the bot's reaction from this message."""
212
+
213
+ await self._client.remove_reaction(self.channel_id, self.id, emoji)
214
+
215
+ async def clear_reactions(self) -> None:
216
+ """|coro| Remove all reactions from this message."""
217
+
218
+ await self._client.clear_reactions(self.channel_id, self.id)
219
+
220
+ async def delete(self, delay: Optional[float] = None) -> None:
221
+ """|coro|
222
+
223
+ Deletes this message.
224
+
225
+ Parameters
226
+ ----------
227
+ delay:
228
+ If provided, wait this many seconds before deleting.
229
+ """
230
+
231
+ if delay is not None:
232
+ await asyncio.sleep(delay)
233
+
234
+ await self._client._http.delete_message(self.channel_id, self.id)
235
+
195
236
  def __repr__(self) -> str:
196
237
  return f"<Message id='{self.id}' channel_id='{self.channel_id}' author='{self.author!r}'>"
197
238
 
@@ -287,7 +328,7 @@ class Embed:
287
328
  self.description: Optional[str] = data.get("description")
288
329
  self.url: Optional[str] = data.get("url")
289
330
  self.timestamp: Optional[str] = data.get("timestamp") # ISO8601 timestamp
290
- self.color: Optional[int] = data.get("color")
331
+ self.color = Color.parse(data.get("color"))
291
332
 
292
333
  self.footer: Optional[EmbedFooter] = (
293
334
  EmbedFooter(data["footer"]) if data.get("footer") else None
@@ -317,7 +358,7 @@ class Embed:
317
358
  if self.timestamp:
318
359
  payload["timestamp"] = self.timestamp
319
360
  if self.color is not None:
320
- payload["color"] = self.color
361
+ payload["color"] = self.color.value
321
362
  if self.footer:
322
363
  payload["footer"] = self.footer.to_dict()
323
364
  if self.image:
@@ -373,6 +414,14 @@ class Attachment:
373
414
  return payload
374
415
 
375
416
 
417
+ class File:
418
+ """Represents a file to be uploaded."""
419
+
420
+ def __init__(self, filename: str, data: bytes):
421
+ self.filename = filename
422
+ self.data = data
423
+
424
+
376
425
  class AllowedMentions:
377
426
  """Represents allowed mentions for a message or interaction response."""
378
427
 
@@ -495,6 +544,12 @@ class Member(User): # Member inherits from User
495
544
  def __repr__(self) -> str:
496
545
  return f"<Member id='{self.id}' username='{self.username}' nick='{self.nick}'>"
497
546
 
547
+ @property
548
+ def display_name(self) -> str:
549
+ """Return the nickname if set, otherwise the username."""
550
+
551
+ return self.nick or self.username
552
+
498
553
  async def kick(self, *, reason: Optional[str] = None) -> None:
499
554
  if not self.guild_id or not self._client:
500
555
  raise DisagreementException("Member.kick requires guild_id and client")
@@ -527,6 +582,34 @@ class Member(User): # Member inherits from User
527
582
  reason=reason,
528
583
  )
529
584
 
585
+ @property
586
+ def top_role(self) -> Optional["Role"]:
587
+ """Return the member's highest role from the guild cache."""
588
+
589
+ if not self.guild_id or not self._client:
590
+ return None
591
+
592
+ guild = self._client.get_guild(self.guild_id)
593
+ if not guild:
594
+ return None
595
+
596
+ if not guild.roles and hasattr(self._client, "fetch_roles"):
597
+ try:
598
+ self._client.loop.run_until_complete(
599
+ self._client.fetch_roles(self.guild_id)
600
+ )
601
+ except RuntimeError:
602
+ future = asyncio.run_coroutine_threadsafe(
603
+ self._client.fetch_roles(self.guild_id), self._client.loop
604
+ )
605
+ future.result()
606
+
607
+ role_objects = [r for r in guild.roles if r.id in self.roles]
608
+ if not role_objects:
609
+ return None
610
+
611
+ return max(role_objects, key=lambda r: r.position)
612
+
530
613
 
531
614
  class PartialEmoji:
532
615
  """Represents a partial emoji, often used in components or reactions.
@@ -853,6 +936,31 @@ class Guild:
853
936
  def get_member(self, user_id: str) -> Optional[Member]:
854
937
  return self._members.get(user_id)
855
938
 
939
+ def get_member_named(self, name: str) -> Optional[Member]:
940
+ """Retrieve a cached member by username or nickname.
941
+
942
+ The lookup is case-insensitive and searches both the username and
943
+ guild nickname for a match.
944
+
945
+ Parameters
946
+ ----------
947
+ name: str
948
+ The username or nickname to search for.
949
+
950
+ Returns
951
+ -------
952
+ Optional[Member]
953
+ The matching member if found, otherwise ``None``.
954
+ """
955
+
956
+ lowered = name.lower()
957
+ for member in self._members.values():
958
+ if member.username.lower() == lowered:
959
+ return member
960
+ if member.nick and member.nick.lower() == lowered:
961
+ return member
962
+ return None
963
+
856
964
  def get_role(self, role_id: str) -> Optional[Role]:
857
965
  return next((role for role in self.roles if role.id == role_id), None)
858
966
 
@@ -893,6 +1001,78 @@ class Channel:
893
1001
  def __repr__(self) -> str:
894
1002
  return f"<Channel id='{self.id}' name='{self.name}' type='{self.type.name if hasattr(self.type, 'name') else self._type_val}'>"
895
1003
 
1004
+ def permission_overwrite_for(
1005
+ self, target: Union["Role", "Member", str]
1006
+ ) -> Optional["PermissionOverwrite"]:
1007
+ """Return the :class:`PermissionOverwrite` for ``target`` if present."""
1008
+
1009
+ if isinstance(target, str):
1010
+ target_id = target
1011
+ else:
1012
+ target_id = target.id
1013
+ for overwrite in self.permission_overwrites:
1014
+ if overwrite.id == target_id:
1015
+ return overwrite
1016
+ return None
1017
+
1018
+ @staticmethod
1019
+ def _apply_overwrite(
1020
+ perms: Permissions, overwrite: Optional["PermissionOverwrite"]
1021
+ ) -> Permissions:
1022
+ if overwrite is None:
1023
+ return perms
1024
+
1025
+ perms &= ~Permissions(int(overwrite.deny))
1026
+ perms |= Permissions(int(overwrite.allow))
1027
+ return perms
1028
+
1029
+ def permissions_for(self, member: "Member") -> Permissions:
1030
+ """Resolve channel permissions for ``member``."""
1031
+
1032
+ if self.guild_id is None:
1033
+ return Permissions(~0)
1034
+
1035
+ if not hasattr(self._client, "get_guild"):
1036
+ return Permissions(0)
1037
+
1038
+ guild = self._client.get_guild(self.guild_id)
1039
+ if guild is None:
1040
+ return Permissions(0)
1041
+
1042
+ base = Permissions(0)
1043
+
1044
+ everyone = guild.get_role(guild.id)
1045
+ if everyone is not None:
1046
+ base |= Permissions(int(everyone.permissions))
1047
+
1048
+ for rid in member.roles:
1049
+ role = guild.get_role(rid)
1050
+ if role is not None:
1051
+ base |= Permissions(int(role.permissions))
1052
+
1053
+ if base & Permissions.ADMINISTRATOR:
1054
+ return Permissions(~0)
1055
+
1056
+ # Apply @everyone overwrite
1057
+ base = self._apply_overwrite(base, self.permission_overwrite_for(guild.id))
1058
+
1059
+ # Role overwrites
1060
+ role_allow = Permissions(0)
1061
+ role_deny = Permissions(0)
1062
+ for rid in member.roles:
1063
+ ow = self.permission_overwrite_for(rid)
1064
+ if ow is not None:
1065
+ role_allow |= Permissions(int(ow.allow))
1066
+ role_deny |= Permissions(int(ow.deny))
1067
+
1068
+ base &= ~role_deny
1069
+ base |= role_allow
1070
+
1071
+ # Member overwrite
1072
+ base = self._apply_overwrite(base, self.permission_overwrite_for(member.id))
1073
+
1074
+ return base
1075
+
896
1076
 
897
1077
  class TextChannel(Channel):
898
1078
  """Represents a guild text channel or announcement channel."""
@@ -907,6 +1087,20 @@ class TextChannel(Channel):
907
1087
  )
908
1088
  self.last_pin_timestamp: Optional[str] = data.get("last_pin_timestamp")
909
1089
 
1090
+
1091
+ def history(
1092
+ self,
1093
+ *,
1094
+ limit: Optional[int] = None,
1095
+ before: Optional[str] = None,
1096
+ after: Optional[str] = None,
1097
+ ) -> AsyncIterator["Message"]:
1098
+ """Return an async iterator over this channel's messages."""
1099
+
1100
+ from .utils import message_pager
1101
+
1102
+ return message_pager(self, limit=limit, before=before, after=after)
1103
+
910
1104
  async def send(
911
1105
  self,
912
1106
  content: Optional[str] = None,
@@ -928,6 +1122,28 @@ class TextChannel(Channel):
928
1122
  components=components,
929
1123
  )
930
1124
 
1125
+ async def purge(
1126
+ self, limit: int, *, before: "Snowflake | None" = None
1127
+ ) -> List["Snowflake"]:
1128
+ """Bulk delete messages from this channel."""
1129
+
1130
+ params: Dict[str, Union[int, str]] = {"limit": limit}
1131
+ if before is not None:
1132
+ params["before"] = before
1133
+
1134
+ messages = await self._client._http.request(
1135
+ "GET", f"/channels/{self.id}/messages", params=params
1136
+ )
1137
+ ids = [m["id"] for m in messages]
1138
+ if not ids:
1139
+ return []
1140
+
1141
+ await self._client._http.bulk_delete_messages(self.id, ids)
1142
+ for mid in ids:
1143
+ self._client._messages.pop(mid, None)
1144
+ return ids
1145
+
1146
+
931
1147
  def __repr__(self) -> str:
932
1148
  return f"<TextChannel id='{self.id}' name='{self.name}' guild_id='{self.guild_id}'>"
933
1149
 
@@ -1045,6 +1261,36 @@ class DMChannel(Channel):
1045
1261
  components=components,
1046
1262
  )
1047
1263
 
1264
+ async def history(
1265
+ self,
1266
+ *,
1267
+ limit: Optional[int] = 100,
1268
+ before: "Snowflake | None" = None,
1269
+ ):
1270
+ """An async iterator over messages in this DM."""
1271
+
1272
+ params: Dict[str, Union[int, str]] = {}
1273
+ if before is not None:
1274
+ params["before"] = before
1275
+
1276
+ fetched = 0
1277
+ while True:
1278
+ to_fetch = 100 if limit is None else min(100, limit - fetched)
1279
+ if to_fetch <= 0:
1280
+ break
1281
+ params["limit"] = to_fetch
1282
+ messages = await self._client._http.request(
1283
+ "GET", f"/channels/{self.id}/messages", params=params.copy()
1284
+ )
1285
+ if not messages:
1286
+ break
1287
+ params["before"] = messages[-1]["id"]
1288
+ for msg in messages:
1289
+ yield Message(msg, self._client)
1290
+ fetched += 1
1291
+ if limit is not None and fetched >= limit:
1292
+ return
1293
+
1048
1294
  def __repr__(self) -> str:
1049
1295
  recipient_repr = self.recipient.username if self.recipient else "Unknown"
1050
1296
  return f"<DMChannel id='{self.id}' recipient='{recipient_repr}'>"
@@ -1090,6 +1336,106 @@ class PartialChannel:
1090
1336
  return f"<PartialChannel id='{self.id}' name='{self.name}' type='{type_name}'>"
1091
1337
 
1092
1338
 
1339
+ class Webhook:
1340
+ """Represents a Discord Webhook."""
1341
+
1342
+ def __init__(
1343
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
1344
+ ):
1345
+ self._client: Optional["Client"] = client_instance
1346
+ self.id: str = data["id"]
1347
+ self.type: int = int(data.get("type", 1))
1348
+ self.guild_id: Optional[str] = data.get("guild_id")
1349
+ self.channel_id: Optional[str] = data.get("channel_id")
1350
+ self.name: Optional[str] = data.get("name")
1351
+ self.avatar: Optional[str] = data.get("avatar")
1352
+ self.token: Optional[str] = data.get("token")
1353
+ self.application_id: Optional[str] = data.get("application_id")
1354
+ self.url: Optional[str] = data.get("url")
1355
+ self.user: Optional[User] = User(data["user"]) if data.get("user") else None
1356
+
1357
+ def __repr__(self) -> str:
1358
+ return f"<Webhook id='{self.id}' name='{self.name}'>"
1359
+
1360
+ @classmethod
1361
+ def from_url(
1362
+ cls, url: str, session: Optional[aiohttp.ClientSession] = None
1363
+ ) -> "Webhook":
1364
+ """Create a minimal :class:`Webhook` from a webhook URL.
1365
+
1366
+ Parameters
1367
+ ----------
1368
+ url:
1369
+ The full Discord webhook URL.
1370
+ session:
1371
+ Unused for now. Present for API compatibility.
1372
+
1373
+ Returns
1374
+ -------
1375
+ Webhook
1376
+ A webhook instance containing only the ``id``, ``token`` and ``url``.
1377
+ """
1378
+
1379
+ parts = url.rstrip("/").split("/")
1380
+ if len(parts) < 2:
1381
+ raise ValueError("Invalid webhook URL")
1382
+ token = parts[-1]
1383
+ webhook_id = parts[-2]
1384
+
1385
+ return cls({"id": webhook_id, "token": token, "url": url})
1386
+
1387
+ async def send(
1388
+ self,
1389
+ content: Optional[str] = None,
1390
+ *,
1391
+ username: Optional[str] = None,
1392
+ avatar_url: Optional[str] = None,
1393
+ tts: bool = False,
1394
+ embed: Optional["Embed"] = None,
1395
+ embeds: Optional[List["Embed"]] = None,
1396
+ components: Optional[List["ActionRow"]] = None,
1397
+ allowed_mentions: Optional[Dict[str, Any]] = None,
1398
+ attachments: Optional[List[Any]] = None,
1399
+ files: Optional[List[Any]] = None,
1400
+ flags: Optional[int] = None,
1401
+ ) -> "Message":
1402
+ """Send a message using this webhook."""
1403
+
1404
+ if not self._client:
1405
+ raise DisagreementException("Webhook is not bound to a Client")
1406
+ assert self.token is not None, "Webhook token missing"
1407
+
1408
+ if embed and embeds:
1409
+ raise ValueError("Cannot provide both embed and embeds.")
1410
+
1411
+ final_embeds_payload: Optional[List[Dict[str, Any]]] = None
1412
+ if embed:
1413
+ final_embeds_payload = [embed.to_dict()]
1414
+ elif embeds:
1415
+ final_embeds_payload = [e.to_dict() for e in embeds]
1416
+
1417
+ components_payload: Optional[List[Dict[str, Any]]] = None
1418
+ if components:
1419
+ components_payload = [c.to_dict() for c in components]
1420
+
1421
+ message_data = await self._client._http.execute_webhook(
1422
+ self.id,
1423
+ self.token,
1424
+ content=content,
1425
+ tts=tts,
1426
+ embeds=final_embeds_payload,
1427
+ components=components_payload,
1428
+ allowed_mentions=allowed_mentions,
1429
+ attachments=attachments,
1430
+ files=files,
1431
+ flags=flags,
1432
+ username=username,
1433
+ avatar_url=avatar_url,
1434
+ )
1435
+
1436
+ return self._client.parse_message(message_data)
1437
+
1438
+
1093
1439
  # --- Message Components ---
1094
1440
 
1095
1441
 
@@ -1433,7 +1779,7 @@ class MediaGallery(Component):
1433
1779
  return payload
1434
1780
 
1435
1781
 
1436
- class File(Component):
1782
+ class FileComponent(Component):
1437
1783
  """Represents a file component."""
1438
1784
 
1439
1785
  def __init__(
@@ -1480,13 +1826,13 @@ class Container(Component):
1480
1826
  def __init__(
1481
1827
  self,
1482
1828
  components: List[Component],
1483
- accent_color: Optional[int] = None,
1829
+ accent_color: Color | int | str | None = None,
1484
1830
  spoiler: bool = False,
1485
1831
  id: Optional[int] = None,
1486
1832
  ):
1487
1833
  super().__init__(ComponentType.CONTAINER)
1488
1834
  self.components = components
1489
- self.accent_color = accent_color
1835
+ self.accent_color = Color.parse(accent_color)
1490
1836
  self.spoiler = spoiler
1491
1837
  self.id = id
1492
1838
 
@@ -1494,7 +1840,7 @@ class Container(Component):
1494
1840
  payload = super().to_dict()
1495
1841
  payload["components"] = [c.to_dict() for c in self.components]
1496
1842
  if self.accent_color:
1497
- payload["accent_color"] = self.accent_color
1843
+ payload["accent_color"] = self.accent_color.value
1498
1844
  if self.spoiler:
1499
1845
  payload["spoiler"] = self.spoiler
1500
1846
  if self.id is not None:
@@ -1614,6 +1960,85 @@ class TypingStart:
1614
1960
  return f"<TypingStart channel_id='{self.channel_id}' user_id='{self.user_id}'>"
1615
1961
 
1616
1962
 
1963
+ class Reaction:
1964
+ """Represents a message reaction event."""
1965
+
1966
+ def __init__(
1967
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
1968
+ ):
1969
+ self._client = client_instance
1970
+ self.user_id: str = data["user_id"]
1971
+ self.channel_id: str = data["channel_id"]
1972
+ self.message_id: str = data["message_id"]
1973
+ self.guild_id: Optional[str] = data.get("guild_id")
1974
+ self.member: Optional[Member] = (
1975
+ Member(data["member"], client_instance) if data.get("member") else None
1976
+ )
1977
+ self.emoji: Dict[str, Any] = data.get("emoji", {})
1978
+
1979
+ def __repr__(self) -> str:
1980
+ emoji_value = self.emoji.get("name") or self.emoji.get("id")
1981
+ return f"<Reaction message_id='{self.message_id}' user_id='{self.user_id}' emoji='{emoji_value}'>"
1982
+
1983
+
1984
+ class GuildMemberRemove:
1985
+ """Represents a GUILD_MEMBER_REMOVE event."""
1986
+
1987
+ def __init__(
1988
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
1989
+ ):
1990
+ self._client = client_instance
1991
+ self.guild_id: str = data["guild_id"]
1992
+ self.user: User = User(data["user"])
1993
+
1994
+ def __repr__(self) -> str:
1995
+ return (
1996
+ f"<GuildMemberRemove guild_id='{self.guild_id}' user_id='{self.user.id}'>"
1997
+ )
1998
+
1999
+
2000
+ class GuildBanAdd:
2001
+ """Represents a GUILD_BAN_ADD event."""
2002
+
2003
+ def __init__(
2004
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2005
+ ):
2006
+ self._client = client_instance
2007
+ self.guild_id: str = data["guild_id"]
2008
+ self.user: User = User(data["user"])
2009
+
2010
+ def __repr__(self) -> str:
2011
+ return f"<GuildBanAdd guild_id='{self.guild_id}' user_id='{self.user.id}'>"
2012
+
2013
+
2014
+ class GuildBanRemove:
2015
+ """Represents a GUILD_BAN_REMOVE event."""
2016
+
2017
+ def __init__(
2018
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2019
+ ):
2020
+ self._client = client_instance
2021
+ self.guild_id: str = data["guild_id"]
2022
+ self.user: User = User(data["user"])
2023
+
2024
+ def __repr__(self) -> str:
2025
+ return f"<GuildBanRemove guild_id='{self.guild_id}' user_id='{self.user.id}'>"
2026
+
2027
+
2028
+ class GuildRoleUpdate:
2029
+ """Represents a GUILD_ROLE_UPDATE event."""
2030
+
2031
+ def __init__(
2032
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2033
+ ):
2034
+ self._client = client_instance
2035
+ self.guild_id: str = data["guild_id"]
2036
+ self.role: Role = Role(data["role"])
2037
+
2038
+ def __repr__(self) -> str:
2039
+ return f"<GuildRoleUpdate guild_id='{self.guild_id}' role_id='{self.role.id}'>"
2040
+
2041
+
1617
2042
  def channel_factory(data: Dict[str, Any], client: "Client") -> Channel:
1618
2043
  """Create a channel object from raw API data."""
1619
2044
  channel_type = data.get("type")
@@ -51,6 +51,8 @@ class ShardManager:
51
51
  verbose=self.client.verbose,
52
52
  shard_id=shard_id,
53
53
  shard_count=self.shard_count,
54
+ max_retries=self.client.gateway_max_retries,
55
+ max_backoff=self.client.gateway_max_backoff,
54
56
  )
55
57
  self.shards.append(Shard(shard_id, self.shard_count, gateway))
56
58
 
disagreement/ui/modal.py CHANGED
@@ -74,7 +74,7 @@ def text_input(
74
74
 
75
75
  item = TextInput(
76
76
  label=label,
77
- custom_id=custom_id,
77
+ custom_id=custom_id or func.__name__,
78
78
  style=style,
79
79
  placeholder=placeholder,
80
80
  required=required,