jubbio.py 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
jubbio/__init__.py ADDED
@@ -0,0 +1,59 @@
1
+ __title__ = "jubbio.py"
2
+ __author__ = "Jubbio Community"
3
+ __license__ = "MIT"
4
+ __version__ = "1.0.0"
5
+
6
+ from .client import Client
7
+ from .models import (
8
+ User,
9
+ BotUser,
10
+ Member,
11
+ Guild,
12
+ Channel,
13
+ Message,
14
+ Role,
15
+ Embed,
16
+ EmbedField,
17
+ EmbedAuthor,
18
+ EmbedFooter,
19
+ EmbedImage,
20
+ EmbedThumbnail,
21
+ ActionRow,
22
+ Button,
23
+ SelectMenu,
24
+ SelectOption,
25
+ Invite,
26
+ Emoji,
27
+ Attachment,
28
+ Webhook,
29
+ Interaction,
30
+ InteractionResponse,
31
+ SlashCommand,
32
+ SlashCommandOption,
33
+ PermissionOverwrite,
34
+ Intents,
35
+ Color,
36
+ Mentions,
37
+ MessageReference,
38
+ )
39
+ from .enums import (
40
+ ChannelType,
41
+ ButtonStyle,
42
+ InteractionType,
43
+ InteractionCallbackType,
44
+ CommandOptionType,
45
+ OverwriteType,
46
+ Status,
47
+ Permissions,
48
+ )
49
+ from .errors import (
50
+ JubbioException,
51
+ HTTPException,
52
+ Forbidden,
53
+ NotFound,
54
+ RateLimited,
55
+ LoginFailure,
56
+ GatewayError,
57
+ InvalidToken,
58
+ InvalidArgument,
59
+ )
jubbio/client.py ADDED
@@ -0,0 +1,191 @@
1
+ from __future__ import annotations
2
+ import asyncio
3
+ import logging
4
+ from typing import Callable, Dict, List, Optional
5
+
6
+ from .http import HTTPClient
7
+ from .gateway import Gateway
8
+ from .models import (
9
+ User, BotUser, Member, Guild, Channel, Message, Role,
10
+ Embed, ActionRow, Mentions, MessageReference, Emoji,
11
+ Invite, Webhook, Interaction, SlashCommand, Intents,
12
+ )
13
+ from .errors import LoginFailure
14
+
15
+ log = logging.getLogger(__name__)
16
+
17
+
18
+ class Client:
19
+
20
+ def __init__(self, intents: Intents = None):
21
+ self.intents = intents or Intents.default()
22
+ self._http: Optional[HTTPClient] = None
23
+ self._gateway: Optional[Gateway] = None
24
+ self._token: Optional[str] = None
25
+ self._closed = False
26
+ self._ready = asyncio.Event()
27
+ self._event_handlers: Dict[str, Callable] = {}
28
+ self._command_handlers: Dict[str, Callable] = {}
29
+ self._component_handlers: Dict[str, Callable] = {}
30
+ self.user: Optional[BotUser] = None
31
+ self.application_id: Optional[str] = None
32
+ self.guilds: List[Guild] = []
33
+
34
+ def event(self, func: Callable) -> Callable:
35
+ if not asyncio.iscoroutinefunction(func):
36
+ raise TypeError("Olay dinleyicisi async olmalıdır")
37
+ self._event_handlers[func.__name__] = func
38
+ return func
39
+
40
+ def command(self, name: str = None):
41
+ def decorator(func: Callable) -> Callable:
42
+ if not asyncio.iscoroutinefunction(func):
43
+ raise TypeError("Komut dinleyicisi async olmalıdır")
44
+ self._command_handlers[name or func.__name__] = func
45
+ return func
46
+ return decorator
47
+
48
+ def component(self, custom_id: str):
49
+ def decorator(func: Callable) -> Callable:
50
+ if not asyncio.iscoroutinefunction(func):
51
+ raise TypeError("Bileşen dinleyicisi async olmalıdır")
52
+ self._component_handlers[custom_id] = func
53
+ return func
54
+ return decorator
55
+
56
+ def on(self, event_name: str):
57
+ def decorator(func: Callable) -> Callable:
58
+ if not asyncio.iscoroutinefunction(func):
59
+ raise TypeError("Olay dinleyicisi async olmalıdır")
60
+ full_name = f"on_{event_name}" if not event_name.startswith("on_") else event_name
61
+ self._event_handlers[full_name] = func
62
+ return func
63
+ return decorator
64
+
65
+ async def _handle_ready(self, data: dict):
66
+ user_data = data.get("user", data.get("bot", {}))
67
+ self.user = BotUser(user_data)
68
+ self.application_id = self.user.application_id
69
+ self.guilds = [Guild(g, http=self._http) for g in data.get("guilds", [])]
70
+ self._ready.set()
71
+
72
+ if "on_ready" in self._event_handlers:
73
+ await self._event_handlers["on_ready"]()
74
+
75
+ async def _dispatch(self, event_name: str, data: dict):
76
+ try:
77
+ if event_name == "on_message":
78
+ message = Message(data, http=self._http)
79
+ if message.author.id == (self.user.id if self.user else None):
80
+ return
81
+ if event_name in self._event_handlers:
82
+ await self._event_handlers[event_name](message)
83
+
84
+ elif event_name == "on_interaction":
85
+ interaction = Interaction(data, http=self._http)
86
+ if interaction.type == 2 and interaction.command_name in self._command_handlers:
87
+ await self._command_handlers[interaction.command_name](interaction)
88
+ elif interaction.type == 3 and interaction.custom_id in self._component_handlers:
89
+ await self._component_handlers[interaction.custom_id](interaction)
90
+ elif event_name in self._event_handlers:
91
+ await self._event_handlers[event_name](interaction)
92
+
93
+ elif event_name in ("on_guild_join", "on_guild_remove"):
94
+ guild = Guild(data, http=self._http)
95
+ if event_name in self._event_handlers:
96
+ await self._event_handlers[event_name](guild)
97
+
98
+ elif event_name in ("on_member_ban", "on_member_unban", "on_member_join", "on_member_remove"):
99
+ member = Member(data, guild_id=data.get("guild_id", ""), http=self._http)
100
+ if event_name in self._event_handlers:
101
+ await self._event_handlers[event_name](member)
102
+
103
+ elif event_name in ("on_invite_create", "on_invite_delete"):
104
+ if event_name in self._event_handlers:
105
+ await self._event_handlers[event_name](Invite(data))
106
+
107
+ else:
108
+ if event_name in self._event_handlers:
109
+ await self._event_handlers[event_name](data)
110
+
111
+ except Exception as e:
112
+ log.error("Olay hatası (%s): %s", event_name, e, exc_info=True)
113
+ if "on_error" in self._event_handlers:
114
+ await self._event_handlers["on_error"](event_name, e)
115
+
116
+ async def start(self, token: str):
117
+ self._token = token
118
+ self._http = HTTPClient(token, client=self)
119
+ self._gateway = Gateway(self, token)
120
+
121
+ try:
122
+ self.user = await self._http.get_me()
123
+ self.application_id = self.user.application_id
124
+ except Exception as e:
125
+ raise LoginFailure(f"Giriş başarısız: {e}") from e
126
+
127
+ await self._gateway.connect()
128
+
129
+ try:
130
+ while not self._closed:
131
+ await asyncio.sleep(1)
132
+ except asyncio.CancelledError:
133
+ pass
134
+
135
+ def run(self, token: str):
136
+ async def runner():
137
+ try:
138
+ await self.start(token)
139
+ except KeyboardInterrupt:
140
+ pass
141
+ finally:
142
+ await self.close()
143
+
144
+ try:
145
+ asyncio.run(runner())
146
+ except KeyboardInterrupt:
147
+ pass
148
+
149
+ async def close(self):
150
+ self._closed = True
151
+ if self._gateway:
152
+ await self._gateway.close()
153
+ if self._http:
154
+ await self._http.close()
155
+
156
+ async def wait_until_ready(self):
157
+ await self._ready.wait()
158
+
159
+ async def get_user(self, user_id: str) -> User:
160
+ return await self._http.get_user(user_id)
161
+
162
+ async def get_guild(self, guild_id: str) -> Guild:
163
+ return await self._http.get_guild(guild_id)
164
+
165
+ async def send_dm(self, user_id: str, content: str = None, **kwargs) -> Message:
166
+ return await self._http.send_dm(user_id, content, **kwargs)
167
+
168
+ async def get_invite(self, code: str) -> Invite:
169
+ return await self._http.get_invite(code)
170
+
171
+ async def register_command(self, command: SlashCommand, guild_id: str = None):
172
+ if not self.application_id:
173
+ raise RuntimeError("Bot henüz hazır değil")
174
+ if guild_id:
175
+ return await self._http.register_guild_command(self.application_id, guild_id, command.to_dict())
176
+ return await self._http.register_global_command(self.application_id, command.to_dict())
177
+
178
+ async def delete_command(self, command_id: str, guild_id: str = None):
179
+ if not self.application_id:
180
+ raise RuntimeError("Bot henüz hazır değil")
181
+ if guild_id:
182
+ await self._http.delete_guild_command(self.application_id, guild_id, command_id)
183
+ else:
184
+ await self._http.delete_global_command(self.application_id, command_id)
185
+
186
+ async def get_commands(self, guild_id: str = None) -> list:
187
+ if not self.application_id:
188
+ raise RuntimeError("Bot henüz hazır değil")
189
+ if guild_id:
190
+ return await self._http.get_guild_commands(self.application_id, guild_id)
191
+ return await self._http.get_global_commands(self.application_id)
jubbio/enums.py ADDED
@@ -0,0 +1,84 @@
1
+ from enum import IntEnum, IntFlag
2
+
3
+
4
+ class ChannelType(IntEnum):
5
+ TEXT = 0
6
+ DM = 1
7
+ VOICE = 2
8
+ CATEGORY = 4
9
+
10
+
11
+ class ButtonStyle(IntEnum):
12
+ PRIMARY = 1
13
+ SECONDARY = 2
14
+ SUCCESS = 3
15
+ DANGER = 4
16
+ LINK = 5
17
+
18
+
19
+ class InteractionType(IntEnum):
20
+ PING = 1
21
+ APPLICATION_COMMAND = 2
22
+ MESSAGE_COMPONENT = 3
23
+
24
+
25
+ class InteractionCallbackType(IntEnum):
26
+ PONG = 1
27
+ CHANNEL_MESSAGE_WITH_SOURCE = 4
28
+ DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE = 5
29
+ DEFERRED_UPDATE_MESSAGE = 6
30
+ UPDATE_MESSAGE = 7
31
+
32
+
33
+ class CommandOptionType(IntEnum):
34
+ SUB_COMMAND = 1
35
+ SUB_COMMAND_GROUP = 2
36
+ STRING = 3
37
+ INTEGER = 4
38
+ BOOLEAN = 5
39
+ USER = 6
40
+ CHANNEL = 7
41
+ ROLE = 8
42
+ NUMBER = 10
43
+
44
+
45
+ class OverwriteType(IntEnum):
46
+ ROLE = 0
47
+ MEMBER = 1
48
+
49
+
50
+ class Status(IntEnum):
51
+ ONLINE = 1
52
+ IDLE = 2
53
+ DND = 3
54
+ OFFLINE = 4
55
+
56
+
57
+ class Permissions(IntFlag):
58
+ ADMINISTRATOR = 1 << 0
59
+ MANAGE_GUILD = 1 << 1
60
+ MANAGE_CHANNELS = 1 << 2
61
+ MANAGE_ROLES = 1 << 3
62
+ MANAGE_MESSAGES = 1 << 4
63
+ KICK_MEMBERS = 1 << 5
64
+ BAN_MEMBERS = 1 << 6
65
+ SEND_MESSAGES = 1 << 7
66
+ READ_MESSAGES = 1 << 8
67
+ EMBED_LINKS = 1 << 9
68
+ ATTACH_FILES = 1 << 10
69
+ MENTION_EVERYONE = 1 << 11
70
+ USE_EXTERNAL_EMOJIS = 1 << 12
71
+ ADD_REACTIONS = 1 << 13
72
+ CONNECT = 1 << 14
73
+ SPEAK = 1 << 15
74
+ MUTE_MEMBERS = 1 << 16
75
+ DEAFEN_MEMBERS = 1 << 17
76
+ MOVE_MEMBERS = 1 << 18
77
+ MANAGE_NICKNAMES = 1 << 19
78
+ MANAGE_WEBHOOKS = 1 << 20
79
+ CREATE_INVITES = 1 << 21
80
+ VIEW_AUDIT_LOG = 1 << 22
81
+ MANAGE_EMOJIS = 1 << 23
82
+ USE_SLASH_COMMANDS = 1 << 24
83
+ NONE = 0
84
+ ALL = (1 << 25) - 1
jubbio/errors.py ADDED
@@ -0,0 +1,57 @@
1
+ class JubbioException(Exception):
2
+ pass
3
+
4
+
5
+ class HTTPException(JubbioException):
6
+ def __init__(self, response, message=None):
7
+ self.response = response
8
+ self.status = response.status
9
+ self.code = 0
10
+
11
+ if isinstance(message, dict):
12
+ self.code = message.get("code", 0)
13
+ self.text = message.get("message", "")
14
+ else:
15
+ self.text = message or ""
16
+
17
+ fmt = f"{self.status} {self.response.reason}"
18
+ if self.text:
19
+ fmt += f": {self.text}"
20
+ if self.code:
21
+ fmt += f" (error code: {self.code})"
22
+
23
+ super().__init__(fmt)
24
+
25
+
26
+ class Forbidden(HTTPException):
27
+ pass
28
+
29
+
30
+ class NotFound(HTTPException):
31
+ pass
32
+
33
+
34
+ class RateLimited(HTTPException):
35
+ def __init__(self, response, message=None):
36
+ super().__init__(response, message)
37
+ self.retry_after = 0.0
38
+ if isinstance(message, dict):
39
+ self.retry_after = message.get("retry_after", 0.0)
40
+
41
+
42
+ class LoginFailure(JubbioException):
43
+ pass
44
+
45
+
46
+ class GatewayError(JubbioException):
47
+ def __init__(self, message=None, code=None):
48
+ self.code = code
49
+ super().__init__(message or f"Gateway hatası (kod: {code})")
50
+
51
+
52
+ class InvalidToken(LoginFailure):
53
+ pass
54
+
55
+
56
+ class InvalidArgument(JubbioException):
57
+ pass
jubbio/gateway.py ADDED
@@ -0,0 +1,190 @@
1
+ from __future__ import annotations
2
+ import asyncio
3
+ import json
4
+ import logging
5
+ from typing import TYPE_CHECKING, Optional
6
+
7
+ import aiohttp
8
+
9
+ from .errors import GatewayError, InvalidToken
10
+
11
+ if TYPE_CHECKING:
12
+ from .client import Client
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+ GATEWAY_URL = "wss://realtime.jubbio.com/ws/bot"
17
+ RECONNECTABLE_CLOSE_CODES = {4000, 4001, 4002, 4003}
18
+ FATAL_CLOSE_CODES = {4004}
19
+
20
+
21
+ class Gateway:
22
+
23
+ def __init__(self, client: "Client", token: str):
24
+ self._client = client
25
+ self._token = token
26
+ self._ws: Optional[aiohttp.ClientWebSocketResponse] = None
27
+ self._session: Optional[aiohttp.ClientSession] = None
28
+ self._heartbeat_task: Optional[asyncio.Task] = None
29
+ self._receive_task: Optional[asyncio.Task] = None
30
+ self._reconnect_count = 0
31
+ self._max_reconnects = 5
32
+ self._closed = False
33
+ self._ready = asyncio.Event()
34
+
35
+ @property
36
+ def is_connected(self) -> bool:
37
+ return self._ws is not None and not self._ws.closed
38
+
39
+ async def connect(self):
40
+ self._closed = False
41
+ self._reconnect_count = 0
42
+ await self._connect()
43
+
44
+ async def _connect(self):
45
+ try:
46
+ if self._session is None or self._session.closed:
47
+ self._session = aiohttp.ClientSession()
48
+
49
+ self._ws = await self._session.ws_connect(
50
+ GATEWAY_URL,
51
+ headers={"Authorization": f"Bot {self._token}"},
52
+ heartbeat=30.0,
53
+ )
54
+
55
+ await self._identify()
56
+ self._receive_task = asyncio.create_task(self._receive_loop())
57
+
58
+ except Exception as e:
59
+ log.error("Gateway bağlantı hatası: %s", e)
60
+ await self._try_reconnect()
61
+
62
+ async def _identify(self):
63
+ identify_data = {"token": self._token}
64
+ if hasattr(self._client, "intents") and self._client.intents:
65
+ identify_data["intents"] = self._client.intents.value
66
+
67
+ await self._send({"op": 2, "d": identify_data})
68
+
69
+ async def _send(self, data: dict):
70
+ if self._ws and not self._ws.closed:
71
+ await self._ws.send_json(data)
72
+
73
+ async def _receive_loop(self):
74
+ try:
75
+ async for msg in self._ws:
76
+ if msg.type == aiohttp.WSMsgType.TEXT:
77
+ await self._handle_message(json.loads(msg.data))
78
+ elif msg.type == aiohttp.WSMsgType.BINARY:
79
+ await self._handle_message(json.loads(msg.data))
80
+ elif msg.type == aiohttp.WSMsgType.ERROR:
81
+ log.error("WebSocket hatası: %s", self._ws.exception())
82
+ break
83
+ elif msg.type in (aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSING, aiohttp.WSMsgType.CLOSED):
84
+ break
85
+ except asyncio.CancelledError:
86
+ return
87
+ except Exception as e:
88
+ log.error("Receive loop hatası: %s", e)
89
+
90
+ if not self._closed:
91
+ close_code = self._ws.close_code if self._ws else None
92
+ await self._handle_close(close_code)
93
+
94
+ async def _handle_message(self, data: dict):
95
+ op = data.get("op")
96
+ event_type = data.get("t")
97
+ event_data = data.get("d", {})
98
+
99
+ if op == 11:
100
+ return
101
+
102
+ if op == 10:
103
+ interval = event_data.get("heartbeat_interval", 30000) / 1000
104
+ self._start_heartbeat(interval)
105
+ return
106
+
107
+ if op == 0 and event_type:
108
+ await self._dispatch_event(event_type, event_data)
109
+
110
+ async def _dispatch_event(self, event_type: str, data: dict):
111
+ event_map = {
112
+ "READY": "on_ready",
113
+ "MESSAGE_CREATE": "on_message",
114
+ "INTERACTION_CREATE": "on_interaction",
115
+ "GUILD_CREATE": "on_guild_join",
116
+ "GUILD_DELETE": "on_guild_remove",
117
+ "GUILD_BAN_ADD": "on_member_ban",
118
+ "GUILD_BAN_REMOVE": "on_member_unban",
119
+ "INVITE_CREATE": "on_invite_create",
120
+ "INVITE_DELETE": "on_invite_delete",
121
+ "PRESENCE_UPDATE": "on_presence_update",
122
+ "GUILD_MEMBER_ADD": "on_member_join",
123
+ "GUILD_MEMBER_REMOVE": "on_member_remove",
124
+ }
125
+
126
+ if event_type == "READY":
127
+ self._ready.set()
128
+ self._reconnect_count = 0
129
+ if hasattr(self._client, "_handle_ready"):
130
+ await self._client._handle_ready(data)
131
+ return
132
+
133
+ handler_name = event_map.get(event_type)
134
+ if handler_name and hasattr(self._client, "_dispatch"):
135
+ await self._client._dispatch(handler_name, data)
136
+
137
+ def _start_heartbeat(self, interval: float):
138
+ if self._heartbeat_task and not self._heartbeat_task.done():
139
+ self._heartbeat_task.cancel()
140
+ self._heartbeat_task = asyncio.create_task(self._heartbeat_loop(interval))
141
+
142
+ async def _heartbeat_loop(self, interval: float):
143
+ try:
144
+ while not self._closed and self.is_connected:
145
+ await self._send({"op": 1, "d": None})
146
+ await asyncio.sleep(interval)
147
+ except asyncio.CancelledError:
148
+ pass
149
+ except Exception as e:
150
+ log.error("Heartbeat hatası: %s", e)
151
+
152
+ async def _handle_close(self, code: int = None):
153
+ if code in FATAL_CLOSE_CODES:
154
+ self._closed = True
155
+ raise InvalidToken("Geçersiz bot token'ı!")
156
+
157
+ if code in RECONNECTABLE_CLOSE_CODES or code is None:
158
+ await self._try_reconnect()
159
+
160
+ async def _try_reconnect(self):
161
+ if self._closed:
162
+ return
163
+
164
+ if self._reconnect_count >= self._max_reconnects:
165
+ self._closed = True
166
+ raise GatewayError(f"Gateway'e {self._max_reconnects} denemeden sonra bağlanılamadı", code=None)
167
+
168
+ self._reconnect_count += 1
169
+ wait_time = min(2 ** self._reconnect_count, 60)
170
+ log.warning("Yeniden bağlanma %d/%d - %ds bekleniyor...", self._reconnect_count, self._max_reconnects, wait_time)
171
+ await asyncio.sleep(wait_time)
172
+ await self._cleanup()
173
+ await self._connect()
174
+
175
+ async def _cleanup(self):
176
+ if self._heartbeat_task and not self._heartbeat_task.done():
177
+ self._heartbeat_task.cancel()
178
+ if self._receive_task and not self._receive_task.done():
179
+ self._receive_task.cancel()
180
+ if self._ws and not self._ws.closed:
181
+ await self._ws.close()
182
+
183
+ async def close(self):
184
+ self._closed = True
185
+ await self._cleanup()
186
+ if self._session and not self._session.closed:
187
+ await self._session.close()
188
+
189
+ async def wait_until_ready(self):
190
+ await self._ready.wait()