disagreement 0.1.0rc3__py3-none-any.whl → 0.2.0rc1__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.
@@ -70,6 +70,10 @@ class Command:
70
70
  if hasattr(callback, "__command_checks__"):
71
71
  self.checks.extend(getattr(callback, "__command_checks__"))
72
72
 
73
+ self.max_concurrency: Optional[Tuple[int, str]] = None
74
+ if hasattr(callback, "__max_concurrency__"):
75
+ self.max_concurrency = getattr(callback, "__max_concurrency__")
76
+
73
77
  def add_check(
74
78
  self, predicate: Callable[["CommandContext"], Awaitable[bool] | bool]
75
79
  ) -> None:
@@ -215,6 +219,7 @@ class CommandHandler:
215
219
  ] = prefix
216
220
  self.commands: Dict[str, Command] = {}
217
221
  self.cogs: Dict[str, "Cog"] = {}
222
+ self._concurrency: Dict[str, Dict[str, int]] = {}
218
223
 
219
224
  from .help import HelpCommand
220
225
 
@@ -300,6 +305,47 @@ class CommandHandler:
300
305
  logger.info("Cog '%s' removed.", cog_name)
301
306
  return cog_to_remove
302
307
 
308
+ def _acquire_concurrency(self, ctx: CommandContext) -> None:
309
+ mc = getattr(ctx.command, "max_concurrency", None)
310
+ if not mc:
311
+ return
312
+ limit, scope = mc
313
+ if scope == "user":
314
+ key = ctx.author.id
315
+ elif scope == "guild":
316
+ key = ctx.message.guild_id or ctx.author.id
317
+ else:
318
+ key = "global"
319
+ buckets = self._concurrency.setdefault(ctx.command.name, {})
320
+ current = buckets.get(key, 0)
321
+ if current >= limit:
322
+ from .errors import MaxConcurrencyReached
323
+
324
+ raise MaxConcurrencyReached(limit)
325
+ buckets[key] = current + 1
326
+
327
+ def _release_concurrency(self, ctx: CommandContext) -> None:
328
+ mc = getattr(ctx.command, "max_concurrency", None)
329
+ if not mc:
330
+ return
331
+ _, scope = mc
332
+ if scope == "user":
333
+ key = ctx.author.id
334
+ elif scope == "guild":
335
+ key = ctx.message.guild_id or ctx.author.id
336
+ else:
337
+ key = "global"
338
+ buckets = self._concurrency.get(ctx.command.name)
339
+ if not buckets:
340
+ return
341
+ current = buckets.get(key, 0)
342
+ if current <= 1:
343
+ buckets.pop(key, None)
344
+ else:
345
+ buckets[key] = current - 1
346
+ if not buckets:
347
+ self._concurrency.pop(ctx.command.name, None)
348
+
303
349
  async def get_prefix(self, message: "Message") -> Union[str, List[str], None]:
304
350
  if callable(self.prefix):
305
351
  if inspect.iscoroutinefunction(self.prefix):
@@ -501,7 +547,11 @@ class CommandHandler:
501
547
  parsed_args, parsed_kwargs = await self._parse_arguments(command, ctx, view)
502
548
  ctx.args = parsed_args
503
549
  ctx.kwargs = parsed_kwargs
504
- await command.invoke(ctx, *parsed_args, **parsed_kwargs)
550
+ self._acquire_concurrency(ctx)
551
+ try:
552
+ await command.invoke(ctx, *parsed_args, **parsed_kwargs)
553
+ finally:
554
+ self._release_concurrency(ctx)
505
555
  except CommandError as e:
506
556
  logger.error("Command error for '%s': %s", command.name, e)
507
557
  if hasattr(self.client, "on_command_error"):
