micup 0.0.1__tar.gz → 0.1.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.
micup-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: micup
3
+ Version: 0.1.0
4
+ Summary: Micup platformu için Python kütüphanesi.
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: aiohttp>=3.9
8
+ Requires-Dist: livekit
9
+
10
+ # micup
11
+
12
+ Micup platformu için Python kütüphanesi. Bot yazmak için mesaj, komut, olay, yetki ve ses yönetimini tek yerden sağlar.
13
+
14
+ ## Kurulum
15
+
16
+ ```
17
+ pip install micup
18
+ ```
19
+
20
+ ## Hızlı başlangıç
21
+
22
+ ```python
23
+ from micup import Bot
24
+
25
+ bot = Bot() # token MICUP_TOKEN ortam değişkeninden okunur
26
+
27
+ @bot.command("!ping")
28
+ async def ping(msg):
29
+ return "pong"
30
+
31
+ bot.run()
32
+ ```
33
+
34
+ Token'ı kodun içine yazma. PowerShell'de:
35
+
36
+ ```powershell
37
+ $env:MICUP_TOKEN = "uygulama_id.gizli_anahtar"
38
+ python bot.py
39
+ ```
40
+
41
+ ## Özellikler
42
+
43
+ - Gateway bağlantısı, kopunca otomatik yeniden bağlanma (resume destekli)
44
+ - Komutlar ve olaylar: `@bot.command(...)`, `@bot.on(...)`
45
+ - Mesaj işlemleri: gönder, düzenle, sil, toplu sil, listele
46
+ - Ham REST erişimi: `bot.rest(...)`
47
+ - Ses: kanala katılma, ayrılma, durum okuma (LiveKit medya bağlantısı dahil)
48
+ - Durum: `bot.set_presence(...)`
49
+ - Yetki bitleri ve snowflake yardımcıları
50
+
51
+ Ayrıntılı kullanım için dokümantasyona bak.
52
+
53
+ ## Lisans
54
+
55
+ Henüz belirlenmedi.
micup-0.1.0/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # micup
2
+
3
+ Micup platformu için Python kütüphanesi. Bot yazmak için mesaj, komut, olay, yetki ve ses yönetimini tek yerden sağlar.
4
+
5
+ ## Kurulum
6
+
7
+ ```
8
+ pip install micup
9
+ ```
10
+
11
+ ## Hızlı başlangıç
12
+
13
+ ```python
14
+ from micup import Bot
15
+
16
+ bot = Bot() # token MICUP_TOKEN ortam değişkeninden okunur
17
+
18
+ @bot.command("!ping")
19
+ async def ping(msg):
20
+ return "pong"
21
+
22
+ bot.run()
23
+ ```
24
+
25
+ Token'ı kodun içine yazma. PowerShell'de:
26
+
27
+ ```powershell
28
+ $env:MICUP_TOKEN = "uygulama_id.gizli_anahtar"
29
+ python bot.py
30
+ ```
31
+
32
+ ## Özellikler
33
+
34
+ - Gateway bağlantısı, kopunca otomatik yeniden bağlanma (resume destekli)
35
+ - Komutlar ve olaylar: `@bot.command(...)`, `@bot.on(...)`
36
+ - Mesaj işlemleri: gönder, düzenle, sil, toplu sil, listele
37
+ - Ham REST erişimi: `bot.rest(...)`
38
+ - Ses: kanala katılma, ayrılma, durum okuma (LiveKit medya bağlantısı dahil)
39
+ - Durum: `bot.set_presence(...)`
40
+ - Yetki bitleri ve snowflake yardımcıları
41
+
42
+ Ayrıntılı kullanım için dokümantasyona bak.
43
+
44
+ ## Lisans
45
+
46
+ Henüz belirlenmedi.
@@ -0,0 +1,12 @@
1
+ from .client import Bot
2
+ from .errors import MicupAPIError, MicupError
3
+ from .models import Message, User
4
+ from .permissions import Permissions
5
+ from .utils import snowflake_to_datetime
6
+ from .voice import Voice
7
+
8
+ __version__ = "0.1.0"
9
+ __all__ = [
10
+ "Bot", "Message", "User", "Voice", "Permissions",
11
+ "MicupAPIError", "MicupError", "snowflake_to_datetime",
12
+ ]
@@ -0,0 +1,384 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import os
5
+ import random
6
+ import aiohttp
7
+ from typing import Any, Callable, Dict, List, Optional
8
+ from .constants import DEFAULT_IGNORED_EVENTS, DEFAULT_ORIGIN, FATAL_CLOSE_CODES, RATE_LIMIT_CLOSE_CODE, RESET_CLOSE_CODES, log
9
+ from .errors import MicupAPIError
10
+ from .models import Message, User
11
+ from .voice import Voice
12
+
13
+
14
+ class Bot:
15
+ def __init__(
16
+ self,
17
+ token: Optional[str] = None,
18
+ *,
19
+ ignored_events: Optional[List[str]] = None,
20
+ origin: str = DEFAULT_ORIGIN,
21
+ presence: Optional[Dict[str, Any]] = None,
22
+ ):
23
+ self.token = token or os.environ.get("MICUP_TOKEN")
24
+ if not self.token:
25
+ raise ValueError("Token yok. Bot(token) ver ya da MICUP_TOKEN ortam değişkenini ayarla.")
26
+
27
+ self.origin = origin.rstrip("/")
28
+ self.ignored_events = list(DEFAULT_IGNORED_EVENTS if ignored_events is None else ignored_events)
29
+ self.presence = presence
30
+
31
+ self.user: Optional[User] = None
32
+ self.session_id: Optional[str] = None
33
+ self.sequence: Optional[int] = None
34
+ self.voice = Voice(self)
35
+
36
+ self._api_url: Optional[str] = None
37
+ self._gateway_url: Optional[str] = None
38
+ self._http: Optional[aiohttp.ClientSession] = None
39
+ self._ws: Optional[aiohttp.ClientWebSocketResponse] = None
40
+ self._heartbeat_interval: Optional[float] = None
41
+ self._running = False
42
+ self._ready = False
43
+ self._handlers: Dict[str, List[Callable]] = {}
44
+ self._commands: Dict[str, Callable] = {}
45
+
46
+
47
+ def command(self, prefix: str):
48
+ def decorator(func: Callable):
49
+ self._commands[prefix] = func
50
+ return func
51
+ return decorator
52
+
53
+ def on(self, event: str):
54
+ def decorator(func: Callable):
55
+ self._handlers.setdefault(event.lower(), []).append(func)
56
+ return func
57
+ return decorator
58
+
59
+ def is_ready(self) -> bool:
60
+ return self._ready
61
+
62
+
63
+ async def _request(self, method: str, path: str, **kwargs) -> Any:
64
+ if self._http is None or self._api_url is None:
65
+ raise RuntimeError("Bot bağlı değil. Önce bot.run() ile başlat.")
66
+ headers = {"Authorization": f"Bot {self.token}"}
67
+ url = f"{self._api_url}{path}"
68
+
69
+ for _ in range(5):
70
+ async with self._http.request(method, url, headers=headers, **kwargs) as resp:
71
+ if resp.status == 204:
72
+ return None
73
+ try:
74
+ data = await resp.json(content_type=None)
75
+ except Exception:
76
+ data = None
77
+
78
+ if resp.status == 429:
79
+ body = data if isinstance(data, dict) else {}
80
+ wait = float(body.get("retry_after", 1))
81
+ log.warning("Rate limit: %.2f sn bekleniyor (%s)", wait, path)
82
+ await asyncio.sleep(wait + 0.05)
83
+ continue
84
+
85
+ if resp.status >= 400:
86
+ body = data if isinstance(data, dict) else {}
87
+ raise MicupAPIError(
88
+ resp.status,
89
+ body.get("code", ""),
90
+ body.get("message", resp.reason or ""),
91
+ body,
92
+ )
93
+ return data
94
+
95
+ raise MicupAPIError(429, "RATE_LIMITED", "Çok fazla yeniden deneme")
96
+
97
+ async def rest(self, method: str, path: str, body: Any = None) -> Any:
98
+ kwargs: Dict[str, Any] = {}
99
+ if body is not None:
100
+ kwargs["json"] = body
101
+ return await self._request(method.upper(), f"/v1{path}", **kwargs)
102
+
103
+ async def send_message(
104
+ self,
105
+ channel_id: str,
106
+ content: Optional[str] = None,
107
+ reply_to: Optional[str] = None,
108
+ **fields: Any,
109
+ ) -> Message:
110
+ body: Dict[str, Any] = {k: v for k, v in fields.items() if v is not None}
111
+ if content is not None:
112
+ body["content"] = content
113
+ body.setdefault("allowed_mentions", {"parse": []})
114
+ if reply_to:
115
+ body["message_reference"] = {"message_id": reply_to, "channel_id": channel_id}
116
+ data = await self._request("POST", f"/v1/channels/{channel_id}/messages", json=body)
117
+ return Message(data, self)
118
+
119
+ async def edit_message(self, channel_id: str, message_id: str, content: str) -> Message:
120
+ data = await self._request(
121
+ "PATCH", f"/v1/channels/{channel_id}/messages/{message_id}", json={"content": content}
122
+ )
123
+ return Message(data, self)
124
+
125
+ async def delete_message(self, channel_id: str, message_id: str) -> None:
126
+ await self._request("DELETE", f"/v1/channels/{channel_id}/messages/{message_id}")
127
+
128
+ async def bulk_delete(self, channel_id: str, message_ids: List[str]) -> int:
129
+ ids = list(message_ids)
130
+ for i in range(0, len(ids), 100):
131
+ await self._request(
132
+ "POST",
133
+ f"/v1/channels/{channel_id}/messages/bulk-delete",
134
+ json={"message_ids": ids[i:i + 100]},
135
+ )
136
+ return len(ids)
137
+
138
+ async def get_messages(
139
+ self,
140
+ channel_id: str,
141
+ limit: int = 50,
142
+ before: Optional[str] = None,
143
+ after: Optional[str] = None,
144
+ around: Optional[str] = None,
145
+ ) -> List[Message]:
146
+ params: Dict[str, Any] = {"limit": max(1, min(int(limit), 100))}
147
+ if before:
148
+ params["before"] = before
149
+ if after:
150
+ params["after"] = after
151
+ if around:
152
+ params["around"] = around
153
+ data = await self._request("GET", f"/v1/channels/{channel_id}/messages", params=params)
154
+ return [Message(m, self) for m in data or []]
155
+
156
+ async def get_channel(self, channel_id: str) -> Dict[str, Any]:
157
+ return await self._request("GET", f"/v1/channels/{channel_id}")
158
+
159
+ async def get_user(self, user_id: str) -> User:
160
+ return User(await self._request("GET", f"/v1/users/{user_id}"))
161
+
162
+
163
+ async def set_presence(
164
+ self,
165
+ status: str,
166
+ *,
167
+ afk: bool = False,
168
+ mobile: bool = False,
169
+ custom_status: Optional[Any] = None,
170
+ ) -> None:
171
+ valid = {"online", "idle", "dnd", "invisible", "offline"}
172
+ if status not in valid:
173
+ raise ValueError(f"Geçersiz durum: {status}. Şunlardan biri olmalı: {sorted(valid)}")
174
+ d: Dict[str, Any] = {"status": status, "afk": afk, "mobile": mobile}
175
+ if custom_status is not None:
176
+ d["custom_status"] = {"text": custom_status} if isinstance(custom_status, str) else custom_status
177
+ await self._send({"op": 3, "d": d})
178
+
179
+ async def _discover(self) -> None:
180
+ async with aiohttp.ClientSession() as session:
181
+ async with session.get(f"{self.origin}/.well-known/fluxer") as resp:
182
+ if resp.status != 200:
183
+ raise MicupAPIError(resp.status, "DISCOVERY_FAILED", "Keşif belgesi alınamadı")
184
+ data = await resp.json(content_type=None)
185
+ self._api_url = data["endpoints"]["api_public"].rstrip("/")
186
+ self._gateway_url = data["endpoints"]["gateway"]
187
+
188
+ async def _send(self, payload: Dict[str, Any]) -> None:
189
+ if self._ws is None:
190
+ raise RuntimeError("Gateway bağlantısı yok")
191
+ await self._ws.send_json(payload)
192
+
193
+ async def _heartbeat(self, ws: aiohttp.ClientWebSocketResponse) -> None:
194
+ try:
195
+ await asyncio.sleep(self._heartbeat_interval * random.random())
196
+ while True:
197
+ await ws.send_json({"op": 1, "d": self.sequence})
198
+ await asyncio.sleep(self._heartbeat_interval)
199
+ except Exception:
200
+ return
201
+
202
+ async def _identify(self) -> None:
203
+ await self._send({
204
+ "op": 2,
205
+ "d": {
206
+ "token": self.token,
207
+ "properties": {"os": "linux", "browser": "micup.py", "device": "micup.py"},
208
+ "presence": self.presence or {"status": "online", "afk": False, "mobile": False},
209
+ "ignored_events": self.ignored_events,
210
+ "flags": 0,
211
+ },
212
+ })
213
+
214
+ async def _resume(self) -> None:
215
+ await self._send({
216
+ "op": 6,
217
+ "d": {"token": self.token, "session_id": self.session_id, "seq": self.sequence},
218
+ })
219
+
220
+ async def _connect(self) -> Optional[int]:
221
+ async with self._http.ws_connect(f"{self._gateway_url}?v=1&encoding=json", max_msg_size=0) as ws:
222
+ self._ws = ws
223
+ hello = await ws.receive_json()
224
+ if hello.get("op") != 10:
225
+ raise MicupAPIError(0, "INVALID_HELLO", "Hello (op 10) bekleniyordu")
226
+ self._heartbeat_interval = hello["d"]["heartbeat_interval"] / 1000.0
227
+
228
+ hb = asyncio.create_task(self._heartbeat(ws))
229
+ try:
230
+ if self.session_id and self.sequence is not None:
231
+ await self._resume()
232
+ else:
233
+ await self._identify()
234
+
235
+ async for raw in ws:
236
+ if raw.type == aiohttp.WSMsgType.TEXT:
237
+ await self._handle(json.loads(raw.data))
238
+ elif raw.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
239
+ break
240
+ finally:
241
+ hb.cancel()
242
+ self._ws = None
243
+
244
+ close_code = ws.close_code
245
+ if close_code in RESET_CLOSE_CODES:
246
+ self.session_id = None
247
+ self.sequence = None
248
+ return close_code
249
+
250
+ async def _handle(self, msg: Dict[str, Any]) -> None:
251
+ op = msg.get("op")
252
+ if op == 0:
253
+ await self._dispatch(msg.get("t"), msg.get("d") or {}, msg.get("s"))
254
+ elif op == 1:
255
+ await self._send({"op": 1, "d": self.sequence})
256
+ elif op == 7:
257
+ await self._ws.close()
258
+ elif op == 9:
259
+ if not msg.get("d"):
260
+ self.session_id = None
261
+ self.sequence = None
262
+ await self._ws.close()
263
+ elif op == 11:
264
+ pass
265
+ else:
266
+ log.debug("Bilinmeyen opcode: %s", op)
267
+
268
+ async def _dispatch(self, event: Optional[str], data: Dict[str, Any], seq: Optional[int]) -> None:
269
+ if seq is not None:
270
+ self.sequence = seq
271
+ if not event:
272
+ return
273
+
274
+ if event == "READY":
275
+ self.session_id = data.get("session_id")
276
+ self.user = User(data.get("user") or {})
277
+ self._ready = True
278
+ log.info("Bağlandı: %s", self.user.username)
279
+ await self._emit("ready", self)
280
+ return
281
+
282
+ if event == "RESUMED":
283
+ self._ready = True
284
+ log.info("Oturum devam ettirildi")
285
+ return
286
+
287
+ if event == "MESSAGE_CREATE":
288
+ msg = Message(data, self)
289
+ await self._emit("message_create", msg)
290
+ try:
291
+ await self._run_commands(msg)
292
+ except Exception:
293
+ log.exception("Komut işlenirken hata")
294
+ return
295
+
296
+ if event == "GUILD_CREATE":
297
+ self.voice._load_guild(data)
298
+ elif event == "VOICE_STATE_UPDATE":
299
+ self.voice._on_state(data)
300
+ elif event == "VOICE_SERVER_UPDATE":
301
+ self.voice._on_server(data)
302
+
303
+ await self._emit(event.lower(), data)
304
+
305
+ async def _emit(self, name: str, *args: Any) -> None:
306
+ for handler in self._handlers.get(name, []):
307
+ await self._call(handler, *args)
308
+
309
+ async def _call(self, func: Callable, *args: Any) -> Any:
310
+ try:
311
+ result = func(*args)
312
+ if asyncio.iscoroutine(result):
313
+ result = await result
314
+ return result
315
+ except Exception:
316
+ log.exception("Handler hatası: %s", getattr(func, "__name__", func))
317
+ return None
318
+
319
+ async def _run_commands(self, msg: Message) -> None:
320
+ if msg.author.bot or (self.user and msg.author.id == self.user.id):
321
+ return
322
+ for prefix in sorted(self._commands, key=len, reverse=True):
323
+ if msg.content == prefix or msg.content.startswith(prefix + " "):
324
+ result = await self._call(self._commands[prefix], msg)
325
+ if isinstance(result, str) and result:
326
+ await msg.reply(result)
327
+ return
328
+
329
+
330
+ async def start(self) -> None:
331
+ self._running = True
332
+ self._http = aiohttp.ClientSession()
333
+ delay = 1.0
334
+ try:
335
+ while self._running:
336
+ try:
337
+ await self._discover()
338
+ code = await self._connect()
339
+ self._ready = False
340
+ await self._emit("disconnect", code, "")
341
+ if not self._running:
342
+ break
343
+ if code in FATAL_CLOSE_CODES:
344
+ log.error("Gateway kalıcı olarak kapattı (%s). Bot durduruldu.", code)
345
+ break
346
+ if code == RATE_LIMIT_CLOSE_CODE:
347
+ log.warning("Çok sık bağlanıldı, 30 sn bekleniyor.")
348
+ await asyncio.sleep(30)
349
+ else:
350
+ await asyncio.sleep(1)
351
+ delay = 1.0
352
+ except asyncio.CancelledError:
353
+ raise
354
+ except Exception as e:
355
+ self._ready = False
356
+ if not self._running:
357
+ break
358
+ await self._emit("error", e)
359
+ log.warning("Bağlantı hatası: %s (%.0f sn sonra tekrar)", e, delay)
360
+ await asyncio.sleep(delay)
361
+ delay = min(delay * 2, 60)
362
+ finally:
363
+ self._running = False
364
+ self._ready = False
365
+ if self._http is not None:
366
+ await self._http.close()
367
+ self._http = None
368
+
369
+ async def close(self) -> None:
370
+ self._running = False
371
+ await self.voice.disconnect_all()
372
+ if self._ws is not None and not self._ws.closed:
373
+ await self._ws.close()
374
+
375
+ async def destroy(self) -> None:
376
+ await self.close()
377
+
378
+ def run(self) -> None:
379
+ if not logging.getLogger().handlers:
380
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
381
+ try:
382
+ asyncio.run(self.start())
383
+ except KeyboardInterrupt:
384
+ log.info("Bot durduruldu")
@@ -0,0 +1,13 @@
1
+ import logging
2
+
3
+
4
+ DEFAULT_ORIGIN = "https://micup.gg"
5
+ SNOWFLAKE_EPOCH_MS = 1420070400000
6
+
7
+ DEFAULT_IGNORED_EVENTS = ["TYPING_START", "PRESENCE_UPDATE"]
8
+
9
+ FATAL_CLOSE_CODES = {4004, 4010, 4011, 4012}
10
+ RESET_CLOSE_CODES = {4001, 4002, 4003, 4005, 4007}
11
+ RATE_LIMIT_CLOSE_CODE = 4008
12
+
13
+ log = logging.getLogger("micup")
@@ -0,0 +1,14 @@
1
+ from typing import Any
2
+
3
+
4
+ class MicupAPIError(Exception):
5
+
6
+ def __init__(self, status: int, code: str, message: str, body: Any = None):
7
+ super().__init__(f"HTTP {status} {code}: {message}")
8
+ self.status = status
9
+ self.code = code
10
+ self.message = message
11
+ self.body = body
12
+
13
+
14
+ MicupError = MicupAPIError
@@ -0,0 +1,50 @@
1
+ from typing import Any, Dict, List, Optional
2
+ from .utils import snowflake_to_datetime
3
+
4
+
5
+ class User:
6
+
7
+ def __init__(self, data: Dict[str, Any]):
8
+ self.raw = data
9
+ self.id: Optional[str] = data.get("id")
10
+ self.username: Optional[str] = data.get("username")
11
+ self.global_name: Optional[str] = data.get("global_name")
12
+ self.bot: bool = bool(data.get("bot", False))
13
+
14
+ def __repr__(self) -> str:
15
+ return f"<User {self.username} ({self.id})>"
16
+
17
+
18
+ class Message:
19
+
20
+ def __init__(self, data: Dict[str, Any], bot: "Bot"):
21
+ self._bot = bot
22
+ self.raw = data
23
+ self.id: str = data["id"]
24
+ self.channel_id: str = data["channel_id"]
25
+ self.guild_id: Optional[str] = data.get("guild_id")
26
+ self.member: Optional[Dict[str, Any]] = data.get("member")
27
+ self.content: str = data.get("content", "")
28
+ self.author = User(data.get("author") or {})
29
+ self.pinned: bool = bool(data.get("pinned", False))
30
+ self.created_at = snowflake_to_datetime(self.id)
31
+
32
+ @property
33
+ def is_bot(self) -> bool:
34
+ return self.author.bot
35
+
36
+ @property
37
+ def args(self) -> List[str]:
38
+ return self.content.split()[1:]
39
+
40
+ async def reply(self, content: Optional[str] = None, **fields: Any) -> "Message":
41
+ return await self._bot.send_message(self.channel_id, content, reply_to=self.id, **fields)
42
+
43
+ async def send(self, content: Optional[str] = None, **fields: Any) -> "Message":
44
+ return await self._bot.send_message(self.channel_id, content, **fields)
45
+
46
+ async def edit(self, content: str) -> "Message":
47
+ return await self._bot.edit_message(self.channel_id, self.id, content)
48
+
49
+ async def delete(self) -> None:
50
+ await self._bot.delete_message(self.channel_id, self.id)
@@ -0,0 +1,30 @@
1
+ from typing import Any
2
+
3
+
4
+ class Permissions:
5
+
6
+ ADMINISTRATOR = 1 << 3
7
+ MANAGE_CHANNELS = 1 << 4
8
+ ADD_REACTIONS = 1 << 6
9
+ VIEW_CHANNEL = 1 << 10
10
+ SEND_MESSAGES = 1 << 11
11
+ MANAGE_MESSAGES = 1 << 13
12
+ EMBED_LINKS = 1 << 14
13
+ ATTACH_FILES = 1 << 15
14
+ READ_MESSAGE_HISTORY = 1 << 16
15
+ MENTION_EVERYONE = 1 << 17
16
+ CONNECT = 1 << 20
17
+ SPEAK = 1 << 21
18
+ MUTE_MEMBERS = 1 << 22
19
+ DEAFEN_MEMBERS = 1 << 23
20
+ MOVE_MEMBERS = 1 << 24
21
+ MANAGE_ROLES = 1 << 28
22
+ STREAM = 1 << 37
23
+ PIN_MESSAGES = 1 << 51
24
+
25
+ @staticmethod
26
+ def has(mask: Any, perm: int) -> bool:
27
+ mask = int(mask)
28
+ if mask & Permissions.ADMINISTRATOR:
29
+ return True
30
+ return (mask & perm) == perm
@@ -0,0 +1,7 @@
1
+ from datetime import datetime, timezone
2
+ from .constants import SNOWFLAKE_EPOCH_MS
3
+
4
+
5
+ def snowflake_to_datetime(snowflake: str) -> datetime:
6
+ ms = (int(snowflake) >> 22) + SNOWFLAKE_EPOCH_MS
7
+ return datetime.fromtimestamp(ms / 1000, tz=timezone.utc)
@@ -0,0 +1,190 @@
1
+ import asyncio
2
+ from typing import Any, Dict, List, Optional
3
+ from .constants import log
4
+ from .errors import MicupAPIError
5
+
6
+
7
+ class Voice:
8
+
9
+ def __init__(self, bot: "Bot"):
10
+ self._bot = bot
11
+ self._states: Dict[str, Dict[str, Dict[str, Any]]] = {}
12
+ self._grants: Dict[str, asyncio.Future] = {}
13
+ self._mine: Dict[str, Dict[str, Any]] = {}
14
+ self._rooms: Dict[str, Any] = {}
15
+
16
+ async def _connect_media(self, guild_id: str, grant: Dict[str, Any]) -> None:
17
+ try:
18
+ from livekit import rtc
19
+ except ImportError:
20
+ raise MicupAPIError(0, "LIVEKIT_MISSING", "Ses medyası için 'pip install livekit' gerekli")
21
+
22
+ old = self._rooms.pop(guild_id, None)
23
+ if old is not None:
24
+ await old.disconnect()
25
+
26
+ room = rtc.Room()
27
+ await room.connect(grant["endpoint"], grant["token"])
28
+ self._rooms[guild_id] = room
29
+ log.info("LiveKit odasına bağlandı (guild %s)", guild_id)
30
+
31
+ async def disconnect_all(self) -> None:
32
+ for guild_id in list(self._rooms):
33
+ room = self._rooms.pop(guild_id)
34
+ try:
35
+ await room.disconnect()
36
+ except Exception as e:
37
+ log.warning("LiveKit kapatma hatası (%s): %s", guild_id, e)
38
+
39
+
40
+ def _load_guild(self, guild: Dict[str, Any]) -> None:
41
+ gid = guild.get("id")
42
+ for st in guild.get("voice_states") or []:
43
+ self._on_state(dict(st, guild_id=st.get("guild_id") or gid))
44
+
45
+ def _on_state(self, st: Dict[str, Any]) -> None:
46
+ gid = st.get("guild_id")
47
+ if gid is None:
48
+ return
49
+ key = st.get("connection_id") or st.get("user_id")
50
+ if not key:
51
+ return
52
+ states = self._states.setdefault(gid, {})
53
+ is_me = self._bot.user is not None and st.get("user_id") == self._bot.user.id
54
+ if st.get("channel_id") is None:
55
+ states.pop(key, None)
56
+ if is_me:
57
+ self._mine.pop(gid, None)
58
+ else:
59
+ states[key] = st
60
+ if is_me and gid in self._mine and st.get("connection_id") == self._mine[gid].get("connection_id"):
61
+ self._mine[gid]["channel_id"] = st.get("channel_id")
62
+
63
+ def _on_server(self, data: Dict[str, Any]) -> None:
64
+ fut = self._grants.get(data.get("channel_id"))
65
+ if fut is not None and not fut.done():
66
+ fut.set_result(data)
67
+
68
+
69
+ def _own_state(self, guild_id: str) -> Optional[Dict[str, Any]]:
70
+ if self._bot.user is None:
71
+ return None
72
+ for st in self._states.get(guild_id, {}).values():
73
+ if st.get("user_id") == self._bot.user.id:
74
+ return st
75
+ return None
76
+
77
+
78
+ def get_states(self, guild_id: str) -> List[Dict[str, Any]]:
79
+ return list(self._states.get(guild_id, {}).values())
80
+
81
+ async def join(
82
+ self,
83
+ guild_id: str,
84
+ channel_id: str,
85
+ *,
86
+ self_mute: bool = False,
87
+ self_deaf: bool = False,
88
+ self_video: bool = False,
89
+ timeout: float = 15.0,
90
+ state_timeout: float = 60.0,
91
+ media: bool = True,
92
+ ) -> Dict[str, Any]:
93
+ fut = asyncio.get_running_loop().create_future()
94
+ self._grants[channel_id] = fut
95
+ try:
96
+ await self._bot._send({
97
+ "op": 4,
98
+ "d": {
99
+ "guild_id": guild_id,
100
+ "channel_id": channel_id,
101
+ "self_mute": self_mute,
102
+ "self_deaf": self_deaf,
103
+ "self_video": self_video,
104
+ "self_stream": False,
105
+ },
106
+ })
107
+ grant = await asyncio.wait_for(fut, timeout)
108
+ except asyncio.TimeoutError:
109
+ raise MicupAPIError(0, "VOICE_TIMEOUT", f"{channel_id} için ses bilgisi gelmedi")
110
+ finally:
111
+ self._grants.pop(channel_id, None)
112
+
113
+ if media:
114
+ await self._connect_media(guild_id, grant)
115
+
116
+ connection_id = grant.get("connection_id")
117
+ waited = 0.0
118
+ while connection_id not in self._states.get(guild_id, {}):
119
+ if waited >= state_timeout:
120
+ print(f"[micup] UYARI: {state_timeout:.0f} sn içinde kendi ses durumu gelmedi, grant ile devam ediliyor")
121
+ break
122
+ await asyncio.sleep(0.5)
123
+ waited += 0.5
124
+ own = self._own_state(guild_id) or {}
125
+ self._mine[guild_id] = {
126
+ "channel_id": channel_id,
127
+ "connection_id": connection_id,
128
+ "self_mute": self_mute,
129
+ "self_deaf": self_deaf,
130
+ "self_video": self_video,
131
+ "self_stream": False,
132
+ }
133
+ return {
134
+ "guild_id": grant.get("guild_id", guild_id),
135
+ "channel_id": channel_id,
136
+ "connection_id": connection_id,
137
+ "session_id": own.get("session_id"),
138
+ "endpoint": grant.get("endpoint"),
139
+ "token": grant.get("token"),
140
+ }
141
+
142
+ def _own_link(self, guild_id: str) -> Optional[Dict[str, Any]]:
143
+ if guild_id in self._mine:
144
+ return self._mine[guild_id]
145
+ st = self._own_state(guild_id)
146
+ if st and st.get("connection_id"):
147
+ return {
148
+ "channel_id": st.get("channel_id"),
149
+ "connection_id": st["connection_id"],
150
+ "self_mute": st.get("self_mute", False),
151
+ "self_deaf": st.get("self_deaf", False),
152
+ "self_video": st.get("self_video", False),
153
+ "self_stream": st.get("self_stream", False),
154
+ }
155
+ return None
156
+
157
+ async def leave(self, guild_id: str) -> None:
158
+ link = self._own_link(guild_id)
159
+ if not link or not link.get("connection_id"):
160
+ raise MicupAPIError(0, "VOICE_NOT_CONNECTED", "Bu sunucuda bağlı bir ses bağlantın yok")
161
+ await self._bot._send({
162
+ "op": 4,
163
+ "d": {"guild_id": guild_id, "channel_id": None, "connection_id": link["connection_id"]},
164
+ })
165
+ self._mine.pop(guild_id, None)
166
+ room = self._rooms.pop(guild_id, None)
167
+ if room is not None:
168
+ await room.disconnect()
169
+
170
+ async def update(self, guild_id: str, *, self_mute: Optional[bool] = None, self_deaf: Optional[bool] = None,
171
+ self_video: Optional[bool] = None) -> None:
172
+ link = self._own_link(guild_id)
173
+ if not link or not link.get("connection_id"):
174
+ raise MicupAPIError(0, "VOICE_NOT_CONNECTED", "Bu sunucuda bağlı bir ses bağlantın yok")
175
+ new = {
176
+ "self_mute": link.get("self_mute", False) if self_mute is None else self_mute,
177
+ "self_deaf": link.get("self_deaf", False) if self_deaf is None else self_deaf,
178
+ "self_video": link.get("self_video", False) if self_video is None else self_video,
179
+ }
180
+ await self._bot._send({
181
+ "op": 4,
182
+ "d": {
183
+ "guild_id": guild_id,
184
+ "channel_id": link["channel_id"],
185
+ "connection_id": link["connection_id"],
186
+ **new,
187
+ "self_stream": link.get("self_stream", False),
188
+ },
189
+ })
190
+ link.update(new)
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: micup
3
+ Version: 0.1.0
4
+ Summary: Micup platformu için Python kütüphanesi.
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: aiohttp>=3.9
8
+ Requires-Dist: livekit
9
+
10
+ # micup
11
+
12
+ Micup platformu için Python kütüphanesi. Bot yazmak için mesaj, komut, olay, yetki ve ses yönetimini tek yerden sağlar.
13
+
14
+ ## Kurulum
15
+
16
+ ```
17
+ pip install micup
18
+ ```
19
+
20
+ ## Hızlı başlangıç
21
+
22
+ ```python
23
+ from micup import Bot
24
+
25
+ bot = Bot() # token MICUP_TOKEN ortam değişkeninden okunur
26
+
27
+ @bot.command("!ping")
28
+ async def ping(msg):
29
+ return "pong"
30
+
31
+ bot.run()
32
+ ```
33
+
34
+ Token'ı kodun içine yazma. PowerShell'de:
35
+
36
+ ```powershell
37
+ $env:MICUP_TOKEN = "uygulama_id.gizli_anahtar"
38
+ python bot.py
39
+ ```
40
+
41
+ ## Özellikler
42
+
43
+ - Gateway bağlantısı, kopunca otomatik yeniden bağlanma (resume destekli)
44
+ - Komutlar ve olaylar: `@bot.command(...)`, `@bot.on(...)`
45
+ - Mesaj işlemleri: gönder, düzenle, sil, toplu sil, listele
46
+ - Ham REST erişimi: `bot.rest(...)`
47
+ - Ses: kanala katılma, ayrılma, durum okuma (LiveKit medya bağlantısı dahil)
48
+ - Durum: `bot.set_presence(...)`
49
+ - Yetki bitleri ve snowflake yardımcıları
50
+
51
+ Ayrıntılı kullanım için dokümantasyona bak.
52
+
53
+ ## Lisans
54
+
55
+ Henüz belirlenmedi.
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ micup/__init__.py
4
+ micup/client.py
5
+ micup/constants.py
6
+ micup/errors.py
7
+ micup/models.py
8
+ micup/permissions.py
9
+ micup/utils.py
10
+ micup/voice.py
11
+ micup.egg-info/PKG-INFO
12
+ micup.egg-info/SOURCES.txt
13
+ micup.egg-info/dependency_links.txt
14
+ micup.egg-info/requires.txt
15
+ micup.egg-info/top_level.txt
@@ -1 +1,2 @@
1
1
  aiohttp>=3.9
