disagreement 0.1.0rc2__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.
disagreement/__init__.py CHANGED
@@ -14,10 +14,10 @@ __title__ = "disagreement"
14
14
  __author__ = "Slipstream"
15
15
  __license__ = "BSD 3-Clause License"
16
16
  __copyright__ = "Copyright 2025 Slipstream"
17
- __version__ = "0.1.0rc2"
17
+ __version__ = "0.2.0rc1"
18
18
 
19
19
  from .client import Client, AutoShardedClient
20
- from .models import Message, User, Reaction
20
+ from .models import Message, User, Reaction, AuditLogEntry
21
21
  from .voice_client import VoiceClient
22
22
  from .audio import AudioSource, FFmpegAudioSource
23
23
  from .typing import Typing
@@ -35,7 +35,11 @@ from .enums import GatewayIntent, GatewayOpcode # Export enums
35
35
  from .error_handler import setup_global_error_handler
36
36
  from .hybrid_context import HybridContext
37
37
  from .ext import tasks
38
+ from .logging_config import setup_logging
38
39
 
39
- # Set up logging if desired
40
- # import logging
41
- # logging.getLogger(__name__).addHandler(logging.NullHandler())
40
+ import logging
41
+
42
+
43
+ # Configure a default logger if none has been configured yet
44
+ if not logging.getLogger().hasHandlers():
45
+ setup_logging(logging.INFO)
disagreement/client.py CHANGED
@@ -12,6 +12,7 @@ from typing import (
12
12
  Any,
13
13
  TYPE_CHECKING,
14
14
  Awaitable,
15
+ AsyncIterator,
15
16
  Union,
16
17
  List,
17
18
  Dict,
@@ -22,7 +23,7 @@ from .http import HTTPClient
22
23
  from .gateway import GatewayClient
23
24
  from .shard_manager import ShardManager
24
25
  from .event_dispatcher import EventDispatcher
25
- from .enums import GatewayIntent, InteractionType, GatewayOpcode
26
+ from .enums import GatewayIntent, InteractionType, GatewayOpcode, VoiceRegion
26
27
  from .errors import DisagreementException, AuthenticationError
27
28
  from .typing import Typing
28
29
  from .ext.commands.core import CommandHandler
@@ -50,6 +51,10 @@ if TYPE_CHECKING:
50
51
  Thread,
51
52
  DMChannel,
52
53
  Webhook,
54
+ GuildTemplate,
55
+ ScheduledEvent,
56
+ AuditLogEntry,
57
+ Invite,
53
58
  )
54
59
  from .ui.view import View
55
60
  from .enums import ChannelType as EnumChannelType
@@ -72,6 +77,9 @@ class Client:
72
77
  command_prefix (Union[str, List[str], Callable[['Client', Message], Union[str, List[str]]]]):
73
78
  The prefix(es) for commands. Defaults to '!'.
74
79
  verbose (bool): If True, print raw HTTP and Gateway traffic for debugging.
