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.
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.0rc3"
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
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()
@@ -698,6 +711,40 @@ class Client:
698
711
  self._webhooks[webhook.id] = webhook
699
712
  return webhook
700
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
+
701
748
  async def fetch_user(self, user_id: Snowflake) -> Optional["User"]:
702
749
  """Fetches a user by ID from Discord."""
703
750
  if self._closed:
@@ -1219,6 +1266,31 @@ class Client:
1219
1266
  print(f"Failed to fetch channel {channel_id}: {e}")
1220
1267
  return None
1221
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
+
1222
1294
  async def create_webhook(
1223
1295
  self, channel_id: Snowflake, payload: Dict[str, Any]
1224
1296
  ) -> "Webhook":
@@ -1249,6 +1321,130 @@ class Client:
1249
1321
 
1250
1322
  await self._http.delete_webhook(webhook_id)
1251
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
+
1252
1448
  # --- Application Command Methods ---
1253
1449
  async def process_interaction(self, interaction: Interaction) -> None:
1254
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).
@@ -1,7 +1,9 @@
1
1
  # disagreement/ext/app_commands/handler.py
2
2
 
3
3
  import inspect
4
+ import json
4
5
  import logging
6
+ import os
5
7
  from typing import (
6
8
  TYPE_CHECKING,
7
9
  Dict,
@@ -67,6 +69,8 @@ if not TYPE_CHECKING:
67
69
 
68
70
  logger = logging.getLogger(__name__)
69
71
 
72
+ COMMANDS_CACHE_FILE = ".disagreement_commands.json"
73
+
70
74
 
71
75
  class AppCommandHandler:
72
76
  """
@@ -84,6 +88,33 @@ class AppCommandHandler:
84
88
  self._app_command_groups: Dict[str, AppCommandGroup] = {}
85
89
  self._converter_registry: Dict[type, type] = {}
86
90
 
91
+ def _load_cached_ids(self) -> Dict[str, Dict[str, str]]:
92
+ try:
93
+ with open(COMMANDS_CACHE_FILE, "r", encoding="utf-8") as fp:
94
+ return json.load(fp)
95
+ except FileNotFoundError:
96
+ return {}
97
+ except json.JSONDecodeError:
98
+ logger.warning("Invalid command cache file. Ignoring.")
99
+ return {}
100
+
101
+ def _save_cached_ids(self, data: Dict[str, Dict[str, str]]) -> None:
102
+ try:
103
+ with open(COMMANDS_CACHE_FILE, "w", encoding="utf-8") as fp:
104
+ json.dump(data, fp, indent=2)
105
+ except Exception as e: # pragma: no cover - logging only
106
+ logger.error("Failed to write command cache: %s", e)
107
+
108
+ def clear_stored_registrations(self) -> None:
109
+ """Remove persisted command registration data."""
110
+ if os.path.exists(COMMANDS_CACHE_FILE):
111
+ os.remove(COMMANDS_CACHE_FILE)
112
+
113
+ def migrate_stored_registrations(self, new_path: str) -> None:
114
+ """Move stored registrations to ``new_path``."""
115
+ if os.path.exists(COMMANDS_CACHE_FILE):
116
+ os.replace(COMMANDS_CACHE_FILE, new_path)
117
+
87
118
  def add_command(self, command: Union["AppCommand", "AppCommandGroup"]) -> None:
88
119
  """Adds an application command or a command group to the handler."""
89
120
  if isinstance(command, AppCommandGroup):
@@ -564,11 +595,13 @@ class AppCommandHandler:
564
595
  Synchronizes (registers/updates) all application commands with Discord.
565
596
  If guild_id is provided, syncs commands for that guild. Otherwise, syncs global commands.
566
597
  """
567
- commands_to_sync: List[Dict[str, Any]] = []
598
+ cache = self._load_cached_ids()
599
+ scope_key = str(guild_id) if guild_id else "global"
600
+ stored = cache.get(scope_key, {})
568
601
 
569
- # Collect commands based on scope (global or specific guild)
570
- # This needs to be more sophisticated to handle guild_ids on commands/groups
602
+ current_payloads: Dict[str, Dict[str, Any]] = {}
571
603
 
604
+ # Collect commands based on scope (global or specific guild)
572
605
  source_commands = (
573
606
  list(self._slash_commands.values())
574
607
  + list(self._user_commands.values())
@@ -577,26 +610,22 @@ class AppCommandHandler:
577
610
  )
578
611
 
579
612
  for cmd_or_group in source_commands:
580
- # Determine if this command/group should be synced for the current scope
581
613
  is_guild_specific_command = (
582
614
  cmd_or_group.guild_ids is not None and len(cmd_or_group.guild_ids) > 0
583
615
  )
584
616
 
585
- if guild_id: # Syncing for a specific guild
586
- # Skip if not a guild-specific command OR if it's for a different guild
617
+ if guild_id:
587
618
  if not is_guild_specific_command or (
588
619
  cmd_or_group.guild_ids is not None
589
620
  and guild_id not in cmd_or_group.guild_ids
590
621
  ):
591
622
  continue
592
- else: # Syncing global commands
623
+ else:
593
624
  if is_guild_specific_command:
594
- continue # Skip guild-specific commands when syncing global
625
+ continue
595
626
 
596
- # Use the to_dict() method from AppCommand or AppCommandGroup
597
627
  try:
598
- payload = cmd_or_group.to_dict()
599
- commands_to_sync.append(payload)
628
+ current_payloads[cmd_or_group.name] = cmd_or_group.to_dict()
600
629
  except AttributeError:
601
630
  logger.warning(
602
631
  "Command or group '%s' does not have a to_dict() method. Skipping.",
@@ -609,32 +638,74 @@ class AppCommandHandler:
609
638
  e,
610
639
  )
611
640
 
612
- if not commands_to_sync:
641
+ if not current_payloads:
613
642
  logger.info(
614
643
  "No commands to sync for %s scope.",
615
644
  f"guild {guild_id}" if guild_id else "global",
616
645
  )
617
646
  return
618
647
 
648
+ names_current = set(current_payloads)
649
+ names_stored = set(stored)
650
+
651
+ to_delete = names_stored - names_current
652
+ to_create = names_current - names_stored
653
+ to_update = names_current & names_stored
654
+
655
+ if not to_delete and not to_create and not to_update:
656
+ logger.info(
657
+ "Application commands already up to date for %s scope.", scope_key
658
+ )
659
+ return
660
+
619
661
  try:
620
- if guild_id:
621
- logger.info(
622
- "Syncing %s commands for guild %s...",
623
- len(commands_to_sync),
624
- guild_id,
625
- )
626
- await self.client._http.bulk_overwrite_guild_application_commands(
627
- application_id, guild_id, commands_to_sync
628
- )
629
- else:
630
- logger.info(
631
- "Syncing %s global commands...",
632
- len(commands_to_sync),
633
- )
634
- await self.client._http.bulk_overwrite_global_application_commands(
635
- application_id, commands_to_sync
636
- )
662
+ for name in to_delete:
663
+ cmd_id = stored[name]
664
+ if guild_id:
665
+ await self.client._http.delete_guild_application_command(
666
+ application_id, guild_id, cmd_id
667
+ )
668
+ else:
669
+ await self.client._http.delete_global_application_command(
670
+ application_id, cmd_id
671
+ )
672
+
673
+ new_ids: Dict[str, str] = {}
674
+ for name in to_create:
675
+ payload = current_payloads[name]
676
+ if guild_id:
677
+ result = await self.client._http.create_guild_application_command(
678
+ application_id, guild_id, payload
679
+ )
680
+ else:
681
+ result = await self.client._http.create_global_application_command(
682
+ application_id, payload
683
+ )
684
+ if result.id:
685
+ new_ids[name] = str(result.id)
686
+
687
+ for name in to_update:
688
+ payload = current_payloads[name]
689
+ cmd_id = stored[name]
690
+ if guild_id:
691
+ await self.client._http.edit_guild_application_command(
692
+ application_id, guild_id, cmd_id, payload
693
+ )
694
+ else:
695
+ await self.client._http.edit_global_application_command(
696
+ application_id, cmd_id, payload
697
+ )
698
+ new_ids[name] = cmd_id
699
+
700
+ final_ids: Dict[str, str] = {}
701
+ for name in names_current:
702
+ if name in new_ids:
703
+ final_ids[name] = new_ids[name]
704
+ else:
705
+ final_ids[name] = stored[name]
706
+
707
+ cache[scope_key] = final_ids
708
+ self._save_cached_ids(cache)
637
709
  logger.info("Command sync successful.")
638
710
  except Exception as e:
639
711
  logger.error("Error syncing application commands: %s", e)
640
- # Consider re-raising or specific error handling
@@ -16,6 +16,7 @@ from .decorators import (
16
16
  check,
17
17
  check_any,
18
18
  cooldown,
19
+ max_concurrency,
19
20
  requires_permissions,
20
21
  )
21
22
  from .errors import (
@@ -28,6 +29,7 @@ from .errors import (
28
29
  CheckAnyFailure,
29
30
  CommandOnCooldown,
30
31
  CommandInvokeError,
32
+ MaxConcurrencyReached,
31
33
  )
32
34
 
33
35
  __all__ = [
@@ -43,6 +45,7 @@ __all__ = [
43
45
  "check",
44
46
  "check_any",
45
47
  "cooldown",
48
+ "max_concurrency",
46
49
  "requires_permissions",
47
50
  # Errors
48
51
  "CommandError",
@@ -54,4 +57,5 @@ __all__ = [
54
57
  "CheckAnyFailure",
55
58
  "CommandOnCooldown",
56
59
  "CommandInvokeError",
60
+ "MaxConcurrencyReached",
57
61
  ]