2
+ livekit
@@ -0,0 +1,17 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "micup"
7
+ version = "0.1.0"
8
+ description = "Micup platformu için Python kütüphanesi."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ dependencies = [
12
+ "aiohttp>=3.9",
13
+ "livekit",
14
+ ]
15
+
16
+ [tool.setuptools]
17
+ packages = ["micup"]
micup-0.0.1/PKG-INFO DELETED
@@ -1,28 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: micup
3
- Version: 0.0.1
4
- Summary: Python library for Micup bots (in development)
5
- Author-email: Renbow <egekoc28@gmail.com>
6
- License: MIT
7
- Classifier: Development Status :: 1 - Planning
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: License :: OSI Approved :: MIT License
10
- Requires-Python: >=3.9
11
- Description-Content-Type: text/markdown
12
- Requires-Dist: aiohttp>=3.9
13
-
14
- # micup
15
-
16
- Micup platformu için Python bot kütüphanesi. Şu an geliştirme aşamasında, henüz kullanılabilir bir sürüm yayınlanmadı.
17
-
18
- Planlanan kullanım:
19
-
20
- from micup import Bot
21
-
22
- bot = Bot()
23
-
24
- @bot.command("!ping")
25
- async def ping(msg):
26
- return "pong"
27
-
28
- bot.run()
micup-0.0.1/README.md DELETED
@@ -1,15 +0,0 @@
1
- # micup
2
-
3
- Micup platformu için Python bot kütüphanesi. Şu an geliştirme aşamasında, henüz kullanılabilir bir sürüm yayınlanmadı.
4
-
5
- Planlanan kullanım:
6
-
7
- from micup import Bot
8
-
9
- bot = Bot()
10
-
11
- @bot.command("!ping")
12
- async def ping(msg):
13
- return "pong"
14
-
15
- bot.run()
micup-0.0.1/micup/init.py DELETED
@@ -1,3 +0,0 @@
1
- """Micup bot kütüphanesi. Geliştirme aşamasında."""
2
-
3
- __version__ = "0.0.1"
@@ -1,28 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: micup
3
- Version: 0.0.1
4
- Summary: Python library for Micup bots (in development)
5
- Author-email: Renbow <egekoc28@gmail.com>
6
- License: MIT
7
- Classifier: Development Status :: 1 - Planning
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: License :: OSI Approved :: MIT License
10
- Requires-Python: >=3.9
11
- Description-Content-Type: text/markdown
12
- Requires-Dist: aiohttp>=3.9
13
-
14
- # micup
15
-
16
- Micup platformu için Python bot kütüphanesi. Şu an geliştirme aşamasında, henüz kullanılabilir bir sürüm yayınlanmadı.
17
-
18
- Planlanan kullanım:
19
-
20
- from micup import Bot
21
-
22
- bot = Bot()
23
-
24
- @bot.command("!ping")
25
- async def ping(msg):
26
- return "pong"
27
-
28
- bot.run()
@@ -1,8 +0,0 @@
1
- README.md
2
- pyproject.toml
3
- micup/init.py
4
- micup.egg-info/PKG-INFO
5
- micup.egg-info/SOURCES.txt
6
- micup.egg-info/dependency_links.txt
7
- micup.egg-info/requires.txt
8
- micup.egg-info/top_level.txt
@@ -1,21 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools>=68", "wheel"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "micup"
7
- version = "0.0.1"
8
- description = "Python library for Micup bots (in development)"
9
- readme = "README.md"
10
- requires-python = ">=3.9"
11
- license = {text = "MIT"}
12
- authors = [{name = "Renbow", email = "egekoc28@gmail.com"}]
13
- dependencies = ["aiohttp>=3.9"]
14
- classifiers = [
15
- "Development Status :: 1 - Planning",
16
- "Programming Language :: Python :: 3",
17
- "License :: OSI Approved :: MIT License",
18
- ]
19
-
20
- [tool.setuptools]
21
- packages = ["micup"]
File without changes