@@ -107,6 +107,33 @@ def check_any(
107
107
  return check(predicate)
108
108
 
109
109
 
110
+ def max_concurrency(
111
+ number: int, per: str = "user"
112
+ ) -> Callable[[Callable[..., Awaitable[None]]], Callable[..., Awaitable[None]]]:
113
+ """Limit how many concurrent invocations of a command are allowed.
114
+
115
+ Parameters
116
+ ----------
117
+ number:
118
+ The maximum number of concurrent invocations.
119
+ per:
120
+ The scope of the limiter. Can be ``"user"``, ``"guild"`` or ``"global"``.
121
+ """
122
+
123
+ if number < 1:
124
+ raise ValueError("Concurrency number must be at least 1.")
125
+ if per not in {"user", "guild", "global"}:
126
+ raise ValueError("per must be 'user', 'guild', or 'global'.")
127
+
128
+ def decorator(
129
+ func: Callable[..., Awaitable[None]],
130
+ ) -> Callable[..., Awaitable[None]]:
131
+ setattr(func, "__max_concurrency__", (number, per))
132
+ return func
133
+
134
+ return decorator
135
+
136
+
110
137
  def cooldown(
111
138
  rate: int, per: float
112
139
  ) -> Callable[[Callable[..., Awaitable[None]]], Callable[..., Awaitable[None]]]:
@@ -72,5 +72,13 @@ class CommandInvokeError(CommandError):
72
72
  super().__init__(f"Error during command invocation: {original}")
73
73
 
74
74
 
75
+ class MaxConcurrencyReached(CommandError):
76
+ """Raised when a command exceeds its concurrency limit."""
77
+
78
+ def __init__(self, limit: int):
79
+ self.limit = limit
80
+ super().__init__(f"Max concurrency of {limit} reached")
81
+
82
+
75
83
  # Add more specific errors as needed, e.g., UserNotFound, ChannelNotFound, etc.
76
84
  # These might inherit from BadArgument.
disagreement/gateway.py CHANGED
@@ -344,6 +344,9 @@ class GatewayClient:
344
344
  raw_event_d_payload if isinstance(raw_event_d_payload, dict) else {}
345
345
  )
346
346
  await self._dispatcher.dispatch(event_name, event_data_to_dispatch)
347
+ await self._dispatcher.dispatch(
348
+ "SHARD_RESUME", {"shard_id": self._shard_id}
349
+ )
347
350
  elif event_name:
348
351
  # For other events, ensure 'd' is a dict, or pass {} if 'd' is null/missing.
349
352
  # Models/parsers in EventDispatcher will need to handle potentially empty dicts.
@@ -508,6 +511,10 @@ class GatewayClient:
508
511
  self._receive_task.cancel()
509
512
  self._receive_task = self._loop.create_task(self._receive_loop())
510
513
 
514
+ await self._dispatcher.dispatch(
515
+ "SHARD_CONNECT", {"shard_id": self._shard_id}
516
+ )
517
+
511
518
  except aiohttp.ClientConnectorError as e:
512
519
  raise GatewayException(
513
520
  f"Failed to connect to Gateway (Connector Error): {e}"
@@ -559,6 +566,10 @@ class GatewayClient:
559
566
  self._last_sequence = None
560
567
  self._resume_gateway_url = None # This might be re-fetched anyway
561
568
 
569
+ await self._dispatcher.dispatch(
570
+ "SHARD_DISCONNECT", {"shard_id": self._shard_id}
571
+ )
572
+
562
573
  @property
563
574
  def latency(self) -> Optional[float]:
564
575
  """Returns the latency between heartbeat and ACK in seconds."""
disagreement/http.py CHANGED
@@ -23,7 +23,7 @@ from .interactions import InteractionResponsePayload
23
23
 
24
24
  if TYPE_CHECKING:
25
25
  from .client import Client
26
- from .models import Message, Webhook, File
26
+ from .models import Message, Webhook, File, StageInstance, Invite
27
27
  from .interactions import ApplicationCommand, Snowflake
28
28
 
29
29
  # Discord API constants
@@ -40,12 +40,27 @@ class HTTPClient:
40
40
  token: str,
41
41
  client_session: Optional[aiohttp.ClientSession] = None,
42
42
  verbose: bool = False,
43
+ **session_kwargs: Any,
43
44
  ):
45
+ """Create a new HTTP client.
46
+
47
+ Parameters
48
+ ----------
49
+ token:
50
+ Bot token for authentication.
51
+ client_session:
52
+ Optional existing :class:`aiohttp.ClientSession`.
53
+ verbose:
54
+ If ``True``, log HTTP requests and responses.
55
+ **session_kwargs:
56
+ Additional options forwarded to :class:`aiohttp.ClientSession`, such
57
+ as ``proxy`` or ``connector``.
58
+ """
59
+
44
60
  self.token = token
