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 +2 -2
- disagreement/client.py +198 -2
- disagreement/color.py +100 -1
- disagreement/enums.py +63 -0
- disagreement/ext/app_commands/commands.py +0 -2
- disagreement/ext/app_commands/handler.py +101 -30
- disagreement/ext/commands/__init__.py +4 -0
- disagreement/ext/commands/core.py +51 -1
- disagreement/ext/commands/decorators.py +27 -0
- disagreement/ext/commands/errors.py +8 -0
- disagreement/gateway.py +11 -0
- disagreement/http.py +172 -6
- disagreement/models.py +203 -4
- {disagreement-0.1.0rc3.dist-info → disagreement-0.2.0rc1.dist-info}/METADATA +17 -1
- {disagreement-0.1.0rc3.dist-info → disagreement-0.2.0rc1.dist-info}/RECORD +18 -18
- {disagreement-0.1.0rc3.dist-info → disagreement-0.2.0rc1.dist-info}/WHEEL +0 -0
- {disagreement-0.1.0rc3.dist-info → disagreement-0.2.0rc1.dist-info}/licenses/LICENSE +0 -0
- {disagreement-0.1.0rc3.dist-info → disagreement-0.2.0rc1.dist-info}/top_level.txt +0 -0
@@ -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
|
-
|
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
|
-
|
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
|
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.
|
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
|