discord-wizard 0.1.0__tar.gz
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.
- discord_wizard-0.1.0/MANIFEST.in +1 -0
- discord_wizard-0.1.0/PKG-INFO +57 -0
- discord_wizard-0.1.0/README.md +30 -0
- discord_wizard-0.1.0/pyproject.toml +62 -0
- discord_wizard-0.1.0/setup.cfg +4 -0
- discord_wizard-0.1.0/src/discord/wizard/__init__.py +28 -0
- discord_wizard-0.1.0/src/discord/wizard/commands/__init__.py +10 -0
- discord_wizard-0.1.0/src/discord/wizard/commands/wrapper.py +85 -0
- discord_wizard-0.1.0/src/discord/wizard/errors/__init__.py +10 -0
- discord_wizard-0.1.0/src/discord/wizard/errors/formatter.py +142 -0
- discord_wizard-0.1.0/src/discord/wizard/errors/handlers.py +81 -0
- discord_wizard-0.1.0/src/discord/wizard/events/__init__.py +117 -0
- discord_wizard-0.1.0/src/discord/wizard/events/filters.py +48 -0
- discord_wizard-0.1.0/src/discord/wizard/events/groups.py +22 -0
- discord_wizard-0.1.0/src/discord/wizard/events/member.py +11 -0
- discord_wizard-0.1.0/src/discord/wizard/events/message.py +10 -0
- discord_wizard-0.1.0/src/discord/wizard/events/reaction.py +9 -0
- discord_wizard-0.1.0/src/discord/wizard/events/role.py +27 -0
- discord_wizard-0.1.0/src/discord/wizard/events/voice.py +42 -0
- discord_wizard-0.1.0/src/discord/wizard/permissions/__init__.py +11 -0
- discord_wizard-0.1.0/src/discord/wizard/permissions/config.py +13 -0
- discord_wizard-0.1.0/src/discord/wizard/permissions/decorators.py +115 -0
- discord_wizard-0.1.0/src/discord/wizard/rateguard/__init__.py +173 -0
- discord_wizard-0.1.0/src/discord/wizard/rateguard/bucket.py +79 -0
- discord_wizard-0.1.0/src/discord/wizard/rateguard/queue.py +101 -0
- discord_wizard-0.1.0/src/discord/wizard/recorder/__init__.py +79 -0
- discord_wizard-0.1.0/src/discord/wizard/recorder/export.py +79 -0
- discord_wizard-0.1.0/src/discord/wizard/recorder/session.py +54 -0
- discord_wizard-0.1.0/src/discord/wizard/recorder/store.py +431 -0
- discord_wizard-0.1.0/src/discord/wizard/simulate/__init__.py +8 -0
- discord_wizard-0.1.0/src/discord/wizard/simulate/cli.py +42 -0
- discord_wizard-0.1.0/src/discord/wizard/simulate/client.py +104 -0
- discord_wizard-0.1.0/src/discord/wizard/simulate/server.py +172 -0
- discord_wizard-0.1.0/src/discord/wizard/simulate/web/app.js +98 -0
- discord_wizard-0.1.0/src/discord/wizard/simulate/web/index.html +111 -0
- discord_wizard-0.1.0/src/discord/wizard/simulate/web/style.css +101 -0
- discord_wizard-0.1.0/src/discord/wizard/tasks/__init__.py +10 -0
- discord_wizard-0.1.0/src/discord/wizard/tasks/cron.py +61 -0
- discord_wizard-0.1.0/src/discord/wizard/tasks/scheduler.py +241 -0
- discord_wizard-0.1.0/src/discord/wizard/ui/__init__.py +18 -0
- discord_wizard-0.1.0/src/discord/wizard/ui/dropdown.py +60 -0
- discord_wizard-0.1.0/src/discord/wizard/ui/form.py +62 -0
- discord_wizard-0.1.0/src/discord/wizard/ui/multiselect.py +80 -0
- discord_wizard-0.1.0/src/discord/wizard/ui/paginator.py +168 -0
- discord_wizard-0.1.0/src/discord/wizard/ui/panel.py +65 -0
- discord_wizard-0.1.0/src/discord/wizard/ui/tabs.py +106 -0
- discord_wizard-0.1.0/src/discord_wizard.egg-info/PKG-INFO +57 -0
- discord_wizard-0.1.0/src/discord_wizard.egg-info/SOURCES.txt +61 -0
- discord_wizard-0.1.0/src/discord_wizard.egg-info/dependency_links.txt +1 -0
- discord_wizard-0.1.0/src/discord_wizard.egg-info/entry_points.txt +2 -0
- discord_wizard-0.1.0/src/discord_wizard.egg-info/requires.txt +9 -0
- discord_wizard-0.1.0/src/discord_wizard.egg-info/top_level.txt +1 -0
- discord_wizard-0.1.0/tests/test_commands.py +123 -0
- discord_wizard-0.1.0/tests/test_errors.py +126 -0
- discord_wizard-0.1.0/tests/test_events.py +448 -0
- discord_wizard-0.1.0/tests/test_permissions.py +340 -0
- discord_wizard-0.1.0/tests/test_rateguard.py +53 -0
- discord_wizard-0.1.0/tests/test_rateguard_bucket.py +120 -0
- discord_wizard-0.1.0/tests/test_rateguard_queue.py +110 -0
- discord_wizard-0.1.0/tests/test_recorder.py +345 -0
- discord_wizard-0.1.0/tests/test_simulate.py +156 -0
- discord_wizard-0.1.0/tests/test_tasks.py +273 -0
- discord_wizard-0.1.0/tests/test_ui.py +452 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include src/discord/wizard/simulate/web/*
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: discord-wizard
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A unified Discord.py utility library — events, rate limiting, UI, permissions, tasks, errors, simulation, and recording.
|
|
5
|
+
Author: Muneeb
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/draganoxfool/discord-wizard
|
|
8
|
+
Project-URL: Repository, https://github.com/draganoxfool/discord-wizard
|
|
9
|
+
Classifier: Development Status :: 1 - Planning
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Classifier: Topic :: Communications :: Chat
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: discord.py>=2.0
|
|
21
|
+
Provides-Extra: postgres
|
|
22
|
+
Requires-Dist: asyncpg; extra == "postgres"
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
27
|
+
|
|
28
|
+
# discord-wizard
|
|
29
|
+
|
|
30
|
+
A unified Discord.py utility library — events, rate limiting, UI, permissions, tasks, errors, simulation, and recording.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from discord.wizard import wizard
|
|
34
|
+
|
|
35
|
+
wizard.events # Human-friendly event namespace
|
|
36
|
+
wizard.rateguard # Automatic rate limit protection
|
|
37
|
+
wizard.ui # Declarative UI components
|
|
38
|
+
wizard.permissions # Decorator-based permission layer
|
|
39
|
+
wizard.tasks # Cron jobs & scheduled tasks
|
|
40
|
+
wizard.errors # Human-readable error formatting
|
|
41
|
+
wizard.simulate # Run your bot without Discord
|
|
42
|
+
wizard.recorder # Record & export everything
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install discord-wizard
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Docs
|
|
52
|
+
|
|
53
|
+
See the `/docs` folder for full planning documentation.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# discord-wizard
|
|
2
|
+
|
|
3
|
+
A unified Discord.py utility library — events, rate limiting, UI, permissions, tasks, errors, simulation, and recording.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from discord.wizard import wizard
|
|
7
|
+
|
|
8
|
+
wizard.events # Human-friendly event namespace
|
|
9
|
+
wizard.rateguard # Automatic rate limit protection
|
|
10
|
+
wizard.ui # Declarative UI components
|
|
11
|
+
wizard.permissions # Decorator-based permission layer
|
|
12
|
+
wizard.tasks # Cron jobs & scheduled tasks
|
|
13
|
+
wizard.errors # Human-readable error formatting
|
|
14
|
+
wizard.simulate # Run your bot without Discord
|
|
15
|
+
wizard.recorder # Record & export everything
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install discord-wizard
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Docs
|
|
25
|
+
|
|
26
|
+
See the `/docs` folder for full planning documentation.
|
|
27
|
+
|
|
28
|
+
## License
|
|
29
|
+
|
|
30
|
+
MIT
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "discord-wizard"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A unified Discord.py utility library — events, rate limiting, UI, permissions, tasks, errors, simulation, and recording."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Muneeb"},
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 1 - Planning",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Software Development :: Libraries",
|
|
24
|
+
"Topic :: Communications :: Chat",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"discord.py>=2.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
postgres = ["asyncpg"]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=7.0",
|
|
34
|
+
"pytest-asyncio",
|
|
35
|
+
"ruff>=0.1.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/draganoxfool/discord-wizard"
|
|
40
|
+
Repository = "https://github.com/draganoxfool/discord-wizard"
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
discord-wizard = "discord.wizard.simulate.cli:main"
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.packages.find]
|
|
46
|
+
where = ["src"]
|
|
47
|
+
include = ["discord.*"]
|
|
48
|
+
exclude = ["discord.__pycache__*", "discord.wizard.__pycache__*"]
|
|
49
|
+
|
|
50
|
+
[tool.setuptools.package-data]
|
|
51
|
+
"discord.wizard.simulate" = ["web/*"]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
asyncio_mode = "auto"
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
|
|
57
|
+
[tool.ruff]
|
|
58
|
+
target-version = "py310"
|
|
59
|
+
line-length = 100
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint]
|
|
62
|
+
select = ["E", "F", "W", "I", "N", "D"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from . import events, rateguard, ui, permissions, tasks, errors, simulate, recorder, commands
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"events",
|
|
5
|
+
"rateguard",
|
|
6
|
+
"ui",
|
|
7
|
+
"permissions",
|
|
8
|
+
"tasks",
|
|
9
|
+
"errors",
|
|
10
|
+
"simulate",
|
|
11
|
+
"recorder",
|
|
12
|
+
"commands",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _Wizard:
|
|
17
|
+
events = events
|
|
18
|
+
rateguard = rateguard
|
|
19
|
+
ui = ui
|
|
20
|
+
permissions = permissions
|
|
21
|
+
tasks = tasks
|
|
22
|
+
errors = errors
|
|
23
|
+
simulate = simulate
|
|
24
|
+
recorder = recorder
|
|
25
|
+
commands = commands
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
wizard = _Wizard()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from discord.ext import commands as _commands
|
|
2
|
+
from discord import app_commands as _app_commands
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BotWrapper:
|
|
6
|
+
def __init__(self, **kwargs):
|
|
7
|
+
self._bot = _commands.Bot(**kwargs)
|
|
8
|
+
|
|
9
|
+
def cmd(self, **kwargs):
|
|
10
|
+
return self._bot.command(**kwargs)
|
|
11
|
+
|
|
12
|
+
def slash(self, **kwargs):
|
|
13
|
+
return self._bot.tree.command(**kwargs)
|
|
14
|
+
|
|
15
|
+
def context_menu(self, **kwargs):
|
|
16
|
+
return self._bot.tree.context_menu(**kwargs)
|
|
17
|
+
|
|
18
|
+
def group(self, **kwargs):
|
|
19
|
+
return self._bot.group(**kwargs)
|
|
20
|
+
|
|
21
|
+
def slash_group(self, **kwargs):
|
|
22
|
+
def decorator(cls):
|
|
23
|
+
group = _app_commands.Group(**kwargs)
|
|
24
|
+
for attr_name in dir(cls):
|
|
25
|
+
if attr_name.startswith("_"):
|
|
26
|
+
continue
|
|
27
|
+
attr = getattr(cls, attr_name)
|
|
28
|
+
if isinstance(attr, _app_commands.Command):
|
|
29
|
+
group.command(attr)
|
|
30
|
+
self._bot.tree.add_command(group)
|
|
31
|
+
return cls
|
|
32
|
+
return decorator
|
|
33
|
+
|
|
34
|
+
async def sync(self, **kwargs):
|
|
35
|
+
return await self._bot.tree.sync(**kwargs)
|
|
36
|
+
|
|
37
|
+
def add_command(self, cmd):
|
|
38
|
+
self._bot.add_command(cmd)
|
|
39
|
+
|
|
40
|
+
def remove_command(self, name):
|
|
41
|
+
self._bot.remove_command(name)
|
|
42
|
+
|
|
43
|
+
def get_command(self, name):
|
|
44
|
+
return self._bot.get_command(name)
|
|
45
|
+
|
|
46
|
+
def add_cog(self, cog, **kwargs):
|
|
47
|
+
return self._bot.add_cog(cog, **kwargs)
|
|
48
|
+
|
|
49
|
+
def remove_cog(self, name, **kwargs):
|
|
50
|
+
return self._bot.remove_cog(name, **kwargs)
|
|
51
|
+
|
|
52
|
+
def get_cog(self, name):
|
|
53
|
+
return self._bot.get_cog(name)
|
|
54
|
+
|
|
55
|
+
def event(self, coro):
|
|
56
|
+
self._bot.event(coro)
|
|
57
|
+
return coro
|
|
58
|
+
|
|
59
|
+
def listen(self, name=None):
|
|
60
|
+
return self._bot.listen(name)
|
|
61
|
+
|
|
62
|
+
def run(self, *args, **kwargs):
|
|
63
|
+
self._bot.run(*args, **kwargs)
|
|
64
|
+
|
|
65
|
+
def dispatch(self, event_name, *args, **kwargs):
|
|
66
|
+
self._bot.dispatch(event_name, *args, **kwargs)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def tree(self):
|
|
70
|
+
return self._bot.tree
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def user(self):
|
|
74
|
+
return self._bot.user
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def commands(self):
|
|
78
|
+
return self._bot.commands
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def cogs(self):
|
|
82
|
+
return self._bot.cogs
|
|
83
|
+
|
|
84
|
+
def __getattr__(self, name):
|
|
85
|
+
return getattr(self._bot, name)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import traceback
|
|
3
|
+
|
|
4
|
+
import discord
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_UTC = datetime.timezone.utc
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _timestamp():
|
|
11
|
+
return datetime.datetime.now(_UTC).strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _extract_context(ctx_or_args):
|
|
15
|
+
ctx = None
|
|
16
|
+
if ctx_or_args:
|
|
17
|
+
if isinstance(ctx_or_args[0], discord.ext.commands.Context):
|
|
18
|
+
ctx = ctx_or_args[0]
|
|
19
|
+
elif isinstance(ctx_or_args[0], discord.Interaction):
|
|
20
|
+
ctx = ctx_or_args[0]
|
|
21
|
+
return ctx
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _guild_name(ctx):
|
|
25
|
+
if hasattr(ctx, "guild") and ctx.guild:
|
|
26
|
+
return f"{ctx.guild.name} ({ctx.guild.id})"
|
|
27
|
+
if hasattr(ctx, "guild_id") and ctx.guild_id:
|
|
28
|
+
return f"guild {ctx.guild_id}"
|
|
29
|
+
return "N/A"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _channel_name(ctx):
|
|
33
|
+
if hasattr(ctx, "channel") and ctx.channel:
|
|
34
|
+
return f"#{ctx.channel.name} ({ctx.channel.id})"
|
|
35
|
+
if hasattr(ctx, "channel_id") and ctx.channel_id:
|
|
36
|
+
return f"channel {ctx.channel_id}"
|
|
37
|
+
return "N/A"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _user_name(ctx):
|
|
41
|
+
if isinstance(ctx, discord.Interaction):
|
|
42
|
+
u = ctx.user
|
|
43
|
+
return f"{u} ({u.id})"
|
|
44
|
+
if hasattr(ctx, "author"):
|
|
45
|
+
u = ctx.author
|
|
46
|
+
return f"{u} ({u.id})"
|
|
47
|
+
return "N/A"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _error_label(error):
|
|
51
|
+
name = type(error).__name__
|
|
52
|
+
hint = ""
|
|
53
|
+
if isinstance(error, discord.Forbidden):
|
|
54
|
+
code = getattr(error, "code", 403)
|
|
55
|
+
hint = f" ({code} Forbidden)"
|
|
56
|
+
elif isinstance(error, discord.NotFound):
|
|
57
|
+
code = getattr(error, "code", 404)
|
|
58
|
+
hint = f" ({code} Not Found)"
|
|
59
|
+
elif isinstance(error, discord.HTTPException):
|
|
60
|
+
code = getattr(error, "code", 400)
|
|
61
|
+
hint = f" ({code})"
|
|
62
|
+
elif isinstance(error, discord.RateLimited):
|
|
63
|
+
hint = " (429 Rate Limited)"
|
|
64
|
+
return f"{name}{hint}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _error_message(error):
|
|
68
|
+
return str(error) or str(type(error))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _error_code(error):
|
|
72
|
+
if isinstance(error, discord.HTTPException):
|
|
73
|
+
return error.code
|
|
74
|
+
if isinstance(error, discord.Forbidden):
|
|
75
|
+
return error.code
|
|
76
|
+
if isinstance(error, discord.NotFound):
|
|
77
|
+
return error.code
|
|
78
|
+
return getattr(error, "code", None)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def format_terminal(error, *args, **kwargs):
|
|
82
|
+
ctx = _extract_context(args) if args else None
|
|
83
|
+
label = _error_label(error)
|
|
84
|
+
msg = _error_message(error)
|
|
85
|
+
ts = _timestamp()
|
|
86
|
+
guild = _guild_name(ctx) if ctx else "N/A"
|
|
87
|
+
channel = _channel_name(ctx) if ctx else "N/A"
|
|
88
|
+
user = _user_name(ctx) if ctx else "N/A"
|
|
89
|
+
|
|
90
|
+
w = max(len(label) + 4, 50)
|
|
91
|
+
top = "┌" + "─" * (w - 2) + "┐"
|
|
92
|
+
bot = "└" + "─" * (w - 2) + "┘"
|
|
93
|
+
gap = " " * (w - 2)
|
|
94
|
+
|
|
95
|
+
lines = [
|
|
96
|
+
top,
|
|
97
|
+
f"│ ERROR {label:<{w - 10}}│",
|
|
98
|
+
f"│{gap}│",
|
|
99
|
+
f"│ Guild: {guild:<{w - 12}}│",
|
|
100
|
+
f"│ Channel: {channel:<{w - 12}}│",
|
|
101
|
+
f"│ User: {user:<{w - 12}}│",
|
|
102
|
+
f"│ Time: {ts:<{w - 12}}│",
|
|
103
|
+
f"│{gap}│",
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
for line in msg.split("\n"):
|
|
107
|
+
for chunk in [line[i:i + w - 6] for i in range(0, len(line), w - 6)]:
|
|
108
|
+
lines.append(f"│ {chunk:<{w - 4}}│")
|
|
109
|
+
|
|
110
|
+
lines.append(bot)
|
|
111
|
+
return "\n".join(lines)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def format_embed(error, *args, **kwargs):
|
|
115
|
+
ctx = _extract_context(args) if args else None
|
|
116
|
+
label = _error_label(error)
|
|
117
|
+
msg = _error_message(error)
|
|
118
|
+
|
|
119
|
+
color = discord.Color.red()
|
|
120
|
+
if isinstance(error, discord.RateLimited):
|
|
121
|
+
color = discord.Color.gold()
|
|
122
|
+
elif isinstance(error, discord.Forbidden):
|
|
123
|
+
color = discord.Color.dark_red()
|
|
124
|
+
|
|
125
|
+
embed = discord.Embed(title=f"\u26a0 Error: {label}", description=msg, color=color, timestamp=datetime.datetime.now(_UTC))
|
|
126
|
+
if ctx:
|
|
127
|
+
embed.add_field(name="Guild", value=_guild_name(ctx), inline=True)
|
|
128
|
+
embed.add_field(name="Channel", value=_channel_name(ctx), inline=True)
|
|
129
|
+
embed.add_field(name="User", value=_user_name(ctx), inline=True)
|
|
130
|
+
|
|
131
|
+
if isinstance(error, discord.Forbidden):
|
|
132
|
+
embed.add_field(name="Fix", value="Grant the bot the required permission(s) in the guild or channel settings.", inline=False)
|
|
133
|
+
elif isinstance(error, discord.NotFound):
|
|
134
|
+
embed.add_field(name="Fix", value="The resource may have been deleted or the ID is incorrect. Check the ID and try again.", inline=False)
|
|
135
|
+
elif isinstance(error, discord.RateLimited):
|
|
136
|
+
retry = getattr(error, "retry_after", None)
|
|
137
|
+
txt = f"Slow down! Wait {retry:.1f}s before the next request." if retry else "Slow down and wait before sending more requests."
|
|
138
|
+
embed.add_field(name="Fix", value=txt, inline=False)
|
|
139
|
+
elif isinstance(error, discord.HTTPException):
|
|
140
|
+
embed.add_field(name="Fix", value="Check the request parameters. The payload may be malformed or too large.", inline=False)
|
|
141
|
+
|
|
142
|
+
return embed
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import traceback
|
|
4
|
+
|
|
5
|
+
import discord
|
|
6
|
+
|
|
7
|
+
from .formatter import format_terminal, format_embed
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_log = logging.getLogger("wizard.errors")
|
|
11
|
+
|
|
12
|
+
_handlers = {}
|
|
13
|
+
_embed_mode = False
|
|
14
|
+
_bot_ref = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register(error_type, handler):
|
|
18
|
+
_handlers[error_type] = handler
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def set_embed_mode(enabled=True):
|
|
22
|
+
global _embed_mode
|
|
23
|
+
_embed_mode = enabled
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def _send_owner_embed(bot, embed):
|
|
27
|
+
try:
|
|
28
|
+
owner_id = bot.owner_id
|
|
29
|
+
owner = bot.get_user(owner_id)
|
|
30
|
+
if owner:
|
|
31
|
+
await owner.send(embed=embed)
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _default_handler(error, *args):
|
|
37
|
+
term = format_terminal(error, *args)
|
|
38
|
+
print(term, flush=True)
|
|
39
|
+
if _embed_mode and _bot_ref:
|
|
40
|
+
try:
|
|
41
|
+
embed = format_embed(error, *args)
|
|
42
|
+
asyncio.create_task(_send_owner_embed(_bot_ref, embed))
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
_log.error("Unhandled error: %s: %s", type(error).__name__, error)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _make_on_error(bot):
|
|
49
|
+
async def on_error(event, *args, **kwargs):
|
|
50
|
+
error = args[-1] if args else kwargs.get("exception", None)
|
|
51
|
+
if not isinstance(error, BaseException):
|
|
52
|
+
return
|
|
53
|
+
_route_error(error, event, *args)
|
|
54
|
+
|
|
55
|
+
return on_error
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _make_tree_on_error(bot):
|
|
59
|
+
async def on_error(interaction, error):
|
|
60
|
+
_route_error(error, "tree", interaction)
|
|
61
|
+
|
|
62
|
+
return on_error
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _route_error(error, event, *args):
|
|
66
|
+
for err_type, handler in _handlers.items():
|
|
67
|
+
if isinstance(error, err_type):
|
|
68
|
+
try:
|
|
69
|
+
handler(error, *args)
|
|
70
|
+
return
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
_default_handler(error, *args)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def enable(bot, *, embed_mode=False):
|
|
77
|
+
global _bot_ref, _embed_mode
|
|
78
|
+
bot.on_error = _make_on_error(bot)
|
|
79
|
+
bot.tree.on_error = _make_tree_on_error(bot)
|
|
80
|
+
_bot_ref = bot
|
|
81
|
+
_embed_mode = embed_mode
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
__all__ = ["member", "message", "voice", "role", "reaction", "group", "enable"]
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class _EventRegistry:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self._handlers = []
|
|
7
|
+
self._bot = None
|
|
8
|
+
self._active_listeners = set()
|
|
9
|
+
|
|
10
|
+
def decorator(self, event_name, raw_event, *, arg_transform=None, condition=None):
|
|
11
|
+
return _EventDecorator(self, event_name, raw_event, arg_transform, condition)
|
|
12
|
+
|
|
13
|
+
def register(self, event_name, func, *, filters=None, group=None):
|
|
14
|
+
self._handlers.append({
|
|
15
|
+
"event": event_name,
|
|
16
|
+
"func": func,
|
|
17
|
+
"filters": filters or {},
|
|
18
|
+
"group": group,
|
|
19
|
+
})
|
|
20
|
+
if self._bot is not None:
|
|
21
|
+
self._wire(event_name)
|
|
22
|
+
|
|
23
|
+
def enable(self, bot):
|
|
24
|
+
self._bot = bot
|
|
25
|
+
for entry in self._handlers:
|
|
26
|
+
self._wire(entry["event"])
|
|
27
|
+
|
|
28
|
+
def _wire(self, event_name):
|
|
29
|
+
raw_event = None
|
|
30
|
+
for entry in self._handlers:
|
|
31
|
+
if entry["event"] == event_name:
|
|
32
|
+
raw_event = getattr(entry["func"], "_wizard_raw_event", None)
|
|
33
|
+
if raw_event is not None:
|
|
34
|
+
break
|
|
35
|
+
if raw_event is None:
|
|
36
|
+
return
|
|
37
|
+
if raw_event not in self._active_listeners:
|
|
38
|
+
self._active_listeners.add(raw_event)
|
|
39
|
+
self._bot.add_listener(self._dispatch(raw_event), raw_event)
|
|
40
|
+
|
|
41
|
+
def _dispatch(self, raw_event):
|
|
42
|
+
registry = self
|
|
43
|
+
|
|
44
|
+
async def listener(*args):
|
|
45
|
+
for entry in registry._handlers:
|
|
46
|
+
func = entry["func"]
|
|
47
|
+
ev_raw = getattr(func, "_wizard_raw_event", None)
|
|
48
|
+
if ev_raw != raw_event:
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
transform = getattr(func, "_wizard_transform", None)
|
|
52
|
+
condition = getattr(func, "_wizard_condition", None)
|
|
53
|
+
|
|
54
|
+
if condition is not None and not condition(*args):
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
dispatch_args = transform(*args) if transform is not None else args
|
|
58
|
+
|
|
59
|
+
from .filters import check_filters
|
|
60
|
+
if not check_filters(entry["filters"], *dispatch_args):
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
await func(*dispatch_args)
|
|
64
|
+
|
|
65
|
+
return listener
|
|
66
|
+
|
|
67
|
+
def handlers(self, event_name=None, group=None):
|
|
68
|
+
result = []
|
|
69
|
+
for entry in self._handlers:
|
|
70
|
+
if event_name is not None and entry["event"] != event_name:
|
|
71
|
+
continue
|
|
72
|
+
if group is not None and entry["group"] != group:
|
|
73
|
+
continue
|
|
74
|
+
result.append(entry)
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
_registry = _EventRegistry()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class _EventDecorator:
|
|
82
|
+
def __init__(self, registry, event_name, raw_event, arg_transform, condition):
|
|
83
|
+
self._registry = registry
|
|
84
|
+
self._event_name = event_name
|
|
85
|
+
self._raw_event = raw_event
|
|
86
|
+
self._arg_transform = arg_transform
|
|
87
|
+
self._condition = condition
|
|
88
|
+
|
|
89
|
+
def __call__(self, func=None, **filters):
|
|
90
|
+
if func is not None:
|
|
91
|
+
self._mark(func)
|
|
92
|
+
self._registry.register(self._event_name, func, filters=filters)
|
|
93
|
+
return func
|
|
94
|
+
|
|
95
|
+
def wrapper(f):
|
|
96
|
+
self._mark(f)
|
|
97
|
+
self._registry.register(self._event_name, f, filters=filters)
|
|
98
|
+
return f
|
|
99
|
+
return wrapper
|
|
100
|
+
|
|
101
|
+
def _mark(self, func):
|
|
102
|
+
func._wizard_event = self._event_name
|
|
103
|
+
func._wizard_raw_event = self._raw_event
|
|
104
|
+
func._wizard_transform = self._arg_transform
|
|
105
|
+
func._wizard_condition = self._condition
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def enable(bot):
|
|
109
|
+
_registry.enable(bot)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
from .member import member
|
|
113
|
+
from .message import message
|
|
114
|
+
from .voice import voice
|
|
115
|
+
from .role import role
|
|
116
|
+
from .reaction import reaction
|
|
117
|
+
from .groups import group
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
def check_filters(filters, *args, **kwargs):
|
|
2
|
+
if not filters:
|
|
3
|
+
return True
|
|
4
|
+
|
|
5
|
+
if "ignore_bots" in filters and filters["ignore_bots"]:
|
|
6
|
+
if args and hasattr(args[0], "bot") and args[0].bot:
|
|
7
|
+
return False
|
|
8
|
+
if args and hasattr(args[0], "author") and getattr(args[0].author, "bot", False):
|
|
9
|
+
return False
|
|
10
|
+
|
|
11
|
+
if "guild_id" in filters:
|
|
12
|
+
guild_id = filters["guild_id"]
|
|
13
|
+
for arg in args:
|
|
14
|
+
g = _get_guild(arg)
|
|
15
|
+
if g is not None and g.id != guild_id:
|
|
16
|
+
return False
|
|
17
|
+
|
|
18
|
+
if "channels" in filters:
|
|
19
|
+
channel_ids = filters["channels"]
|
|
20
|
+
channel = _get_channel(args)
|
|
21
|
+
if channel is not None and channel.id not in channel_ids:
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
if "has_role" in filters:
|
|
25
|
+
role_name = filters["has_role"]
|
|
26
|
+
for arg in args:
|
|
27
|
+
if hasattr(arg, "roles"):
|
|
28
|
+
if not any(r.name == role_name for r in arg.roles):
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_guild(obj):
|
|
35
|
+
if hasattr(obj, "guild"):
|
|
36
|
+
return obj.guild
|
|
37
|
+
if hasattr(obj, "guild_id"):
|
|
38
|
+
return type("GuildRef", (), {"id": obj.guild_id})()
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_channel(args):
|
|
43
|
+
for arg in args:
|
|
44
|
+
if hasattr(arg, "channel"):
|
|
45
|
+
return arg.channel
|
|
46
|
+
if hasattr(arg, "id") and hasattr(arg, "name"):
|
|
47
|
+
return arg
|
|
48
|
+
return None
|