45
- self._session: Optional[aiohttp.ClientSession] = (
46
- client_session # Can be externally managed
47
- )
48
- self.user_agent = f"DiscordBot (https://github.com/yourusername/disagreement, {__version__})" # Customize URL
61
+ self._session: Optional[aiohttp.ClientSession] = client_session
62
+ self._session_kwargs: Dict[str, Any] = session_kwargs
63
+ self.user_agent = f"DiscordBot (https://github.com/Slipstreamm/disagreement, {__version__})" # Customize URL
49
64
 
50
65
  self.verbose = verbose
51
66
 
@@ -53,7 +68,7 @@ class HTTPClient:
53
68
 
54
69
  async def _ensure_session(self):
55
70
  if self._session is None or self._session.closed:
56
- self._session = aiohttp.ClientSession()
71
+ self._session = aiohttp.ClientSession(**self._session_kwargs)
57
72
 
58
73
  async def close(self):
59
74
  """Closes the underlying aiohttp.ClientSession."""
@@ -409,6 +424,30 @@ class HTTPClient:
409
424
  """Fetches a channel by ID."""
410
425
  return await self.request("GET", f"/channels/{channel_id}")
411
426
 
427
+ async def get_channel_invites(
428
+ self, channel_id: "Snowflake"
429
+ ) -> List[Dict[str, Any]]:
430
+ """Fetches the invites for a channel."""
431
+
432
+ return await self.request("GET", f"/channels/{channel_id}/invites")
433
+
434
+ async def create_invite(
435
+ self, channel_id: "Snowflake", payload: Dict[str, Any]
436
+ ) -> "Invite":
437
+ """Creates an invite for a channel."""
438
+
439
+ data = await self.request(
440
+ "POST", f"/channels/{channel_id}/invites", payload=payload
441
+ )
442
+ from .models import Invite
443
+
444
+ return Invite.from_dict(data)
445
+
446
+ async def delete_invite(self, code: str) -> None:
447
+ """Deletes an invite by code."""
448
+
449
+ await self.request("DELETE", f"/invites/{code}")
450
+
412
451
  async def create_webhook(
413
452
  self, channel_id: "Snowflake", payload: Dict[str, Any]
414
453
  ) -> "Webhook":
@@ -589,6 +628,87 @@ class HTTPClient:
589
628
  """Fetches a guild object for a given guild ID."""
590
629
  return await self.request("GET", f"/guilds/{guild_id}")
591
630
 
