modifyself 0.2.4__tar.gz → 0.2.6__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.2.4 → modifyself-0.2.6}/PKG-INFO +1 -1
- modifyself-0.2.6/modifyself/interactions.py +216 -0
- modifyself-0.2.6/modifyself/voice.py +277 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself.egg-info/PKG-INFO +1 -1
- {modifyself-0.2.4 → modifyself-0.2.6}/pyproject.toml +1 -1
- modifyself-0.2.4/modifyself/interactions.py +0 -0
- modifyself-0.2.4/modifyself/voice.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/LICENSE +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/README.md +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/__init__.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/activity.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/client.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/commands/__init__.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/commands/checks.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/commands/cog.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/commands/context.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/commands/converters.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/commands/core.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/components.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/core/bitfield.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/core/enums.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/core/mixins.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/core/snowflake.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/errors.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/gateway/__init__.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/gateway/dispatcher.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/gateway/heartbeat.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/gateway/websocket.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/headers.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/http/__init__.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/http/client.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/http/ratelimit.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/http/route.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/__init__.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/base.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/billing.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/channel.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/guild.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/member.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/message.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/relationship.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/settings.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/user.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/webhook.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/state.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/types.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/utils.py +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself.egg-info/SOURCES.txt +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself.egg-info/dependency_links.txt +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself.egg-info/requires.txt +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/modifyself.egg-info/top_level.txt +0 -0
- {modifyself-0.2.4 → modifyself-0.2.6}/setup.cfg +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interaction handling (buttons, selects, modals).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Optional, Dict, Any, List, Union, Callable, Awaitable
|
|
7
|
+
from enum import IntEnum
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
|
|
10
|
+
from .models.message import Message
|
|
11
|
+
from .models.user import User
|
|
12
|
+
from .models.member import Member
|
|
13
|
+
from .components import ComponentType, ButtonStyle, TextInputStyle, ActionRow, Component
|
|
14
|
+
|
|
15
|
+
class InteractionType(IntEnum):
|
|
16
|
+
PING = 1
|
|
17
|
+
APPLICATION_COMMAND = 2
|
|
18
|
+
MESSAGE_COMPONENT = 3
|
|
19
|
+
APPLICATION_COMMAND_AUTOCOMPLETE = 4
|
|
20
|
+
MODAL_SUBMIT = 5
|
|
21
|
+
|
|
22
|
+
class InteractionCallbackType(IntEnum):
|
|
23
|
+
PONG = 1
|
|
24
|
+
CHANNEL_MESSAGE_WITH_SOURCE = 4
|
|
25
|
+
DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE = 5
|
|
26
|
+
DEFERRED_UPDATE_MESSAGE = 6
|
|
27
|
+
UPDATE_MESSAGE = 7
|
|
28
|
+
APPLICATION_COMMAND_AUTOCOMPLETE_RESULT = 8
|
|
29
|
+
MODAL = 9
|
|
30
|
+
|
|
31
|
+
class Interaction:
|
|
32
|
+
"""Represents a Discord interaction."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, state, data: dict):
|
|
35
|
+
self._state = state
|
|
36
|
+
self.id = int(data["id"])
|
|
37
|
+
self.application_id = int(data["application_id"])
|
|
38
|
+
self.type = InteractionType(data["type"])
|
|
39
|
+
self.token = data["token"]
|
|
40
|
+
self.version = data["version"]
|
|
41
|
+
self.guild_id = int(data["guild_id"]) if data.get("guild_id") else None
|
|
42
|
+
self.channel_id = int(data["channel_id"]) if data.get("channel_id") else None
|
|
43
|
+
|
|
44
|
+
# Message
|
|
45
|
+
self.message = None
|
|
46
|
+
if data.get("message"):
|
|
47
|
+
self.message = Message(state=self._state, data=data["message"])
|
|
48
|
+
|
|
49
|
+
# Member/User
|
|
50
|
+
self.member = None
|
|
51
|
+
self.user = None
|
|
52
|
+
if data.get("member"):
|
|
53
|
+
self.member = Member(state=self._state, data=data["member"], guild_id=self.guild_id)
|
|
54
|
+
self.user = self.member.user
|
|
55
|
+
elif data.get("user"):
|
|
56
|
+
self.user = User(state=self._state, data=data["user"])
|
|
57
|
+
|
|
58
|
+
# Data
|
|
59
|
+
self.data = data.get("data", {})
|
|
60
|
+
self.custom_id = self.data.get("custom_id")
|
|
61
|
+
|
|
62
|
+
# Values (for selects)
|
|
63
|
+
self.values = self.data.get("values")
|
|
64
|
+
|
|
65
|
+
# Component type
|
|
66
|
+
self.component_type = self.data.get("component_type")
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def author(self):
|
|
70
|
+
"""The user who triggered the interaction."""
|
|
71
|
+
return self.user or (self.member.user if self.member else None)
|
|
72
|
+
|
|
73
|
+
async def respond(
|
|
74
|
+
self,
|
|
75
|
+
content: Optional[str] = None,
|
|
76
|
+
embeds: Optional[List[Dict[str, Any]]] = None,
|
|
77
|
+
components: Optional[List[ActionRow]] = None,
|
|
78
|
+
**kwargs,
|
|
79
|
+
) -> Dict[str, Any]:
|
|
80
|
+
"""Respond to the interaction."""
|
|
81
|
+
payload = {
|
|
82
|
+
"type": InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
|
|
83
|
+
"data": {},
|
|
84
|
+
}
|
|
85
|
+
if content is not None:
|
|
86
|
+
payload["data"]["content"] = content
|
|
87
|
+
if embeds is not None:
|
|
88
|
+
payload["data"]["embeds"] = embeds
|
|
89
|
+
if components is not None:
|
|
90
|
+
payload["data"]["components"] = [c.to_dict() for c in components]
|
|
91
|
+
payload["data"].update(kwargs)
|
|
92
|
+
|
|
93
|
+
await self._state.http.request(
|
|
94
|
+
method="POST",
|
|
95
|
+
url=f"/interactions/{self.id}/{self.token}/callback",
|
|
96
|
+
json=payload,
|
|
97
|
+
)
|
|
98
|
+
return payload
|
|
99
|
+
|
|
100
|
+
async def update(
|
|
101
|
+
self,
|
|
102
|
+
content: Optional[str] = None,
|
|
103
|
+
embeds: Optional[List[Dict[str, Any]]] = None,
|
|
104
|
+
components: Optional[List[ActionRow]] = None,
|
|
105
|
+
**kwargs,
|
|
106
|
+
) -> Dict[str, Any]:
|
|
107
|
+
"""Update the message that the interaction was triggered from."""
|
|
108
|
+
payload = {
|
|
109
|
+
"type": InteractionCallbackType.UPDATE_MESSAGE,
|
|
110
|
+
"data": {},
|
|
111
|
+
}
|
|
112
|
+
if content is not None:
|
|
113
|
+
payload["data"]["content"] = content
|
|
114
|
+
if embeds is not None:
|
|
115
|
+
payload["data"]["embeds"] = embeds
|
|
116
|
+
if components is not None:
|
|
117
|
+
payload["data"]["components"] = [c.to_dict() for c in components]
|
|
118
|
+
payload["data"].update(kwargs)
|
|
119
|
+
|
|
120
|
+
await self._state.http.request(
|
|
121
|
+
method="POST",
|
|
122
|
+
url=f"/interactions/{self.id}/{self.token}/callback",
|
|
123
|
+
json=payload,
|
|
124
|
+
)
|
|
125
|
+
return payload
|
|
126
|
+
|
|
127
|
+
async def defer(self, ephemeral: bool = False) -> None:
|
|
128
|
+
"""Defer the interaction response."""
|
|
129
|
+
payload = {
|
|
130
|
+
"type": InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
|
|
131
|
+
"data": {"flags": 64 if ephemeral else 0},
|
|
132
|
+
}
|
|
133
|
+
await self._state.http.request(
|
|
134
|
+
method="POST",
|
|
135
|
+
url=f"/interactions/{self.id}/{self.token}/callback",
|
|
136
|
+
json=payload,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def defer_update(self) -> None:
|
|
140
|
+
"""Defer the interaction response for message updates."""
|
|
141
|
+
payload = {
|
|
142
|
+
"type": InteractionCallbackType.DEFERRED_UPDATE_MESSAGE,
|
|
143
|
+
"data": {},
|
|
144
|
+
}
|
|
145
|
+
await self._state.http.request(
|
|
146
|
+
method="POST",
|
|
147
|
+
url=f"/interactions/{self.id}/{self.token}/callback",
|
|
148
|
+
json=payload,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
async def show_modal(self, modal) -> None:
|
|
152
|
+
"""Show a modal to the user."""
|
|
153
|
+
payload = {
|
|
154
|
+
"type": InteractionCallbackType.MODAL,
|
|
155
|
+
"data": modal.to_dict(),
|
|
156
|
+
}
|
|
157
|
+
await self._state.http.request(
|
|
158
|
+
method="POST",
|
|
159
|
+
url=f"/interactions/{self.id}/{self.token}/callback",
|
|
160
|
+
json=payload,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
async def edit_original_response(
|
|
164
|
+
self,
|
|
165
|
+
content: Optional[str] = None,
|
|
166
|
+
embeds: Optional[List[Dict[str, Any]]] = None,
|
|
167
|
+
components: Optional[List[ActionRow]] = None,
|
|
168
|
+
**kwargs,
|
|
169
|
+
) -> Dict[str, Any]:
|
|
170
|
+
"""Edit the original response message."""
|
|
171
|
+
payload = {}
|
|
172
|
+
if content is not None:
|
|
173
|
+
payload["content"] = content
|
|
174
|
+
if embeds is not None:
|
|
175
|
+
payload["embeds"] = embeds
|
|
176
|
+
if components is not None:
|
|
177
|
+
payload["components"] = [c.to_dict() for c in components]
|
|
178
|
+
payload.update(kwargs)
|
|
179
|
+
|
|
180
|
+
return await self._state.http.request(
|
|
181
|
+
method="PATCH",
|
|
182
|
+
url=f"/webhooks/{self.application_id}/{self.token}/messages/@original",
|
|
183
|
+
json=payload,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
async def delete_original_response(self) -> None:
|
|
187
|
+
"""Delete the original response message."""
|
|
188
|
+
await self._state.http.request(
|
|
189
|
+
method="DELETE",
|
|
190
|
+
url=f"/webhooks/{self.application_id}/{self.token}/messages/@original",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class InteractionHandler:
|
|
195
|
+
"""Manages interaction callbacks."""
|
|
196
|
+
|
|
197
|
+
def __init__(self):
|
|
198
|
+
self._handlers: Dict[str, Callable] = {}
|
|
199
|
+
|
|
200
|
+
def on(self, custom_id: str):
|
|
201
|
+
"""Decorator to register a handler for a specific custom_id."""
|
|
202
|
+
def decorator(func: Callable[[Interaction], Awaitable[None]]):
|
|
203
|
+
self._handlers[custom_id] = func
|
|
204
|
+
return func
|
|
205
|
+
return decorator
|
|
206
|
+
|
|
207
|
+
async def handle(self, interaction: Interaction) -> bool:
|
|
208
|
+
"""Handle an interaction."""
|
|
209
|
+
if interaction.custom_id in self._handlers:
|
|
210
|
+
await self._handlers[interaction.custom_id](interaction)
|
|
211
|
+
return True
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# Global interaction handler instance
|
|
216
|
+
interaction_handler = InteractionHandler()
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Discord voice support.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import socket
|
|
9
|
+
import struct
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import uuid
|
|
13
|
+
from typing import Optional, List, Dict, Any, Callable, Union
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import nacl.secret
|
|
18
|
+
import nacl.utils
|
|
19
|
+
HAS_NACL = True
|
|
20
|
+
except ImportError:
|
|
21
|
+
HAS_NACL = False
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import opuslib
|
|
25
|
+
HAS_OPUS = True
|
|
26
|
+
except ImportError:
|
|
27
|
+
HAS_OPUS = False
|
|
28
|
+
|
|
29
|
+
from .http.route import Route
|
|
30
|
+
from .errors import VoiceError
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
class VoiceState:
|
|
35
|
+
"""Voice connection state."""
|
|
36
|
+
def __init__(self):
|
|
37
|
+
self.session_id = None
|
|
38
|
+
self.token = None
|
|
39
|
+
self.endpoint = None
|
|
40
|
+
self.guild_id = None
|
|
41
|
+
self.user_id = None
|
|
42
|
+
self.channel_id = None
|
|
43
|
+
self.mute = False
|
|
44
|
+
self.deaf = False
|
|
45
|
+
self.self_mute = False
|
|
46
|
+
self.self_deaf = False
|
|
47
|
+
self.self_stream = False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class VoiceClient:
|
|
51
|
+
"""Represents a connection to a Discord voice channel."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, client, channel_id: int, guild_id: int, endpoint: str, token: str, session_id: str):
|
|
54
|
+
self.client = client
|
|
55
|
+
self.channel_id = channel_id
|
|
56
|
+
self.guild_id = guild_id
|
|
57
|
+
self.endpoint = endpoint
|
|
58
|
+
self.token = token
|
|
59
|
+
self.session_id = session_id
|
|
60
|
+
self._state = VoiceState()
|
|
61
|
+
self._state.channel_id = channel_id
|
|
62
|
+
self._state.guild_id = guild_id
|
|
63
|
+
self._state.session_id = session_id
|
|
64
|
+
self._state.token = token
|
|
65
|
+
self._state.endpoint = endpoint
|
|
66
|
+
|
|
67
|
+
self._ws = None
|
|
68
|
+
self._connected = False
|
|
69
|
+
self._closed = False
|
|
70
|
+
self._sequence = 0
|
|
71
|
+
self._timestamp = 0
|
|
72
|
+
self._ssrc = None
|
|
73
|
+
self._encryption = None
|
|
74
|
+
self._secret_key = None
|
|
75
|
+
self._voice_ip = None
|
|
76
|
+
self._voice_port = None
|
|
77
|
+
self._sock = None
|
|
78
|
+
|
|
79
|
+
self._heartbeat_task = None
|
|
80
|
+
self._receive_task = None
|
|
81
|
+
self._audio_task = None
|
|
82
|
+
self._audio_queue = asyncio.Queue()
|
|
83
|
+
self._ready = asyncio.Event()
|
|
84
|
+
|
|
85
|
+
self._handlers = {
|
|
86
|
+
"on_ready": [],
|
|
87
|
+
"on_speaking": [],
|
|
88
|
+
"on_disconnect": [],
|
|
89
|
+
"on_error": [],
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
logger.info(f"Voice client created for guild {guild_id}, channel {channel_id}")
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def is_connected(self) -> bool:
|
|
96
|
+
return self._connected and not self._closed
|
|
97
|
+
|
|
98
|
+
async def connect(self) -> bool:
|
|
99
|
+
"""Connect to the voice channel."""
|
|
100
|
+
if self._connected:
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
import websockets
|
|
105
|
+
ws_url = f"wss://{self.endpoint}?v=4"
|
|
106
|
+
self._ws = await websockets.connect(ws_url)
|
|
107
|
+
|
|
108
|
+
await self._send({
|
|
109
|
+
"op": 0,
|
|
110
|
+
"d": {
|
|
111
|
+
"server_id": str(self.guild_id),
|
|
112
|
+
"user_id": str(self.client.user.id),
|
|
113
|
+
"session_id": self.session_id,
|
|
114
|
+
"token": self.token,
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
ready = await self._wait_for_event("ready", timeout=10)
|
|
119
|
+
if ready:
|
|
120
|
+
self._connected = True
|
|
121
|
+
self._ready.set()
|
|
122
|
+
logger.info("Voice connection ready")
|
|
123
|
+
await self._on_ready()
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.error(f"Voice connection failed: {e}")
|
|
130
|
+
await self._on_error(e)
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
async def disconnect(self) -> None:
|
|
134
|
+
"""Disconnect from voice channel."""
|
|
135
|
+
if self._closed:
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
self._closed = True
|
|
139
|
+
self._connected = False
|
|
140
|
+
self._ready.clear()
|
|
141
|
+
|
|
142
|
+
if self._heartbeat_task:
|
|
143
|
+
self._heartbeat_task.cancel()
|
|
144
|
+
|
|
145
|
+
if self._receive_task:
|
|
146
|
+
self._receive_task.cancel()
|
|
147
|
+
|
|
148
|
+
if self._audio_task:
|
|
149
|
+
self._audio_task.cancel()
|
|
150
|
+
|
|
151
|
+
if self._sock:
|
|
152
|
+
self._sock.close()
|
|
153
|
+
self._sock = None
|
|
154
|
+
|
|
155
|
+
if self._ws:
|
|
156
|
+
await self._ws.close()
|
|
157
|
+
self._ws = None
|
|
158
|
+
|
|
159
|
+
logger.info("Voice connection closed")
|
|
160
|
+
|
|
161
|
+
async def send_audio(self, data: bytes) -> None:
|
|
162
|
+
"""Send audio data to the voice channel."""
|
|
163
|
+
if not self._connected:
|
|
164
|
+
raise VoiceError("Not connected to voice")
|
|
165
|
+
await self._audio_queue.put(data)
|
|
166
|
+
|
|
167
|
+
async def _send(self, data: dict) -> None:
|
|
168
|
+
"""Send a WebSocket message."""
|
|
169
|
+
if self._ws:
|
|
170
|
+
await self._ws.send(json.dumps(data))
|
|
171
|
+
|
|
172
|
+
async def _wait_for_event(self, event_type: str, timeout: float = 10) -> Optional[dict]:
|
|
173
|
+
"""Wait for a specific voice event."""
|
|
174
|
+
start = time.time()
|
|
175
|
+
while time.time() - start < timeout:
|
|
176
|
+
if self._ws:
|
|
177
|
+
try:
|
|
178
|
+
msg = await asyncio.wait_for(self._ws.recv(), timeout=1)
|
|
179
|
+
data = json.loads(msg)
|
|
180
|
+
if data.get("op") == 2 and data.get("t") == event_type.upper():
|
|
181
|
+
return data
|
|
182
|
+
except asyncio.TimeoutError:
|
|
183
|
+
continue
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.error(f"Error waiting for event: {e}")
|
|
186
|
+
break
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
def on(self, event: str, callback: Callable):
|
|
190
|
+
"""Register an event handler."""
|
|
191
|
+
if event not in self._handlers:
|
|
192
|
+
self._handlers[event] = []
|
|
193
|
+
self._handlers[event].append(callback)
|
|
194
|
+
|
|
195
|
+
async def _on_ready(self):
|
|
196
|
+
for callback in self._handlers.get("on_ready", []):
|
|
197
|
+
try:
|
|
198
|
+
if asyncio.iscoroutinefunction(callback):
|
|
199
|
+
await callback(self)
|
|
200
|
+
else:
|
|
201
|
+
callback(self)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.error(f"Error in on_ready handler: {e}")
|
|
204
|
+
|
|
205
|
+
async def _on_error(self, error: Exception):
|
|
206
|
+
for callback in self._handlers.get("on_error", []):
|
|
207
|
+
try:
|
|
208
|
+
if asyncio.iscoroutinefunction(callback):
|
|
209
|
+
await callback(error)
|
|
210
|
+
else:
|
|
211
|
+
callback(error)
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.error(f"Error in on_error handler: {e}")
|
|
214
|
+
|
|
215
|
+
def __repr__(self) -> str:
|
|
216
|
+
return f"<VoiceClient guild={self.guild_id} channel={self.channel_id} connected={self._connected}>"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class VoiceManager:
|
|
220
|
+
"""Manages voice connections."""
|
|
221
|
+
|
|
222
|
+
def __init__(self, client):
|
|
223
|
+
self.client = client
|
|
224
|
+
self._connections: Dict[int, VoiceClient] = {}
|
|
225
|
+
self._state: Dict[int, VoiceState] = {}
|
|
226
|
+
|
|
227
|
+
async def connect(
|
|
228
|
+
self,
|
|
229
|
+
channel_id: int,
|
|
230
|
+
guild_id: int,
|
|
231
|
+
endpoint: str,
|
|
232
|
+
token: str,
|
|
233
|
+
session_id: str,
|
|
234
|
+
) -> VoiceClient:
|
|
235
|
+
"""Create and connect a voice client."""
|
|
236
|
+
if guild_id in self._connections:
|
|
237
|
+
await self._connections[guild_id].disconnect()
|
|
238
|
+
|
|
239
|
+
client = VoiceClient(
|
|
240
|
+
self.client,
|
|
241
|
+
channel_id,
|
|
242
|
+
guild_id,
|
|
243
|
+
endpoint,
|
|
244
|
+
token,
|
|
245
|
+
session_id,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
await client.connect()
|
|
249
|
+
self._connections[guild_id] = client
|
|
250
|
+
return client
|
|
251
|
+
|
|
252
|
+
def get_client(self, guild_id: int) -> Optional[VoiceClient]:
|
|
253
|
+
"""Get the voice client for a guild."""
|
|
254
|
+
return self._connections.get(guild_id)
|
|
255
|
+
|
|
256
|
+
async def disconnect(self, guild_id: int) -> None:
|
|
257
|
+
"""Disconnect a voice client."""
|
|
258
|
+
if guild_id in self._connections:
|
|
259
|
+
await self._connections[guild_id].disconnect()
|
|
260
|
+
del self._connections[guild_id]
|
|
261
|
+
|
|
262
|
+
async def disconnect_all(self) -> None:
|
|
263
|
+
"""Disconnect all voice clients."""
|
|
264
|
+
for guild_id in list(self._connections.keys()):
|
|
265
|
+
await self.disconnect(guild_id)
|
|
266
|
+
|
|
267
|
+
def update_state(self, guild_id: int, state_data: dict) -> None:
|
|
268
|
+
"""Update voice state."""
|
|
269
|
+
state = self._state.get(guild_id)
|
|
270
|
+
if not state:
|
|
271
|
+
state = VoiceState()
|
|
272
|
+
self._state[guild_id] = state
|
|
273
|
+
|
|
274
|
+
state.session_id = state_data.get("session_id")
|
|
275
|
+
state.token = state_data.get("token")
|
|
276
|
+
state.endpoint = state_data.get("endpoint")
|
|
277
|
+
state.channel_id = state_data.get("channel_id")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|