80
+ http_options (Optional[Dict[str, Any]]): Extra options passed to
81
+ :class:`HTTPClient` for creating the internal
82
+ :class:`aiohttp.ClientSession`.
75
83
  """
76
84
 
77
85
  def __init__(
@@ -88,6 +96,7 @@ class Client:
88
96
  shard_count: Optional[int] = None,
89
97
  gateway_max_retries: int = 5,
90
98
  gateway_max_backoff: float = 60.0,
99
+ http_options: Optional[Dict[str, Any]] = None,
91
100
  ):
92
101
  if not token:
93
102
  raise ValueError("A bot token must be provided.")
@@ -101,7 +110,11 @@ class Client:
101
110
  setup_global_error_handler(self.loop)
102
111
 
103
112
  self.verbose: bool = verbose
104
- self._http: HTTPClient = HTTPClient(token=self.token, verbose=verbose)
113
+ self._http: HTTPClient = HTTPClient(
114
+ token=self.token,
115
+ verbose=verbose,
116
+ **(http_options or {}),
117
+ )
105
118
  self._event_dispatcher: EventDispatcher = EventDispatcher(client_instance=self)
106
119
  self._gateway: Optional[GatewayClient] = (
107
120
  None # Initialized in run() or connect()
@@ -123,14 +136,10 @@ class Client:
123
136
 
124
137
  self._closed: bool = False
125
138
  self._ready_event: asyncio.Event = asyncio.Event()
126
- self.application_id: Optional[Snowflake] = None # For Application Commands
127
139
  self.user: Optional["User"] = (
128
140
  None # The bot's own user object, populated on READY
129
141
  )
130
142
 
131
- # Initialize AppCommandHandler
132
- self.app_command_handler: AppCommandHandler = AppCommandHandler(client=self)
133
-
134
143
  # Internal Caches
135
144
  self._guilds: Dict[Snowflake, "Guild"] = {}
136
145
  self._channels: Dict[Snowflake, "Channel"] = (
@@ -702,6 +711,40 @@ class Client:
702
711
  self._webhooks[webhook.id] = webhook
703
712
  return webhook
704
713
 
714
+ def parse_template(self, data: Dict[str, Any]) -> "GuildTemplate":
715
+ """Parses template data into a GuildTemplate object."""
716
+
717
+ from .models import GuildTemplate
718
+
719
+ return GuildTemplate(data, client_instance=self)
720
+
721
+ def parse_scheduled_event(self, data: Dict[str, Any]) -> "ScheduledEvent":
722
+ """Parses scheduled event data and updates cache."""
723
+
724
+ from .models import ScheduledEvent
725
+
726
+ event = ScheduledEvent(data, client_instance=self)
727
+ # Cache by ID under guild if guild cache exists
728
+ guild = self._guilds.get(event.guild_id)
729
+ if guild is not None:
730
+ events = getattr(guild, "_scheduled_events", {})
731
+ events[event.id] = event
732
+ setattr(guild, "_scheduled_events", events)
733
+ return event
734
+
735
+ def parse_audit_log_entry(self, data: Dict[str, Any]) -> "AuditLogEntry":
736
+ """Parses audit log entry data."""
737
+ from .models import AuditLogEntry
738
+
739
+ return AuditLogEntry(data, client_instance=self)
740
+
741
+ def parse_invite(self, data: Dict[str, Any]) -> "Invite":
742
+ """Parses invite data into an :class:`Invite`."""
743
+
744
+ from .models import Invite
745
+
746
+ return Invite.from_dict(data)
747
+
705
748
  async def fetch_user(self, user_id: Snowflake) -> Optional["User"]:
706
749
  """Fetches a user by ID from Discord."""
707
750
  if self._closed:
@@ -1223,6 +1266,31 @@ class Client:
1223
1266
  print(f"Failed to fetch channel {channel_id}: {e}")
1224
1267
  return None
1225
1268
 
1269
+ async def fetch_audit_logs(
1270
+ self, guild_id: Snowflake, **filters: Any
1271
+ ) -> AsyncIterator["AuditLogEntry"]:
1272
+ """Fetch audit log entries for a guild."""
1273
+ if self._closed:
1274
+ raise DisagreementException("Client is closed.")
1275
+
1276
+ data = await self._http.get_audit_logs(guild_id, **filters)
1277
+ for entry in data.get("audit_log_entries", []):
1278
+ yield self.parse_audit_log_entry(entry)
1279
+
1280
+ async def fetch_voice_regions(self) -> List[VoiceRegion]:
1281
+ """Fetches available voice regions."""
1282
+
1283
+ if self._closed:
1284
+ raise DisagreementException("Client is closed.")
1285
+
1286
+ data = await self._http.get_voice_regions()
1287
+ regions = []
1288
+ for region in data:
1289
+ region_id = region.get("id")
1290
+ if region_id:
1291
+ regions.append(VoiceRegion(region_id))
1292
+ return regions
1293
+
1226
1294
  async def create_webhook(
1227
1295
  self, channel_id: Snowflake, payload: Dict[str, Any]
1228
1296
  ) -> "Webhook":
@@ -1253,6 +1321,130 @@ class Client:
1253
1321
 
1254
1322
  await self._http.delete_webhook(webhook_id)
1255
1323
 
1324
+ async def fetch_templates(self, guild_id: Snowflake) -> List["GuildTemplate"]:
1325
+ """|coro| Fetch all templates for a guild."""
1326
+
1327
+ if self._closed:
1328
+ raise DisagreementException("Client is closed.")
1329
+
1330
+ data = await self._http.get_guild_templates(guild_id)
1331
+ return [self.parse_template(t) for t in data]
1332
+
1333
+ async def create_template(
1334
+ self, guild_id: Snowflake, payload: Dict[str, Any]
1335
+ ) -> "GuildTemplate":
1336
+ """|coro| Create a template for a guild."""
1337
+
1338
+ if self._closed:
1339
+ raise DisagreementException("Client is closed.")
1340
+
1341
+ data = await self._http.create_guild_template(guild_id, payload)
1342
+ return self.parse_template(data)
1343
+
1344
+ async def sync_template(
1345
+ self, guild_id: Snowflake, template_code: str
1346
+ ) -> "GuildTemplate":
1347
+ """|coro| Sync a template to the guild's current state."""
1348
+
1349
+ if self._closed:
1350
+ raise DisagreementException("Client is closed.")
1351
+
1352
+ data = await self._http.sync_guild_template(guild_id, template_code)
1353
+ return self.parse_template(data)
1354
+
1355
+ async def delete_template(self, guild_id: Snowflake, template_code: str) -> None:
1356
+ """|coro| Delete a guild template."""
1357
+
1358
+ if self._closed:
1359
+ raise DisagreementException("Client is closed.")
1360
+
1361
+ await self._http.delete_guild_template(guild_id, template_code)
1362
+
1363
+ async def fetch_scheduled_events(
1364
+ self, guild_id: Snowflake
1365
+ ) -> List["ScheduledEvent"]:
1366
+ """|coro| Fetch all scheduled events for a guild."""
1367
+
1368
+ if self._closed:
1369
+ raise DisagreementException("Client is closed.")
1370
+
1371
+ data = await self._http.get_guild_scheduled_events(guild_id)
1372
+ return [self.parse_scheduled_event(ev) for ev in data]
1373
+
1374
+ async def fetch_scheduled_event(
1375
+ self, guild_id: Snowflake, event_id: Snowflake
1376
+ ) -> Optional["ScheduledEvent"]:
1377
+ """|coro| Fetch a single scheduled event."""
1378
+
1379
+ if self._closed:
1380
+ raise DisagreementException("Client is closed.")
1381
+
1382
+ try:
1383
+ data = await self._http.get_guild_scheduled_event(guild_id, event_id)
1384
+ return self.parse_scheduled_event(data)
1385
+ except DisagreementException as e:
1386
+ print(f"Failed to fetch scheduled event {event_id}: {e}")
1387
+ return None
1388
+
1389
+ async def create_scheduled_event(
1390
+ self, guild_id: Snowflake, payload: Dict[str, Any]
1391
+ ) -> "ScheduledEvent":
1392
+ """|coro| Create a scheduled event in a guild."""
1393
+
1394
+ if self._closed:
1395
+ raise DisagreementException("Client is closed.")
1396
+
1397
+ data = await self._http.create_guild_scheduled_event(guild_id, payload)
1398
+ return self.parse_scheduled_event(data)
1399
+
1400
+ async def edit_scheduled_event(
1401
+ self, guild_id: Snowflake, event_id: Snowflake, payload: Dict[str, Any]
1402
+ ) -> "ScheduledEvent":
1403
+ """|coro| Edit an existing scheduled event."""
1404
+
1405
+ if self._closed:
1406
+ raise DisagreementException("Client is closed.")
1407
+
1408
+ data = await self._http.edit_guild_scheduled_event(guild_id, event_id, payload)
1409
+ return self.parse_scheduled_event(data)
1410
+
1411
+ async def delete_scheduled_event(
1412
+ self, guild_id: Snowflake, event_id: Snowflake
1413
+ ) -> None:
1414
+ """|coro| Delete a scheduled event."""
1415
+
1416
+ if self._closed:
1417
+ raise DisagreementException("Client is closed.")
1418
+
1419
+ await self._http.delete_guild_scheduled_event(guild_id, event_id)
1420
+
1421
+ async def create_invite(
1422
+ self, channel_id: Snowflake, payload: Dict[str, Any]
1423
+ ) -> "Invite":
1424
+ """|coro| Create an invite for the given channel."""
1425
+
1426
+ if self._closed:
1427
+ raise DisagreementException("Client is closed.")
1428
+
1429
+ return await self._http.create_invite(channel_id, payload)
1430
+
1431
+ async def delete_invite(self, code: str) -> None:
1432
+ """|coro| Delete an invite by code."""
1433
+
1434
+ if self._closed:
1435
+ raise DisagreementException("Client is closed.")
1436
+
1437
+ await self._http.delete_invite(code)
1438
+
1439
+ async def fetch_invites(self, channel_id: Snowflake) -> List["Invite"]:
1440
+ """|coro| Fetch all invites for a channel."""
1441
+
1442
+ if self._closed:
1443
+ raise DisagreementException("Client is closed.")
1444
+
1445
+ data = await self._http.get_channel_invites(channel_id)
1446
+ return [self.parse_invite(inv) for inv in data]
1447
+
1256
1448
  # --- Application Command Methods ---