631
+ async def get_guild_templates(self, guild_id: "Snowflake") -> List[Dict[str, Any]]:
632
+ """Fetches all templates for the given guild."""
633
+ return await self.request("GET", f"/guilds/{guild_id}/templates")
634
+
635
+ async def create_guild_template(
636
+ self, guild_id: "Snowflake", payload: Dict[str, Any]
637
+ ) -> Dict[str, Any]:
638
+ """Creates a guild template."""
639
+ return await self.request(
640
+ "POST", f"/guilds/{guild_id}/templates", payload=payload
641
+ )
642
+
643
+ async def sync_guild_template(
644
+ self, guild_id: "Snowflake", template_code: str
645
+ ) -> Dict[str, Any]:
646
+ """Syncs a guild template to the guild's current state."""
647
+ return await self.request(
648
+ "PUT",
649
+ f"/guilds/{guild_id}/templates/{template_code}",
650
+ )
651
+
652
+ async def delete_guild_template(
653
+ self, guild_id: "Snowflake", template_code: str
654
+ ) -> None:
655
+ """Deletes a guild template."""
656
+ await self.request("DELETE", f"/guilds/{guild_id}/templates/{template_code}")
657
+
658
+ async def get_guild_scheduled_events(
659
+ self, guild_id: "Snowflake"
660
+ ) -> List[Dict[str, Any]]:
661
+ """Returns a list of scheduled events for the guild."""
662
+
663
+ return await self.request("GET", f"/guilds/{guild_id}/scheduled-events")
664
+
665
+ async def get_guild_scheduled_event(
666
+ self, guild_id: "Snowflake", event_id: "Snowflake"
667
+ ) -> Dict[str, Any]:
668
+ """Returns a guild scheduled event."""
669
+
670
+ return await self.request(
671
+ "GET", f"/guilds/{guild_id}/scheduled-events/{event_id}"
672
+ )
673
+
674
+ async def create_guild_scheduled_event(
675
+ self, guild_id: "Snowflake", payload: Dict[str, Any]
676
+ ) -> Dict[str, Any]:
677
+ """Creates a guild scheduled event."""
678
+
679
+ return await self.request(
680
+ "POST", f"/guilds/{guild_id}/scheduled-events", payload=payload
681
+ )
682
+
683
+ async def edit_guild_scheduled_event(
684
+ self, guild_id: "Snowflake", event_id: "Snowflake", payload: Dict[str, Any]
685
+ ) -> Dict[str, Any]:
686
+ """Edits a guild scheduled event."""
687
+
688
+ return await self.request(
689
+ "PATCH",
690
+ f"/guilds/{guild_id}/scheduled-events/{event_id}",
691
+ payload=payload,
692
+ )
693
+
694
+ async def delete_guild_scheduled_event(
695
+ self, guild_id: "Snowflake", event_id: "Snowflake"
696
+ ) -> None:
697
+ """Deletes a guild scheduled event."""
698
+
699
+ await self.request("DELETE", f"/guilds/{guild_id}/scheduled-events/{event_id}")
700
+
701
+ async def get_audit_logs(
702
+ self, guild_id: "Snowflake", **filters: Any
703
+ ) -> Dict[str, Any]:
704
+ """Fetches audit log entries for a guild."""
705
+ params = {k: v for k, v in filters.items() if v is not None}
706
+ return await self.request(
707
+ "GET",
708
+ f"/guilds/{guild_id}/audit-logs",
709
+ params=params if params else None,
710
+ )
711
+
592
712
  # Add other methods like:
593
713
  # async def get_guild(self, guild_id: str) -> Dict[str, Any]: ...
594
714
  # async def create_reaction(self, channel_id: str, message_id: str, emoji: str) -> None: ...
@@ -873,3 +993,49 @@ class HTTPClient:
873
993
  async def trigger_typing(self, channel_id: str) -> None:
874
994
  """Sends a typing indicator to the specified channel."""
875
995
  await self.request("POST", f"/channels/{channel_id}/typing")
996
+
997
+ async def start_stage_instance(
998
+ self, payload: Dict[str, Any], reason: Optional[str] = None
999
+ ) -> "StageInstance":
1000
+ """Starts a stage instance."""
1001
+
1002
+ headers = {"X-Audit-Log-Reason": reason} if reason else None
1003
+ data = await self.request(
1004
+ "POST", "/stage-instances", payload=payload, custom_headers=headers
1005
+ )
1006
+ from .models import StageInstance
1007
+
1008
+ return StageInstance(data)
1009
+
1010
+ async def edit_stage_instance(
1011
+ self,
1012
+ channel_id: "Snowflake",
1013
+ payload: Dict[str, Any],
1014
+ reason: Optional[str] = None,
1015
+ ) -> "StageInstance":
1016
+ """Edits an existing stage instance."""
1017
+
1018
+ headers = {"X-Audit-Log-Reason": reason} if reason else None
1019
+ data = await self.request(
1020
+ "PATCH",
1021
+ f"/stage-instances/{channel_id}",
1022
+ payload=payload,
1023
+ custom_headers=headers,
1024
+ )
1025
+ from .models import StageInstance
1026
+
1027
+ return StageInstance(data)
1028
+
1029
+ async def end_stage_instance(
1030
+ self, channel_id: "Snowflake", reason: Optional[str] = None
1031
+ ) -> None:
1032
+ """Ends a stage instance."""
1033
+
1034
+ headers = {"X-Audit-Log-Reason": reason} if reason else None
1035
+ await self.request(
1036
+ "DELETE", f"/stage-instances/{channel_id}", custom_headers=headers
1037
+ )
1038
+
1039
+ async def get_voice_regions(self) -> List[Dict[str, Any]]:
1040
+ """Returns available voice regions."""
1041
+ return await self.request("GET", "/voice/regions")
disagreement/models.py CHANGED
@@ -6,6 +6,7 @@ Data models for Discord objects.
6
6
 
7
7
  import asyncio
8
8
  import json
