scurrypy 0.3.3__py3-none-any.whl → 0.3.4.1__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.
Potentially problematic release.
This version of scurrypy might be problematic. Click here for more details.
- discord/client.py +112 -106
- discord/dispatch/event_dispatcher.py +19 -3
- discord/events/channel_events.py +7 -1
- discord/events/guild_events.py +3 -1
- discord/events/message_events.py +4 -3
- discord/events/reaction_events.py +5 -4
- discord/gateway.py +37 -57
- discord/http.py +4 -16
- {scurrypy-0.3.3.dist-info → scurrypy-0.3.4.1.dist-info}/METADATA +1 -1
- {scurrypy-0.3.3.dist-info → scurrypy-0.3.4.1.dist-info}/RECORD +13 -13
- {scurrypy-0.3.3.dist-info → scurrypy-0.3.4.1.dist-info}/WHEEL +0 -0
- {scurrypy-0.3.3.dist-info → scurrypy-0.3.4.1.dist-info}/licenses/LICENSE +0 -0
- {scurrypy-0.3.3.dist-info → scurrypy-0.3.4.1.dist-info}/top_level.txt +0 -0
discord/client.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
1
3
|
from .config import BaseConfig
|
|
2
4
|
from .intents import Intents
|
|
3
5
|
from .error import DiscordError
|
|
@@ -29,6 +31,11 @@ class Client(ClientLike):
|
|
|
29
31
|
prefix (str, optional): set message prefix if using command prefixes
|
|
30
32
|
quiet (bool, optional): if INFO, DEBUG, and WARN should be logged
|
|
31
33
|
"""
|
|
34
|
+
if not token:
|
|
35
|
+
raise ValueError("Token is required")
|
|
36
|
+
if not application_id:
|
|
37
|
+
raise ValueError("Application ID is required")
|
|
38
|
+
|
|
32
39
|
from .logger import Logger
|
|
33
40
|
from .gateway import GatewayClient
|
|
34
41
|
from .http import HTTPClient
|
|
@@ -82,30 +89,41 @@ class Client(ClientLike):
|
|
|
82
89
|
return func
|
|
83
90
|
return decorator
|
|
84
91
|
|
|
85
|
-
def command(self, command: SlashCommand | MessageCommand | UserCommand,
|
|
86
|
-
"""Decorator
|
|
92
|
+
def command(self, command: SlashCommand | MessageCommand | UserCommand, guild_ids: list[int] | None = None):
|
|
93
|
+
"""Decorator to register a function as a command handler.
|
|
87
94
|
|
|
88
95
|
Args:
|
|
89
|
-
command (SlashCommand | MessageCommand | UserCommand): command to register
|
|
90
|
-
|
|
96
|
+
command (SlashCommand | MessageCommand | UserCommand): The command to register.
|
|
97
|
+
guild_ids (list[int] | None): Guild IDs for guild-specific commands. None for global commands.
|
|
91
98
|
"""
|
|
92
99
|
def decorator(func):
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
self.command_dispatcher.message_command
|
|
96
|
-
|
|
97
|
-
self.command_dispatcher.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
# Map command types to dispatcher registration functions
|
|
101
|
+
handler_map = {
|
|
102
|
+
MessageCommand: self.command_dispatcher.message_command,
|
|
103
|
+
UserCommand: self.command_dispatcher.user_command,
|
|
104
|
+
SlashCommand: self.command_dispatcher.command,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# Resolve dispatcher method based on command type
|
|
108
|
+
for cls, handler in handler_map.items():
|
|
109
|
+
if isinstance(command, cls):
|
|
110
|
+
handler(command.name, func)
|
|
111
|
+
break
|
|
100
112
|
else:
|
|
101
|
-
raise ValueError(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"Command {getattr(command, 'name', '<unnamed>')} must be one of "
|
|
115
|
+
f"SlashCommand, UserCommand, MessageCommand; got {type(command).__name__}."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Queue command for later registration
|
|
119
|
+
if guild_ids:
|
|
120
|
+
gids = [guild_ids] if isinstance(guild_ids, int) else guild_ids
|
|
121
|
+
for gid in gids:
|
|
122
|
+
self._guild_commands.setdefault(gid, []).append(command)
|
|
107
123
|
else:
|
|
108
124
|
self._global_commands.append(command)
|
|
125
|
+
|
|
126
|
+
return func # ensure original function is preserved
|
|
109
127
|
return decorator
|
|
110
128
|
|
|
111
129
|
def event(self, event_name: str):
|
|
@@ -137,7 +155,7 @@ class Client(ClientLike):
|
|
|
137
155
|
"""
|
|
138
156
|
self._shutdown_hooks.append(func)
|
|
139
157
|
|
|
140
|
-
def
|
|
158
|
+
def fetch_application(self, application_id: int):
|
|
141
159
|
"""Creates an interactable application resource.
|
|
142
160
|
|
|
143
161
|
Args:
|
|
@@ -150,7 +168,7 @@ class Client(ClientLike):
|
|
|
150
168
|
|
|
151
169
|
return Application(application_id, self._http)
|
|
152
170
|
|
|
153
|
-
def
|
|
171
|
+
def fetch_guild(self, guild_id: int):
|
|
154
172
|
"""Creates an interactable guild resource.
|
|
155
173
|
|
|
156
174
|
Args:
|
|
@@ -163,7 +181,7 @@ class Client(ClientLike):
|
|
|
163
181
|
|
|
164
182
|
return Guild(guild_id, self._http)
|
|
165
183
|
|
|
166
|
-
def
|
|
184
|
+
def fetch_channel(self, channel_id: int):
|
|
167
185
|
"""Creates an interactable channel resource.
|
|
168
186
|
|
|
169
187
|
Args:
|
|
@@ -176,7 +194,7 @@ class Client(ClientLike):
|
|
|
176
194
|
|
|
177
195
|
return Channel(channel_id, self._http)
|
|
178
196
|
|
|
179
|
-
def
|
|
197
|
+
def fetch_message(self, channel_id: int, message_id: int):
|
|
180
198
|
"""Creates an interactable message resource.
|
|
181
199
|
|
|
182
200
|
Args:
|
|
@@ -190,7 +208,7 @@ class Client(ClientLike):
|
|
|
190
208
|
|
|
191
209
|
return Message(message_id, channel_id, self._http)
|
|
192
210
|
|
|
193
|
-
def
|
|
211
|
+
def fetch_user(self, user_id: int):
|
|
194
212
|
"""Creates an interactable user resource.
|
|
195
213
|
|
|
196
214
|
Args:
|
|
@@ -213,13 +231,11 @@ class Client(ClientLike):
|
|
|
213
231
|
self._logger.log_info(f"Guild {guild_id} already queued, skipping clear.")
|
|
214
232
|
return
|
|
215
233
|
|
|
216
|
-
|
|
234
|
+
self._guild_commands[guild_id] = []
|
|
217
235
|
|
|
218
236
|
async def _listen(self):
|
|
219
237
|
"""Main event loop for incoming gateway requests."""
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
while True:
|
|
238
|
+
while self._ws.is_connected():
|
|
223
239
|
try:
|
|
224
240
|
message = await self._ws.receive()
|
|
225
241
|
if not message:
|
|
@@ -227,17 +243,17 @@ class Client(ClientLike):
|
|
|
227
243
|
|
|
228
244
|
op_code = message.get('op')
|
|
229
245
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
246
|
+
match op_code:
|
|
247
|
+
case 0:
|
|
248
|
+
dispatch_type = message.get('t')
|
|
249
|
+
self._logger.log_info(f"DISPATCH -> {dispatch_type}")
|
|
250
|
+
event_data = message.get('d')
|
|
251
|
+
self._ws.sequence = message.get('s') or self._ws.sequence
|
|
235
252
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
253
|
+
if dispatch_type == "READY":
|
|
254
|
+
self._ws.session_id = event_data.get("session_id")
|
|
255
|
+
self._ws.connect_url = event_data.get("resume_gateway_url", self._ws.connect_url)
|
|
239
256
|
|
|
240
|
-
try:
|
|
241
257
|
if self.prefix_dispatcher.prefix and dispatch_type == 'MESSAGE_CREATE':
|
|
242
258
|
await self.prefix_dispatcher.dispatch(event_data)
|
|
243
259
|
|
|
@@ -245,107 +261,97 @@ class Client(ClientLike):
|
|
|
245
261
|
await self.command_dispatcher.dispatch(event_data)
|
|
246
262
|
|
|
247
263
|
await self.dispatcher.dispatch(dispatch_type, event_data)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
elif op_code == 9:
|
|
258
|
-
self._ws.session_id = None
|
|
259
|
-
self._ws.sequence = None
|
|
260
|
-
raise ConnectionError("Invalid session.")
|
|
264
|
+
case 7:
|
|
265
|
+
raise ConnectionError("Reconnect requested by server.")
|
|
266
|
+
case 9:
|
|
267
|
+
self._ws.session_id = None
|
|
268
|
+
self._ws.sequence = None
|
|
269
|
+
raise ConnectionError("Invalid session.")
|
|
270
|
+
case 11:
|
|
271
|
+
self._logger.log_debug("Heartbeat ACK received")
|
|
272
|
+
|
|
261
273
|
except asyncio.CancelledError:
|
|
262
|
-
|
|
274
|
+
break
|
|
263
275
|
except DiscordError as e:
|
|
264
276
|
if e.fatal:
|
|
265
|
-
|
|
277
|
+
self._logger.log_error(f"Fatal DiscordError: {e}")
|
|
278
|
+
break
|
|
266
279
|
else:
|
|
267
280
|
self._logger.log_warn(f"Recoverable DiscordError: {e}")
|
|
268
281
|
except ConnectionError as e:
|
|
269
282
|
self._logger.log_warn(f"Connection lost: {e}")
|
|
270
|
-
|
|
271
|
-
await asyncio.sleep(2)
|
|
283
|
+
raise
|
|
272
284
|
|
|
273
285
|
async def start(self):
|
|
274
286
|
"""Runs the main lifecycle of the bot.
|
|
275
287
|
Handles connection setup, heartbeat management, event loop, and automatic reconnects.
|
|
276
288
|
"""
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
await self._http.start_session()
|
|
282
|
-
await self._ws.connect()
|
|
283
|
-
await self._ws.start_heartbeat()
|
|
289
|
+
try:
|
|
290
|
+
await self._http.start_session()
|
|
291
|
+
await self._ws.connect()
|
|
292
|
+
await self._ws.start_heartbeat()
|
|
284
293
|
|
|
294
|
+
while self._ws.is_connected():
|
|
285
295
|
if self._ws.session_id and self._ws.sequence:
|
|
286
296
|
await self._ws.reconnect()
|
|
287
297
|
else:
|
|
288
298
|
await self._ws.identify()
|
|
289
299
|
|
|
290
300
|
if not self._is_set_up:
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
self._logger.log_info(f"Setting hook {hook.__name__}")
|
|
294
|
-
await hook(self)
|
|
295
|
-
self._logger.log_high_priority("Hooks set up.")
|
|
301
|
+
await self.startup()
|
|
302
|
+
self._is_set_up = True
|
|
296
303
|
|
|
297
|
-
|
|
298
|
-
|
|
304
|
+
await self._listen()
|
|
305
|
+
|
|
306
|
+
# If we get here, connection was lost - reconnect
|
|
307
|
+
await self._ws.close()
|
|
308
|
+
await asyncio.sleep(5)
|
|
309
|
+
await self._ws.connect()
|
|
310
|
+
await self._ws.start_heartbeat()
|
|
299
311
|
|
|
300
|
-
|
|
301
|
-
|
|
312
|
+
except asyncio.CancelledError:
|
|
313
|
+
self._logger.log_high_priority("Connection cancelled via KeyboardInterrupt.")
|
|
314
|
+
except Exception as e:
|
|
315
|
+
self._logger.log_error(f"{type(e).__name__} - {e}")
|
|
316
|
+
finally:
|
|
317
|
+
await self.close()
|
|
302
318
|
|
|
303
|
-
|
|
319
|
+
async def startup(self):
|
|
320
|
+
try:
|
|
321
|
+
if self._setup_hooks:
|
|
322
|
+
for hook in self._setup_hooks:
|
|
323
|
+
self._logger.log_info(f"Setting hook {hook.__name__}")
|
|
324
|
+
await hook(self)
|
|
325
|
+
self._logger.log_high_priority("Hooks set up.")
|
|
304
326
|
|
|
305
|
-
|
|
327
|
+
# register GUILD commands
|
|
328
|
+
await self.command_dispatcher._register_guild_commands(self._guild_commands)
|
|
306
329
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
self._logger.log_info("Cancelling connection...")
|
|
314
|
-
break
|
|
315
|
-
except DiscordError as e:
|
|
316
|
-
self._logger.log_error(f"Fatal DiscordError: {e}")
|
|
317
|
-
break
|
|
318
|
-
except Exception as e:
|
|
319
|
-
self._logger.log_error(f"Unspecified Error Type {type(e).__name__} - {e}")
|
|
320
|
-
break
|
|
321
|
-
finally:
|
|
322
|
-
# Run hooks (with safe catching)
|
|
323
|
-
for hook in self._shutdown_hooks:
|
|
324
|
-
try:
|
|
325
|
-
self._logger.log_info(f"Executing shutdown hook {hook.__name__}")
|
|
326
|
-
await hook(self)
|
|
327
|
-
except Exception as e:
|
|
328
|
-
self._logger.log_error(f"{type(e).__name__}: {e}")
|
|
329
|
-
|
|
330
|
-
# Always close resources
|
|
331
|
-
try:
|
|
332
|
-
await self._ws.close()
|
|
333
|
-
except Exception as e:
|
|
334
|
-
self._logger.log_warn(f"WebSocket close failed: {e}")
|
|
335
|
-
|
|
336
|
-
try:
|
|
337
|
-
await self._http.close_session()
|
|
338
|
-
except Exception as e:
|
|
339
|
-
self._logger.log_warn(f"HTTP session close failed: {e}")
|
|
330
|
+
# register GLOBAL commands
|
|
331
|
+
await self.command_dispatcher._register_global_commands(self._global_commands)
|
|
332
|
+
|
|
333
|
+
self._logger.log_high_priority("Commands set up.")
|
|
334
|
+
except Exception:
|
|
335
|
+
raise
|
|
340
336
|
|
|
337
|
+
async def close(self):
|
|
338
|
+
"""Gracefully close HTTP and websocket connections."""
|
|
339
|
+
# Close HTTP first since it's more important
|
|
340
|
+
self._logger.log_debug("Closing HTTP session...")
|
|
341
|
+
await self._http.close_session()
|
|
341
342
|
|
|
343
|
+
# Then try websocket with short timeout
|
|
344
|
+
try:
|
|
345
|
+
self._logger.log_debug("Closing websocket connection...")
|
|
346
|
+
await asyncio.wait_for(self._ws.close(), timeout=1.0)
|
|
347
|
+
except:
|
|
348
|
+
pass # Don't care if websocket won't close
|
|
349
|
+
|
|
342
350
|
def run(self):
|
|
343
351
|
"""Starts the bot.
|
|
344
352
|
Handles starting the session, WS, and heartbeat, reconnection logic,
|
|
345
353
|
setting up emojis and hooks, and then listens for gateway events.
|
|
346
|
-
"""
|
|
347
|
-
import asyncio
|
|
348
|
-
|
|
354
|
+
"""
|
|
349
355
|
try:
|
|
350
356
|
asyncio.run(self.start())
|
|
351
357
|
except KeyboardInterrupt:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import importlib
|
|
2
2
|
from ..client_like import ClientLike
|
|
3
3
|
|
|
4
4
|
from ..resources.message import Message
|
|
@@ -9,8 +9,24 @@ class EventDispatcher:
|
|
|
9
9
|
RESOURCE_MAP = { # maps discord events to their respective dataclass (lazy loading)
|
|
10
10
|
'READY': ('discord.events.ready_event', 'ReadyEvent'),
|
|
11
11
|
|
|
12
|
-
'
|
|
12
|
+
'GUILD_CREATE': ('discord.events.guild_events', 'GuildCreateEvent'),
|
|
13
|
+
'GUILD_UPDATE': ('discord.events.guild_events', 'GuildUpdateEvent'),
|
|
14
|
+
'GUILD_DELETE': ('discord.events.guild_events', 'GuildDeleteEvent'),
|
|
15
|
+
|
|
16
|
+
'CHANNEL_CREATE': ('discord.events.channel_events', 'GuildChannelCreateEvent'),
|
|
17
|
+
'CHANNEL_UPDATE': ('discord.events.channel_events', 'GuildChannelUpdateEvent'),
|
|
18
|
+
'CHANNEL_DELETE': ('discord.events.channel_events', 'GuildChannelDeleteEvent'),
|
|
19
|
+
'CHANNEL_PINS_UPDATE': ('discord.events.channel_events', 'ChannelPinsUpdateEvent'),
|
|
20
|
+
|
|
21
|
+
'MESSAGE_CREATE': ('discord.events.message_events', 'MessageCreateEvent'),
|
|
22
|
+
'MESSAGE_UPDATE': ('discord.events.message_events', 'MessageUpdateEvent'),
|
|
23
|
+
'MESSAGE_DELETE': ('discord.events.message_events', 'MessageDeleteEvent'),
|
|
13
24
|
|
|
25
|
+
'MESSAGE_REACTION_ADD': ('discord.events.reaction_events', 'ReactionAddEvent'),
|
|
26
|
+
'MESSAGE_REACTION_REMOVE': ('discord.events.reaction_events', 'ReactionRemoveEvent'),
|
|
27
|
+
'MESSAGE_REACTION_REMOVE_ALL': ('discord.events.reaction_events', 'ReactionRemoveAllEvent'),
|
|
28
|
+
'MESSAGE_REACTION_REMOVE_EMOJI': ('discord.events.reaction_events', 'ReactionRemoveEmojiEvent'),
|
|
29
|
+
|
|
14
30
|
# and other events...
|
|
15
31
|
}
|
|
16
32
|
|
|
@@ -59,7 +75,7 @@ class EventDispatcher:
|
|
|
59
75
|
|
|
60
76
|
module_name, class_name = module_info
|
|
61
77
|
|
|
62
|
-
module = import_module(module_name)
|
|
78
|
+
module = importlib.import_module(module_name)
|
|
63
79
|
if not module:
|
|
64
80
|
print(f"Cannot find module '{module_name}'!")
|
|
65
81
|
return
|
discord/events/channel_events.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
+
from ..model import DataModel
|
|
2
3
|
from typing import Optional
|
|
3
4
|
|
|
4
5
|
@dataclass
|
|
5
|
-
class GuildChannelEvent:
|
|
6
|
+
class GuildChannelEvent(DataModel):
|
|
6
7
|
"""Base guild channel event."""
|
|
7
8
|
|
|
8
9
|
id: int
|
|
@@ -48,5 +49,10 @@ class GuildChannelDeleteEvent(GuildChannelEvent):
|
|
|
48
49
|
class ChannelPinsUpdateEvent:
|
|
49
50
|
"""Pin update event."""
|
|
50
51
|
channel_id: int
|
|
52
|
+
"""ID of channel where the pins were updated."""
|
|
53
|
+
|
|
51
54
|
guild_id: Optional[int]
|
|
55
|
+
"""ID of the guild where the pins were updated."""
|
|
56
|
+
|
|
52
57
|
last_pin_timestamp: Optional[str] # ISO8601 timestamp of last pinned message
|
|
58
|
+
"""ISO8601 formatted timestamp of the last pinned message in the channel."""
|
discord/events/guild_events.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
+
from ..model import DataModel
|
|
3
|
+
|
|
2
4
|
from ..models.member import MemberModel
|
|
3
5
|
from ..resources.channel import Channel
|
|
4
6
|
|
|
5
7
|
@dataclass
|
|
6
|
-
class GuildEvent:
|
|
8
|
+
class GuildEvent(DataModel):
|
|
7
9
|
"""Base guild event."""
|
|
8
10
|
joined_at: str
|
|
9
11
|
"""ISO8601 timestamp of when app joined the guild."""
|
discord/events/message_events.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from typing import Optional
|
|
3
|
+
from ..model import DataModel
|
|
3
4
|
|
|
4
5
|
from ..resources.message import Message
|
|
5
6
|
from ..models.member import MemberModel
|
|
6
7
|
|
|
7
8
|
@dataclass
|
|
8
|
-
class MessageCreateEvent:
|
|
9
|
+
class MessageCreateEvent(DataModel):
|
|
9
10
|
"""Received when a message is created."""
|
|
10
11
|
message: Message
|
|
11
12
|
"""Message resource object. See [`Resource.Message`][discord.resources.message.Message]."""
|
|
@@ -17,7 +18,7 @@ class MessageCreateEvent:
|
|
|
17
18
|
"""Partial Member object of the author of the message. See [`MemberModel`][discord.models.member.MemberModel]."""
|
|
18
19
|
|
|
19
20
|
@dataclass
|
|
20
|
-
class MessageUpdateEvent:
|
|
21
|
+
class MessageUpdateEvent(DataModel):
|
|
21
22
|
"""Received when a message is updated."""
|
|
22
23
|
message: Message
|
|
23
24
|
"""Message resource object. See [`Resource.Message`][discord.resources.message.Message]."""
|
|
@@ -29,7 +30,7 @@ class MessageUpdateEvent:
|
|
|
29
30
|
"""Partial Member object of the author of the message. See [`MemberModel`][discord.models.member.MemberModel]."""
|
|
30
31
|
|
|
31
32
|
@dataclass
|
|
32
|
-
class MessageDeleteEvent:
|
|
33
|
+
class MessageDeleteEvent(DataModel):
|
|
33
34
|
"""Received when a message is deleted."""
|
|
34
35
|
|
|
35
36
|
id: int
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from typing import Optional
|
|
3
|
+
from ..model import DataModel
|
|
3
4
|
|
|
4
5
|
from ..models.member import MemberModel
|
|
5
6
|
from ..models.emoji import EmojiModel
|
|
@@ -14,7 +15,7 @@ class ReactionType:
|
|
|
14
15
|
"""A super emoji."""
|
|
15
16
|
|
|
16
17
|
@dataclass
|
|
17
|
-
class ReactionAddEvent:
|
|
18
|
+
class ReactionAddEvent(DataModel):
|
|
18
19
|
"""Reaction added event."""
|
|
19
20
|
|
|
20
21
|
type: int
|
|
@@ -45,7 +46,7 @@ class ReactionAddEvent:
|
|
|
45
46
|
"""ID of the user who sent the message where the reaction was added."""
|
|
46
47
|
|
|
47
48
|
@dataclass
|
|
48
|
-
class ReactionRemoveEvent:
|
|
49
|
+
class ReactionRemoveEvent(DataModel):
|
|
49
50
|
"""Reaction removed event."""
|
|
50
51
|
|
|
51
52
|
type: int
|
|
@@ -69,7 +70,7 @@ class ReactionRemoveEvent:
|
|
|
69
70
|
burst: bool
|
|
70
71
|
"""If the emoji of the removed reaction is super."""
|
|
71
72
|
|
|
72
|
-
class ReactionRemoveAllEvent:
|
|
73
|
+
class ReactionRemoveAllEvent(DataModel):
|
|
73
74
|
"""Remove all reactions event."""
|
|
74
75
|
|
|
75
76
|
channel_id: int
|
|
@@ -82,7 +83,7 @@ class ReactionRemoveAllEvent:
|
|
|
82
83
|
"""ID of the guild where all reaction were removed (if in a guild)."""
|
|
83
84
|
|
|
84
85
|
@dataclass
|
|
85
|
-
class ReactionRemoveEmojiEvent:
|
|
86
|
+
class ReactionRemoveEmojiEvent(DataModel):
|
|
86
87
|
"""All reactions of a specific emoji removed."""
|
|
87
88
|
|
|
88
89
|
emoji: EmojiModel
|
discord/gateway.py
CHANGED
|
@@ -53,57 +53,46 @@ class GatewayClient:
|
|
|
53
53
|
|
|
54
54
|
async def connect(self):
|
|
55
55
|
"""Established websocket connection to Discord."""
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
except Exception as e:
|
|
60
|
-
self._logger.log_error(f"Websocket Connection Error {type(e).__name__} - {e}")
|
|
61
|
-
|
|
56
|
+
self.ws = await websockets.connect(self.connect_url + self.url_params)
|
|
57
|
+
self._logger.log_high_priority("Connected to Discord.")
|
|
58
|
+
|
|
62
59
|
async def receive(self):
|
|
63
60
|
"""Receives and logs messages from the gateway.
|
|
64
61
|
|
|
65
62
|
Returns:
|
|
66
63
|
(dict): parsed JSON data
|
|
67
64
|
"""
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
65
|
+
|
|
66
|
+
message = await asyncio.wait_for(self.ws.recv(), timeout=60)
|
|
67
|
+
|
|
68
|
+
if message:
|
|
69
|
+
data: dict = json.loads(message)
|
|
70
|
+
self._logger.log_debug(f"Received: {DISCORD_OP_CODES.get(data.get('op'))} - {json.dumps(data, indent=4)}")
|
|
71
|
+
self._logger.log_info(f"Received: {DISCORD_OP_CODES.get(data.get('op'))}")
|
|
72
|
+
return data
|
|
73
|
+
|
|
74
|
+
return None
|
|
75
|
+
|
|
80
76
|
async def send(self, message: dict):
|
|
81
77
|
"""Sends a JSON-encoded message to the gateway.
|
|
82
78
|
|
|
83
79
|
Args:
|
|
84
80
|
message (dict): the message to send
|
|
85
81
|
"""
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
self._logger.log_error(f"Error on send: {type(e).__name__} - {e}")
|
|
90
|
-
|
|
82
|
+
self._logger.log_debug(f"Sending payload: {message}")
|
|
83
|
+
await self.ws.send(json.dumps(message))
|
|
84
|
+
|
|
91
85
|
async def send_heartbeat_loop(self):
|
|
92
86
|
"""Background task that sends heartbeat payloads in regular intervals.
|
|
93
87
|
Retries until cancelled.
|
|
94
88
|
"""
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
except asyncio.CancelledError:
|
|
103
|
-
self._logger.log_debug("Heartbeat task cancelled")
|
|
104
|
-
except Exception as e:
|
|
105
|
-
self._logger.log_error(f"Error on heartbeat send: {type(e).__name__} - {e}")
|
|
106
|
-
|
|
89
|
+
while self.ws:
|
|
90
|
+
await asyncio.sleep(self.heartbeat_interval / 1000)
|
|
91
|
+
hb_data = {"op": 1, "d": self.sequence}
|
|
92
|
+
await self.send(hb_data)
|
|
93
|
+
self._logger.log_debug(f"Sending: {hb_data}")
|
|
94
|
+
self._logger.log_info("Heartbeat sent.")
|
|
95
|
+
|
|
107
96
|
async def identify(self):
|
|
108
97
|
"""Sends the IDENIFY payload (token, intents, connection properties).
|
|
109
98
|
Must be sent after connecting to the WS.
|
|
@@ -127,15 +116,12 @@ class GatewayClient:
|
|
|
127
116
|
|
|
128
117
|
async def start_heartbeat(self):
|
|
129
118
|
"""Waits for initial HELLO event, hydrates the HelloEvent class, and begins the heartbeat."""
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
except Exception as e:
|
|
137
|
-
self._logger.log_error(f"Heartbeat Task Error: {type(e).__name__} - {e}")
|
|
138
|
-
|
|
119
|
+
data = await self.receive()
|
|
120
|
+
hello = HelloEvent.from_dict(data.get('d'))
|
|
121
|
+
self.heartbeat_interval = hello.heartbeat_interval
|
|
122
|
+
self.heartbeat = asyncio.create_task(self.send_heartbeat_loop())
|
|
123
|
+
self._logger.log_high_priority("Heartbeat started.")
|
|
124
|
+
|
|
139
125
|
async def reconnect(self):
|
|
140
126
|
"""Sends RESUME payload to reconnect with the same session ID and sequence number
|
|
141
127
|
as provided by Discord.
|
|
@@ -156,20 +142,14 @@ class GatewayClient:
|
|
|
156
142
|
if self.heartbeat:
|
|
157
143
|
self._logger.log_high_priority(f"Cancelling heartbeat...")
|
|
158
144
|
self.heartbeat.cancel()
|
|
159
|
-
|
|
160
|
-
await asyncio.wait_for(self.heartbeat, timeout=3) # Add timeout to avoid hanging
|
|
161
|
-
except asyncio.CancelledError:
|
|
162
|
-
self._logger.log_debug("Heartbeat cancelled by CancelledError.")
|
|
163
|
-
except asyncio.TimeoutError:
|
|
164
|
-
self._logger.log_error("Heartbeat cancel timed out.")
|
|
165
|
-
except Exception as e:
|
|
166
|
-
self._logger.log_error(f"Unexpected error cancelling heartbeat: {type(e).__name__} - {e}")
|
|
145
|
+
await asyncio.wait_for(self.heartbeat, timeout=3) # Add timeout to avoid hanging
|
|
167
146
|
self.heartbeat = None
|
|
168
147
|
|
|
169
148
|
if self.ws:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
self._logger.log_high_priority("WebSocket closed.")
|
|
173
|
-
except Exception as e:
|
|
174
|
-
self._logger.log_error(f"Error while closing websocket: {type(e).__name__} - {e}")
|
|
149
|
+
await self.ws.close()
|
|
150
|
+
self._logger.log_high_priority("WebSocket closed.")
|
|
175
151
|
self.ws = None
|
|
152
|
+
|
|
153
|
+
def is_connected(self):
|
|
154
|
+
"""Helper function to tell whether the websocket is still active."""
|
|
155
|
+
return self.ws is not None
|
discord/http.py
CHANGED
|
@@ -269,24 +269,12 @@ class HTTPClient:
|
|
|
269
269
|
if now < self.global_reset:
|
|
270
270
|
await asyncio.sleep(self.global_reset - now)
|
|
271
271
|
|
|
272
|
-
async def close_session(self):
|
|
273
|
-
"""Gracefully shuts down all workes and closes aiohttp session."""
|
|
274
|
-
# Stop
|
|
272
|
+
async def close_session(self):
|
|
273
|
+
"""Gracefully shuts down all workes and closes aiohttp session."""
|
|
274
|
+
# Stop workers
|
|
275
275
|
for q in self.bucket_queues.values():
|
|
276
276
|
await q.queue.put(self._sentinel)
|
|
277
|
-
|
|
278
|
-
# Stop pending worker
|
|
279
277
|
await self.pending_queue.put(self._sentinel)
|
|
280
|
-
|
|
281
|
-
# Cancel all tasks except the current one
|
|
282
|
-
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
|
283
|
-
for task in tasks:
|
|
284
|
-
task.cancel()
|
|
285
|
-
|
|
286
|
-
# Wait for tasks to acknowledge cancellation
|
|
287
|
-
await asyncio.gather(*tasks, return_exceptions=True)
|
|
288
|
-
|
|
289
|
-
# Close the aiohttp session
|
|
278
|
+
|
|
290
279
|
if self.session and not self.session.closed:
|
|
291
280
|
await self.session.close()
|
|
292
|
-
self._logger.log_debug("Session closed successfully.")
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
discord/__init__.py,sha256=cETkxHmm0s9YkSJgn-1daQhnbL96fuD7L9SIg2t5vBg,6823
|
|
2
|
-
discord/client.py,sha256=
|
|
2
|
+
discord/client.py,sha256=LCK6dzQxt6YEy_JiMNpvaJ0WQPRODS_YVvy1SEPeXt8,13582
|
|
3
3
|
discord/client_like.py,sha256=JyJq0XBq0vKuPBJ_ZnYf5yAAuX1zz_2B1TZBQE-BYbQ,473
|
|
4
4
|
discord/config.py,sha256=OH1A2mNKhDlGvQYASEsVUx2pNxP1YQ2a7a7z-IM5xFg,200
|
|
5
5
|
discord/error.py,sha256=AlislRTna554cM6KC0KrwKugzYDYtx_9C8_3QFe4XDc,2070
|
|
6
|
-
discord/gateway.py,sha256=
|
|
7
|
-
discord/http.py,sha256=
|
|
6
|
+
discord/gateway.py,sha256=1TVUKrd3JovoM4df5-GlMZ0kz15Xls5V48ShXDSlK3Q,5334
|
|
7
|
+
discord/http.py,sha256=BI7bCjedh8zuu5pdOLhI-XwMZfdVj5qP9ZZY6WJqtgo,10511
|
|
8
8
|
discord/intents.py,sha256=Lf2fogFFDqilZeKJv7tcUgKmMW3D7ykK4bBNi-zDzYA,2866
|
|
9
9
|
discord/logger.py,sha256=qAmOc3geCcCCqPhdi61SVWzMDewmM8Q_KWhTcjO46j8,4726
|
|
10
10
|
discord/model.py,sha256=CmuxyoWLWokE_UvCQ9M7U9Cr7JH9R7ULMv9KMwzXjDQ,3105
|
|
11
11
|
discord/dispatch/__init__.py,sha256=m7ixrbNhOV9QRORXPw6LSwxofQMAvLmPFBweBZu9ACc,20
|
|
12
12
|
discord/dispatch/command_dispatcher.py,sha256=pyJOQaZLZYrHUEs6HEWp8XMKTMZX4SBrwTizGKIeUG8,5904
|
|
13
|
-
discord/dispatch/event_dispatcher.py,sha256=
|
|
13
|
+
discord/dispatch/event_dispatcher.py,sha256=1A7Qof_IzTi5_14IMPxIQDpvo3-Sj-X0KZWOuVGH53k,3764
|
|
14
14
|
discord/dispatch/prefix_dispatcher.py,sha256=4mkn3cuXTjdEChbewkbZQqd_sMKm4jePSFKKOPbt12g,2065
|
|
15
15
|
discord/events/__init__.py,sha256=xE8YtJ7NKZkm7MLnohDQIbezh3ColmLR-3BMiZabt3k,18
|
|
16
|
-
discord/events/channel_events.py,sha256=
|
|
17
|
-
discord/events/guild_events.py,sha256=
|
|
16
|
+
discord/events/channel_events.py,sha256=t9UL4JjDqulAP_XepQ8MRMW54pNRqCbIK3M8tauzf9I,1556
|
|
17
|
+
discord/events/guild_events.py,sha256=Ok9tW3tjcwtbiqJgbe-42d9-R3-2RzqmIgBHEP-2Pcc,896
|
|
18
18
|
discord/events/hello_event.py,sha256=O8Ketu_N943cnGaFkGsAHfWhgKXFQCYCqSD3EqdsXjA,225
|
|
19
19
|
discord/events/interaction_events.py,sha256=5hlYOsV1ROiRXISAGCKcZo8vfk6oqiZcNzzZjSteiag,4361
|
|
20
|
-
discord/events/message_events.py,sha256=
|
|
21
|
-
discord/events/reaction_events.py,sha256=
|
|
20
|
+
discord/events/message_events.py,sha256=M5xdaJH1zRzdZk0oN0Jykaeu9k09EjgZjeiIT_EkL1A,1475
|
|
21
|
+
discord/events/reaction_events.py,sha256=xx7GD-fqakhJmS-X-HbuAUg9pg6Gqo_KRtLTdPJu7UE,2643
|
|
22
22
|
discord/events/ready_event.py,sha256=c3Pf4ndNYV2byuliADi8pUxpuvKXa9FLKNz_uzIWGso,794
|
|
23
23
|
discord/models/__init__.py,sha256=ZKhFO5eX4GbTRdvi4CU4z2hO-HQU29WZw2x4DujvARY,18
|
|
24
24
|
discord/models/application.py,sha256=2sXtRysUc2TJ40FjdcrWgosmwMrp_h3ybddubQMixKM,924
|
|
@@ -48,8 +48,8 @@ discord/resources/guild.py,sha256=Unld1lWY3XynmRHU2FCi3-LA9VNp2thMI2BlILUTTxk,81
|
|
|
48
48
|
discord/resources/interaction.py,sha256=mr4kLecQpl3AtgnNeqnj3eZIwQ73Clutdi-gnoSMWVU,5237
|
|
49
49
|
discord/resources/message.py,sha256=RtvcCRx0lwW-mHPl3aNYoEvGffrvtpLsQ2fVWckywVI,7527
|
|
50
50
|
discord/resources/user.py,sha256=vk89TnCVi-6ZgbDs_TZTCXrx_NfFS5Q9Wi_itYoaoyg,3085
|
|
51
|
-
scurrypy-0.3.
|
|
52
|
-
scurrypy-0.3.
|
|
53
|
-
scurrypy-0.3.
|
|
54
|
-
scurrypy-0.3.
|
|
55
|
-
scurrypy-0.3.
|
|
51
|
+
scurrypy-0.3.4.1.dist-info/licenses/LICENSE,sha256=NtspfRMAlryd1Eev4BYi9EFbKhvdmlCJJ2-ADUoEBoI,426
|
|
52
|
+
scurrypy-0.3.4.1.dist-info/METADATA,sha256=n5LZHS1N0dlKDDo6DIGXih1tIty503u8UnmTIqTRQ6I,2956
|
|
53
|
+
scurrypy-0.3.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
54
|
+
scurrypy-0.3.4.1.dist-info/top_level.txt,sha256=fJkrNbR-_8ubMBUcDEJBcfkpECrvSEmMrNKgvLlQFoM,8
|
|
55
|
+
scurrypy-0.3.4.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|