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.
Files changed (63) hide show
  1. discord_wizard-0.1.0/MANIFEST.in +1 -0
  2. discord_wizard-0.1.0/PKG-INFO +57 -0
  3. discord_wizard-0.1.0/README.md +30 -0
  4. discord_wizard-0.1.0/pyproject.toml +62 -0
  5. discord_wizard-0.1.0/setup.cfg +4 -0
  6. discord_wizard-0.1.0/src/discord/wizard/__init__.py +28 -0
  7. discord_wizard-0.1.0/src/discord/wizard/commands/__init__.py +10 -0
  8. discord_wizard-0.1.0/src/discord/wizard/commands/wrapper.py +85 -0
  9. discord_wizard-0.1.0/src/discord/wizard/errors/__init__.py +10 -0
  10. discord_wizard-0.1.0/src/discord/wizard/errors/formatter.py +142 -0
  11. discord_wizard-0.1.0/src/discord/wizard/errors/handlers.py +81 -0
  12. discord_wizard-0.1.0/src/discord/wizard/events/__init__.py +117 -0
  13. discord_wizard-0.1.0/src/discord/wizard/events/filters.py +48 -0
  14. discord_wizard-0.1.0/src/discord/wizard/events/groups.py +22 -0
  15. discord_wizard-0.1.0/src/discord/wizard/events/member.py +11 -0
  16. discord_wizard-0.1.0/src/discord/wizard/events/message.py +10 -0
  17. discord_wizard-0.1.0/src/discord/wizard/events/reaction.py +9 -0
  18. discord_wizard-0.1.0/src/discord/wizard/events/role.py +27 -0
  19. discord_wizard-0.1.0/src/discord/wizard/events/voice.py +42 -0
  20. discord_wizard-0.1.0/src/discord/wizard/permissions/__init__.py +11 -0
  21. discord_wizard-0.1.0/src/discord/wizard/permissions/config.py +13 -0
  22. discord_wizard-0.1.0/src/discord/wizard/permissions/decorators.py +115 -0
  23. discord_wizard-0.1.0/src/discord/wizard/rateguard/__init__.py +173 -0
  24. discord_wizard-0.1.0/src/discord/wizard/rateguard/bucket.py +79 -0
  25. discord_wizard-0.1.0/src/discord/wizard/rateguard/queue.py +101 -0
  26. discord_wizard-0.1.0/src/discord/wizard/recorder/__init__.py +79 -0
  27. discord_wizard-0.1.0/src/discord/wizard/recorder/export.py +79 -0
  28. discord_wizard-0.1.0/src/discord/wizard/recorder/session.py +54 -0
  29. discord_wizard-0.1.0/src/discord/wizard/recorder/store.py +431 -0
  30. discord_wizard-0.1.0/src/discord/wizard/simulate/__init__.py +8 -0
  31. discord_wizard-0.1.0/src/discord/wizard/simulate/cli.py +42 -0
  32. discord_wizard-0.1.0/src/discord/wizard/simulate/client.py +104 -0
  33. discord_wizard-0.1.0/src/discord/wizard/simulate/server.py +172 -0
  34. discord_wizard-0.1.0/src/discord/wizard/simulate/web/app.js +98 -0
  35. discord_wizard-0.1.0/src/discord/wizard/simulate/web/index.html +111 -0
  36. discord_wizard-0.1.0/src/discord/wizard/simulate/web/style.css +101 -0
  37. discord_wizard-0.1.0/src/discord/wizard/tasks/__init__.py +10 -0
  38. discord_wizard-0.1.0/src/discord/wizard/tasks/cron.py +61 -0
  39. discord_wizard-0.1.0/src/discord/wizard/tasks/scheduler.py +241 -0
  40. discord_wizard-0.1.0/src/discord/wizard/ui/__init__.py +18 -0
  41. discord_wizard-0.1.0/src/discord/wizard/ui/dropdown.py +60 -0
  42. discord_wizard-0.1.0/src/discord/wizard/ui/form.py +62 -0
  43. discord_wizard-0.1.0/src/discord/wizard/ui/multiselect.py +80 -0
  44. discord_wizard-0.1.0/src/discord/wizard/ui/paginator.py +168 -0
  45. discord_wizard-0.1.0/src/discord/wizard/ui/panel.py +65 -0
  46. discord_wizard-0.1.0/src/discord/wizard/ui/tabs.py +106 -0
  47. discord_wizard-0.1.0/src/discord_wizard.egg-info/PKG-INFO +57 -0
  48. discord_wizard-0.1.0/src/discord_wizard.egg-info/SOURCES.txt +61 -0
  49. discord_wizard-0.1.0/src/discord_wizard.egg-info/dependency_links.txt +1 -0
  50. discord_wizard-0.1.0/src/discord_wizard.egg-info/entry_points.txt +2 -0
  51. discord_wizard-0.1.0/src/discord_wizard.egg-info/requires.txt +9 -0
  52. discord_wizard-0.1.0/src/discord_wizard.egg-info/top_level.txt +1 -0
  53. discord_wizard-0.1.0/tests/test_commands.py +123 -0
  54. discord_wizard-0.1.0/tests/test_errors.py +126 -0
  55. discord_wizard-0.1.0/tests/test_events.py +448 -0
  56. discord_wizard-0.1.0/tests/test_permissions.py +340 -0
  57. discord_wizard-0.1.0/tests/test_rateguard.py +53 -0
  58. discord_wizard-0.1.0/tests/test_rateguard_bucket.py +120 -0
  59. discord_wizard-0.1.0/tests/test_rateguard_queue.py +110 -0
  60. discord_wizard-0.1.0/tests/test_recorder.py +345 -0
  61. discord_wizard-0.1.0/tests/test_simulate.py +156 -0
  62. discord_wizard-0.1.0/tests/test_tasks.py +273 -0
  63. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,10 @@
1
+ from discord.ext import commands as ext
2
+ from discord import app_commands as app
3
+
4
+ from .wrapper import BotWrapper
5
+
6
+ __all__ = ["bot", "BotWrapper", "ext", "app"]
7
+
8
+
9
+ def bot(**kwargs):
10
+ return BotWrapper(**kwargs)
@@ -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,10 @@
1
+ from .formatter import format_terminal, format_embed
2
+ from .handlers import enable, register, set_embed_mode
3
+
4
+ __all__ = [
5
+ "enable",
6
+ "register",
7
+ "set_embed_mode",
8
+ "format_terminal",
9
+ "format_embed",
10
+ ]
@@ -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