1257
1449
  async def process_interaction(self, interaction: Interaction) -> None:
1258
1450
  """Internal method to process an interaction from the gateway."""
disagreement/color.py CHANGED
@@ -46,11 +46,110 @@ class Color:
46
46
  def blue(cls) -> "Color":
47
47
  return cls(0x0000FF)
48
48
 
49
+ # Discord brand colors
50
+ @classmethod
51
+ def blurple(cls) -> "Color":
52
+ """Discord brand blurple (#5865F2)."""
53
+ return cls(0x5865F2)
54
+
55
+ @classmethod
56
+ def light_blurple(cls) -> "Color":
57
+ """Light blurple used by Discord (#E0E3FF)."""
58
+ return cls(0xE0E3FF)
59
+
60
+ @classmethod
61
+ def legacy_blurple(cls) -> "Color":
62
+ """Legacy Discord blurple (#7289DA)."""
63
+ return cls(0x7289DA)
64
+
65
+ # Additional assorted colors
66
+ @classmethod
67
+ def teal(cls) -> "Color":
68
+ return cls(0x1ABC9C)
69
+
70
+ @classmethod
71
+ def dark_teal(cls) -> "Color":
72
+ return cls(0x11806A)
73
+
74
+ @classmethod
75
+ def brand_green(cls) -> "Color":
76
+ return cls(0x57F287)
77
+
78
+ @classmethod
79
+ def dark_green(cls) -> "Color":
80
+ return cls(0x206694)
81
+
82
+ @classmethod
83
+ def orange(cls) -> "Color":
84
+ return cls(0xE67E22)
85
+
86
+ @classmethod
87
+ def dark_orange(cls) -> "Color":
88
+ return cls(0xA84300)
89
+
90
+ @classmethod
91
+ def brand_red(cls) -> "Color":
92
+ return cls(0xED4245)
93
+
94
+ @classmethod
95
+ def dark_red(cls) -> "Color":
96
+ return cls(0x992D22)
97
+
98
+ @classmethod
99
+ def magenta(cls) -> "Color":
100
+ return cls(0xE91E63)
101
+
102
+ @classmethod
103
+ def dark_magenta(cls) -> "Color":
104
+ return cls(0xAD1457)
105
+
106
+ @classmethod
107
+ def purple(cls) -> "Color":
108
+ return cls(0x9B59B6)
109
+
110
+ @classmethod
111
+ def dark_purple(cls) -> "Color":
112
+ return cls(0x71368A)
113
+
114
+ @classmethod
115
+ def yellow(cls) -> "Color":
116
+ return cls(0xF1C40F)
117
+
118
+ @classmethod
119
+ def dark_gold(cls) -> "Color":
120
+ return cls(0xC27C0E)
121
+
122
+ @classmethod
123
+ def light_gray(cls) -> "Color":
124
+ return cls(0x99AAB5)
125
+
126
+ @classmethod
127
+ def dark_gray(cls) -> "Color":
128
+ return cls(0x2C2F33)
129
+
130
+ @classmethod
131
+ def lighter_gray(cls) -> "Color":
132
+ return cls(0xBFBFBF)
133
+
134
+ @classmethod
135
+ def darker_gray(cls) -> "Color":
136
+ return cls(0x23272A)
137
+
138
+ @classmethod
139
+ def black(cls) -> "Color":
140
+ return cls(0x000000)
141
+
142
+ @classmethod
143
+ def white(cls) -> "Color":
144
+ return cls(0xFFFFFF)
145
+
49
146
  def to_rgb(self) -> tuple[int, int, int]:
