kroxy 0.1.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.
kroxy/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ kroxy - A professional Python utility library.
3
+ Author: @kroxy
4
+ """
5
+
6
+ __version__ = "0.1.0"
7
+ __author__ = "kroxy"
8
+ __license__ = "MIT"
9
+
10
+ from kroxy import discord
11
+ from kroxy import website
12
+
13
+ __all__ = ["discord", "website"]
@@ -0,0 +1,22 @@
1
+ """
2
+ kroxy.discord - Professional Discord utilities and helpers.
3
+ """
4
+
5
+ from kroxy.discord.api import DiscordAPI
6
+ from kroxy.discord.commands import SlashCommand, PrefixCommand
7
+ from kroxy.discord.utils import Utils
8
+ from kroxy.discord.antinuke import AntiNuke
9
+ from kroxy.discord.checkers import Checkers
10
+ from kroxy.discord.giveaway import Giveaway
11
+ from kroxy.discord.music import MusicPlayer
12
+
13
+ __all__ = [
14
+ "DiscordAPI",
15
+ "SlashCommand",
16
+ "PrefixCommand",
17
+ "Utils",
18
+ "AntiNuke",
19
+ "Checkers",
20
+ "Giveaway",
21
+ "MusicPlayer",
22
+ ]
@@ -0,0 +1,146 @@
1
+ """
2
+ kroxy.discord.antinuke - Anti-nuke protection system for Discord servers.
3
+ """
4
+
5
+ import asyncio
6
+ import time
7
+ from collections import defaultdict
8
+ from typing import Dict, List, Optional, Callable, Any
9
+
10
+
11
+ class ActionLog:
12
+ """Tracks recent actions per user for rate-limit detection."""
13
+
14
+ def __init__(self, threshold: int, window: int):
15
+ self.threshold = threshold
16
+ self.window = window # seconds
17
+ self._log: Dict[int, List[float]] = defaultdict(list)
18
+
19
+ def record(self, user_id: int) -> int:
20
+ """Record an action and return the count within the window."""
21
+ now = time.time()
22
+ self._log[user_id] = [t for t in self._log[user_id] if now - t < self.window]
23
+ self._log[user_id].append(now)
24
+ return len(self._log[user_id])
25
+
26
+ def exceeded(self, user_id: int) -> bool:
27
+ """Return True if the user has exceeded the threshold."""
28
+ return self.record(user_id) >= self.threshold
29
+
30
+ def reset(self, user_id: int):
31
+ self._log.pop(user_id, None)
32
+
33
+
34
+ class AntiNuke:
35
+ """
36
+ Anti-nuke system that monitors destructive actions and auto-punishes abusers.
37
+
38
+ Usage:
39
+ antinuke = AntiNuke(bot, whitelist=[owner_id])
40
+ antinuke.on_trigger = my_punishment_callback
41
+ """
42
+
43
+ DEFAULT_LIMITS = {
44
+ "ban": (3, 10), # 3 bans in 10s
45
+ "kick": (3, 10),
46
+ "channel_delete": (3, 10),
47
+ "channel_create": (5, 10),
48
+ "role_delete": (3, 10),
49
+ "role_create": (5, 10),
50
+ "webhook_create": (3, 10),
51
+ "webhook_delete": (3, 10),
52
+ "invite_delete": (5, 10),
53
+ "mass_mention": (5, 5),
54
+ "bot_add": (2, 30),
55
+ }
56
+
57
+ def __init__(self, whitelist: List[int] = None, limits: Dict = None):
58
+ self.whitelist: List[int] = whitelist or []
59
+ limits = limits or {}
60
+ merged = {**self.DEFAULT_LIMITS, **limits}
61
+ self._trackers: Dict[str, ActionLog] = {
62
+ action: ActionLog(threshold, window)
63
+ for action, (threshold, window) in merged.items()
64
+ }
65
+ self._punished: set = set()
66
+ self.on_trigger: Optional[Callable] = None
67
+ self.punishment: str = "ban" # "ban" | "kick" | "strip_roles"
68
+
69
+ def is_whitelisted(self, user_id: int) -> bool:
70
+ return user_id in self.whitelist
71
+
72
+ def add_whitelist(self, user_id: int):
73
+ if user_id not in self.whitelist:
74
+ self.whitelist.append(user_id)
75
+
76
+ def remove_whitelist(self, user_id: int):
77
+ self.whitelist = [u for u in self.whitelist if u != user_id]
78
+
79
+ async def check(self, action: str, user_id: int, guild: Any = None) -> bool:
80
+ """
81
+ Check an action for a user. Returns True if limit exceeded.
82
+ Calls self.on_trigger(action, user_id, guild) if set.
83
+ """
84
+ if self.is_whitelisted(user_id):
85
+ return False
86
+ if user_id in self._punished:
87
+ return True
88
+
89
+ tracker = self._trackers.get(action)
90
+ if tracker is None:
91
+ return False
92
+
93
+ if tracker.exceeded(user_id):
94
+ self._punished.add(user_id)
95
+ if self.on_trigger:
96
+ try:
97
+ await self.on_trigger(action=action, user_id=user_id, guild=guild)
98
+ except Exception:
99
+ pass
100
+ return True
101
+ return False
102
+
103
+ def reset_user(self, user_id: int):
104
+ """Clear all action logs and punishment flag for a user."""
105
+ for tracker in self._trackers.values():
106
+ tracker.reset(user_id)
107
+ self._punished.discard(user_id)
108
+
109
+ def get_stats(self) -> Dict:
110
+ """Return current limits configuration."""
111
+ return {
112
+ action: {"threshold": t.threshold, "window": t.window}
113
+ for action, t in self._trackers.items()
114
+ }
115
+
116
+ # ── Convenience event handlers (wire these to your event system) ──────────
117
+
118
+ async def on_member_ban(self, user_id: int, guild: Any = None):
119
+ return await self.check("ban", user_id, guild)
120
+
121
+ async def on_member_kick(self, user_id: int, guild: Any = None):
122
+ return await self.check("kick", user_id, guild)
123
+
124
+ async def on_channel_delete(self, user_id: int, guild: Any = None):
125
+ return await self.check("channel_delete", user_id, guild)
126
+
127
+ async def on_channel_create(self, user_id: int, guild: Any = None):
128
+ return await self.check("channel_create", user_id, guild)
129
+
130
+ async def on_role_delete(self, user_id: int, guild: Any = None):
131
+ return await self.check("role_delete", user_id, guild)
132
+
133
+ async def on_role_create(self, user_id: int, guild: Any = None):
134
+ return await self.check("role_create", user_id, guild)
135
+
136
+ async def on_webhook_create(self, user_id: int, guild: Any = None):
137
+ return await self.check("webhook_create", user_id, guild)
138
+
139
+ async def on_webhook_delete(self, user_id: int, guild: Any = None):
140
+ return await self.check("webhook_delete", user_id, guild)
141
+
142
+ async def on_bot_add(self, user_id: int, guild: Any = None):
143
+ return await self.check("bot_add", user_id, guild)
144
+
145
+ async def on_mass_mention(self, user_id: int, guild: Any = None):
146
+ return await self.check("mass_mention", user_id, guild)
kroxy/discord/api.py ADDED
@@ -0,0 +1,128 @@
1
+ """
2
+ kroxy.discord.api - Discord REST API wrapper helpers.
3
+ """
4
+
5
+ import aiohttp
6
+ import asyncio
7
+ from typing import Optional, Any, Dict
8
+
9
+ DISCORD_API_BASE = "https://discord.com/api/v10"
10
+
11
+
12
+ class DiscordAPI:
13
+ """Async Discord REST API client."""
14
+
15
+ def __init__(self, token: str, bot: bool = True):
16
+ prefix = "Bot" if bot else "Bearer"
17
+ self.headers = {
18
+ "Authorization": f"{prefix} {token}",
19
+ "Content-Type": "application/json",
20
+ }
21
+ self._session: Optional[aiohttp.ClientSession] = None
22
+
23
+ async def _get_session(self) -> aiohttp.ClientSession:
24
+ if self._session is None or self._session.closed:
25
+ self._session = aiohttp.ClientSession(headers=self.headers)
26
+ return self._session
27
+
28
+ async def close(self):
29
+ """Close the underlying HTTP session."""
30
+ if self._session and not self._session.closed:
31
+ await self._session.close()
32
+
33
+ async def request(self, method: str, endpoint: str, **kwargs) -> Any:
34
+ """Make a raw API request."""
35
+ session = await self._get_session()
36
+ url = f"{DISCORD_API_BASE}{endpoint}"
37
+ async with session.request(method, url, **kwargs) as resp:
38
+ resp.raise_for_status()
39
+ return await resp.json()
40
+
41
+ async def get_guild(self, guild_id: int) -> Dict:
42
+ """Fetch a guild by ID."""
43
+ return await self.request("GET", f"/guilds/{guild_id}")
44
+
45
+ async def get_channel(self, channel_id: int) -> Dict:
46
+ """Fetch a channel by ID."""
47
+ return await self.request("GET", f"/channels/{channel_id}")
48
+
49
+ async def get_user(self, user_id: int) -> Dict:
50
+ """Fetch a user by ID."""
51
+ return await self.request("GET", f"/users/{user_id}")
52
+
53
+ async def send_message(self, channel_id: int, content: str = None,
54
+ embed: Dict = None, components: list = None) -> Dict:
55
+ """Send a message to a channel."""
56
+ payload: Dict[str, Any] = {}
57
+ if content:
58
+ payload["content"] = content
59
+ if embed:
60
+ payload["embeds"] = [embed]
61
+ if components:
62
+ payload["components"] = components
63
+ return await self.request("POST", f"/channels/{channel_id}/messages", json=payload)
64
+
65
+ async def delete_message(self, channel_id: int, message_id: int) -> None:
66
+ """Delete a message."""
67
+ await self.request("DELETE", f"/channels/{channel_id}/messages/{message_id}")
68
+
69
+ async def ban_member(self, guild_id: int, user_id: int,
70
+ reason: str = None, delete_message_days: int = 0) -> None:
71
+ """Ban a member from a guild."""
72
+ payload = {"delete_message_days": delete_message_days}
73
+ headers = {}
74
+ if reason:
75
+ headers["X-Audit-Log-Reason"] = reason
76
+ await self.request("PUT", f"/guilds/{guild_id}/bans/{user_id}",
77
+ json=payload, headers=headers)
78
+
79
+ async def unban_member(self, guild_id: int, user_id: int) -> None:
80
+ """Unban a member."""
81
+ await self.request("DELETE", f"/guilds/{guild_id}/bans/{user_id}")
82
+
83
+ async def kick_member(self, guild_id: int, user_id: int, reason: str = None) -> None:
84
+ """Kick a member from a guild."""
85
+ headers = {}
86
+ if reason:
87
+ headers["X-Audit-Log-Reason"] = reason
88
+ await self.request("DELETE", f"/guilds/{guild_id}/members/{user_id}",
89
+ headers=headers)
90
+
91
+ async def add_role(self, guild_id: int, user_id: int, role_id: int) -> None:
92
+ """Add a role to a member."""
93
+ await self.request("PUT", f"/guilds/{guild_id}/members/{user_id}/roles/{role_id}")
94
+
95
+ async def remove_role(self, guild_id: int, user_id: int, role_id: int) -> None:
96
+ """Remove a role from a member."""
97
+ await self.request("DELETE", f"/guilds/{guild_id}/members/{user_id}/roles/{role_id}")
98
+
99
+ async def create_channel(self, guild_id: int, name: str,
100
+ channel_type: int = 0, **kwargs) -> Dict:
101
+ """Create a guild channel."""
102
+ payload = {"name": name, "type": channel_type, **kwargs}
103
+ return await self.request("POST", f"/guilds/{guild_id}/channels", json=payload)
104
+
105
+ async def delete_channel(self, channel_id: int) -> Dict:
106
+ """Delete a channel."""
107
+ return await self.request("DELETE", f"/channels/{channel_id}")
108
+
109
+ async def get_audit_logs(self, guild_id: int, limit: int = 50) -> Dict:
110
+ """Fetch guild audit logs."""
111
+ return await self.request("GET", f"/guilds/{guild_id}/audit-logs",
112
+ params={"limit": limit})
113
+
114
+ async def get_invites(self, guild_id: int) -> list:
115
+ """Get all guild invites."""
116
+ return await self.request("GET", f"/guilds/{guild_id}/invites")
117
+
118
+ async def delete_invite(self, invite_code: str) -> None:
119
+ """Delete an invite by code."""
120
+ await self.request("DELETE", f"/invites/{invite_code}")
121
+
122
+ async def get_webhooks(self, guild_id: int) -> list:
123
+ """Get all webhooks in a guild."""
124
+ return await self.request("GET", f"/guilds/{guild_id}/webhooks")
125
+
126
+ async def delete_webhook(self, webhook_id: int) -> None:
127
+ """Delete a webhook."""
128
+ await self.request("DELETE", f"/webhooks/{webhook_id}")
@@ -0,0 +1,159 @@
1
+ """
2
+ kroxy.discord.checkers - Permission and role check utilities.
3
+ """
4
+
5
+ from typing import List, Union, Any, Optional
6
+
7
+
8
+ class CheckFailed(Exception):
9
+ """Raised when a permission/role check fails."""
10
+ def __init__(self, message: str):
11
+ self.message = message
12
+ super().__init__(message)
13
+
14
+
15
+ class Checkers:
16
+ """
17
+ Collection of Discord permission and role checks.
18
+ Each method raises CheckFailed on failure or returns True on success.
19
+ """
20
+
21
+ # ── Permission checks ─────────────────────────────────────────────────────
22
+
23
+ @staticmethod
24
+ def has_permission(member_permissions: int, permission_flag: int,
25
+ admin_bypass: bool = True) -> bool:
26
+ """Check if a permissions bitfield includes a flag."""
27
+ admin_flag = 1 << 3
28
+ if admin_bypass and (member_permissions & admin_flag):
29
+ return True
30
+ return bool(member_permissions & permission_flag)
31
+
32
+ @staticmethod
33
+ def require_permission(member_permissions: int, permission_flag: int,
34
+ permission_name: str = "required permission") -> bool:
35
+ if not Checkers.has_permission(member_permissions, permission_flag):
36
+ raise CheckFailed(f"You need the `{permission_name}` permission.")
37
+ return True
38
+
39
+ @staticmethod
40
+ def require_admin(member_permissions: int) -> bool:
41
+ admin_flag = 1 << 3
42
+ if not (member_permissions & admin_flag):
43
+ raise CheckFailed("You need the `Administrator` permission.")
44
+ return True
45
+
46
+ @staticmethod
47
+ def require_manage_guild(member_permissions: int) -> bool:
48
+ return Checkers.require_permission(member_permissions, 1 << 5, "Manage Server")
49
+
50
+ @staticmethod
51
+ def require_manage_roles(member_permissions: int) -> bool:
52
+ return Checkers.require_permission(member_permissions, 1 << 28, "Manage Roles")
53
+
54
+ @staticmethod
55
+ def require_ban_members(member_permissions: int) -> bool:
56
+ return Checkers.require_permission(member_permissions, 1 << 2, "Ban Members")
57
+
58
+ @staticmethod
59
+ def require_kick_members(member_permissions: int) -> bool:
60
+ return Checkers.require_permission(member_permissions, 1 << 1, "Kick Members")
61
+
62
+ @staticmethod
63
+ def require_manage_channels(member_permissions: int) -> bool:
64
+ return Checkers.require_permission(member_permissions, 1 << 4, "Manage Channels")
65
+
66
+ @staticmethod
67
+ def require_manage_messages(member_permissions: int) -> bool:
68
+ return Checkers.require_permission(member_permissions, 1 << 13, "Manage Messages")
69
+
70
+ @staticmethod
71
+ def require_manage_webhooks(member_permissions: int) -> bool:
72
+ return Checkers.require_permission(member_permissions, 1 << 29, "Manage Webhooks")
73
+
74
+ # ── Role checks ───────────────────────────────────────────────────────────
75
+
76
+ @staticmethod
77
+ def has_role(member_role_ids: List[int], role_id: int) -> bool:
78
+ """Check if a member has a specific role."""
79
+ return role_id in member_role_ids
80
+
81
+ @staticmethod
82
+ def has_any_role(member_role_ids: List[int], role_ids: List[int]) -> bool:
83
+ """Check if a member has at least one of the given roles."""
84
+ return bool(set(member_role_ids) & set(role_ids))
85
+
86
+ @staticmethod
87
+ def has_all_roles(member_role_ids: List[int], role_ids: List[int]) -> bool:
88
+ """Check if a member has all of the given roles."""
89
+ return set(role_ids).issubset(set(member_role_ids))
90
+
91
+ @staticmethod
92
+ def require_role(member_role_ids: List[int], role_id: int,
93
+ role_name: str = "a required role") -> bool:
94
+ if not Checkers.has_role(member_role_ids, role_id):
95
+ raise CheckFailed(f"You need the **{role_name}** role.")
96
+ return True
97
+
98
+ @staticmethod
99
+ def require_any_role(member_role_ids: List[int], role_ids: List[int]) -> bool:
100
+ if not Checkers.has_any_role(member_role_ids, role_ids):
101
+ raise CheckFailed("You don't have any of the required roles.")
102
+ return True
103
+
104
+ # ── Hierarchy checks ──────────────────────────────────────────────────────
105
+
106
+ @staticmethod
107
+ def check_hierarchy(executor_top_role_pos: int, target_top_role_pos: int,
108
+ bot_top_role_pos: int) -> bool:
109
+ """
110
+ Ensure the executor and bot are higher than the target in the role hierarchy.
111
+ Raises CheckFailed on violation.
112
+ """
113
+ if target_top_role_pos >= executor_top_role_pos:
114
+ raise CheckFailed("You cannot perform this action on someone with an equal or higher role.")
115
+ if target_top_role_pos >= bot_top_role_pos:
116
+ raise CheckFailed("I cannot perform this action on someone with a role equal to or higher than mine.")
117
+ return True
118
+
119
+ # ── Guild-level checks ────────────────────────────────────────────────────
120
+
121
+ @staticmethod
122
+ def is_guild_owner(user_id: int, guild_owner_id: int) -> bool:
123
+ return user_id == guild_owner_id
124
+
125
+ @staticmethod
126
+ def require_guild_owner(user_id: int, guild_owner_id: int) -> bool:
127
+ if not Checkers.is_guild_owner(user_id, guild_owner_id):
128
+ raise CheckFailed("Only the server owner can use this command.")
129
+ return True
130
+
131
+ @staticmethod
132
+ def is_bot(is_bot_flag: bool) -> bool:
133
+ return is_bot_flag
134
+
135
+ @staticmethod
136
+ def block_bots(is_bot_flag: bool) -> bool:
137
+ if is_bot_flag:
138
+ raise CheckFailed("Bots cannot use this command.")
139
+ return True
140
+
141
+ # ── Channel checks ────────────────────────────────────────────────────────
142
+
143
+ @staticmethod
144
+ def require_guild_only(guild_id: Optional[int]) -> bool:
145
+ if guild_id is None:
146
+ raise CheckFailed("This command can only be used in a server.")
147
+ return True
148
+
149
+ @staticmethod
150
+ def require_dm_only(guild_id: Optional[int]) -> bool:
151
+ if guild_id is not None:
152
+ raise CheckFailed("This command can only be used in DMs.")
153
+ return True
154
+
155
+ @staticmethod
156
+ def require_nsfw(channel_is_nsfw: bool) -> bool:
157
+ if not channel_is_nsfw:
158
+ raise CheckFailed("This command can only be used in NSFW channels.")
159
+ return True
@@ -0,0 +1,187 @@
1
+ """
2
+ kroxy.discord.commands - Slash and prefix command builders.
3
+ """
4
+
5
+ from typing import Callable, Optional, List, Dict, Any
6
+ import functools
7
+
8
+
9
+ class Option:
10
+ """Represents a slash command option."""
11
+
12
+ TYPES = {
13
+ "sub_command": 1,
14
+ "sub_command_group": 2,
15
+ "string": 3,
16
+ "integer": 4,
17
+ "boolean": 5,
18
+ "user": 6,
19
+ "channel": 7,
20
+ "role": 8,
21
+ "mentionable": 9,
22
+ "number": 10,
23
+ "attachment": 11,
24
+ }
25
+
26
+ def __init__(self, name: str, description: str, option_type: str = "string",
27
+ required: bool = False, choices: List[Dict] = None):
28
+ self.name = name
29
+ self.description = description
30
+ self.type = self.TYPES.get(option_type, 3)
31
+ self.required = required
32
+ self.choices = choices or []
33
+
34
+ def to_dict(self) -> Dict:
35
+ data = {
36
+ "name": self.name,
37
+ "description": self.description,
38
+ "type": self.type,
39
+ "required": self.required,
40
+ }
41
+ if self.choices:
42
+ data["choices"] = self.choices
43
+ return data
44
+
45
+
46
+ class SlashCommand:
47
+ """Slash command builder and decorator."""
48
+
49
+ def __init__(self, name: str, description: str,
50
+ options: List[Option] = None,
51
+ default_member_permissions: Optional[str] = None,
52
+ dm_permission: bool = True):
53
+ self.name = name
54
+ self.description = description
55
+ self.options = options or []
56
+ self.default_member_permissions = default_member_permissions
57
+ self.dm_permission = dm_permission
58
+ self._callback: Optional[Callable] = None
59
+
60
+ def __call__(self, func: Callable):
61
+ self._callback = func
62
+ functools.update_wrapper(self, func)
63
+ return self
64
+
65
+ async def invoke(self, interaction: Any, **kwargs):
66
+ """Invoke the command callback."""
67
+ if self._callback is None:
68
+ raise RuntimeError(f"No callback set for slash command '{self.name}'")
69
+ return await self._callback(interaction, **kwargs)
70
+
71
+ def to_dict(self) -> Dict:
72
+ data = {
73
+ "name": self.name,
74
+ "description": self.description,
75
+ "options": [opt.to_dict() for opt in self.options],
76
+ "dm_permission": self.dm_permission,
77
+ }
78
+ if self.default_member_permissions is not None:
79
+ data["default_member_permissions"] = self.default_member_permissions
80
+ return data
81
+
82
+ @staticmethod
83
+ def decorator(name: str, description: str, options: List[Option] = None,
84
+ permissions: str = None):
85
+ """Decorator factory for slash commands."""
86
+ def wrapper(func: Callable) -> SlashCommand:
87
+ cmd = SlashCommand(
88
+ name=name,
89
+ description=description,
90
+ options=options,
91
+ default_member_permissions=permissions,
92
+ )
93
+ cmd._callback = func
94
+ return cmd
95
+ return wrapper
96
+
97
+
98
+ class PrefixCommand:
99
+ """Prefix (text-based) command builder and decorator."""
100
+
101
+ def __init__(self, name: str, aliases: List[str] = None,
102
+ description: str = "", cooldown: int = 0,
103
+ required_permissions: List[str] = None):
104
+ self.name = name
105
+ self.aliases = aliases or []
106
+ self.description = description
107
+ self.cooldown = cooldown
108
+ self.required_permissions = required_permissions or []
109
+ self._callback: Optional[Callable] = None
110
+ self._cooldowns: Dict[int, float] = {}
111
+
112
+ def __call__(self, func: Callable):
113
+ self._callback = func
114
+ functools.update_wrapper(self, func)
115
+ return self
116
+
117
+ async def invoke(self, ctx: Any, *args, **kwargs):
118
+ """Invoke the command with cooldown check."""
119
+ import time
120
+ if self.cooldown > 0:
121
+ user_id = getattr(ctx.author, "id", 0)
122
+ last_used = self._cooldowns.get(user_id, 0)
123
+ now = time.time()
124
+ if now - last_used < self.cooldown:
125
+ remaining = self.cooldown - (now - last_used)
126
+ raise CooldownError(f"Command on cooldown. Try again in {remaining:.1f}s.")
127
+ self._cooldowns[user_id] = now
128
+
129
+ if self._callback is None:
130
+ raise RuntimeError(f"No callback set for prefix command '{self.name}'")
131
+ return await self._callback(ctx, *args, **kwargs)
132
+
133
+ @staticmethod
134
+ def decorator(name: str, aliases: List[str] = None,
135
+ description: str = "", cooldown: int = 0,
136
+ permissions: List[str] = None):
137
+ """Decorator factory for prefix commands."""
138
+ def wrapper(func: Callable) -> PrefixCommand:
139
+ cmd = PrefixCommand(
140
+ name=name,
141
+ aliases=aliases,
142
+ description=description,
143
+ cooldown=cooldown,
144
+ required_permissions=permissions,
145
+ )
146
+ cmd._callback = func
147
+ return cmd
148
+ return wrapper
149
+
150
+
151
+ class CooldownError(Exception):
152
+ """Raised when a command is on cooldown."""
153
+ pass
154
+
155
+
156
+ class CommandRegistry:
157
+ """Registry to manage all slash and prefix commands."""
158
+
159
+ def __init__(self):
160
+ self._slash: Dict[str, SlashCommand] = {}
161
+ self._prefix: Dict[str, PrefixCommand] = {}
162
+
163
+ def add_slash(self, command: SlashCommand):
164
+ self._slash[command.name] = command
165
+
166
+ def add_prefix(self, command: PrefixCommand):
167
+ self._prefix[command.name] = command
168
+ for alias in command.aliases:
169
+ self._prefix[alias] = command
170
+
171
+ def get_slash(self, name: str) -> Optional[SlashCommand]:
172
+ return self._slash.get(name)
173
+
174
+ def get_prefix(self, name: str) -> Optional[PrefixCommand]:
175
+ return self._prefix.get(name)
176
+
177
+ def all_slash(self) -> List[SlashCommand]:
178
+ return list(self._slash.values())
179
+
180
+ def all_prefix(self) -> List[PrefixCommand]:
181
+ seen = set()
182
+ result = []
183
+ for cmd in self._prefix.values():
184
+ if cmd.name not in seen:
185
+ seen.add(cmd.name)
186
+ result.append(cmd)
187
+ return result