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 +59 -0
- jubbio/client.py +191 -0
- jubbio/enums.py +84 -0
- jubbio/errors.py +57 -0
- jubbio/gateway.py +190 -0
- jubbio/http.py +513 -0
- jubbio/models.py +611 -0
- jubbio_py-1.0.0.dist-info/METADATA +251 -0
- jubbio_py-1.0.0.dist-info/RECORD +12 -0
- jubbio_py-1.0.0.dist-info/WHEEL +5 -0
- jubbio_py-1.0.0.dist-info/licenses/LICENSE +21 -0
- jubbio_py-1.0.0.dist-info/top_level.txt +1 -0
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()
|