50
147
  return ((self.value >> 16) & 0xFF, (self.value >> 8) & 0xFF, self.value & 0xFF)
51
148
 
52
149
  @classmethod
53
- def parse(cls, value: "Color | int | str | tuple[int, int, int] | None") -> "Color | None":
150
+ def parse(
151
+ cls, value: "Color | int | str | tuple[int, int, int] | None"
152
+ ) -> "Color | None":
54
153
  """Convert ``value`` to a :class:`Color` instance.
55
154
 
56
155
  Parameters
disagreement/enums.py CHANGED
@@ -278,6 +278,62 @@ class GuildFeature(str, Enum): # Changed from IntEnum to Enum
278
278
  return str(value)
279
279
 
280
280
 
281
+ # --- Guild Scheduled Event Enums ---
282
+
283
+
284
+ class GuildScheduledEventPrivacyLevel(IntEnum):
285
+ """Privacy level for a scheduled event."""
286
+
287
+ GUILD_ONLY = 2
288
+
289
+
290
+ class GuildScheduledEventStatus(IntEnum):
291
+ """Status of a scheduled event."""
292
+
293
+ SCHEDULED = 1
294
+ ACTIVE = 2
295
+ COMPLETED = 3
296
+ CANCELED = 4
297
+
298
+
299
+ class GuildScheduledEventEntityType(IntEnum):
300
+ """Entity type for a scheduled event."""
301
+
302
+ STAGE_INSTANCE = 1
303
+ VOICE = 2
304
+ EXTERNAL = 3
305
+
306
+
307
+ class VoiceRegion(str, Enum):
308
+ """Voice region identifier."""
309
+
310
+ AMSTERDAM = "amsterdam"
311
+ BRAZIL = "brazil"
312
+ DUBAI = "dubai"
313
+ EU_CENTRAL = "eu-central"
314
+ EU_WEST = "eu-west"
315
+ EUROPE = "europe"
316
+ FRANKFURT = "frankfurt"
317
+ HONGKONG = "hongkong"
318
+ INDIA = "india"
319
+ JAPAN = "japan"
320
+ RUSSIA = "russia"
321
+ SINGAPORE = "singapore"
322
+ SOUTHAFRICA = "southafrica"
323
+ SOUTH_KOREA = "south-korea"
324
+ SYDNEY = "sydney"
325
+ US_CENTRAL = "us-central"
326
+ US_EAST = "us-east"
327
+ US_SOUTH = "us-south"
328
+ US_WEST = "us-west"
329
+ VIP_US_EAST = "vip-us-east"
330
+ VIP_US_WEST = "vip-us-west"
331
+
332
+ @classmethod
333
+ def _missing_(cls, value): # type: ignore
334
+ return str(value)
335
+
336
+
281
337
  # --- Channel Enums ---
282
338
 
283
339
 
@@ -305,6 +361,13 @@ class ChannelType(IntEnum):
305
361
  GUILD_MEDIA = 16 # (Still in development) a channel that can only contain media
306
362
 
307
363
 
364
+ class StageInstancePrivacyLevel(IntEnum):
365
+ """Privacy level of a stage instance."""
366
+
367
+ PUBLIC = 1
368
+ GUILD_ONLY = 2
369
+
370
+
308
371
  class OverwriteType(IntEnum):
309
372
  """Type of target for a permission overwrite."""
310
373
 
@@ -202,8 +202,6 @@ class MessageCommand(AppCommand):
202
202
  super().__init__(callback, type=ApplicationCommandType.MESSAGE, **kwargs)
203
203
 
204
204
 
205
-
206
-
207
205
  class AppCommandGroup:
208
206
  """
209
207
  Represents a group of application commands (subcommands or subcommand groups).