hikari-arc 0.4.0__py3-none-any.whl → 0.6.0__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.
- arc/__init__.py +55 -5
- arc/abc/__init__.py +32 -1
- arc/abc/client.py +207 -67
- arc/abc/command.py +245 -34
- arc/abc/error_handler.py +33 -2
- arc/abc/hookable.py +24 -0
- arc/abc/limiter.py +61 -0
- arc/abc/option.py +73 -5
- arc/abc/plugin.py +185 -29
- arc/client.py +103 -33
- arc/command/message.py +21 -18
- arc/command/option/attachment.py +9 -5
- arc/command/option/bool.py +9 -6
- arc/command/option/channel.py +9 -5
- arc/command/option/float.py +11 -7
- arc/command/option/int.py +11 -7
- arc/command/option/mentionable.py +9 -5
- arc/command/option/role.py +9 -5
- arc/command/option/str.py +11 -7
- arc/command/option/user.py +9 -5
- arc/command/slash.py +222 -197
- arc/command/user.py +20 -17
- arc/context/autocomplete.py +1 -0
- arc/context/base.py +216 -105
- arc/errors.py +52 -10
- arc/events.py +5 -1
- arc/extension.py +23 -0
- arc/internal/about.py +1 -1
- arc/internal/deprecation.py +3 -4
- arc/internal/options.py +106 -0
- arc/internal/sigparse.py +19 -1
- arc/internal/sync.py +13 -10
- arc/internal/types.py +34 -15
- arc/locale.py +28 -0
- arc/plugin.py +56 -5
- arc/utils/__init__.py +53 -2
- arc/utils/hooks/__init__.py +25 -0
- arc/utils/{hooks.py → hooks/basic.py} +28 -1
- arc/utils/hooks/limiters.py +217 -0
- arc/utils/ratelimiter.py +243 -0
- {hikari_arc-0.4.0.dist-info → hikari_arc-0.6.0.dist-info}/METADATA +13 -8
- hikari_arc-0.6.0.dist-info/RECORD +52 -0
- hikari_arc-0.4.0.dist-info/RECORD +0 -47
- {hikari_arc-0.4.0.dist-info → hikari_arc-0.6.0.dist-info}/LICENSE +0 -0
- {hikari_arc-0.4.0.dist-info → hikari_arc-0.6.0.dist-info}/WHEEL +0 -0
- {hikari_arc-0.4.0.dist-info → hikari_arc-0.6.0.dist-info}/top_level.txt +0 -0
arc/abc/command.py
CHANGED
|
@@ -10,8 +10,10 @@ import hikari
|
|
|
10
10
|
|
|
11
11
|
from arc.abc.error_handler import HasErrorHandler
|
|
12
12
|
from arc.abc.hookable import Hookable, HookResult
|
|
13
|
+
from arc.abc.limiter import LimiterProto
|
|
13
14
|
from arc.abc.option import OptionBase
|
|
14
15
|
from arc.context import AutodeferMode
|
|
16
|
+
from arc.errors import CommandPublishFailedError
|
|
15
17
|
from arc.internal.types import (
|
|
16
18
|
BuilderT,
|
|
17
19
|
ClientT,
|
|
@@ -24,8 +26,10 @@ from arc.internal.types import (
|
|
|
24
26
|
from arc.locale import CommandLocaleRequest
|
|
25
27
|
|
|
26
28
|
if t.TYPE_CHECKING:
|
|
29
|
+
import typing_extensions as te
|
|
30
|
+
|
|
27
31
|
from arc.abc.plugin import PluginBase
|
|
28
|
-
from arc.context import Context
|
|
32
|
+
from arc.context.base import Context
|
|
29
33
|
|
|
30
34
|
|
|
31
35
|
class CommandProto(t.Protocol):
|
|
@@ -46,15 +50,29 @@ class CommandProto(t.Protocol):
|
|
|
46
50
|
def qualified_name(self) -> t.Sequence[str]:
|
|
47
51
|
"""The fully qualified name of this command."""
|
|
48
52
|
|
|
53
|
+
@property
|
|
54
|
+
@abc.abstractmethod
|
|
55
|
+
def display_name(self) -> str:
|
|
56
|
+
"""The display name of this command. This is what is shown in the Discord client.
|
|
57
|
+
|
|
58
|
+
!!! note
|
|
59
|
+
Slash commands can also be mentioned, see [SlashCommand.make_mention][arc.command.SlashCommand.make_mention].
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CallableCommandProto(CommandProto, t.Protocol[ClientT]):
|
|
64
|
+
"""A protocol for any command-like object that can be called directly.
|
|
49
65
|
|
|
50
|
-
|
|
51
|
-
"""
|
|
66
|
+
This includes commands and subcommands, but not groups or subgroups.
|
|
67
|
+
"""
|
|
52
68
|
|
|
53
69
|
name: str
|
|
54
70
|
"""The name of the command."""
|
|
55
71
|
name_localizations: t.Mapping[hikari.Locale, str]
|
|
56
72
|
"""The name of the command in different locales."""
|
|
73
|
+
|
|
57
74
|
callback: CommandCallbackT[ClientT]
|
|
75
|
+
"""The callback to invoke when this command is called."""
|
|
58
76
|
|
|
59
77
|
@property
|
|
60
78
|
@abc.abstractmethod
|
|
@@ -66,6 +84,35 @@ class CallableCommandProto(t.Protocol[ClientT]):
|
|
|
66
84
|
def qualified_name(self) -> t.Sequence[str]:
|
|
67
85
|
"""The fully qualified name of this command."""
|
|
68
86
|
|
|
87
|
+
@property
|
|
88
|
+
@abc.abstractmethod
|
|
89
|
+
def display_name(self) -> str:
|
|
90
|
+
"""The display name of this command. This is what is shown in the Discord client.
|
|
91
|
+
|
|
92
|
+
!!! note
|
|
93
|
+
Slash commands can also be mentioned, see [SlashCommand.make_mention][arc.command.SlashCommand.make_mention].
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
@abc.abstractmethod
|
|
98
|
+
def hooks(self) -> t.MutableSequence[HookT[ClientT]]:
|
|
99
|
+
"""The pre-execution hooks for this command."""
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
@abc.abstractmethod
|
|
103
|
+
def post_hooks(self) -> t.MutableSequence[PostHookT[ClientT]]:
|
|
104
|
+
"""The post-execution hooks for this command."""
|
|
105
|
+
|
|
106
|
+
@abc.abstractmethod
|
|
107
|
+
def reset_all_limiters(self, context: Context[ClientT]) -> None:
|
|
108
|
+
"""Reset all limiters for this command.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
context : Context
|
|
113
|
+
The context to reset the limiters for.
|
|
114
|
+
"""
|
|
115
|
+
|
|
69
116
|
@abc.abstractmethod
|
|
70
117
|
async def __call__(self, ctx: Context[ClientT], *args: t.Any, **kwargs: t.Any) -> None:
|
|
71
118
|
"""Call the callback of the command with the given context and arguments.
|
|
@@ -110,55 +157,92 @@ class CallableCommandProto(t.Protocol[ClientT]):
|
|
|
110
157
|
async def _handle_exception(self, ctx: Context[ClientT], exc: Exception) -> None:
|
|
111
158
|
...
|
|
112
159
|
|
|
160
|
+
@abc.abstractmethod
|
|
113
161
|
def _resolve_hooks(self) -> t.Sequence[HookT[ClientT]]:
|
|
114
162
|
"""Resolve all pre-execution hooks that apply to this object."""
|
|
115
163
|
...
|
|
116
164
|
|
|
165
|
+
@abc.abstractmethod
|
|
117
166
|
def _resolve_post_hooks(self) -> t.Sequence[PostHookT[ClientT]]:
|
|
118
167
|
"""Resolve all post-execution hooks that apply to this object."""
|
|
119
168
|
...
|
|
120
169
|
|
|
121
170
|
|
|
171
|
+
@t.final
|
|
172
|
+
@attr.define(slots=True, kw_only=True, weakref_slot=False)
|
|
173
|
+
class _CommandSettings:
|
|
174
|
+
"""All the command settings that need to propagate and be inherited."""
|
|
175
|
+
|
|
176
|
+
autodefer: AutodeferMode | hikari.UndefinedType
|
|
177
|
+
default_permissions: hikari.Permissions | hikari.UndefinedType
|
|
178
|
+
is_nsfw: bool | hikari.UndefinedType
|
|
179
|
+
is_dm_enabled: bool | hikari.UndefinedType
|
|
180
|
+
|
|
181
|
+
def apply(self, other: te.Self) -> te.Self:
|
|
182
|
+
"""Apply 'other' to this, copying all the non-undefined settings to it."""
|
|
183
|
+
return type(self)(
|
|
184
|
+
autodefer=other.autodefer if other.autodefer is not hikari.UNDEFINED else self.autodefer,
|
|
185
|
+
default_permissions=other.default_permissions
|
|
186
|
+
if other.default_permissions is not hikari.UNDEFINED
|
|
187
|
+
else self.default_permissions,
|
|
188
|
+
is_nsfw=other.is_nsfw if other.is_nsfw is not hikari.UNDEFINED else self.is_nsfw,
|
|
189
|
+
is_dm_enabled=other.is_dm_enabled if other.is_dm_enabled is not hikari.UNDEFINED else self.is_dm_enabled,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
def default(cls) -> te.Self:
|
|
194
|
+
"""Get the default command settings."""
|
|
195
|
+
return cls(autodefer=AutodeferMode.ON, default_permissions=hikari.UNDEFINED, is_nsfw=False, is_dm_enabled=True)
|
|
196
|
+
|
|
197
|
+
|
|
122
198
|
@attr.define(slots=True, kw_only=True)
|
|
123
199
|
class CommandBase(HasErrorHandler[ClientT], Hookable[ClientT], t.Generic[ClientT, BuilderT]):
|
|
124
|
-
"""An abstract base class for all application commands.
|
|
200
|
+
"""An abstract base class for all application commands.
|
|
201
|
+
|
|
202
|
+
This notably does not include subcommands & subgroups as those are in reality options.
|
|
203
|
+
"""
|
|
125
204
|
|
|
126
205
|
name: str
|
|
127
206
|
"""The name of this command."""
|
|
128
207
|
|
|
129
|
-
|
|
130
|
-
"""The client that is handling this command."""
|
|
131
|
-
|
|
132
|
-
_plugin: PluginBase[ClientT] | None = attr.field(init=False, default=None)
|
|
133
|
-
"""The plugin that this command belongs to, if any."""
|
|
134
|
-
|
|
135
|
-
guilds: hikari.UndefinedOr[t.Sequence[hikari.Snowflake]] = hikari.UNDEFINED
|
|
208
|
+
guilds: t.Sequence[hikari.Snowflake] | hikari.UndefinedType = hikari.UNDEFINED
|
|
136
209
|
"""The guilds this command is available in."""
|
|
137
210
|
|
|
138
|
-
|
|
211
|
+
_autodefer: AutodeferMode | hikari.UndefinedType = attr.field(default=hikari.UNDEFINED, alias="autodefer")
|
|
139
212
|
"""If ON, this command will be automatically deferred if it takes longer than 2 seconds to respond."""
|
|
140
213
|
|
|
141
|
-
|
|
214
|
+
_is_dm_enabled: bool | hikari.UndefinedType = attr.field(default=hikari.UNDEFINED, alias="is_dm_enabled")
|
|
142
215
|
"""Whether this command is enabled in DMs."""
|
|
143
216
|
|
|
144
|
-
|
|
217
|
+
_default_permissions: hikari.Permissions | hikari.UndefinedType = attr.field(
|
|
218
|
+
default=hikari.UNDEFINED, alias="default_permissions"
|
|
219
|
+
)
|
|
145
220
|
"""The default permissions for this command.
|
|
146
221
|
Keep in mind that guild administrators can change this, it should only be used to provide safe defaults."""
|
|
147
222
|
|
|
223
|
+
_is_nsfw: bool | hikari.UndefinedType = attr.field(default=hikari.UNDEFINED, alias="is_nsfw")
|
|
224
|
+
"""Whether this command is NSFW. If true, the command will only be available in NSFW channels."""
|
|
225
|
+
|
|
148
226
|
name_localizations: t.Mapping[hikari.Locale, str] = attr.field(factory=dict)
|
|
149
227
|
"""The localizations for this command's name."""
|
|
150
228
|
|
|
151
|
-
is_nsfw: bool = False
|
|
152
|
-
"""Whether this command is NSFW. If true, the command will only be available in NSFW channels."""
|
|
153
|
-
|
|
154
229
|
_instances: dict[hikari.Snowflake | None, hikari.PartialCommand] = attr.field(factory=dict)
|
|
155
230
|
"""A mapping of guild IDs to command instances. None corresponds to the global instance, if any."""
|
|
156
231
|
|
|
232
|
+
_client: ClientT | None = attr.field(init=False, default=None)
|
|
233
|
+
"""The client that is handling this command."""
|
|
234
|
+
|
|
235
|
+
_plugin: PluginBase[ClientT] | None = attr.field(init=False, default=None)
|
|
236
|
+
"""The plugin that this command belongs to, if any."""
|
|
237
|
+
|
|
157
238
|
_error_handler: ErrorHandlerCallbackT[ClientT] | None = attr.field(init=False, default=None)
|
|
239
|
+
"""The error handler for this command."""
|
|
158
240
|
|
|
159
241
|
_hooks: list[HookT[ClientT]] = attr.field(init=False, factory=list)
|
|
242
|
+
"""The pre-execution hooks for this command."""
|
|
160
243
|
|
|
161
244
|
_post_hooks: list[PostHookT[ClientT]] = attr.field(init=False, factory=list)
|
|
245
|
+
"""The post-execution hooks for this command."""
|
|
162
246
|
|
|
163
247
|
@property
|
|
164
248
|
def error_handler(self) -> ErrorHandlerCallbackT[ClientT] | None:
|
|
@@ -195,7 +279,7 @@ class CommandBase(HasErrorHandler[ClientT], Hookable[ClientT], t.Generic[ClientT
|
|
|
195
279
|
"""The client that is handling this command."""
|
|
196
280
|
if self._client is None:
|
|
197
281
|
raise RuntimeError(
|
|
198
|
-
f"Command '{self.
|
|
282
|
+
f"Command '{self.display_name}' was not included in a client, '{type(self).__name__}.client' cannot be accessed until it is included in a client."
|
|
199
283
|
)
|
|
200
284
|
return self._client
|
|
201
285
|
|
|
@@ -204,6 +288,43 @@ class CommandBase(HasErrorHandler[ClientT], Hookable[ClientT], t.Generic[ClientT
|
|
|
204
288
|
"""The plugin that this command belongs to, if any."""
|
|
205
289
|
return self._plugin
|
|
206
290
|
|
|
291
|
+
@property
|
|
292
|
+
def autodefer(self) -> AutodeferMode:
|
|
293
|
+
"""The resolved autodefer configuration for this command."""
|
|
294
|
+
settings = self._resolve_settings()
|
|
295
|
+
return settings.autodefer if settings.autodefer is not hikari.UNDEFINED else AutodeferMode.ON
|
|
296
|
+
|
|
297
|
+
@property
|
|
298
|
+
def is_dm_enabled(self) -> bool:
|
|
299
|
+
"""Whether this command is enabled in DMs."""
|
|
300
|
+
settings = self._resolve_settings()
|
|
301
|
+
return settings.is_dm_enabled if settings.is_dm_enabled is not hikari.UNDEFINED else True
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def default_permissions(self) -> hikari.Permissions | hikari.UndefinedType:
|
|
305
|
+
"""The resolved default permissions for this command."""
|
|
306
|
+
return self._resolve_settings().default_permissions
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def is_nsfw(self) -> bool:
|
|
310
|
+
"""Whether this command is NSFW. If true, the command will only be available in NSFW channels."""
|
|
311
|
+
settings = self._resolve_settings()
|
|
312
|
+
return settings.is_nsfw if settings.is_nsfw is not hikari.UNDEFINED else False
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def instances(self) -> t.Mapping[hikari.Snowflake | None, hikari.PartialCommand]:
|
|
316
|
+
"""A mapping of guild IDs to command instances. None corresponds to the global instance, if any."""
|
|
317
|
+
return self._instances
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
def display_name(self) -> str:
|
|
321
|
+
"""The display name of this command. This is what is shown in the Discord client.
|
|
322
|
+
|
|
323
|
+
!!! note
|
|
324
|
+
Slash commands can also be mentioned, see [SlashCommand.make_mention][arc.command.SlashCommand.make_mention].
|
|
325
|
+
"""
|
|
326
|
+
return self.name
|
|
327
|
+
|
|
207
328
|
def _register_instance(
|
|
208
329
|
self, instance: hikari.PartialCommand, guild: hikari.SnowflakeishOr[hikari.PartialGuild] | None = None
|
|
209
330
|
) -> None:
|
|
@@ -221,6 +342,24 @@ class CommandBase(HasErrorHandler[ClientT], Hookable[ClientT], t.Generic[ClientT
|
|
|
221
342
|
else:
|
|
222
343
|
await self.client._on_error(ctx, exc)
|
|
223
344
|
|
|
345
|
+
def _resolve_settings(self) -> _CommandSettings:
|
|
346
|
+
"""Resolve all settings that apply to this command."""
|
|
347
|
+
if self._plugin:
|
|
348
|
+
settings = self._plugin._resolve_settings()
|
|
349
|
+
elif self._client:
|
|
350
|
+
settings = self._client._cmd_settings
|
|
351
|
+
else:
|
|
352
|
+
settings = _CommandSettings.default()
|
|
353
|
+
|
|
354
|
+
return settings.apply(
|
|
355
|
+
_CommandSettings(
|
|
356
|
+
autodefer=self._autodefer,
|
|
357
|
+
default_permissions=self._default_permissions,
|
|
358
|
+
is_nsfw=self._is_nsfw,
|
|
359
|
+
is_dm_enabled=self._is_dm_enabled,
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
|
|
224
363
|
def _resolve_hooks(self) -> list[HookT[ClientT]]:
|
|
225
364
|
plugin_hooks = self.plugin._resolve_hooks() if self.plugin else []
|
|
226
365
|
return self.client._hooks + plugin_hooks + self._hooks
|
|
@@ -246,13 +385,15 @@ class CommandBase(HasErrorHandler[ClientT], Hookable[ClientT], t.Generic[ClientT
|
|
|
246
385
|
raise RuntimeError("Cannot publish command without a client.")
|
|
247
386
|
|
|
248
387
|
kwargs = self._to_dict()
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
388
|
+
try:
|
|
389
|
+
if self.command_type is hikari.CommandType.SLASH:
|
|
390
|
+
created = await self.client.app.rest.create_slash_command(self.client.application, **kwargs)
|
|
391
|
+
else:
|
|
392
|
+
created = await self.client.app.rest.create_context_menu_command(
|
|
393
|
+
self.client.application, type=self.command_type, **kwargs
|
|
394
|
+
)
|
|
395
|
+
except Exception as e:
|
|
396
|
+
raise CommandPublishFailedError(self, f"Failed to publish command '{self.display_name}'") from e
|
|
256
397
|
|
|
257
398
|
self._instances[hikari.Snowflake(guild) if guild else None] = created
|
|
258
399
|
|
|
@@ -321,8 +462,8 @@ class CommandBase(HasErrorHandler[ClientT], Hookable[ClientT], t.Generic[ClientT
|
|
|
321
462
|
|
|
322
463
|
def _client_remove_hook(self, client: ClientT) -> None:
|
|
323
464
|
"""Called when the client requests the command be removed from it."""
|
|
324
|
-
self._client = None
|
|
325
465
|
self.client._remove_command(self)
|
|
466
|
+
self._client = None
|
|
326
467
|
|
|
327
468
|
def _plugin_include_hook(self, plugin: PluginBase[ClientT]) -> None:
|
|
328
469
|
"""Called when the plugin requests the command be added to it."""
|
|
@@ -360,10 +501,10 @@ class CommandBase(HasErrorHandler[ClientT], Hookable[ClientT], t.Generic[ClientT
|
|
|
360
501
|
try:
|
|
361
502
|
hooks = command._resolve_hooks()
|
|
362
503
|
for hook in hooks:
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
res =
|
|
504
|
+
res = hook(ctx)
|
|
505
|
+
|
|
506
|
+
if inspect.isawaitable(res):
|
|
507
|
+
res = await res
|
|
367
508
|
|
|
368
509
|
res = t.cast(HookResult | None, res)
|
|
369
510
|
|
|
@@ -405,13 +546,27 @@ class CommandBase(HasErrorHandler[ClientT], Hookable[ClientT], t.Generic[ClientT
|
|
|
405
546
|
|
|
406
547
|
|
|
407
548
|
@attr.define(slots=True, kw_only=True)
|
|
408
|
-
class CallableCommandBase(CommandBase[ClientT, BuilderT]):
|
|
409
|
-
"""A command that can be called directly. Note that this does not include subcommands, as those are options."""
|
|
549
|
+
class CallableCommandBase(CommandBase[ClientT, BuilderT], CallableCommandProto[ClientT]):
|
|
550
|
+
"""A top-level command that can be called directly. Note that this does not include subcommands, as those are options."""
|
|
410
551
|
|
|
411
552
|
callback: CommandCallbackT[ClientT]
|
|
412
553
|
"""The callback to invoke when this command is called."""
|
|
413
554
|
|
|
414
|
-
_invoke_task: asyncio.Task[t.Any] | None = attr.field(init=False, default=None)
|
|
555
|
+
_invoke_task: asyncio.Task[t.Any] | None = attr.field(init=False, default=None, repr=False)
|
|
556
|
+
|
|
557
|
+
def reset_all_limiters(self, context: Context[ClientT]) -> None:
|
|
558
|
+
"""Reset all limiters for this command.
|
|
559
|
+
|
|
560
|
+
Parameters
|
|
561
|
+
----------
|
|
562
|
+
context : Context
|
|
563
|
+
The context to reset the limiters for.
|
|
564
|
+
"""
|
|
565
|
+
limiters: t.Generator[LimiterProto[ClientT], None, None] = (
|
|
566
|
+
lim for lim in self._resolve_hooks() if isinstance(lim, LimiterProto)
|
|
567
|
+
)
|
|
568
|
+
for limiter in limiters:
|
|
569
|
+
limiter.reset(context)
|
|
415
570
|
|
|
416
571
|
async def __call__(self, ctx: Context[ClientT], *args: t.Any, **kwargs: t.Any) -> None:
|
|
417
572
|
await self.callback(ctx, *args, **kwargs)
|
|
@@ -430,6 +585,7 @@ class CallableCommandBase(CommandBase[ClientT, BuilderT]):
|
|
|
430
585
|
ParentT = t.TypeVar("ParentT")
|
|
431
586
|
|
|
432
587
|
|
|
588
|
+
@attr.define(slots=True, kw_only=True)
|
|
433
589
|
class SubCommandBase(OptionBase[ClientT], HasErrorHandler[ClientT], Hookable[ClientT], t.Generic[ClientT, ParentT]):
|
|
434
590
|
"""An abstract base class for all slash subcommands and subgroups."""
|
|
435
591
|
|
|
@@ -439,9 +595,18 @@ class SubCommandBase(OptionBase[ClientT], HasErrorHandler[ClientT], Hookable[Cli
|
|
|
439
595
|
|
|
440
596
|
_post_hooks: list[PostHookT[ClientT]] = attr.field(factory=list, init=False)
|
|
441
597
|
|
|
442
|
-
|
|
598
|
+
_parent: ParentT | None = attr.field(default=None, init=False, alias="parent")
|
|
443
599
|
"""The parent of this subcommand or subgroup."""
|
|
444
600
|
|
|
601
|
+
@property
|
|
602
|
+
@abc.abstractmethod
|
|
603
|
+
def display_name(self) -> str:
|
|
604
|
+
"""The display name of this command. This is what is shown in the Discord client.
|
|
605
|
+
|
|
606
|
+
!!! note
|
|
607
|
+
Slash commands can also be mentioned, see [SlashCommand.make_mention][arc.command.SlashCommand.make_mention].
|
|
608
|
+
"""
|
|
609
|
+
|
|
445
610
|
@property
|
|
446
611
|
def error_handler(self) -> ErrorHandlerCallbackT[ClientT] | None:
|
|
447
612
|
"""The error handler for this object."""
|
|
@@ -461,3 +626,49 @@ class SubCommandBase(OptionBase[ClientT], HasErrorHandler[ClientT], Hookable[Cli
|
|
|
461
626
|
def post_hooks(self) -> t.MutableSequence[PostHookT[ClientT]]:
|
|
462
627
|
"""The post-execution hooks for this object."""
|
|
463
628
|
return self._post_hooks
|
|
629
|
+
|
|
630
|
+
@property
|
|
631
|
+
def parent(self) -> ParentT:
|
|
632
|
+
"""The parent of this subcommand or subgroup."""
|
|
633
|
+
if self._parent is None:
|
|
634
|
+
raise RuntimeError(
|
|
635
|
+
f"Subcommand '{self.name}' was not included in a parent, '{type(self).__name__}.parent' cannot be accessed until it is included in a parent."
|
|
636
|
+
)
|
|
637
|
+
return self._parent
|
|
638
|
+
|
|
639
|
+
def reset_all_limiters(self, context: Context[ClientT]) -> None:
|
|
640
|
+
"""Reset all limiters for this command.
|
|
641
|
+
|
|
642
|
+
Parameters
|
|
643
|
+
----------
|
|
644
|
+
context : Context
|
|
645
|
+
The context to reset the limiters for.
|
|
646
|
+
"""
|
|
647
|
+
limiters: t.Generator[LimiterProto[ClientT], None, None] = (
|
|
648
|
+
lim for lim in self._resolve_hooks() if isinstance(lim, LimiterProto)
|
|
649
|
+
)
|
|
650
|
+
for limiter in limiters:
|
|
651
|
+
limiter.reset(context)
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
# MIT License
|
|
655
|
+
#
|
|
656
|
+
# Copyright (c) 2023-present hypergonial
|
|
657
|
+
#
|
|
658
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
659
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
660
|
+
# in the Software without restriction, including without limitation the rights
|
|
661
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
662
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
663
|
+
# furnished to do so, subject to the following conditions:
|
|
664
|
+
#
|
|
665
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
666
|
+
# copies or substantial portions of the Software.
|
|
667
|
+
#
|
|
668
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
669
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
670
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
671
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
672
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
673
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
674
|
+
# SOFTWARE.
|
arc/abc/error_handler.py
CHANGED
|
@@ -45,12 +45,20 @@ class HasErrorHandler(abc.ABC, t.Generic[ClientT]):
|
|
|
45
45
|
@client.include
|
|
46
46
|
@arc.slash_command("foo", "Foo command description")
|
|
47
47
|
async def foo(ctx: arc.GatewayContext) -> None:
|
|
48
|
-
raise
|
|
48
|
+
raise RuntimeError("foo")
|
|
49
49
|
|
|
50
50
|
@foo.set_error_handler
|
|
51
51
|
async def foo_error_handler(ctx: arc.GatewayContext, exc: Exception) -> None:
|
|
52
|
-
|
|
52
|
+
if isinstance(exc, RuntimeError):
|
|
53
|
+
await ctx.respond("foo failed")
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
raise exc
|
|
53
57
|
```
|
|
58
|
+
|
|
59
|
+
!!! warning
|
|
60
|
+
Errors that cannot be handled by the error handler should be re-raised.
|
|
61
|
+
Otherwise they will not propagate to the next error handler.
|
|
54
62
|
"""
|
|
55
63
|
|
|
56
64
|
def decorator(func: ErrorHandlerCallbackT[ClientT]) -> ErrorHandlerCallbackT[ClientT]:
|
|
@@ -65,3 +73,26 @@ class HasErrorHandler(abc.ABC, t.Generic[ClientT]):
|
|
|
65
73
|
@abc.abstractmethod
|
|
66
74
|
async def _handle_exception(self, ctx: Context[ClientT], exc: Exception) -> None:
|
|
67
75
|
"""Handle an exception or propagate it to the next error handler if it cannot be handled."""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# MIT License
|
|
79
|
+
#
|
|
80
|
+
# Copyright (c) 2023-present hypergonial
|
|
81
|
+
#
|
|
82
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
83
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
84
|
+
# in the Software without restriction, including without limitation the rights
|
|
85
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
86
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
87
|
+
# furnished to do so, subject to the following conditions:
|
|
88
|
+
#
|
|
89
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
90
|
+
# copies or substantial portions of the Software.
|
|
91
|
+
#
|
|
92
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
93
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
94
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
95
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
96
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
97
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
98
|
+
# SOFTWARE.
|
arc/abc/hookable.py
CHANGED
|
@@ -9,6 +9,7 @@ if t.TYPE_CHECKING:
|
|
|
9
9
|
import typing_extensions as te
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
@t.final
|
|
12
13
|
class HookResult:
|
|
13
14
|
"""The result of a hook.
|
|
14
15
|
|
|
@@ -138,3 +139,26 @@ def with_post_hook(hook: PostHookT[ClientT]) -> t.Callable[[HookableT], Hookable
|
|
|
138
139
|
return hookable
|
|
139
140
|
|
|
140
141
|
return decorator
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# MIT License
|
|
145
|
+
#
|
|
146
|
+
# Copyright (c) 2023-present hypergonial
|
|
147
|
+
#
|
|
148
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
149
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
150
|
+
# in the Software without restriction, including without limitation the rights
|
|
151
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
152
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
153
|
+
# furnished to do so, subject to the following conditions:
|
|
154
|
+
#
|
|
155
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
156
|
+
# copies or substantial portions of the Software.
|
|
157
|
+
#
|
|
158
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
159
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
160
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
161
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
162
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
163
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
164
|
+
# SOFTWARE.
|
arc/abc/limiter.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import typing as t
|
|
5
|
+
|
|
6
|
+
from arc.internal.types import ClientT
|
|
7
|
+
|
|
8
|
+
if t.TYPE_CHECKING:
|
|
9
|
+
from arc.abc.hookable import HookResult
|
|
10
|
+
from arc.context.base import Context
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@t.runtime_checkable
|
|
14
|
+
class LimiterProto(t.Protocol, t.Generic[ClientT]):
|
|
15
|
+
"""A protocol that all limiter hooks should implement.
|
|
16
|
+
A limiter is simply a special type of hook with added methods.
|
|
17
|
+
|
|
18
|
+
If you're looking to integrate your own ratelimiter implementation,
|
|
19
|
+
you should make sure to implement all methods defined here.
|
|
20
|
+
|
|
21
|
+
!!! tip
|
|
22
|
+
An easy (but not necessary) way to ensure you've implemented all methods
|
|
23
|
+
is to inherit from this protocol.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@abc.abstractmethod
|
|
27
|
+
async def __call__(self, ctx: Context[ClientT]) -> HookResult:
|
|
28
|
+
"""Call the limiter with the given context.
|
|
29
|
+
Implementations should raise an exception if the limiter is ratelimited
|
|
30
|
+
or abort via [`HookResult`][arc.abc.hookable.HookResult].
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
ctx : Context
|
|
35
|
+
The context to evaluate the ratelimit under.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
@abc.abstractmethod
|
|
39
|
+
def reset(self, ctx: Context[ClientT]) -> None:
|
|
40
|
+
"""Reset the limiter for the given context.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
ctx : Context
|
|
45
|
+
The context to reset the ratelimit for.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
@abc.abstractmethod
|
|
49
|
+
def is_rate_limited(self, ctx: Context[ClientT]) -> bool:
|
|
50
|
+
"""Check if the limiter is rate limited for the given context.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
ctx : Context
|
|
55
|
+
The context to evaluate the ratelimit under.
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
bool
|
|
60
|
+
Whether or not the limiter is ratelimited.
|
|
61
|
+
"""
|