scurrypy 0.3.2__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/__init__.py +221 -8
- discord/client.py +129 -115
- discord/dispatch/event_dispatcher.py +32 -27
- discord/events/__init__.py +0 -32
- discord/events/channel_events.py +7 -1
- discord/events/guild_events.py +4 -2
- discord/events/message_events.py +4 -3
- discord/events/reaction_events.py +5 -4
- discord/gateway.py +37 -57
- discord/http.py +4 -16
- discord/logger.py +4 -3
- discord/models/__init__.py +0 -7
- discord/parts/__init__.py +0 -26
- discord/parts/message.py +2 -1
- discord/resources/__init__.py +0 -9
- discord/resources/interaction.py +0 -2
- scurrypy-0.3.4.1.dist-info/METADATA +92 -0
- {scurrypy-0.3.2.dist-info → scurrypy-0.3.4.1.dist-info}/RECORD +21 -21
- scurrypy-0.3.2.dist-info/METADATA +0 -85
- {scurrypy-0.3.2.dist-info → scurrypy-0.3.4.1.dist-info}/WHEEL +0 -0
- {scurrypy-0.3.2.dist-info → scurrypy-0.3.4.1.dist-info}/licenses/LICENSE +0 -0
- {scurrypy-0.3.2.dist-info → scurrypy-0.3.4.1.dist-info}/top_level.txt +0 -0
discord/__init__.py
CHANGED
|
@@ -1,10 +1,223 @@
|
|
|
1
1
|
# discord
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
import importlib
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
# top-level
|
|
8
|
+
"Logger",
|
|
9
|
+
"Client",
|
|
10
|
+
"Intents",
|
|
11
|
+
"set_intents",
|
|
12
|
+
"BaseConfig",
|
|
13
|
+
|
|
14
|
+
# events
|
|
15
|
+
"ReadyEvent",
|
|
16
|
+
"ReactionAddEvent",
|
|
17
|
+
"ReactionRemoveEvent",
|
|
18
|
+
"ReactionRemoveEmojiEvent",
|
|
19
|
+
"ReactionRemoveAllEvent",
|
|
20
|
+
"GuildCreateEvent",
|
|
21
|
+
"GuildUpdateEvent",
|
|
22
|
+
"GuildDeleteEvent",
|
|
23
|
+
"MessageCreateEvent",
|
|
24
|
+
"MessageUpdateEvent",
|
|
25
|
+
"MessageDeleteEvent",
|
|
26
|
+
"GuildChannelCreateEvent",
|
|
27
|
+
"GuildChannelUpdateEvent",
|
|
28
|
+
"GuildChannelDeleteEvent",
|
|
29
|
+
"ChannelPinsUpdateEvent",
|
|
30
|
+
"InteractionEvent",
|
|
31
|
+
|
|
32
|
+
# models
|
|
33
|
+
"ApplicationModel",
|
|
34
|
+
"EmojiModel",
|
|
35
|
+
"GuildModel",
|
|
36
|
+
"MemberModel",
|
|
37
|
+
"UserModel",
|
|
38
|
+
"RoleModel",
|
|
39
|
+
|
|
40
|
+
# parts
|
|
41
|
+
"GuildChannel",
|
|
42
|
+
"Role",
|
|
43
|
+
"MessageBuilder",
|
|
44
|
+
"ModalBuilder",
|
|
45
|
+
"EmbedBuilder",
|
|
46
|
+
"ActionRow",
|
|
47
|
+
"StringSelect",
|
|
48
|
+
"UserSelect",
|
|
49
|
+
"RoleSelect",
|
|
50
|
+
"ChannelSelect",
|
|
51
|
+
"MentionableSelect",
|
|
52
|
+
"SlashCommand",
|
|
53
|
+
"MessageCommand",
|
|
54
|
+
"UserCommand",
|
|
55
|
+
"Container",
|
|
56
|
+
"Section",
|
|
57
|
+
"MediaGalleryItem",
|
|
58
|
+
"Label",
|
|
59
|
+
|
|
60
|
+
# resources
|
|
61
|
+
"Guild",
|
|
62
|
+
"Channel",
|
|
63
|
+
"Message",
|
|
64
|
+
"BotEmojis",
|
|
65
|
+
"User",
|
|
66
|
+
"Interaction",
|
|
67
|
+
"Application",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
# For editor support / autocomplete
|
|
71
|
+
if TYPE_CHECKING:
|
|
72
|
+
from .logger import Logger
|
|
73
|
+
from .client import Client
|
|
74
|
+
from .intents import Intents, set_intents
|
|
75
|
+
from .config import BaseConfig
|
|
76
|
+
|
|
77
|
+
# events
|
|
78
|
+
from .events.ready_event import ReadyEvent
|
|
79
|
+
from .events.reaction_events import (
|
|
80
|
+
ReactionAddEvent,
|
|
81
|
+
ReactionRemoveEvent,
|
|
82
|
+
ReactionRemoveEmojiEvent,
|
|
83
|
+
ReactionRemoveAllEvent,
|
|
84
|
+
)
|
|
85
|
+
from .events.guild_events import (
|
|
86
|
+
GuildCreateEvent,
|
|
87
|
+
GuildUpdateEvent,
|
|
88
|
+
GuildDeleteEvent,
|
|
89
|
+
)
|
|
90
|
+
from .events.message_events import (
|
|
91
|
+
MessageCreateEvent,
|
|
92
|
+
MessageUpdateEvent,
|
|
93
|
+
MessageDeleteEvent,
|
|
94
|
+
)
|
|
95
|
+
from .events.channel_events import (
|
|
96
|
+
GuildChannelCreateEvent,
|
|
97
|
+
GuildChannelUpdateEvent,
|
|
98
|
+
GuildChannelDeleteEvent,
|
|
99
|
+
ChannelPinsUpdateEvent,
|
|
100
|
+
)
|
|
101
|
+
from .events.interaction_events import InteractionEvent
|
|
102
|
+
|
|
103
|
+
# models
|
|
104
|
+
from .models.application import ApplicationModel
|
|
105
|
+
from .models.emoji import EmojiModel
|
|
106
|
+
from .models.guild import GuildModel
|
|
107
|
+
from .models.member import MemberModel
|
|
108
|
+
from .models.user import UserModel
|
|
109
|
+
from .models.role import RoleModel
|
|
110
|
+
|
|
111
|
+
# parts
|
|
112
|
+
from .parts.channel import GuildChannel
|
|
113
|
+
from .parts.role import Role
|
|
114
|
+
from .parts.message import MessageBuilder
|
|
115
|
+
from .parts.modal import ModalBuilder
|
|
116
|
+
from .parts.embed import EmbedBuilder
|
|
117
|
+
from .parts.action_row import (
|
|
118
|
+
ActionRow,
|
|
119
|
+
StringSelect,
|
|
120
|
+
UserSelect,
|
|
121
|
+
RoleSelect,
|
|
122
|
+
ChannelSelect,
|
|
123
|
+
MentionableSelect
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
from .parts.command import (
|
|
127
|
+
SlashCommand,
|
|
128
|
+
MessageCommand,
|
|
129
|
+
UserCommand
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
from .parts.components_v2 import (
|
|
133
|
+
Container,
|
|
134
|
+
Section,
|
|
135
|
+
MediaGalleryItem,
|
|
136
|
+
Label
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# resources
|
|
140
|
+
from .resources.guild import Guild
|
|
141
|
+
from .resources.channel import Channel
|
|
142
|
+
from .resources.message import Message
|
|
143
|
+
from .resources.bot_emojis import BotEmojis
|
|
144
|
+
from .resources.user import User
|
|
145
|
+
from .resources.interaction import Interaction
|
|
146
|
+
from .resources.application import Application
|
|
147
|
+
|
|
148
|
+
# Lazy loader
|
|
149
|
+
def __getattr__(name: str):
|
|
150
|
+
if name not in __all__:
|
|
151
|
+
raise AttributeError(f"module {__name__} has no attribute {name}")
|
|
152
|
+
|
|
153
|
+
mapping = {
|
|
154
|
+
# top-level
|
|
155
|
+
"Logger": "discord.logger",
|
|
156
|
+
"Client": "discord.client",
|
|
157
|
+
"Intents": "discord.intents",
|
|
158
|
+
"set_intents": "discord.intents",
|
|
159
|
+
"BaseConfig": "discord.config",
|
|
160
|
+
|
|
161
|
+
# events
|
|
162
|
+
"ReadyEvent": "discord.events.ready_event",
|
|
163
|
+
"ReactionAddEvent": "discord.events.reaction_events",
|
|
164
|
+
"ReactionRemoveEvent": "discord.events.reaction_events",
|
|
165
|
+
"ReactionRemoveEmojiEvent": "discord.events.reaction_events",
|
|
166
|
+
"ReactionRemoveAllEvent": "discord.events.reaction_events",
|
|
167
|
+
"GuildCreateEvent": "discord.events.guild_events",
|
|
168
|
+
"GuildUpdateEvent": "discord.events.guild_events",
|
|
169
|
+
"GuildDeleteEvent": "discord.events.guild_events",
|
|
170
|
+
"MessageCreateEvent": "discord.events.message_events",
|
|
171
|
+
"MessageUpdateEvent": "discord.events.message_events",
|
|
172
|
+
"MessageDeleteEvent": "discord.events.message_events",
|
|
173
|
+
"GuildChannelCreateEvent": "discord.events.channel_events",
|
|
174
|
+
"GuildChannelUpdateEvent": "discord.events.channel_events",
|
|
175
|
+
"GuildChannelDeleteEvent": "discord.events.channel_events",
|
|
176
|
+
"ChannelPinsUpdateEvent": "discord.events.channel_events",
|
|
177
|
+
"InteractionEvent": "discord.events.interaction_events",
|
|
178
|
+
|
|
179
|
+
# models
|
|
180
|
+
'ApplicationModel': "discord.models.application",
|
|
181
|
+
'EmojiModel': "discord.models.emoji",
|
|
182
|
+
'GuildModel': "discord.models.guild",
|
|
183
|
+
'MemberModel': "discord.models.member",
|
|
184
|
+
'UserModel': "discord.models.user",
|
|
185
|
+
'RoleModel': "discord.models.role",
|
|
186
|
+
|
|
187
|
+
# parts
|
|
188
|
+
'GuildChannel': "discord.parts.channel",
|
|
189
|
+
'Role': "discord.parts.role",
|
|
190
|
+
'MessageBuilder': "discord.parts.message",
|
|
191
|
+
'ModalBuilder': "discord.parts.modal",
|
|
192
|
+
'EmbedBuilder': "discord.parts.embed",
|
|
193
|
+
'ActionRow': "discord.parts.action_row",
|
|
194
|
+
'StringSelect': "discord.parts.action_row",
|
|
195
|
+
'UserSelect': "discord.parts.action_row",
|
|
196
|
+
'RoleSelect': "discord.parts.action_row",
|
|
197
|
+
'ChannelSelect': "discord.parts.action_row",
|
|
198
|
+
'MentionableSelect': "discord.parts.action_row",
|
|
199
|
+
'SlashCommand': "discord.parts.command",
|
|
200
|
+
'MessageCommand': "discord.parts.command",
|
|
201
|
+
'UserCommand': "discord.parts.command",
|
|
202
|
+
'Container': "discord.parts.components_v2",
|
|
203
|
+
'Section': "discord.parts.components_v2",
|
|
204
|
+
'MediaGalleryItem': "discord.parts.components_v2",
|
|
205
|
+
'Label': "discord.parts.components_v2",
|
|
206
|
+
|
|
207
|
+
# resources
|
|
208
|
+
'Guild': "discord.resources.guild",
|
|
209
|
+
'Channel': "discord.resources.channel",
|
|
210
|
+
'Message': "discord.resources.message",
|
|
211
|
+
'BotEmojis': "discord.resources.bot_emojis",
|
|
212
|
+
'User': "discord.resources.user",
|
|
213
|
+
'Interaction': "discord.resources.interaction",
|
|
214
|
+
'Application': "discord.resources.application"
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module = importlib.import_module(mapping[name])
|
|
218
|
+
attr = getattr(module, name)
|
|
219
|
+
globals()[name] = attr # cache it for future lookups
|
|
220
|
+
return attr
|
|
221
|
+
|
|
222
|
+
def __dir__():
|
|
223
|
+
return sorted(list(globals().keys()) + __all__)
|
discord/client.py
CHANGED
|
@@ -1,26 +1,12 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
|
|
3
|
-
from .
|
|
4
|
-
from .gateway import GatewayClient
|
|
5
|
-
from .http import HTTPClient
|
|
3
|
+
from .config import BaseConfig
|
|
6
4
|
from .intents import Intents
|
|
7
5
|
from .error import DiscordError
|
|
8
|
-
from .config import BaseConfig
|
|
9
6
|
from .client_like import ClientLike
|
|
10
7
|
|
|
11
|
-
from .resources.guild import Guild
|
|
12
|
-
from .resources.channel import Channel
|
|
13
|
-
from .resources.message import Message
|
|
14
|
-
from .resources.bot_emojis import BotEmojis
|
|
15
|
-
from .resources.user import User
|
|
16
|
-
from .resources.application import Application
|
|
17
|
-
|
|
18
8
|
from .parts.command import SlashCommand, MessageCommand, UserCommand
|
|
19
9
|
|
|
20
|
-
from .dispatch.event_dispatcher import EventDispatcher
|
|
21
|
-
from .dispatch.prefix_dispatcher import PrefixDispatcher
|
|
22
|
-
from .dispatch.command_dispatcher import CommandDispatcher
|
|
23
|
-
|
|
24
10
|
class Client(ClientLike):
|
|
25
11
|
"""Main entry point for Discord bots.
|
|
26
12
|
Ties together the moving parts: gateway, HTTP, event dispatching, command handling, and resource managers.
|
|
@@ -45,6 +31,19 @@ class Client(ClientLike):
|
|
|
45
31
|
prefix (str, optional): set message prefix if using command prefixes
|
|
46
32
|
quiet (bool, optional): if INFO, DEBUG, and WARN should be logged
|
|
47
33
|
"""
|
|
34
|
+
if not token:
|
|
35
|
+
raise ValueError("Token is required")
|
|
36
|
+
if not application_id:
|
|
37
|
+
raise ValueError("Application ID is required")
|
|
38
|
+
|
|
39
|
+
from .logger import Logger
|
|
40
|
+
from .gateway import GatewayClient
|
|
41
|
+
from .http import HTTPClient
|
|
42
|
+
from .resources.bot_emojis import BotEmojis
|
|
43
|
+
from .dispatch.event_dispatcher import EventDispatcher
|
|
44
|
+
from .dispatch.prefix_dispatcher import PrefixDispatcher
|
|
45
|
+
from .dispatch.command_dispatcher import CommandDispatcher
|
|
46
|
+
|
|
48
47
|
self.token = token
|
|
49
48
|
self.application_id = application_id
|
|
50
49
|
self.config = config
|
|
@@ -90,30 +89,41 @@ class Client(ClientLike):
|
|
|
90
89
|
return func
|
|
91
90
|
return decorator
|
|
92
91
|
|
|
93
|
-
def command(self, command: SlashCommand | MessageCommand | UserCommand,
|
|
94
|
-
"""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.
|
|
95
94
|
|
|
96
95
|
Args:
|
|
97
|
-
command (SlashCommand | MessageCommand | UserCommand): command to register
|
|
98
|
-
|
|
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.
|
|
99
98
|
"""
|
|
100
99
|
def decorator(func):
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
self.command_dispatcher.message_command
|
|
104
|
-
|
|
105
|
-
self.command_dispatcher.
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
108
112
|
else:
|
|
109
|
-
raise ValueError(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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)
|
|
115
123
|
else:
|
|
116
124
|
self._global_commands.append(command)
|
|
125
|
+
|
|
126
|
+
return func # ensure original function is preserved
|
|
117
127
|
return decorator
|
|
118
128
|
|
|
119
129
|
def event(self, event_name: str):
|
|
@@ -145,7 +155,7 @@ class Client(ClientLike):
|
|
|
145
155
|
"""
|
|
146
156
|
self._shutdown_hooks.append(func)
|
|
147
157
|
|
|
148
|
-
def
|
|
158
|
+
def fetch_application(self, application_id: int):
|
|
149
159
|
"""Creates an interactable application resource.
|
|
150
160
|
|
|
151
161
|
Args:
|
|
@@ -154,9 +164,11 @@ class Client(ClientLike):
|
|
|
154
164
|
Returns:
|
|
155
165
|
(Application): the Application resource
|
|
156
166
|
"""
|
|
167
|
+
from .resources.application import Application
|
|
168
|
+
|
|
157
169
|
return Application(application_id, self._http)
|
|
158
170
|
|
|
159
|
-
def
|
|
171
|
+
def fetch_guild(self, guild_id: int):
|
|
160
172
|
"""Creates an interactable guild resource.
|
|
161
173
|
|
|
162
174
|
Args:
|
|
@@ -165,9 +177,11 @@ class Client(ClientLike):
|
|
|
165
177
|
Returns:
|
|
166
178
|
(Guild): the Guild resource
|
|
167
179
|
"""
|
|
180
|
+
from .resources.guild import Guild
|
|
181
|
+
|
|
168
182
|
return Guild(guild_id, self._http)
|
|
169
183
|
|
|
170
|
-
def
|
|
184
|
+
def fetch_channel(self, channel_id: int):
|
|
171
185
|
"""Creates an interactable channel resource.
|
|
172
186
|
|
|
173
187
|
Args:
|
|
@@ -176,9 +190,11 @@ class Client(ClientLike):
|
|
|
176
190
|
Returns:
|
|
177
191
|
(Channel): the Channel resource
|
|
178
192
|
"""
|
|
193
|
+
from .resources.channel import Channel
|
|
194
|
+
|
|
179
195
|
return Channel(channel_id, self._http)
|
|
180
196
|
|
|
181
|
-
def
|
|
197
|
+
def fetch_message(self, channel_id: int, message_id: int):
|
|
182
198
|
"""Creates an interactable message resource.
|
|
183
199
|
|
|
184
200
|
Args:
|
|
@@ -188,9 +204,11 @@ class Client(ClientLike):
|
|
|
188
204
|
Returns:
|
|
189
205
|
(Message): the Message resource
|
|
190
206
|
"""
|
|
207
|
+
from .resources.message import Message
|
|
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:
|
|
@@ -199,6 +217,8 @@ class Client(ClientLike):
|
|
|
199
217
|
Returns:
|
|
200
218
|
(User): the User resource
|
|
201
219
|
"""
|
|
220
|
+
from .resources.user import User
|
|
221
|
+
|
|
202
222
|
return User(user_id, self._http)
|
|
203
223
|
|
|
204
224
|
async def clear_guild_commands(self, guild_id: int):
|
|
@@ -211,11 +231,11 @@ class Client(ClientLike):
|
|
|
211
231
|
self._logger.log_info(f"Guild {guild_id} already queued, skipping clear.")
|
|
212
232
|
return
|
|
213
233
|
|
|
214
|
-
|
|
234
|
+
self._guild_commands[guild_id] = []
|
|
215
235
|
|
|
216
236
|
async def _listen(self):
|
|
217
237
|
"""Main event loop for incoming gateway requests."""
|
|
218
|
-
while
|
|
238
|
+
while self._ws.is_connected():
|
|
219
239
|
try:
|
|
220
240
|
message = await self._ws.receive()
|
|
221
241
|
if not message:
|
|
@@ -223,17 +243,17 @@ class Client(ClientLike):
|
|
|
223
243
|
|
|
224
244
|
op_code = message.get('op')
|
|
225
245
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
231
252
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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)
|
|
235
256
|
|
|
236
|
-
try:
|
|
237
257
|
if self.prefix_dispatcher.prefix and dispatch_type == 'MESSAGE_CREATE':
|
|
238
258
|
await self.prefix_dispatcher.dispatch(event_data)
|
|
239
259
|
|
|
@@ -241,103 +261,97 @@ class Client(ClientLike):
|
|
|
241
261
|
await self.command_dispatcher.dispatch(event_data)
|
|
242
262
|
|
|
243
263
|
await self.dispatcher.dispatch(dispatch_type, event_data)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
elif op_code == 9:
|
|
254
|
-
self._ws.session_id = None
|
|
255
|
-
self._ws.sequence = None
|
|
256
|
-
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
|
+
|
|
257
273
|
except asyncio.CancelledError:
|
|
258
|
-
|
|
274
|
+
break
|
|
259
275
|
except DiscordError as e:
|
|
260
276
|
if e.fatal:
|
|
261
|
-
|
|
277
|
+
self._logger.log_error(f"Fatal DiscordError: {e}")
|
|
278
|
+
break
|
|
262
279
|
else:
|
|
263
280
|
self._logger.log_warn(f"Recoverable DiscordError: {e}")
|
|
264
281
|
except ConnectionError as e:
|
|
265
282
|
self._logger.log_warn(f"Connection lost: {e}")
|
|
266
|
-
|
|
267
|
-
await asyncio.sleep(2)
|
|
283
|
+
raise
|
|
268
284
|
|
|
269
285
|
async def start(self):
|
|
270
286
|
"""Runs the main lifecycle of the bot.
|
|
271
287
|
Handles connection setup, heartbeat management, event loop, and automatic reconnects.
|
|
272
288
|
"""
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
await self._ws.start_heartbeat()
|
|
289
|
+
try:
|
|
290
|
+
await self._http.start_session()
|
|
291
|
+
await self._ws.connect()
|
|
292
|
+
await self._ws.start_heartbeat()
|
|
278
293
|
|
|
294
|
+
while self._ws.is_connected():
|
|
279
295
|
if self._ws.session_id and self._ws.sequence:
|
|
280
296
|
await self._ws.reconnect()
|
|
281
297
|
else:
|
|
282
298
|
await self._ws.identify()
|
|
283
299
|
|
|
284
300
|
if not self._is_set_up:
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
self._logger.log_info(f"Setting hook {hook.__name__}")
|
|
288
|
-
await hook(self)
|
|
289
|
-
self._logger.log_high_priority("Hooks set up.")
|
|
301
|
+
await self.startup()
|
|
302
|
+
self._is_set_up = True
|
|
290
303
|
|
|
291
|
-
|
|
292
|
-
|
|
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()
|
|
293
311
|
|
|
294
|
-
|
|
295
|
-
|
|
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()
|
|
296
318
|
|
|
297
|
-
|
|
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.")
|
|
298
326
|
|
|
299
|
-
|
|
327
|
+
# register GUILD commands
|
|
328
|
+
await self.command_dispatcher._register_guild_commands(self._guild_commands)
|
|
300
329
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
await self._ws.close()
|
|
304
|
-
await asyncio.sleep(2)
|
|
305
|
-
continue
|
|
306
|
-
except asyncio.CancelledError:
|
|
307
|
-
self._logger.log_info("Cancelling connection...")
|
|
308
|
-
break
|
|
309
|
-
except DiscordError as e:
|
|
310
|
-
self._logger.log_error(f"Fatal DiscordError: {e}")
|
|
311
|
-
break
|
|
312
|
-
except Exception as e:
|
|
313
|
-
self._logger.log_error(f"Unspecified Error Type {type(e).__name__} - {e}")
|
|
314
|
-
break
|
|
315
|
-
finally:
|
|
316
|
-
# Run hooks (with safe catching)
|
|
317
|
-
for hook in self._shutdown_hooks:
|
|
318
|
-
try:
|
|
319
|
-
self._logger.log_info(f"Executing shutdown hook {hook.__name__}")
|
|
320
|
-
await hook(self)
|
|
321
|
-
except Exception as e:
|
|
322
|
-
self._logger.log_error(f"{type(e).__name__}: {e}")
|
|
323
|
-
|
|
324
|
-
# Always close resources
|
|
325
|
-
try:
|
|
326
|
-
await self._ws.close()
|
|
327
|
-
except Exception as e:
|
|
328
|
-
self._logger.log_warn(f"WebSocket close failed: {e}")
|
|
329
|
-
|
|
330
|
-
try:
|
|
331
|
-
await self._http.close_session()
|
|
332
|
-
except Exception as e:
|
|
333
|
-
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)
|
|
334
332
|
|
|
333
|
+
self._logger.log_high_priority("Commands set up.")
|
|
334
|
+
except Exception:
|
|
335
|
+
raise
|
|
335
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()
|
|
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
|
+
|
|
336
350
|
def run(self):
|
|
337
351
|
"""Starts the bot.
|
|
338
352
|
Handles starting the session, WS, and heartbeat, reconnection logic,
|
|
339
353
|
setting up emojis and hooks, and then listens for gateway events.
|
|
340
|
-
"""
|
|
354
|
+
"""
|
|
341
355
|
try:
|
|
342
356
|
asyncio.run(self.start())
|
|
343
357
|
except KeyboardInterrupt:
|