scurrypy 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.

Potentially problematic release.


This version of scurrypy might be problematic. Click here for more details.

Files changed (56) hide show
  1. scurrypy-0.1.0/LICENSE +5 -0
  2. scurrypy-0.1.0/PKG-INFO +8 -0
  3. scurrypy-0.1.0/README.md +12 -0
  4. scurrypy-0.1.0/discord/__init__.py +9 -0
  5. scurrypy-0.1.0/discord/client.py +312 -0
  6. scurrypy-0.1.0/discord/dispatch/__init__.py +1 -0
  7. scurrypy-0.1.0/discord/dispatch/command_dispatcher.py +156 -0
  8. scurrypy-0.1.0/discord/dispatch/event_dispatcher.py +85 -0
  9. scurrypy-0.1.0/discord/dispatch/prefix_dispatcher.py +53 -0
  10. scurrypy-0.1.0/discord/error.py +63 -0
  11. scurrypy-0.1.0/discord/events/__init__.py +33 -0
  12. scurrypy-0.1.0/discord/events/channel_events.py +52 -0
  13. scurrypy-0.1.0/discord/events/guild_events.py +38 -0
  14. scurrypy-0.1.0/discord/events/hello_event.py +9 -0
  15. scurrypy-0.1.0/discord/events/interaction_events.py +145 -0
  16. scurrypy-0.1.0/discord/events/message_events.py +43 -0
  17. scurrypy-0.1.0/discord/events/reaction_events.py +99 -0
  18. scurrypy-0.1.0/discord/events/ready_event.py +30 -0
  19. scurrypy-0.1.0/discord/gateway.py +175 -0
  20. scurrypy-0.1.0/discord/http.py +292 -0
  21. scurrypy-0.1.0/discord/intents.py +87 -0
  22. scurrypy-0.1.0/discord/logger.py +147 -0
  23. scurrypy-0.1.0/discord/model.py +88 -0
  24. scurrypy-0.1.0/discord/models/__init__.py +8 -0
  25. scurrypy-0.1.0/discord/models/application.py +37 -0
  26. scurrypy-0.1.0/discord/models/emoji.py +34 -0
  27. scurrypy-0.1.0/discord/models/guild.py +35 -0
  28. scurrypy-0.1.0/discord/models/integration.py +23 -0
  29. scurrypy-0.1.0/discord/models/member.py +27 -0
  30. scurrypy-0.1.0/discord/models/role.py +53 -0
  31. scurrypy-0.1.0/discord/models/user.py +15 -0
  32. scurrypy-0.1.0/discord/parts/__init__.py +28 -0
  33. scurrypy-0.1.0/discord/parts/action_row.py +258 -0
  34. scurrypy-0.1.0/discord/parts/attachment.py +18 -0
  35. scurrypy-0.1.0/discord/parts/channel.py +20 -0
  36. scurrypy-0.1.0/discord/parts/command.py +102 -0
  37. scurrypy-0.1.0/discord/parts/component_types.py +5 -0
  38. scurrypy-0.1.0/discord/parts/components_v2.py +270 -0
  39. scurrypy-0.1.0/discord/parts/embed.py +154 -0
  40. scurrypy-0.1.0/discord/parts/message.py +179 -0
  41. scurrypy-0.1.0/discord/parts/modal.py +21 -0
  42. scurrypy-0.1.0/discord/parts/role.py +39 -0
  43. scurrypy-0.1.0/discord/resources/__init__.py +10 -0
  44. scurrypy-0.1.0/discord/resources/application.py +94 -0
  45. scurrypy-0.1.0/discord/resources/bot_emojis.py +49 -0
  46. scurrypy-0.1.0/discord/resources/channel.py +192 -0
  47. scurrypy-0.1.0/discord/resources/guild.py +265 -0
  48. scurrypy-0.1.0/discord/resources/interaction.py +155 -0
  49. scurrypy-0.1.0/discord/resources/message.py +223 -0
  50. scurrypy-0.1.0/discord/resources/user.py +111 -0
  51. scurrypy-0.1.0/pyproject.toml +22 -0
  52. scurrypy-0.1.0/scurrypy.egg-info/PKG-INFO +8 -0
  53. scurrypy-0.1.0/scurrypy.egg-info/SOURCES.txt +54 -0
  54. scurrypy-0.1.0/scurrypy.egg-info/dependency_links.txt +1 -0
  55. scurrypy-0.1.0/scurrypy.egg-info/top_level.txt +1 -0
  56. scurrypy-0.1.0/setup.cfg +4 -0
