modifyself 0.1.4__tar.gz → 0.2.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.
- {modifyself-0.1.4 → modifyself-0.2.0}/PKG-INFO +1 -1
- modifyself-0.2.0/modifyself/__init__.py +12 -0
- modifyself-0.2.0/modifyself/activity.py +0 -0
- modifyself-0.2.0/modifyself/client.py +638 -0
- modifyself-0.2.0/modifyself/components.py +0 -0
- modifyself-0.2.0/modifyself/interactions.py +0 -0
- modifyself-0.2.0/modifyself/models/billing.py +0 -0
- modifyself-0.2.0/modifyself/models/relationship.py +0 -0
- modifyself-0.2.0/modifyself/models/settings.py +0 -0
- modifyself-0.2.0/modifyself/models/webhook.py +0 -0
- modifyself-0.2.0/modifyself/types.py +162 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/utils.py +33 -7
- modifyself-0.2.0/modifyself/voice.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself.egg-info/PKG-INFO +1 -1
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself.egg-info/SOURCES.txt +9 -1
- {modifyself-0.1.4 → modifyself-0.2.0}/pyproject.toml +29 -29
- modifyself-0.1.4/modifyself/__init__.py +0 -12
- modifyself-0.1.4/modifyself/client.py +0 -432
- modifyself-0.1.4/modifyself/types.py +0 -106
- {modifyself-0.1.4 → modifyself-0.2.0}/LICENSE +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/README.md +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/commands/__init__.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/commands/checks.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/commands/cog.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/commands/context.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/commands/converters.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/commands/core.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/core/bitfield.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/core/enums.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/core/mixins.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/core/snowflake.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/errors.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/gateway/__init__.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/gateway/dispatcher.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/gateway/heartbeat.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/gateway/websocket.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/headers.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/http/__init__.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/http/client.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/http/ratelimit.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/http/route.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/models/__init__.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/models/base.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/models/channel.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/models/guild.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/models/member.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/models/message.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/models/user.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself/state.py +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself.egg-info/dependency_links.txt +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself.egg-info/requires.txt +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/modifyself.egg-info/top_level.txt +0 -0
- {modifyself-0.1.4 → modifyself-0.2.0}/setup.cfg +0 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
modifyself ? a clean, pythonic Discord self-bot library.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.4"
|
|
6
|
+
|
|
7
|
+
from .client import Client
|
|
8
|
+
from .commands.core import command
|
|
9
|
+
from .commands.cog import Cog
|
|
10
|
+
from .commands.context import Context
|
|
11
|
+
|
|
12
|
+
__all__ = ["Client", "command", "Cog", "Context"]
|
|
File without changes
|
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main client class for modifyself.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import inspect
|
|
7
|
+
import logging
|
|
8
|
+
import signal
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Callable, Any, Optional, List, Dict, Union
|
|
11
|
+
|
|
12
|
+
from .http.client import HTTPClient
|
|
13
|
+
from .http.route import Route
|
|
14
|
+
from .gateway.websocket import GatewayWebSocket
|
|
15
|
+
from .gateway.dispatcher import EventDispatcher
|
|
16
|
+
from .state import ConnectionState
|
|
17
|
+
from .commands.core import Command, command
|
|
18
|
+
from .commands.context import Context
|
|
19
|
+
from .commands.cog import Cog
|
|
20
|
+
from .errors import CommandError, ConversionError, CommandNotFound
|
|
21
|
+
from .models.message import Message
|
|
22
|
+
from .models.relationship import Relationship
|
|
23
|
+
from .models.billing import PaymentSource, Subscription
|
|
24
|
+
from .models.settings import GuildSettings, UserSettings
|
|
25
|
+
from .models.webhook import Webhook, WebhookMessage
|
|
26
|
+
from .headers import HeaderSpoofer, EMULATION
|
|
27
|
+
from .utils import send_notification, START_IMAGE, ERROR_IMAGE
|
|
28
|
+
from .interactions import Interaction, InteractionHandler, interaction_handler
|
|
29
|
+
from .voice import VoiceManager, VoiceClient, VoiceState
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Client:
|
|
35
|
+
"""
|
|
36
|
+
The main client for interacting with Discord.
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
bot = Client(token="your_token")
|
|
40
|
+
|
|
41
|
+
@bot.event
|
|
42
|
+
async def on_ready():
|
|
43
|
+
print(f"Logged in as {bot.user}")
|
|
44
|
+
|
|
45
|
+
@bot.command()
|
|
46
|
+
async def ping(ctx):
|
|
47
|
+
await ctx.reply("Pong!")
|
|
48
|
+
|
|
49
|
+
bot.run()
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
*,
|
|
55
|
+
token: str,
|
|
56
|
+
command_prefix: Union[str, Callable[["Client", Message], str]] = "!",
|
|
57
|
+
owner_ids: Optional[List[int]] = None,
|
|
58
|
+
proxy: Optional[str] = None,
|
|
59
|
+
notifications: bool = True,
|
|
60
|
+
):
|
|
61
|
+
self.token = token
|
|
62
|
+
self.command_prefix = command_prefix
|
|
63
|
+
self.owner_ids = set(owner_ids) if owner_ids else set()
|
|
64
|
+
self._notifications = notifications
|
|
65
|
+
|
|
66
|
+
self._headers = HeaderSpoofer(token, EMULATION)
|
|
67
|
+
self._http = HTTPClient(
|
|
68
|
+
token,
|
|
69
|
+
headers=self._headers,
|
|
70
|
+
proxy=proxy,
|
|
71
|
+
)
|
|
72
|
+
self._state = ConnectionState(self._http)
|
|
73
|
+
self._dispatcher = EventDispatcher(self._state)
|
|
74
|
+
self._gateway = GatewayWebSocket(
|
|
75
|
+
self._dispatcher,
|
|
76
|
+
token,
|
|
77
|
+
headers=self._headers,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
self._event_handlers: Dict[str, List[Callable]] = {}
|
|
81
|
+
self._once_handlers: Dict[str, List[Callable]] = {}
|
|
82
|
+
|
|
83
|
+
self._commands: Dict[str, Command] = {}
|
|
84
|
+
self._cogs: Dict[str, Cog] = {}
|
|
85
|
+
|
|
86
|
+
self._ready = asyncio.Event()
|
|
87
|
+
self._closed = False
|
|
88
|
+
self._task: Optional[asyncio.Task] = None
|
|
89
|
+
self._close_task: Optional[asyncio.Task] = None
|
|
90
|
+
self._error_handled = False
|
|
91
|
+
|
|
92
|
+
self._interaction_handlers: Dict[str, Callable] = {}
|
|
93
|
+
|
|
94
|
+
self._voice = VoiceManager(self)
|
|
95
|
+
|
|
96
|
+
self._dispatcher.on("READY", self._on_ready_internal)
|
|
97
|
+
self._dispatcher.on("MESSAGE_CREATE", self._on_message_create_internal)
|
|
98
|
+
self._dispatcher.on("INTERACTION_CREATE", self._on_interaction_create)
|
|
99
|
+
self._dispatcher.on("VOICE_STATE_UPDATE", self._on_voice_state_update)
|
|
100
|
+
|
|
101
|
+
if self._notifications:
|
|
102
|
+
send_notification(
|
|
103
|
+
title="🚀 modifyself Initialized",
|
|
104
|
+
message="Client created successfully. Starting connection...",
|
|
105
|
+
image_url=START_IMAGE,
|
|
106
|
+
timeout=3
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def user(self):
|
|
111
|
+
return self._state.user
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def guilds(self):
|
|
115
|
+
return list(self._state._guilds.values())
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def users(self):
|
|
119
|
+
return list(self._state._users.values())
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def latency(self) -> float:
|
|
123
|
+
return self._gateway.latency
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def listeners(self) -> Dict[str, List]:
|
|
127
|
+
return dict(self._dispatcher._handlers)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def commands(self) -> List[Command]:
|
|
131
|
+
return list(self._commands.values())
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def cogs(self) -> List[Cog]:
|
|
135
|
+
return list(self._cogs.values())
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def voice(self) -> VoiceManager:
|
|
139
|
+
"""Access voice manager."""
|
|
140
|
+
return self._voice
|
|
141
|
+
|
|
142
|
+
def event(self, coro: Callable):
|
|
143
|
+
if not asyncio.iscoroutinefunction(coro):
|
|
144
|
+
raise TypeError("Event handlers must be coroutines")
|
|
145
|
+
name = coro.__name__.replace("on_", "", 1).upper()
|
|
146
|
+
self._dispatcher.on(name, coro)
|
|
147
|
+
self._event_handlers.setdefault(name, []).append(coro)
|
|
148
|
+
logger.debug("Registered event handler for %s", name)
|
|
149
|
+
return coro
|
|
150
|
+
|
|
151
|
+
def listen(self, name: Optional[str] = None):
|
|
152
|
+
def decorator(coro: Callable):
|
|
153
|
+
if not asyncio.iscoroutinefunction(coro):
|
|
154
|
+
raise TypeError("Listeners must be coroutines")
|
|
155
|
+
event_name = (name or coro.__name__).upper()
|
|
156
|
+
self._dispatcher.on(event_name, coro)
|
|
157
|
+
return coro
|
|
158
|
+
return decorator
|
|
159
|
+
|
|
160
|
+
def add_listener(self, event: str, handler: Callable):
|
|
161
|
+
event = event.upper()
|
|
162
|
+
self._dispatcher.on(event, handler)
|
|
163
|
+
logger.info("[client] listener registered: %s -> %s", event, handler)
|
|
164
|
+
|
|
165
|
+
def remove_listener(self, event: str, handler: Callable):
|
|
166
|
+
self._dispatcher.off(event.upper(), handler)
|
|
167
|
+
|
|
168
|
+
def command(self, *, name: Optional[str] = None, aliases: Optional[List[str]] = None, **kwargs):
|
|
169
|
+
def decorator(func: Callable):
|
|
170
|
+
cmd = Command(func, name=name, aliases=aliases, **kwargs)
|
|
171
|
+
self.add_command(cmd)
|
|
172
|
+
func._command = cmd
|
|
173
|
+
return func
|
|
174
|
+
return decorator
|
|
175
|
+
|
|
176
|
+
def add_command(self, cmd: Command):
|
|
177
|
+
if cmd.name in self._commands:
|
|
178
|
+
raise ValueError(f"Command {cmd.name} is already registered")
|
|
179
|
+
|
|
180
|
+
for alias in cmd.aliases:
|
|
181
|
+
if alias in self._commands:
|
|
182
|
+
raise ValueError(f"Alias {alias} is already registered")
|
|
183
|
+
|
|
184
|
+
self._commands[cmd.name] = cmd
|
|
185
|
+
for alias in cmd.aliases:
|
|
186
|
+
self._commands[alias] = cmd
|
|
187
|
+
logger.debug("Registered command: %s", cmd.name)
|
|
188
|
+
|
|
189
|
+
def remove_command(self, name: str):
|
|
190
|
+
cmd = self._commands.pop(name, None)
|
|
191
|
+
if cmd and cmd.name == name:
|
|
192
|
+
for alias in cmd.aliases:
|
|
193
|
+
self._commands.pop(alias, None)
|
|
194
|
+
|
|
195
|
+
def get_command(self, name: str) -> Optional[Command]:
|
|
196
|
+
return self._commands.get(name)
|
|
197
|
+
|
|
198
|
+
def add_cog(self, cog: Cog):
|
|
199
|
+
if cog.name in self._cogs:
|
|
200
|
+
raise ValueError(f"Cog {cog.name} is already loaded")
|
|
201
|
+
cog._inject(self)
|
|
202
|
+
self._cogs[cog.name] = cog
|
|
203
|
+
cog.cog_load()
|
|
204
|
+
logger.info("Loaded cog: %s", cog.name)
|
|
205
|
+
|
|
206
|
+
def remove_cog(self, name: str):
|
|
207
|
+
cog = self._cogs.pop(name, None)
|
|
208
|
+
if cog:
|
|
209
|
+
cog.cog_unload()
|
|
210
|
+
cog._eject(self)
|
|
211
|
+
logger.info("Unloaded cog: %s", name)
|
|
212
|
+
|
|
213
|
+
def get_cog(self, name: str) -> Optional[Cog]:
|
|
214
|
+
return self._cogs.get(name)
|
|
215
|
+
|
|
216
|
+
def interaction_handler(self, custom_id: str):
|
|
217
|
+
"""Decorator to register an interaction handler."""
|
|
218
|
+
def decorator(func: Callable):
|
|
219
|
+
self._interaction_handlers[custom_id] = func
|
|
220
|
+
return func
|
|
221
|
+
return decorator
|
|
222
|
+
|
|
223
|
+
async def _on_ready_internal(self, user):
|
|
224
|
+
self._ready.set()
|
|
225
|
+
logger.debug("Ready event received internally")
|
|
226
|
+
|
|
227
|
+
if self._notifications:
|
|
228
|
+
username = user.name if user else "Unknown"
|
|
229
|
+
send_notification(
|
|
230
|
+
title="✅ Bot is Running",
|
|
231
|
+
message=f"Logged in as {username}",
|
|
232
|
+
image_url=START_IMAGE,
|
|
233
|
+
timeout=5
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
async def _on_message_create_internal(self, message: Message):
|
|
237
|
+
logger.debug(f"📩 MESSAGE_CREATE: {message.content[:50] if message.content else '(empty)'}")
|
|
238
|
+
await self._process_commands(message)
|
|
239
|
+
|
|
240
|
+
async def _on_interaction_create(self, data: dict):
|
|
241
|
+
"""Handle interaction_create events."""
|
|
242
|
+
try:
|
|
243
|
+
interaction = Interaction(self._state, data)
|
|
244
|
+
|
|
245
|
+
if interaction.custom_id and interaction.custom_id in self._interaction_handlers:
|
|
246
|
+
await self._interaction_handlers[interaction.custom_id](interaction)
|
|
247
|
+
else:
|
|
248
|
+
await interaction_handler.handle(interaction)
|
|
249
|
+
except Exception as e:
|
|
250
|
+
logger.exception(f"Error handling interaction: {e}")
|
|
251
|
+
|
|
252
|
+
async def _on_voice_state_update(self, data: dict):
|
|
253
|
+
"""Handle voice state updates."""
|
|
254
|
+
guild_id = int(data.get("guild_id", 0))
|
|
255
|
+
if not guild_id:
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
voice_state = data.get("voice_state", {})
|
|
259
|
+
self._voice.update_state(guild_id, voice_state)
|
|
260
|
+
|
|
261
|
+
async def _process_commands(self, message: Message):
|
|
262
|
+
if not self.user or message.author.id != self.user.id:
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
if message.author.bot:
|
|
266
|
+
logger.debug("Message from bot, ignoring")
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
if callable(self.command_prefix):
|
|
270
|
+
prefix = self.command_prefix(self, message)
|
|
271
|
+
else:
|
|
272
|
+
prefix = self.command_prefix
|
|
273
|
+
|
|
274
|
+
if not prefix:
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
content = message.content
|
|
278
|
+
if not content:
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
logger.debug(f"Checking command: content='{content}', prefix='{prefix}'")
|
|
282
|
+
|
|
283
|
+
if not content.startswith(prefix):
|
|
284
|
+
if self.user:
|
|
285
|
+
mention = f"<@{self.user.id}>"
|
|
286
|
+
mention_nick = f"<@!{self.user.id}>"
|
|
287
|
+
if content.startswith(mention):
|
|
288
|
+
content = content[len(mention):].lstrip()
|
|
289
|
+
logger.debug(f"Stripped mention: '{content}'")
|
|
290
|
+
elif content.startswith(mention_nick):
|
|
291
|
+
content = content[len(mention_nick):].lstrip()
|
|
292
|
+
logger.debug(f"Stripped mention_nick: '{content}'")
|
|
293
|
+
else:
|
|
294
|
+
return
|
|
295
|
+
else:
|
|
296
|
+
return
|
|
297
|
+
else:
|
|
298
|
+
content = content[len(prefix):].strip()
|
|
299
|
+
logger.debug(f"Stripped prefix: '{content}'")
|
|
300
|
+
|
|
301
|
+
if not content:
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
parts = content.split()
|
|
305
|
+
name = parts[0]
|
|
306
|
+
args = parts[1:]
|
|
307
|
+
|
|
308
|
+
logger.info(f"Command detected: {name} with args: {args}")
|
|
309
|
+
|
|
310
|
+
command = self._commands.get(name)
|
|
311
|
+
if not command:
|
|
312
|
+
logger.debug(f"Command not found: {name}")
|
|
313
|
+
for handler in self._event_handlers.get("COMMAND_NOT_FOUND", []):
|
|
314
|
+
try:
|
|
315
|
+
await handler(message, name)
|
|
316
|
+
except Exception:
|
|
317
|
+
logger.exception("Error in command not found handler")
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
ctx = Context(
|
|
321
|
+
message=message,
|
|
322
|
+
command=command,
|
|
323
|
+
args=args,
|
|
324
|
+
kwargs={},
|
|
325
|
+
bot=self,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
await command.invoke(ctx)
|
|
330
|
+
logger.info(f"Command {name} executed successfully")
|
|
331
|
+
except CommandError as exc:
|
|
332
|
+
logger.warning("Command error in %s: %s", command.name, exc)
|
|
333
|
+
self._handle_error(f"Command error in {command.name}: {exc}")
|
|
334
|
+
for handler in self._event_handlers.get("COMMAND_ERROR", []):
|
|
335
|
+
try:
|
|
336
|
+
await handler(ctx, exc)
|
|
337
|
+
except Exception:
|
|
338
|
+
logger.exception("Error in command error handler")
|
|
339
|
+
except Exception as exc:
|
|
340
|
+
logger.exception("Unexpected error in command %s", command.name)
|
|
341
|
+
self._handle_error(f"Unexpected error in {command.name}: {exc}")
|
|
342
|
+
for handler in self._event_handlers.get("COMMAND_ERROR", []):
|
|
343
|
+
try:
|
|
344
|
+
await handler(ctx, exc)
|
|
345
|
+
except Exception:
|
|
346
|
+
pass
|
|
347
|
+
|
|
348
|
+
def _handle_error(self, error_message: str):
|
|
349
|
+
if self._notifications and not self._error_handled:
|
|
350
|
+
self._error_handled = True
|
|
351
|
+
send_notification(
|
|
352
|
+
title="❌ Error Occurred",
|
|
353
|
+
message=error_message[:100] + ("..." if len(error_message) > 100 else ""),
|
|
354
|
+
image_url=ERROR_IMAGE,
|
|
355
|
+
timeout=10
|
|
356
|
+
)
|
|
357
|
+
asyncio.get_event_loop().call_later(2, lambda: setattr(self, '_error_handled', False))
|
|
358
|
+
|
|
359
|
+
async def start(self):
|
|
360
|
+
self._closed = False
|
|
361
|
+
self._error_handled = False
|
|
362
|
+
try:
|
|
363
|
+
await self._gateway.connect()
|
|
364
|
+
except Exception as exc:
|
|
365
|
+
logger.exception("Gateway connection error: %s", exc)
|
|
366
|
+
self._handle_error(f"Failed to connect: {exc}")
|
|
367
|
+
raise
|
|
368
|
+
|
|
369
|
+
async def close(self):
|
|
370
|
+
if self._closed:
|
|
371
|
+
return
|
|
372
|
+
|
|
373
|
+
self._closed = True
|
|
374
|
+
self._ready.clear()
|
|
375
|
+
|
|
376
|
+
await self._voice.disconnect_all()
|
|
377
|
+
await self._gateway.close()
|
|
378
|
+
await self._http.close()
|
|
379
|
+
|
|
380
|
+
logger.info("Client closed")
|
|
381
|
+
|
|
382
|
+
def run(self):
|
|
383
|
+
loop = asyncio.new_event_loop()
|
|
384
|
+
asyncio.set_event_loop(loop)
|
|
385
|
+
|
|
386
|
+
self._close_task = None
|
|
387
|
+
|
|
388
|
+
def signal_handler(sig):
|
|
389
|
+
logger.info("Received signal %s, shutting down...", sig)
|
|
390
|
+
if loop.is_running():
|
|
391
|
+
asyncio.run_coroutine_threadsafe(self.close(), loop)
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
395
|
+
loop.add_signal_handler(sig, lambda s=sig: signal_handler(s))
|
|
396
|
+
except NotImplementedError:
|
|
397
|
+
pass
|
|
398
|
+
|
|
399
|
+
async def runner():
|
|
400
|
+
try:
|
|
401
|
+
await self.start()
|
|
402
|
+
except Exception as exc:
|
|
403
|
+
self._handle_error(f"Bot crashed: {exc}")
|
|
404
|
+
raise
|
|
405
|
+
finally:
|
|
406
|
+
await self.close()
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
loop.run_until_complete(runner())
|
|
410
|
+
except KeyboardInterrupt:
|
|
411
|
+
logger.info("Keyboard interrupt received, shutting down...")
|
|
412
|
+
except Exception as exc:
|
|
413
|
+
self._handle_error(f"Bot crashed: {exc}")
|
|
414
|
+
raise
|
|
415
|
+
finally:
|
|
416
|
+
try:
|
|
417
|
+
loop.run_until_complete(loop.shutdown_asyncgens())
|
|
418
|
+
finally:
|
|
419
|
+
loop.close()
|
|
420
|
+
|
|
421
|
+
async def wait_until_ready(self):
|
|
422
|
+
await self._ready.wait()
|
|
423
|
+
|
|
424
|
+
# ============================================================
|
|
425
|
+
# FETCH METHODS
|
|
426
|
+
# ============================================================
|
|
427
|
+
async def fetch_user(self, user_id: int):
|
|
428
|
+
data = await self._http.request(Route.user(user_id))
|
|
429
|
+
return self._state._add_user(data)
|
|
430
|
+
|
|
431
|
+
async def fetch_guild(self, guild_id: int):
|
|
432
|
+
data = await self._http.request(Route.guild(guild_id))
|
|
433
|
+
return self._state._add_guild(data)
|
|
434
|
+
|
|
435
|
+
async def fetch_channel(self, channel_id: int):
|
|
436
|
+
data = await self._http.request(Route.channel(channel_id))
|
|
437
|
+
return self._state._add_channel(data)
|
|
438
|
+
|
|
439
|
+
async def fetch_message(self, channel_id: int, message_id: int):
|
|
440
|
+
data = await self._http.request(Route.channel_message(channel_id, message_id))
|
|
441
|
+
return self._state._store_message(data)
|
|
442
|
+
|
|
443
|
+
def get_guild(self, guild_id: int):
|
|
444
|
+
return self._state._guilds.get(guild_id)
|
|
445
|
+
|
|
446
|
+
def get_channel(self, channel_id: int):
|
|
447
|
+
return self._state._channels.get(channel_id)
|
|
448
|
+
|
|
449
|
+
def get_user(self, user_id: int):
|
|
450
|
+
return self._state._users.get(user_id)
|
|
451
|
+
|
|
452
|
+
# ============================================================
|
|
453
|
+
# RELATIONSHIPS
|
|
454
|
+
# ============================================================
|
|
455
|
+
async def get_relationships(self) -> List[Relationship]:
|
|
456
|
+
data = await self._http.request(Route("GET", "/users/@me/relationships"))
|
|
457
|
+
return [Relationship(state=self._state, data=r) for r in data]
|
|
458
|
+
|
|
459
|
+
async def add_friend(self, user_id: int, username: str = None) -> Dict[str, Any]:
|
|
460
|
+
payload = {"username": username} if username else {}
|
|
461
|
+
return await self._http.request(
|
|
462
|
+
Route("POST", f"/users/@me/relationships/{user_id}"),
|
|
463
|
+
json=payload
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
async def remove_friend(self, user_id: int) -> None:
|
|
467
|
+
await self._http.request(
|
|
468
|
+
Route("DELETE", f"/users/@me/relationships/{user_id}")
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
async def block_user(self, user_id: int) -> None:
|
|
472
|
+
await self._http.request(
|
|
473
|
+
Route("PUT", f"/users/@me/relationships/{user_id}"),
|
|
474
|
+
json={"type": 2}
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
async def unblock_user(self, user_id: int) -> None:
|
|
478
|
+
await self._http.request(
|
|
479
|
+
Route("DELETE", f"/users/@me/relationships/{user_id}")
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
# ============================================================
|
|
483
|
+
# BILLING
|
|
484
|
+
# ============================================================
|
|
485
|
+
async def get_payment_sources(self) -> List[PaymentSource]:
|
|
486
|
+
data = await self._http.request(Route("GET", "/users/@me/billing/payment-sources"))
|
|
487
|
+
return [PaymentSource(state=self._state, data=s) for s in data]
|
|
488
|
+
|
|
489
|
+
async def get_subscriptions(self) -> List[Subscription]:
|
|
490
|
+
data = await self._http.request(Route("GET", "/users/@me/billing/subscriptions"))
|
|
491
|
+
return [Subscription(state=self._state, data=s) for s in data]
|
|
492
|
+
|
|
493
|
+
async def get_entitlements(self) -> List[Dict[str, Any]]:
|
|
494
|
+
return await self._http.request(Route("GET", "/users/@me/entitlements"))
|
|
495
|
+
|
|
496
|
+
async def get_skus(self) -> List[Dict[str, Any]]:
|
|
497
|
+
return await self._http.request(Route("GET", "/users/@me/entitlements/skus"))
|
|
498
|
+
|
|
499
|
+
# ============================================================
|
|
500
|
+
# SETTINGS
|
|
501
|
+
# ============================================================
|
|
502
|
+
async def get_user_settings(self) -> UserSettings:
|
|
503
|
+
data = await self._http.request(Route("GET", "/users/@me/settings"))
|
|
504
|
+
return UserSettings(data)
|
|
505
|
+
|
|
506
|
+
async def update_user_settings(self, **kwargs) -> Dict[str, Any]:
|
|
507
|
+
return await self._http.request(
|
|
508
|
+
Route("PATCH", "/users/@me/settings"),
|
|
509
|
+
json=kwargs
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
async def set_status(self, status: str) -> Dict[str, Any]:
|
|
513
|
+
return await self.update_user_settings(status=status)
|
|
514
|
+
|
|
515
|
+
async def set_theme(self, theme: str) -> Dict[str, Any]:
|
|
516
|
+
return await self.update_user_settings(theme=theme)
|
|
517
|
+
|
|
518
|
+
async def set_language(self, locale: str) -> Dict[str, Any]:
|
|
519
|
+
return await self.update_user_settings(locale=locale)
|
|
520
|
+
|
|
521
|
+
async def set_dev_mode(self, enabled: bool) -> Dict[str, Any]:
|
|
522
|
+
return await self.update_user_settings(developer_mode=enabled)
|
|
523
|
+
|
|
524
|
+
async def get_guild_settings(self, guild_id: int) -> GuildSettings:
|
|
525
|
+
data = await self._http.request(
|
|
526
|
+
Route("GET", f"/users/@me/guilds/{guild_id}/settings")
|
|
527
|
+
)
|
|
528
|
+
return GuildSettings(state=self._state, data=data)
|
|
529
|
+
|
|
530
|
+
async def update_guild_settings(self, guild_id: int, **kwargs) -> Dict[str, Any]:
|
|
531
|
+
return await self._http.request(
|
|
532
|
+
Route("PATCH", f"/users/@me/guilds/{guild_id}/settings"),
|
|
533
|
+
json=kwargs
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# ============================================================
|
|
537
|
+
# PROFILE
|
|
538
|
+
# ============================================================
|
|
539
|
+
async def get_profile(self, user_id: int = None) -> Dict[str, Any]:
|
|
540
|
+
endpoint = f"/users/{user_id}/profile" if user_id else "/users/@me/profile"
|
|
541
|
+
return await self._http.request(Route("GET", endpoint))
|
|
542
|
+
|
|
543
|
+
async def update_avatar(self, avatar_data: str) -> Dict[str, Any]:
|
|
544
|
+
return await self._http.request(
|
|
545
|
+
Route("PATCH", "/users/@me"),
|
|
546
|
+
json={"avatar": avatar_data}
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
async def update_banner(self, banner_data: str) -> Dict[str, Any]:
|
|
550
|
+
return await self._http.request(
|
|
551
|
+
Route("PATCH", "/users/@me"),
|
|
552
|
+
json={"banner": banner_data}
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
async def update_bio(self, bio: str) -> Dict[str, Any]:
|
|
556
|
+
return await self._http.request(
|
|
557
|
+
Route("PATCH", "/users/@me"),
|
|
558
|
+
json={"bio": bio}
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
async def update_display_name(self, name: str) -> Dict[str, Any]:
|
|
562
|
+
return await self._http.request(
|
|
563
|
+
Route("PATCH", "/users/@me"),
|
|
564
|
+
json={"global_name": name}
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
async def get_connections(self) -> List[Dict[str, Any]]:
|
|
568
|
+
return await self._http.request(Route("GET", "/users/@me/connections"))
|
|
569
|
+
|
|
570
|
+
# ============================================================
|
|
571
|
+
# WEBHOOKS
|
|
572
|
+
# ============================================================
|
|
573
|
+
async def get_webhook(self, webhook_id: int) -> Webhook:
|
|
574
|
+
"""Get a webhook by ID."""
|
|
575
|
+
data = await self._http.request(Route("GET", f"/webhooks/{webhook_id}"))
|
|
576
|
+
return Webhook(state=self._state, data=data)
|
|
577
|
+
|
|
578
|
+
async def get_webhook_with_token(self, webhook_id: int, token: str) -> Webhook:
|
|
579
|
+
"""Get a webhook by ID and token."""
|
|
580
|
+
data = await self._http.request(
|
|
581
|
+
method="GET",
|
|
582
|
+
url=f"/webhooks/{webhook_id}/{token}",
|
|
583
|
+
)
|
|
584
|
+
return Webhook(state=self._state, data=data)
|
|
585
|
+
|
|
586
|
+
async def get_channel_webhooks(self, channel_id: int) -> List[Webhook]:
|
|
587
|
+
"""Get all webhooks in a channel."""
|
|
588
|
+
data = await self._http.request(Route("GET", f"/channels/{channel_id}/webhooks"))
|
|
589
|
+
return [Webhook(state=self._state, data=w) for w in data]
|
|
590
|
+
|
|
591
|
+
async def get_guild_webhooks(self, guild_id: int) -> List[Webhook]:
|
|
592
|
+
"""Get all webhooks in a guild."""
|
|
593
|
+
data = await self._http.request(Route("GET", f"/guilds/{guild_id}/webhooks"))
|
|
594
|
+
return [Webhook(state=self._state, data=w) for w in data]
|
|
595
|
+
|
|
596
|
+
async def create_webhook(
|
|
597
|
+
self,
|
|
598
|
+
channel_id: int,
|
|
599
|
+
name: str,
|
|
600
|
+
avatar: Optional[str] = None,
|
|
601
|
+
) -> Webhook:
|
|
602
|
+
"""Create a webhook in a channel."""
|
|
603
|
+
payload = {"name": name}
|
|
604
|
+
if avatar:
|
|
605
|
+
payload["avatar"] = avatar
|
|
606
|
+
data = await self._http.request(
|
|
607
|
+
Route("POST", f"/channels/{channel_id}/webhooks"),
|
|
608
|
+
json=payload,
|
|
609
|
+
)
|
|
610
|
+
return Webhook(state=self._state, data=data)
|
|
611
|
+
|
|
612
|
+
# ============================================================
|
|
613
|
+
# VOICE
|
|
614
|
+
# ============================================================
|
|
615
|
+
async def create_voice_client(
|
|
616
|
+
self,
|
|
617
|
+
channel_id: int,
|
|
618
|
+
guild_id: int,
|
|
619
|
+
endpoint: str,
|
|
620
|
+
token: str,
|
|
621
|
+
session_id: str,
|
|
622
|
+
) -> VoiceClient:
|
|
623
|
+
"""Create and connect a voice client."""
|
|
624
|
+
return await self._voice.connect(
|
|
625
|
+
channel_id,
|
|
626
|
+
guild_id,
|
|
627
|
+
endpoint,
|
|
628
|
+
token,
|
|
629
|
+
session_id,
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
def get_voice_client(self, guild_id: int) -> Optional[VoiceClient]:
|
|
633
|
+
"""Get the voice client for a guild."""
|
|
634
|
+
return self._voice.get_client(guild_id)
|
|
635
|
+
|
|
636
|
+
async def disconnect_voice(self, guild_id: int) -> None:
|
|
637
|
+
"""Disconnect voice client for a guild."""
|
|
638
|
+
await self._voice.disconnect(guild_id)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|