loopbot-discord-sdk 1.0.0__py3-none-any.whl → 1.0.5__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.
- loopbot/bot.py +1 -1
- loopbot/client.py +102 -22
- {loopbot_discord_sdk-1.0.0.dist-info → loopbot_discord_sdk-1.0.5.dist-info}/METADATA +1 -2
- {loopbot_discord_sdk-1.0.0.dist-info → loopbot_discord_sdk-1.0.5.dist-info}/RECORD +6 -6
- {loopbot_discord_sdk-1.0.0.dist-info → loopbot_discord_sdk-1.0.5.dist-info}/WHEEL +0 -0
- {loopbot_discord_sdk-1.0.0.dist-info → loopbot_discord_sdk-1.0.5.dist-info}/top_level.txt +0 -0
loopbot/bot.py
CHANGED
loopbot/client.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
WebSocket Client for Loop Discord SDK (Lightweight version)
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
@@ -7,18 +7,23 @@ import json
|
|
|
7
7
|
from typing import Any, Callable, Dict, List, Optional
|
|
8
8
|
|
|
9
9
|
import aiohttp
|
|
10
|
-
from aiohttp_sse_client import client as sse_client
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
class Client:
|
|
14
|
-
"""HTTP client for communicating with the Loop API"""
|
|
13
|
+
"""HTTP/WebSocket client for communicating with the Loop API"""
|
|
15
14
|
|
|
16
|
-
def __init__(self, token: str, base_url: str = "https://
|
|
15
|
+
def __init__(self, token: str, base_url: str = "https://api.loopbot.app"):
|
|
17
16
|
self.token = token
|
|
18
17
|
self.base_url = base_url.rstrip("/")
|
|
19
18
|
self._session: Optional[aiohttp.ClientSession] = None
|
|
20
|
-
self.
|
|
19
|
+
self._ws: Optional[aiohttp.ClientWebSocketResponse] = None
|
|
20
|
+
self._ws_task: Optional[asyncio.Task] = None
|
|
21
21
|
self._connected = False
|
|
22
|
+
self._reconnect_attempts = 0
|
|
23
|
+
self._max_reconnect_attempts = 10
|
|
24
|
+
self._reconnect_delay = 1.0
|
|
25
|
+
self._on_interaction: Optional[Callable[[Dict[str, Any]], None]] = None
|
|
26
|
+
self._heartbeat_task: Optional[asyncio.Task] = None
|
|
22
27
|
|
|
23
28
|
@property
|
|
24
29
|
def headers(self) -> Dict[str, str]:
|
|
@@ -27,6 +32,11 @@ class Client:
|
|
|
27
32
|
"Content-Type": "application/json",
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
@property
|
|
36
|
+
def ws_url(self) -> str:
|
|
37
|
+
"""Convert HTTP URL to WebSocket URL"""
|
|
38
|
+
return self.base_url.replace("https://", "wss://").replace("http://", "ws://") + "/api/sdk/ws"
|
|
39
|
+
|
|
30
40
|
async def _get_session(self) -> aiohttp.ClientSession:
|
|
31
41
|
if self._session is None or self._session.closed:
|
|
32
42
|
self._session = aiohttp.ClientSession()
|
|
@@ -63,25 +73,94 @@ class Client:
|
|
|
63
73
|
self._connected = True
|
|
64
74
|
return result
|
|
65
75
|
|
|
66
|
-
async def
|
|
67
|
-
"""Connect to
|
|
68
|
-
|
|
76
|
+
async def connect_ws(self, on_interaction: Callable[[Dict[str, Any]], None]) -> None:
|
|
77
|
+
"""Connect to WebSocket for receiving interactions"""
|
|
78
|
+
self._on_interaction = on_interaction
|
|
79
|
+
await self._create_websocket()
|
|
69
80
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
async def _create_websocket(self) -> None:
|
|
82
|
+
"""Create and manage WebSocket connection"""
|
|
83
|
+
session = await self._get_session()
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
self._ws = await session.ws_connect(
|
|
87
|
+
self.ws_url,
|
|
88
|
+
headers=self.headers,
|
|
89
|
+
heartbeat=30.0,
|
|
90
|
+
)
|
|
91
|
+
print("[Loop SDK] WebSocket connected")
|
|
92
|
+
self._reconnect_attempts = 0
|
|
93
|
+
|
|
94
|
+
# Start heartbeat
|
|
95
|
+
self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
|
|
96
|
+
|
|
97
|
+
# Message loop
|
|
98
|
+
async for msg in self._ws:
|
|
99
|
+
if msg.type == aiohttp.WSMsgType.TEXT:
|
|
78
100
|
try:
|
|
79
|
-
|
|
80
|
-
|
|
101
|
+
data = json.loads(msg.data)
|
|
102
|
+
if data.get("type") == "interaction" and self._on_interaction:
|
|
103
|
+
self._on_interaction(data.get("data", {}))
|
|
104
|
+
elif data.get("type") == "connected":
|
|
105
|
+
print("[Loop SDK] Authenticated with API")
|
|
106
|
+
elif data.get("type") == "pong":
|
|
107
|
+
pass # Heartbeat response
|
|
81
108
|
except json.JSONDecodeError as e:
|
|
82
|
-
print(f"[Loop SDK] Failed to parse
|
|
83
|
-
elif
|
|
84
|
-
|
|
109
|
+
print(f"[Loop SDK] Failed to parse message: {e}")
|
|
110
|
+
elif msg.type == aiohttp.WSMsgType.ERROR:
|
|
111
|
+
print(f"[Loop SDK] WebSocket error: {self._ws.exception()}")
|
|
112
|
+
break
|
|
113
|
+
elif msg.type == aiohttp.WSMsgType.CLOSED:
|
|
114
|
+
break
|
|
115
|
+
|
|
116
|
+
except Exception as e:
|
|
117
|
+
print(f"[Loop SDK] WebSocket connection error: {e}")
|
|
118
|
+
finally:
|
|
119
|
+
if self._heartbeat_task:
|
|
120
|
+
self._heartbeat_task.cancel()
|
|
121
|
+
self._heartbeat_task = None
|
|
122
|
+
await self._attempt_reconnect()
|
|
123
|
+
|
|
124
|
+
async def _heartbeat_loop(self) -> None:
|
|
125
|
+
"""Send periodic heartbeats"""
|
|
126
|
+
try:
|
|
127
|
+
while True:
|
|
128
|
+
await asyncio.sleep(30)
|
|
129
|
+
if self._ws and not self._ws.closed:
|
|
130
|
+
await self._ws.send_json({"type": "ping"})
|
|
131
|
+
except asyncio.CancelledError:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
async def _attempt_reconnect(self) -> None:
|
|
135
|
+
"""Attempt to reconnect to WebSocket"""
|
|
136
|
+
if self._reconnect_attempts >= self._max_reconnect_attempts:
|
|
137
|
+
print("[Loop SDK] Max reconnect attempts reached")
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
self._reconnect_attempts += 1
|
|
141
|
+
delay = self._reconnect_delay * (2 ** (self._reconnect_attempts - 1))
|
|
142
|
+
|
|
143
|
+
print(f"[Loop SDK] Reconnecting in {delay}s (attempt {self._reconnect_attempts}/{self._max_reconnect_attempts})")
|
|
144
|
+
|
|
145
|
+
await asyncio.sleep(delay)
|
|
146
|
+
|
|
147
|
+
if self._on_interaction:
|
|
148
|
+
await self._create_websocket()
|
|
149
|
+
|
|
150
|
+
async def connect_sse(self, on_interaction: Callable[[Dict[str, Any]], None]) -> None:
|
|
151
|
+
"""Legacy SSE method - now uses WebSocket internally"""
|
|
152
|
+
await self.connect_ws(on_interaction)
|
|
153
|
+
|
|
154
|
+
async def disconnect_ws(self) -> None:
|
|
155
|
+
"""Disconnect WebSocket"""
|
|
156
|
+
if self._heartbeat_task:
|
|
157
|
+
self._heartbeat_task.cancel()
|
|
158
|
+
self._heartbeat_task = None
|
|
159
|
+
|
|
160
|
+
if self._ws and not self._ws.closed:
|
|
161
|
+
await self._ws.close()
|
|
162
|
+
self._ws = None
|
|
163
|
+
self._on_interaction = None
|
|
85
164
|
|
|
86
165
|
async def respond(self, interaction_id: str, response: Dict[str, Any]) -> None:
|
|
87
166
|
"""Send interaction response"""
|
|
@@ -118,6 +197,8 @@ class Client:
|
|
|
118
197
|
|
|
119
198
|
async def disconnect(self) -> None:
|
|
120
199
|
"""Disconnect from API"""
|
|
200
|
+
await self.disconnect_ws()
|
|
201
|
+
|
|
121
202
|
if self._connected:
|
|
122
203
|
try:
|
|
123
204
|
await self.request("POST", "/sdk/disconnect", {})
|
|
@@ -802,4 +883,3 @@ class Client:
|
|
|
802
883
|
"webhookToken": webhook_token,
|
|
803
884
|
"messageId": message_id,
|
|
804
885
|
})
|
|
805
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loopbot-discord-sdk
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.5
|
|
4
4
|
Summary: Official Loop Discord SDK for Python - Build Discord bots without websockets
|
|
5
5
|
Author-email: Loop <contact@loopbot.app>
|
|
6
6
|
License: MIT
|
|
@@ -25,7 +25,6 @@ Classifier: Typing :: Typed
|
|
|
25
25
|
Requires-Python: >=3.8
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
Requires-Dist: aiohttp>=3.8.0
|
|
28
|
-
Requires-Dist: aiohttp-sse-client>=0.2.1
|
|
29
28
|
Provides-Extra: dev
|
|
30
29
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
31
30
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
loopbot/__init__.py,sha256=oIkHZq8f8ygeHGLMiYgTd8cB_PmrNFDcNZnkccES7ws,1197
|
|
2
|
-
loopbot/bot.py,sha256=
|
|
3
|
-
loopbot/client.py,sha256=
|
|
2
|
+
loopbot/bot.py,sha256=ne_dgvYqIq7jW1cNS8-jjbX7IKXIDtTHzxvReh7JZDM,20625
|
|
3
|
+
loopbot/client.py,sha256=s7UG3Fd_GUA6qpil5fbH3d3VFXJMDPjUX513_1Og4Fs,30124
|
|
4
4
|
loopbot/types.py,sha256=MZcQgVvVDSs60UqAF5lWkHNM3yo9KLKDxZaU6tKndZQ,1645
|
|
5
5
|
loopbot/builders/__init__.py,sha256=STrS9XOMPsVDzx9VAvbOZW2f8fp6mITbZWzdkgmqNe4,790
|
|
6
6
|
loopbot/builders/action_row.py,sha256=ouQKGpcb9KIWwUWJzUT6kH7Ov60UCZi5FjWlXC6sRVw,1225
|
|
@@ -20,7 +20,7 @@ loopbot/context/button.py,sha256=5-XO6mYNMx2FPHhv8ijiUrsALOkxzSal-NKvVvXvMnM,756
|
|
|
20
20
|
loopbot/context/command.py,sha256=i7Z8o3F5mLvQDDSVdio8LMB5-zPzMKtSiPsK1WgNg4c,3076
|
|
21
21
|
loopbot/context/modal.py,sha256=QTgKDRZP7g43S3ScR5dMGKzZolUDWMtVmYqEip4GJXI,1476
|
|
22
22
|
loopbot/context/select.py,sha256=8ozLkBNdlZBdQdeCbJ2545j3YkwsCBUnFpHWQPW5VIw,954
|
|
23
|
-
loopbot_discord_sdk-1.0.
|
|
24
|
-
loopbot_discord_sdk-1.0.
|
|
25
|
-
loopbot_discord_sdk-1.0.
|
|
26
|
-
loopbot_discord_sdk-1.0.
|
|
23
|
+
loopbot_discord_sdk-1.0.5.dist-info/METADATA,sha256=6pv-i-_TC8C0h0rCqNOxdtPOGOf_MVWSyCRsAcPD2rk,12462
|
|
24
|
+
loopbot_discord_sdk-1.0.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
25
|
+
loopbot_discord_sdk-1.0.5.dist-info/top_level.txt,sha256=4sZzEMpMwz6lUz7RJzVFFh7LY-vr5W1-RAfn_bF6tks,8
|
|
26
|
+
loopbot_discord_sdk-1.0.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|