scurrypy-0.1.0/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Copyright (c) 2025 Furmissile. All rights reserved.
2
+
3
+ Derivative works are not permitted -- the source code is made available for educational and reference purposes only, and is not exempt from licensing.
4
+ This software is provided on an "as-is" basis and the author is not responsible for any liability that may come from misuse.
5
+ No commercial profit may be drawn off of derivatives of this software project for any reason.
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: scurrypy
3
+ Version: 0.1.0
4
+ Summary: Discord API Wrapper in Python
5
+ Author: Furmissile
6
+ Requires-Python: >=3.10
7
+ License-File: LICENSE
8
+ Dynamic: license-file
@@ -0,0 +1,12 @@
1
+ # __Welcome to ScurryPy__
2
+ Yet another Discord API wrapper in Python!
3
+
4
+ While this wrapper is mainly used for various squirrel-related shenanigans, it can also be used for more generic bot purposes.
5
+
6
+ ## Features
7
+ * Command and event handling
8
+ * Declarative style using decorators
9
+ * Supports both legacy and new features
10
+ * Respects Discord's rate limits
11
+
12
+ See the docs for more!
@@ -0,0 +1,9 @@
1
+ # discord
2
+
3
+ from .logger import Logger
4
+ from .client import Client
5
+ from .intents import Intents, set_intents
6
+
7
+ from .events import *
8
+ from .resources import *
9
+ from .parts import *
@@ -0,0 +1,312 @@
1
+ import asyncio
2
+
3
+ from .logger import Logger
4
+ from .gateway import GatewayClient
5
+ from .http import HTTPClient
6
+ from .intents import Intents
7
+ from .error import DiscordError
8
+
9
+ from .resources.guild import Guild
10
+ from .resources.channel import Channel
11
+ from .resources.message import Message
12
+ from .resources.bot_emojis import BotEmojis
13
+ from .resources.user import User
14
+ from .resources.application import Application
15
+
16
+ from .parts.command import SlashCommand, MessageCommand, UserCommand
17
+
18
+ from .dispatch.event_dispatcher import EventDispatcher
19
+ from .dispatch.prefix_dispatcher import PrefixDispatcher
20
+ from .dispatch.command_dispatcher import CommandDispatcher
21
+
22
+ class Client:
23
+ """Main entry point for Discord bots.
24
+ Ties together the moving parts: gateway, HTTP, event dispatching, command handling, and resource managers.
25
+ """
26
+ def __init__(self,
27
+ *,
28
+ token: str,
29
+ application_id: int,
30
+ intents: int = Intents.DEFAULT,
31
+ debug_mode: bool = False,
32
+ prefix = None,
33
+ quiet: bool = False
34
+ ):
35
+ """
36
+ Args:
37
+ token (str): the bot's token
38
+ application_id (int): the bot's user ID
39
+ intents (int, optional): gateway intents. Defaults to Intents.DEFAULT.
40
+ debug_mode (bool, optional): toggle debug messages. Defaults to False.
41
+ prefix (str, optional): set message prefix if using command prefixes
42
+ quiet (bool, optional): if INFO, DEBUG, and WARN should be logged
43
+ """
44
+ self.token = token
45
+ self.application_id = application_id
46
+
47
+ self._logger = Logger(debug_mode, quiet)
48
+ self._ws = GatewayClient(token, intents, self._logger)
49
+ self._http = HTTPClient(token, self._logger)
50
+
51
+ if prefix and (intents & Intents.MESSAGE_CONTENT == 0):
52
+ self._logger.log_warn('Prefix set without message content enabled.')
53
+
54
+ self.dispatcher = EventDispatcher(self.application_id, self._http, self._logger)
55
+ self.prefix_dispatcher = PrefixDispatcher(self._http, self._logger, prefix)
56
+ self.command_dispatcher = CommandDispatcher(self.application_id, self._http, self._logger)
57
+
58
+ self._global_commands = [] # SlashCommand
59
+ self._guild_commands = {} # {guild_id : [commands], ...}
60
+
61
+ self._is_set_up = False
62
+ self._setup_hooks = []
63
+
64
+ self.emojis = BotEmojis(self._http, self.application_id)
65
+
66
+ def prefix_command(self, func):
67
+ """Decorator registers prefix commands by the name of the function.
68
+
69
+ Args:
70
+ func (callable): callback handle for command response
71
+ """
72
+ self.prefix_dispatcher.register(func.__name__, func)
73
+
74
+ def component(self, func):
75
+ """Decorator registers a function for a component handler.
76
+
77
+ Args:
78
+ func (callable): callback handle for component response
79
+ """
80
+ self.command_dispatcher.component(func)
81
+
82
+ def command(self, command: SlashCommand | MessageCommand | UserCommand, guild_id: int = 0):
83
+ """Decorator registers a function for a command handler.
84
+
85
+ Args:
86
+ command (SlashCommand | MessageCommand | UserCommand): command to register
87
+ guild_id (int): ID of guild in which to register command (if a guild command)
88
+ """
89
+ def decorator(func):
90
+ # hash out command type
91
+ if isinstance(command, MessageCommand):
92
+ self.command_dispatcher.message_command(func)
93
+ elif isinstance(command, UserCommand):
94
+ self.command_dispatcher.user_command(func)
95
+ elif isinstance(command, SlashCommand):
96
+ self.command_dispatcher.command(func)
97
+ else:
98
+ raise ValueError(f'Command {func.__name__} expected to be of type SlashCommand, UserCommand, MessageCommand; \
99
+ got {type(command).__name__}.')
100
+
101
+ # then hash out if this command should be guild or global level
102
+ if guild_id != 0:
103
+ self._guild_commands.setdefault(guild_id, []).append(command)
104
+ else:
105
+ self._global_commands.append(command)
106
+ return decorator
107
+
108
+ def event(self, event_name: str):
109
+ """Decorator registers a function for an event handler.
110
+
111
+ Args:
112
+ event_name (str): event name (must be a valid event)
113
+ """
114
+ def decorator(func):
115
+ self.dispatcher.register(event_name, func)
116
+ return func
117
+ return decorator
118
+
119
+ def setup_hook(self, func):
120
+ """Decorator registers a setup hook.
121
+ (Runs once before the bot starts listening)
122
+
123
+ Args:
124
+ func (callable): callback to the setup function
125
+ """
126
+ self._setup_hooks.append(func)
127
+
128
+ def application_from_id(self, application_id: int):
129
+ """Creates an interactable application resource.
130
+
131
+ Args:
132
+ application_id (int): id of target application
133
+
134
+ Returns:
135
+ (Application): the Application resource
136
+ """
137
+ return Application(application_id, self._http)
138
+
139
+ def guild_from_id(self, guild_id: int):
140
+ """Creates an interactable guild resource.
141
+
142
+ Args:
143
+ guild_id (int): id of target guild
144
+
145
+ Returns:
146
+ (Guild): the Guild resource
147
+ """
148
+ return Guild(guild_id, self._http)
149
+
150
+ def channel_from_id(self, channel_id: int):
151
+ """Creates an interactable channel resource.
152
+
153
+ Args:
154
+ channel_id (int): id of target channel
155
+
156
+ Returns:
157
+ (Channel): the Channel resource
158
+ """
159
+ return Channel(channel_id, self._http)
160
+
161
+ def message_from_id(self, channel_id: int, message_id: int):
162
+ """Creates an interactable message resource.
163
+
164
+ Args:
165
+ message_id (int): id of target message
166
+ channel_id (int): channel id of target message
167
+
168
+ Returns:
169
+ (Message): the Message resource
170
+ """
171
+ return Message(message_id, channel_id, self._http)
172
+
173
+ def user_from_id(self, user_id: int):
174
+ """Creates an interactable user resource.
175
+
176
+ Args:
177
+ user_id (int): id of target user
178
+
179
+ Returns:
180
+ (User): the User resource
181
+ """
182
+ return User(user_id, self._http)
183
+
184
+ async def clear_guild_commands(self, guild_id: int):
185
+ """Clear a guild's slash commands.
186
+
187
+ Args:
188
+ guild_id (int): id of the target guild
189
+ """
190
+ if self._guild_commands.get(guild_id):
191
+ self._logger.log_info(f"Guild {guild_id} already queued, skipping clear.")
192
+ return
193
+
194
+ await self.command_dispatcher._register_guild_commands({guild_id: []})
195
+
196
+ async def _listen(self):
197
+ """Main event loop for incoming gateway requests."""
198
+ while True:
199
+ try:
200
+ message = await self._ws.receive()
201
+ if not message:
202
+ raise ConnectionError("No message received.")
203
+
204
+ op_code = message.get('op')
205
+
206
+ if op_code == 0:
207
+ dispatch_type = message.get('t')
208
+ self._logger.log_info(f"DISPATCH -> {dispatch_type}")
209
+ event_data = message.get('d')
210
+ self._ws.sequence = message.get('s') or self._ws.sequence
211
+
212
+ if dispatch_type == "READY":
213
+ self._ws.session_id = event_data.get("session_id")
214
+ self._ws.connect_url = event_data.get("resume_gateway_url", self._ws.connect_url)
215
+
216
+ try:
217
+ if self.prefix_dispatcher.prefix and dispatch_type == 'MESSAGE_CREATE':
218
+ await self.prefix_dispatcher.dispatch(event_data)
219
+
220
+ elif dispatch_type == 'INTERACTION_CREATE':
221
+ await self.command_dispatcher.dispatch(event_data)
222
+
223
+ await self.dispatcher.dispatch(dispatch_type, event_data)
224
+ except DiscordError as e:
225
+ if e.fatal:
226
+ raise # let run() handle fatal errors
227
+ else:
228
+ self._logger.log_warn(f"Recoverable DiscordError: {e}")
229
+ continue # keep listening
230
+
231
+ elif op_code == 7:
232
+ raise ConnectionError("Reconnect requested by server.")
233
+ elif op_code == 9:
234
+ self._ws.session_id = None
235
+ self._ws.sequence = None
236
+ raise ConnectionError("Invalid session.")
237
+ except asyncio.CancelledError:
238
+ raise
239
+ except DiscordError as e:
240
+ if e.fatal:
241
+ raise # propagate fatal errors
242
+ else:
243
+ self._logger.log_warn(f"Recoverable DiscordError: {e}")
244
+ except ConnectionError as e:
245
+ self._logger.log_warn(f"Connection lost: {e}")
246
+ await self._ws.close()
247
+ await asyncio.sleep(2)
248
+
249
+ async def start(self):
250
+ """Runs the main lifecycle of the bot.
251
+ Handles connection setup, heartbeat management, event loop, and automatic reconnects.
252
+ """
253
+ while True:
254
+ try:
255
+ await self._http.start_session()
256
+ await self._ws.connect()
257
+ await self._ws.start_heartbeat()
258
+
259
+ if self._ws.session_id and self._ws.sequence:
260
+ await self._ws.reconnect()
261
+ else:
262
+ await self._ws.identify()
263
+
264
+ if not self._is_set_up:
265
+ if self._setup_hooks:
266
+ for hook in self._setup_hooks:
267
+ self._logger.log_info(f"Setting hook {hook.__name__}")
268
+ await hook()
269
+ self._logger.log_high_priority("Hooks set up.")
270
+
271
+ # register GUILD commands
272
+ await self.command_dispatcher._register_guild_commands(self._guild_commands)
273
+
274
+ # register GLOBAL commands
275
+ await self.command_dispatcher._register_global_commands(self._global_commands)
276
+
277
+ self._is_set_up = True
278
+
279
+ await self._listen()
280
+
281
+ except ConnectionError as e:
282
+ self._logger.log_warn("Connection lost. Attempting reconnect...")
283
+ await self._ws.close()
284
+ await asyncio.sleep(2)
285
+ continue
286
+ except asyncio.CancelledError:
287
+ self._logger.log_info("Cancelling connection...")
288
+ break
289
+ except DiscordError as e:
290
+ self._logger.log_error(f"Fatal DiscordError: {e}")
291
+ break
292
+ except Exception as e:
293
+ self._logger.log_error(f"Unspecified Error Type {type(e).__name__} - {e}")
294
+ break
295
+ finally:
296
+ await self._ws.close()
297
+ await self._http.close_session()
298
+
299
+ def run(self):
300
+ """Starts the bot.
301
+ Handles starting the session, WS, and heartbeat, reconnection logic,
302
+ setting up emojis and hooks, and then listens for gateway events.
303
+ """
304
+ try:
305
+ asyncio.run(self.start())
306
+ except KeyboardInterrupt:
307
+ self._logger.log_debug("Shutdown requested via KeyboardInterrupt.")
308
+ except Exception as e:
309
+ self._logger.log_error(f"{type(e).__name__} {e}")
310
+ finally:
311
+ self._logger.log_high_priority("Bot shutting down.")
312
+ self._logger.close()
@@ -0,0 +1 @@
1
+ # discord/dispatch
@@ -0,0 +1,156 @@
1
+ import asyncio
2
+
3
+ from ..http import HTTPClient
4
+ from ..logger import Logger
5
+
6
+ from ..events.interaction_events import ApplicationCommandData, MessageComponentData, ModalData, InteractionEvent
7
+ from ..resources.interaction import Interaction, InteractionDataTypes
8
+
9
+ class InteractionTypes:
10
+ """Interaction types constants."""
11
+
12
+ APPLICATION_COMMAND = 2
13
+ """Slash command interaction."""
14
+
15
+ MESSAGE_COMPONENT = 3
16
+ """Message component interaction (e.g., button, select menu, etc.)."""
17
+
18
+ MODAL_SUBMIT = 5
19
+ """Modal submit interaction."""
20
+
21
+ class CommandDispatcher:
22
+ """Central hub for registering and dispatching interaction responses."""
23
+
24
+ RESOURCE_MAP = { # maps discord events to their respective dataclass
25
+ InteractionTypes.APPLICATION_COMMAND: ApplicationCommandData,
26
+ InteractionTypes.MESSAGE_COMPONENT: MessageComponentData,
27
+ InteractionTypes.MODAL_SUBMIT: ModalData
28
+ }
29
+ """Maps [`InteractionTypes`][discord.dispatch.command_dispatcher.InteractionTypes] to their respective dataclass."""
30
+
31
+ def __init__(self, application_id: int, http: HTTPClient, logger: Logger):
32
+ self.application_id = application_id
33
+ """Bot's application ID."""
34
+
35
+ self._http = http
36
+ """HTTP session for requests."""
37
+
38
+ self._logger = logger
39
+ """Logger instance to log events."""
40
+
41
+ self._component_handlers = {}
42
+ """Mapping of component custom IDs to handler."""
43
+
44
+ self._handlers = {}
45
+ """Mapping of command names to handler."""
46
+
47
+ self._message_handlers = {}
48
+ """Mapping of message command names to handler."""
49
+
50
+ self._user_handlers = {}
51
+ """Mapping of user command names to handler."""
52
+
53
+ async def _register_guild_commands(self, commands: dict):
54
+ """Registers a command at the guild level.
55
+
56
+ Args:
57
+ commands (dict): mapping of guild IDs to respective serialized command data
58
+ """
59
+
60
+ for guild_id, cmds in commands.items():
61
+ # register commands PER GUILD
62
+ await self._http.request(
63
+ 'PUT',
64
+ f"applications/{self.application_id}/guilds/{guild_id}/commands",
65
+ [command._to_dict() for command in cmds]
66
+ )
67
+
68
+ async def _register_global_commands(self, commands: list):
69
+ """Registers a command at the global/bot level. (ALL GUILDS)
70
+
71
+ Args:
72
+ commands (list): list of serialized commands
73
+ """
74
+
75
+ global_commands = [command._to_dict() for command in commands]
76
+
77
+ await self._http.request('PUT', f"applications/{self.application_id}/commands", global_commands)
78
+
79
+ def command(self, handler):
80
+ """Decorator to register slash commands.
81
+
82
+ Args:
83
+ handler (callable): callback handle for command response
84
+ """
85
+ self._handlers[handler.__name__] = handler
86
+
87
+ def component(self, handler):
88
+ """Decorator to register component interactions.
89
+
90
+ Args:
91
+ handler (callable): callback handle for component response
92
+ """
93
+ self._component_handlers[handler.__name__] = handler
94
+
95
+ def user_command(self, handler):
96
+ """Decorator to register user commands.
97
+
98
+ Args:
99
+ handler (callable): callback handle for user command response
100
+ """
101
+ self._user_handlers[handler.__name__] = handler
102
+
103
+ def message_command(self, handler):
104
+ """Decorator to register message commands.
105
+
106
+ Args:
107
+ handler (callable): callback handle for message command response
108
+ """
109
+ self._message_handlers[handler.__name__] = handler
110
+
111
+ async def dispatch(self, data: dict):
112
+ """Dispatch a response to an `INTERACTION_CREATE` event
113
+
114
+ Args:
115
+ data (dict): interaction data
116
+ """
117
+ event = InteractionEvent(interaction=Interaction.from_dict(data, self._http))
118
+
119
+ event_data_obj = self.RESOURCE_MAP.get(event.interaction.type)
120
+
121
+ if not event_data_obj:
122
+ return
123
+
124
+ event.data = event_data_obj.from_dict(data.get('data'))
125
+ handler = None
126
+ name = None
127
+
128
+ match event.interaction.type:
129
+ case InteractionTypes.APPLICATION_COMMAND:
130
+ name = event.data.name
131
+
132
+ match event.data.type:
133
+ case InteractionDataTypes.SLASH_COMMAND:
134
+ handler = self._handlers.get(name)
135
+ case InteractionDataTypes.USER_COMMAND:
136
+ handler = self._user_handlers.get(name)
137
+ case InteractionDataTypes.MESSAGE_COMMAND:
138
+ handler = self._message_handlers.get(name)
139
+
140
+ case InteractionTypes.MESSAGE_COMPONENT:
141
+ name = event.data.custom_id
142
+ handler = self._component_handlers.get(name)
143
+
144
+ case InteractionTypes.MODAL_SUBMIT:
145
+ name = event.data.custom_id
146
+ handler = self._component_handlers.get(name)
147
+
148
+ if not handler:
149
+ self._logger.log_warn(f"No handler registered for interaction '{name}'")
150
+ return
151
+
152
+ try:
153
+ await handler(event) # NOTE: treat command options as args!
154
+ self._logger.log_info(f"Interaction Event '{name}' Acknowledged.")
155
+ except Exception as e:
156
+ self._logger.log_error(f"Error in interaction '{name}': {e}")
@@ -0,0 +1,85 @@
1
+ from ..http import HTTPClient
2
+ from ..logger import Logger
3
+
4
+ from ..events.ready_event import *
5
+ from ..events.reaction_events import *
6
+ from ..events.guild_events import *
7
+ from ..events.message_events import *
8
+ from ..events.channel_events import *
9
+ from ..events.interaction_events import *
10
+
11
+ from ..resources.message import Message
12
+
13
+ class EventDispatcher:
14
+ """Central hub for handling Discord Gateway events."""
15
+ RESOURCE_MAP = { # maps discord events to their respective dataclass
16
+ 'READY': ReadyEvent,
17
+
18
+ "MESSAGE_CREATE": MessageCreateEvent,
19
+ "MESSAGE_UPDATE": MessageUpdateEvent,
20
+ 'MESSAGE_DELETE': MessageDeleteEvent,
21
+
22
+ 'MESSAGE_REACTION_ADD': ReactionAddEvent,
23
+ 'MESSAGE_REACTION_REMOVE': ReactionRemoveEvent,
24
+ 'MESSAGE_REACTION_REMOVE_ALL': ReactionRemoveAllEvent,
25
+ 'MESSAGE_REACTION_REMOVE_EMOJI': ReactionRemoveEmojiEvent,
26
+
27
+ 'CHANNEL_CREATE': GuildChannelCreateEvent,
28
+ 'CHANNEL_UPDATE': GuildChannelUpdateEvent,
29
+ 'CHANNEL_DELETE': GuildChannelDeleteEvent,
30
+
31
+ 'CHANNEL_PINS_UPDATE': ChannelPinsUpdateEvent,
32
+
33
+ 'GUILD_CREATE': GuildCreateEvent,
34
+ 'GUILD_UPDATE': GuildUpdateEvent,
35
+ 'GUILD_DELETE': GuildDeleteEvent,
36
+
37
+ 'INTERACTION_CREATE': InteractionEvent
38
+
39
+ # and other events...
40
+ }
41
+ """Mapping of event names to respective dataclass."""
42
+
43
+ def __init__(self, application_id: int, http: HTTPClient, logger: Logger):
44
+ self.application_id = application_id
45
+ """Bot's ID."""
46
+
47
+ self._http = http
48
+ """HTTP session for requests."""
49
+
50
+ self._logger = logger
51
+ """HTTP session for requests"""
52
+
53
+ self._handlers = {}
54
+ """Mapping of event names to handler."""
55
+
56
+ def register(self, event_name: str, handler):
57
+ """Registers the given handler to the given event name.
58
+ (Event name must be a valid Discord event)
59
+
60
+ Args:
61
+ event_name (str): name of the event
62
+ handler (callable): callback to handle event
63
+ """
64
+ self._handlers[event_name] = handler
65
+
66
+ async def dispatch(self, event_name: str, data: dict):
67
+ """Hydrate the corresponding dataclass and call the handler.
68
+
69
+ Args:
70
+ event_name (str): name of the event
71
+ data (dict): Discord's raw event payload
72
+ """
73
+ cls = self.RESOURCE_MAP.get(event_name)
74
+
75
+ if not cls:
76
+ return
77
+
78
+ if isinstance(cls, Message) and cls.author.id == self.application_id:
79
+ return # ignore bot's own messages
80
+
81
+ handler = self._handlers.get(event_name, None)
82
+ if handler:
83
+ obj = cls.from_dict(data, self._http)
84
+ await handler(obj)
85
+ self._logger.log_info(f"Event {event_name} Acknowledged.")
@@ -0,0 +1,53 @@
1
+ from ..http import HTTPClient
2
+ from ..logger import Logger
3
+
4
+ from ..events.message_events import MessageCreateEvent
5
+
6
+ from ..resources.message import Message
7
+ from ..models.member import MemberModel
8
+
9
+ class PrefixDispatcher:
10
+ """Handles text-based command messages that start with a specific prefix."""
11
+ def __init__(self, http: HTTPClient, logger: Logger, prefix: str):
12
+ self._http = http
13
+ """HTTP session for requests."""
14
+
15
+ self._logger = logger
16
+ """Logger instance to log events."""
17
+
18
+ self.prefix = prefix
19
+ """User-defined command prefix."""
20
+
21
+ self._handlers = {}
22
+ """Mapping of command prefix names to handler"""
23
+
24
+ def register(self, name: str, handler):
25
+ """Registers a handler for a command name (case-insensitive)
26
+
27
+ Args:
28
+ name (str): name of handler (and command)
29
+ handler (callable): handler callback
30
+ """
31
+ self._handlers[name.lower()] = handler
32
+
33
+ async def dispatch(self, data: dict):
34
+ """Hydrate the corresponding dataclass and call the handler.
35
+
36
+ Args:
37
+ data (dict): Discord's raw event payload
38
+ """
39
+ event = MessageCreateEvent(
40
+ guild_id=data.get('guild_id'),
41
+ message=Message.from_dict(data, self._http),
42
+ member=MemberModel.from_dict(data.get('member'))
43
+ )
44
+
45
+ if event.message._has_prefix(self.prefix):
46
+ command, *args = event.message._extract_args(self.prefix)
47
+ handler = self._handlers.get(command)
48
+ if handler:
49
+ try:
50
+ await handler(event, *args)
51
+ self._logger.log_info(f"Prefix Event '{command}' Acknowledged.")
52
+ except Exception as e:
53
+ self._logger.log_error(f"Error in prefix command '{command}': {e}")