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.
Files changed (52) hide show
  1. {modifyself-0.2.4 → modifyself-0.2.6}/PKG-INFO +1 -1
  2. modifyself-0.2.6/modifyself/interactions.py +216 -0
  3. modifyself-0.2.6/modifyself/voice.py +277 -0
  4. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself.egg-info/PKG-INFO +1 -1
  5. {modifyself-0.2.4 → modifyself-0.2.6}/pyproject.toml +1 -1
  6. modifyself-0.2.4/modifyself/interactions.py +0 -0
  7. modifyself-0.2.4/modifyself/voice.py +0 -0
  8. {modifyself-0.2.4 → modifyself-0.2.6}/LICENSE +0 -0
  9. {modifyself-0.2.4 → modifyself-0.2.6}/README.md +0 -0
  10. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/__init__.py +0 -0
  11. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/activity.py +0 -0
  12. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/client.py +0 -0
  13. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/commands/__init__.py +0 -0
  14. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/commands/checks.py +0 -0
  15. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/commands/cog.py +0 -0
  16. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/commands/context.py +0 -0
  17. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/commands/converters.py +0 -0
  18. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/commands/core.py +0 -0
  19. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/components.py +0 -0
  20. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/core/bitfield.py +0 -0
  21. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/core/enums.py +0 -0
  22. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/core/mixins.py +0 -0
  23. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/core/snowflake.py +0 -0
  24. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/errors.py +0 -0
  25. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/gateway/__init__.py +0 -0
  26. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/gateway/dispatcher.py +0 -0
  27. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/gateway/heartbeat.py +0 -0
  28. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/gateway/websocket.py +0 -0
  29. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/headers.py +0 -0
  30. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/http/__init__.py +0 -0
  31. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/http/client.py +0 -0
  32. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/http/ratelimit.py +0 -0
  33. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/http/route.py +0 -0
  34. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/__init__.py +0 -0
  35. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/base.py +0 -0
  36. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/billing.py +0 -0
  37. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/channel.py +0 -0
  38. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/guild.py +0 -0
  39. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/member.py +0 -0
  40. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/message.py +0 -0
  41. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/relationship.py +0 -0
  42. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/settings.py +0 -0
  43. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/user.py +0 -0
  44. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/models/webhook.py +0 -0
  45. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/state.py +0 -0
  46. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/types.py +0 -0
  47. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself/utils.py +0 -0
  48. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself.egg-info/SOURCES.txt +0 -0
  49. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself.egg-info/dependency_links.txt +0 -0
  50. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself.egg-info/requires.txt +0 -0
  51. {modifyself-0.2.4 → modifyself-0.2.6}/modifyself.egg-info/top_level.txt +0 -0
  52. {modifyself-0.2.4 → modifyself-0.2.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modifyself
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: A clean, pythonic Discord self-bot library
5
5
  Author-email: modifyself <modifyself@example.com>
6
6
  License-Expression: MIT
@@ -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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modifyself
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: A clean, pythonic Discord self-bot library
5
5
  Author-email: modifyself <modifyself@example.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "modifyself"
7
- version = "0.2.4"
7
+ version = "0.2.6"
8
8
  description = "A clean, pythonic Discord self-bot library"
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes