Skip to content

Commit d266350

Browse files
i0bsFayeDelpre-commit-ci[bot]
authored
refactor: better Gateway for reconnection control. (#451)
* refactor: beginning Gateway connection rewrite. * feat: add `UnavailableGuild` model object. * refactor: Abstract away from gateway headers, utilise internal events/data to daemonise, correct some attributes. * feat!: Implement somewhat working reconnection, reinclude dispatching, fix heartbeat packet sending This still doesn't work properly, it just hangs later than stable. * refactor: Switch heartbeater bool back to Event obj. * refactor: correclty parse reconnection logic. * docs: clarify new gateway structure. * fix: Fix import typos and reordered import references * fix: Fix http loop invocation for py 3.10, add _ready to gateway slot. * fix: Fix event loop object creation on py 3.10 * fix: Fix await runtime error and close code checking. * refactor: reset heartbeater on death, read from closed/closing states. * ci: correct from checks. * refactor: `wait` on the event instead of `sleep` which targets the whole loop * fix: Switch asyncio loop delay on sending multiple heartbeat sending. Co-authored-by: DeltaXWizard <33706469+deltaxwizard@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent e0161c6 commit d266350

File tree

8 files changed

+480
-379
lines changed

8 files changed

+480
-379
lines changed

interactions/api/gateway.py

Lines changed: 371 additions & 306 deletions
Large diffs are not rendered by default.

interactions/api/gateway.pyi

Lines changed: 57 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,71 @@
1-
from asyncio import AbstractEventLoop
2-
from threading import Event, Thread
3-
from typing import Any, List, Optional, Union
1+
from asyncio import (
2+
AbstractEventLoop,
3+
Event,
4+
Task,
5+
)
6+
from logging import Logger
7+
from typing import Any, Dict, List, Optional, Tuple
48

9+
from aiohttp import ClientWebSocketResponse
10+
11+
from ..api.models.gw import Presence
12+
from ..base import get_logger
13+
from ..models.misc import MISSING
514
from .dispatch import Listener
615
from .http import HTTPClient
7-
from .models.gw import Presence
816
from .models.flags import Intents
917

10-
class Heartbeat(Thread):
11-
ws: Any
12-
interval: Union[int, float]
18+
log: Logger = get_logger("gateway")
19+
20+
__all__ = ("_Heartbeat", "WebSocketClient")
21+
22+
class _Heartbeat:
1323
event: Event
14-
def __init__(self, ws: Any, interval: int) -> None: ...
15-
def run(self) -> None: ...
16-
def stop(self) -> None: ...
24+
delay: float
25+
def __init__(self, loop: AbstractEventLoop) -> None: ...
1726

18-
class WebSocket:
19-
intents: Intents
20-
loop: AbstractEventLoop
21-
dispatch: Listener
22-
session: Any
23-
session_id: Optional[int]
24-
sequence: Optional[int]
25-
keep_alive: Optional[Heartbeat]
26-
closed: bool
27-
http: Optional[HTTPClient]
28-
options: dict
27+
class WebSocketClient:
28+
_loop: AbstractEventLoop
29+
_dispatch: Listener
30+
_http: HTTPClient
31+
_client: Optional[ClientWebSocketResponse]
32+
_closed: bool
33+
_options: dict
34+
_intents: Intents
35+
_ready: dict
36+
__heartbeater: _Heartbeat
37+
__shard: Optional[List[Tuple[int]]]
38+
__presence: Optional[Presence]
39+
__task: Optional[Task]
40+
session_id: int
41+
sequence: str
2942
def __init__(
3043
self,
44+
token: str,
3145
intents: Intents,
32-
session_id: Optional[int] = None,
33-
sequence: Optional[int] = None,
46+
session_id: Optional[int] = MISSING,
47+
sequence: Optional[int] = MISSING,
3448
) -> None: ...
35-
async def recv(self) -> Optional[Any]: ...
36-
async def connect(
37-
self, token: str, shard: Optional[List[int]] = None, presence: Optional[Presence] = None
49+
async def _manage_heartbeat(self) -> None: ...
50+
async def __restart(self): ...
51+
async def _establish_connection(
52+
self, shard: Optional[List[Tuple[int]]] = MISSING, presence: Optional[Presence] = MISSING
3853
) -> None: ...
39-
async def handle_connection(
40-
self, stream: dict, shard: Optional[List[int]] = None, presence: Optional[Presence] = None
54+
async def _handle_connection(
55+
self,
56+
stream: Dict[str, Any],
57+
shard: Optional[List[Tuple[int]]] = MISSING,
58+
presence: Optional[Presence] = MISSING,
4159
) -> None: ...
42-
def handle_dispatch(self, event: str, data: dict) -> None: ...
43-
def contextualize(self, data: dict) -> object: ...
44-
async def send(self, data: Union[str, dict]) -> None: ...
45-
async def identify(
46-
self, shard: Optional[List[int]] = None, presence: Optional[Presence] = None
60+
@property
61+
async def __receive_packet_stream(self) -> Optional[Dict[str, Any]]: ...
62+
async def _send_packet(self, data: Dict[str, Any]) -> None: ...
63+
async def __identify(
64+
self, shard: Optional[List[Tuple[int]]] = None, presence: Optional[Presence] = None
4765
) -> None: ...
48-
async def resume(self) -> None: ...
49-
async def heartbeat(self) -> None: ...
50-
def check_sub_auto(self, option) -> tuple: ...
51-
def check_sub_command(self, option) -> dict: ...
66+
async def __resume(self) -> None: ...
67+
async def __heartbeat(self) -> None: ...
68+
@property
69+
def shard(self) -> None: ...
70+
@property
71+
def presence(self) -> None: ...

interactions/api/models/guild.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,26 @@ def __init__(self, **kwargs):
129129
self.channel_id = Snowflake(self.channel_id) if self._json.get("channel_id") else None
130130

131131

132+
class UnavailableGuild(DictSerializerMixin):
133+
"""
134+
A class object representing how a guild that is unavailable.
135+
136+
.. note::
137+
This object only seems to show up during the connection process
138+
of the client to the Gateway when the ``READY`` event is dispatched.
139+
This event will pass fields with ``guilds`` where this becomes
140+
present.
141+
142+
:ivar Snowflake id: The ID of the unavailable guild.
143+
:ivar bool unavailable: Whether the guild is unavailable or not.
144+
"""
145+
146+
__slots__ = ("_json", "id", "unavailable")
147+
148+
def __init__(self, **kwargs):
149+
super().__init__(**kwargs)
150+
151+
132152
class Guild(DictSerializerMixin):
133153
"""
134154
A class object representing how a guild is registered.

interactions/api/models/guild.pyi

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ class StageInstance(DictSerializerMixin):
4545
discoverable_disabled: bool
4646
def __init__(self, **kwargs): ...
4747

48+
class UnavailableGuild(DictSerializerMixin):
49+
_json: dict
50+
id: Snowflake
51+
unavailable: bool
52+
def __init__(self, **kwargs): ...
53+
4854
class Guild(DictSerializerMixin):
4955
_json: dict
5056
_client: HTTPClient

interactions/api/models/gw.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
from datetime import datetime
22

3-
from interactions.api.models.channel import Channel, ThreadMember
4-
from interactions.api.models.presence import PresenceActivity
5-
from interactions.models.command import Permission
6-
3+
from .channel import Channel, ThreadMember
74
from .member import Member
85
from .message import Emoji, Sticker
96
from .misc import ClientStatus, DictSerializerMixin, Snowflake
7+
from .presence import PresenceActivity
108
from .role import Role
119
from .user import User
1210

@@ -32,14 +30,15 @@ def __init__(self, **kwargs):
3230
)
3331
self.guild_id = Snowflake(self.guild_id) if self._json.get("guild_id") else None
3432
self.id = Snowflake(self.id) if self._json.get("id") else None
35-
self.permissions = (
36-
[
37-
Permission(**_permission) if isinstance(_permission, dict) else _permission
38-
for _permission in self._json.get("permissions")
39-
]
40-
if self._json.get("permissions")
41-
else None
42-
)
33+
# TODO: fix the circular import hell from this.
34+
# self.permissions = (
35+
# [
36+
# Permission(**_permission) if isinstance(_permission, dict) else _permission
37+
# for _permission in self._json.get("permissions")
38+
# ]
39+
# if self._json.get("permissions")
40+
# else None
41+
# )
4342

4443

4544
class ChannelPins(DictSerializerMixin):

interactions/client.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from .api.cache import Cache
1313
from .api.cache import Item as Build
1414
from .api.error import InteractionException, JSONException
15-
from .api.gateway import WebSocket
15+
from .api.gateway import WebSocketClient
1616
from .api.http import HTTPClient
1717
from .api.models.flags import Intents
1818
from .api.models.guild import Guild
@@ -36,7 +36,7 @@ class Client:
3636
3737
:ivar AbstractEventLoop _loop: The asynchronous event loop of the client.
3838
:ivar HTTPClient _http: The user-facing HTTP connection to the Web API, as its own separate client.
39-
:ivar WebSocket _websocket: An object-orientation of a websocket server connection to the Gateway.
39+
:ivar WebSocketClient _websocket: An object-orientation of a websocket server connection to the Gateway.
4040
:ivar Intents _intents: The Gateway intents of the application. Defaults to ``Intents.DEFAULT``.
4141
:ivar Optional[List[Tuple[int]]] _shard: The list of bucketed shards for the application's connection.
4242
:ivar Optional[Presence] _presence: The RPC-like presence shown on an application once connected.
@@ -77,7 +77,7 @@ def __init__(
7777
self._loop = get_event_loop()
7878
self._http = HTTPClient(token=token)
7979
self._intents = kwargs.get("intents", Intents.DEFAULT)
80-
self._websocket = WebSocket(intents=self._intents)
80+
self._websocket = WebSocketClient(token=token, intents=self._intents)
8181
self._shard = kwargs.get("shards", [])
8282
self._presence = kwargs.get("presence")
8383
self._token = token
@@ -104,10 +104,10 @@ def start(self) -> None:
104104

105105
def __register_events(self) -> None:
106106
"""Registers all raw gateway events to the known events."""
107-
self._websocket.dispatch.register(self.__raw_socket_create)
108-
self._websocket.dispatch.register(self.__raw_channel_create, "on_channel_create")
109-
self._websocket.dispatch.register(self.__raw_message_create, "on_message_create")
110-
self._websocket.dispatch.register(self.__raw_guild_create, "on_guild_create")
107+
self._websocket._dispatch.register(self.__raw_socket_create)
108+
self._websocket._dispatch.register(self.__raw_channel_create, "on_channel_create")
109+
self._websocket._dispatch.register(self.__raw_message_create, "on_message_create")
110+
self._websocket._dispatch.register(self.__raw_guild_create, "on_guild_create")
111111

112112
async def __compare_sync(self, data: dict, pool: List[dict]) -> bool:
113113
"""
@@ -302,8 +302,8 @@ async def _ready(self) -> None:
302302

303303
async def _login(self) -> None:
304304
"""Makes a login with the Discord API."""
305-
while not self._websocket.closed:
306-
await self._websocket.connect(self._token, self._shard, self._presence)
305+
while not self._websocket._closed:
306+
await self._websocket._establish_connection(self._shard, self._presence)
307307

308308
def event(self, coro: Coroutine, name: Optional[str] = MISSING) -> Callable[..., Any]:
309309
"""
@@ -317,7 +317,7 @@ def event(self, coro: Coroutine, name: Optional[str] = MISSING) -> Callable[...,
317317
:return: A callable response.
318318
:rtype: Callable[..., Any]
319319
"""
320-
self._websocket.dispatch.register(coro, name if name is not MISSING else coro.__name__)
320+
self._websocket._dispatch.register(coro, name if name is not MISSING else coro.__name__)
321321
return coro
322322

323323
def __check_command(

interactions/client.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ from types import ModuleType
33
from typing import Any, Callable, Coroutine, Dict, List, NoReturn, Optional, Tuple, Union
44

55
from .api.cache import Cache
6-
from .api.gateway import WebSocket
6+
from .api.gateway import WebSocketClient
77
from .api.http import HTTPClient
88
from .api.models.flags import Intents
99
from .api.models.guild import Guild
@@ -21,7 +21,7 @@ _cache: Optional[Cache] = None
2121
class Client:
2222
_loop: AbstractEventLoop
2323
_http: HTTPClient
24-
_websocket: WebSocket
24+
_websocket: WebSocketClient
2525
_intents: Intents
2626
_shard: Optional[List[Tuple[int]]]
2727
_presence: Optional[Presence]

simple_bot.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,9 @@ async def on_ready():
1313
print("bot is now online.")
1414

1515

16-
@bot.event
17-
async def on_message_create(message: interactions.Message):
18-
await bot._http.send_message(channel_id=852402668294766615, content=message.content)
19-
20-
21-
@bot.command(
22-
type=interactions.ApplicationCommandType.MESSAGE,
23-
name="simple testing command",
24-
scope=852402668294766612,
25-
)
26-
async def simple_testing_command(ctx):
27-
await ctx.send("Hello world!")
16+
@bot.command(name="intent", description="h")
17+
async def intent(ctx):
18+
await ctx.send("hola")
2819

2920

3021
bot.start()

0 commit comments

Comments
 (0)