9
+ from dataclasses import dataclass
9
10
  from typing import Any, AsyncIterator, Dict, List, Optional, TYPE_CHECKING, Union
10
11
 
11
12
  import aiohttp # pylint: disable=import-error
@@ -22,6 +23,9 @@ from .enums import ( # These enums will need to be defined in disagreement/enum
22
23
  ChannelType,
23
24
  ComponentType,
24
25
  ButtonStyle, # Added for Button
26
+ GuildScheduledEventPrivacyLevel,
27
+ GuildScheduledEventStatus,
28
+ GuildScheduledEventEntityType,
25
29
  # SelectMenuType will be part of ComponentType or a new enum if needed
26
30
  )
27
31
  from .permissions import Permissions
@@ -1159,6 +1163,85 @@ class VoiceChannel(Channel):
1159
1163
  return f"<VoiceChannel id='{self.id}' name='{self.name}' guild_id='{self.guild_id}'>"
1160
1164
 
1161
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
+
1162
1245
  class CategoryChannel(Channel):
1163
1246
  """Represents a guild category channel."""
1164
1247
 
@@ -1433,6 +1516,33 @@ class Webhook:
1433
1516
  return self._client.parse_message(message_data)
1434
1517
 
1435
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
+
1436
1546
  # --- Message Components ---
1437
1547
 
1438
1548
 
@@ -1978,6 +2088,77 @@ class Reaction:
1978
2088
  return f"<Reaction message_id='{self.message_id}' user_id='{self.user_id}' emoji='{emoji_value}'>"
1979
2089
 
1980
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
+
1981
2162
  class GuildMemberRemove:
1982
2163
  """Represents a GUILD_MEMBER_REMOVE event."""
1983
2164
 
@@ -2036,6 +2217,25 @@ class GuildRoleUpdate:
2036
2217
  return f"<GuildRoleUpdate guild_id='{self.guild_id}' role_id='{self.role.id}'>"
2037
2218
 
2038
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
+
2039
2239
  def channel_factory(data: Dict[str, Any], client: "Client") -> Channel:
2040
2240
  """Create a channel object from raw API data."""
2041
2241
  channel_type = data.get("type")
@@ -2045,11 +2245,10 @@ def channel_factory(data: Dict[str, Any], client: "Client") -> Channel:
2045
2245
  ChannelType.GUILD_ANNOUNCEMENT.value,
2046
2246
  ):
2047
2247
  return TextChannel(data, client)
2048
- if channel_type in (
2049
- ChannelType.GUILD_VOICE.value,
2050
- ChannelType.GUILD_STAGE_VOICE.value,
2051
- ):
2248
+ if channel_type == ChannelType.GUILD_VOICE.value:
2052
2249
  return VoiceChannel(data, client)
2250
+ if channel_type == ChannelType.GUILD_STAGE_VOICE.value:
2251
+ return StageChannel(data, client)
2053
2252
  if channel_type == ChannelType.GUILD_CATEGORY.value:
2054
2253
  return CategoryChannel(data, client)
2055
2254
  if channel_type in (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: disagreement
3
- Version: 0.1.0rc3
3
+ Version: 0.2.0rc1
4
4
  Summary: A Python library for the Discord API.
5
5
  Author-email: Slipstream <me@slipstreamm.dev>
6
6
  License: BSD 3-Clause
@@ -119,6 +119,22 @@ setup_logging(logging.INFO)
119
119
  setup_logging(logging.DEBUG, file="bot.log")
120
120
  ```
121
121
 
122
+ ### HTTP Session Options
123
+
124
+ Pass additional keyword arguments to ``aiohttp.ClientSession`` using the
125
+ ``http_options`` parameter when constructing :class:`disagreement.Client`:
126
+
127
+ ```python
128
+ client = disagreement.Client(
129
+ token=token,
130
+ http_options={"proxy": "http://localhost:8080"},
131
+ )
132
+ ```
133
+
134
+ These options are forwarded to ``HTTPClient`` when it creates the underlying
135
+ ``aiohttp.ClientSession``. You can specify a custom ``connector`` or any other
136
+ session parameter supported by ``aiohttp``.
137
+
122
138
  ### Defining Subcommands with `AppCommandGroup`
123
139
 
124
140
  ```python