python-seatalk-bot 1.0.2__tar.gz → 1.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.
- {python_seatalk_bot-1.0.2 → python_seatalk_bot-1.1.0}/PKG-INFO +2 -1
- {python_seatalk_bot-1.0.2 → python_seatalk_bot-1.1.0}/pyproject.toml +2 -1
- {python_seatalk_bot-1.0.2 → python_seatalk_bot-1.1.0}/src/seatalk/core/__init__.py +28 -31
- {python_seatalk_bot-1.0.2 → python_seatalk_bot-1.1.0}/src/seatalk/ext/__init__.py +46 -71
- python_seatalk_bot-1.1.0/src/seatalk/ext/updater.py +254 -0
- {python_seatalk_bot-1.0.2 → python_seatalk_bot-1.1.0}/README.md +0 -0
- {python_seatalk_bot-1.0.2 → python_seatalk_bot-1.1.0}/src/seatalk/__init__.py +0 -0
- {python_seatalk_bot-1.0.2 → python_seatalk_bot-1.1.0}/src/seatalk/ext/context.py +0 -0
- {python_seatalk_bot-1.0.2 → python_seatalk_bot-1.1.0}/src/seatalk/ext/jobs.py +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: python-seatalk-bot
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Python wrapper for the SeaTalk API, inspired by python-telegram-bot.
|
|
5
5
|
Requires-Dist: aiohttp
|
|
6
|
+
Requires-Dist: websockets>=15.0.1
|
|
6
7
|
Requires-Python: >=3.9
|
|
7
8
|
Project-URL: Repository, https://github.com/heyzec/python-seatalk-bot
|
|
8
9
|
Description-Content-Type: text/markdown
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-seatalk-bot"
|
|
3
|
-
version = "1.0
|
|
3
|
+
version = "1.1.0"
|
|
4
4
|
description = "Python wrapper for the SeaTalk API, inspired by python-telegram-bot."
|
|
5
5
|
requires-python = ">=3.9"
|
|
6
6
|
dependencies = [
|
|
7
7
|
"aiohttp",
|
|
8
|
+
"websockets>=15.0.1",
|
|
8
9
|
]
|
|
9
10
|
readme = "README.md"
|
|
10
11
|
|
|
@@ -208,8 +208,9 @@ class Update:
|
|
|
208
208
|
date = (
|
|
209
209
|
data.event.message.message_sent_time.unwrap()
|
|
210
210
|
) # suddenly this API exposes this extra field
|
|
211
|
+
print("the data is ", data)
|
|
211
212
|
group_id = data.event.group_id.unwrap()
|
|
212
|
-
thread_id = data.event.thread_id.unwrap()
|
|
213
|
+
thread_id = data.event.message.thread_id.unwrap()
|
|
213
214
|
chat_id = Chat.join_group_and_thread_id(group_id, thread_id)
|
|
214
215
|
chat = Chat(chat_id)
|
|
215
216
|
employee_code = data.event.message.sender.employee_code.unwrap()
|
|
@@ -287,8 +288,8 @@ class Bot:
|
|
|
287
288
|
)
|
|
288
289
|
|
|
289
290
|
def __init__(self, app_id: str, app_secret: str):
|
|
290
|
-
self.app_id = app_id
|
|
291
|
-
self.app_secret = app_secret
|
|
291
|
+
self.app_id: str = app_id
|
|
292
|
+
self.app_secret: str = app_secret
|
|
292
293
|
|
|
293
294
|
async def create_session_if_necessary(self):
|
|
294
295
|
if self.session is None: # pyright: ignore[reportUnnecessaryComparison]
|
|
@@ -315,8 +316,16 @@ class Bot:
|
|
|
315
316
|
) as r:
|
|
316
317
|
return (await r.json())["app_access_token"]
|
|
317
318
|
|
|
318
|
-
async def
|
|
319
|
+
async def send(self, url: str, data: dict[str, Any]):
|
|
319
320
|
await self.create_session_if_necessary()
|
|
321
|
+
|
|
322
|
+
r = await self.session.post(url, json=data)
|
|
323
|
+
if (await r.json())["code"] == 100:
|
|
324
|
+
await self.refresh_session()
|
|
325
|
+
r = await self.session.post(url, json=data)
|
|
326
|
+
return await r.json()
|
|
327
|
+
|
|
328
|
+
async def send_message(self, chat_id: str, text: str):
|
|
320
329
|
data: dict[str, Any] = {
|
|
321
330
|
"message": {
|
|
322
331
|
"tag": "text",
|
|
@@ -326,7 +335,6 @@ class Bot:
|
|
|
326
335
|
},
|
|
327
336
|
},
|
|
328
337
|
}
|
|
329
|
-
print(data)
|
|
330
338
|
if Chat.is_employee_code(chat_id):
|
|
331
339
|
print("Sending message to bot user with employee code:", chat_id)
|
|
332
340
|
data["employee_code"] = chat_id
|
|
@@ -338,15 +346,16 @@ class Bot:
|
|
|
338
346
|
data["message"]["thread_id"] = thread_id
|
|
339
347
|
url = SEND_MESSAGE_TO_GROUP_CHAT_URL
|
|
340
348
|
|
|
341
|
-
|
|
342
|
-
if
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
349
|
+
res = await self.send(url, data)
|
|
350
|
+
if res["code"] != 0:
|
|
351
|
+
print("Failed to send message:", res)
|
|
352
|
+
return None
|
|
353
|
+
chat = Chat(chat_id)
|
|
354
|
+
return Message(
|
|
355
|
+
message_id=res["message_id"], chat=chat, date=0, from_user=User("")
|
|
356
|
+
) # dummy message
|
|
346
357
|
|
|
347
358
|
async def send_image(self, chat_id: str, image: str):
|
|
348
|
-
await self.create_session_if_necessary()
|
|
349
|
-
# b = base64.b64encode(image.read()).decode()
|
|
350
359
|
data: dict[str, Any] = {
|
|
351
360
|
"message": {
|
|
352
361
|
"tag": "image",
|
|
@@ -364,14 +373,9 @@ class Bot:
|
|
|
364
373
|
data["message"]["thread_id"] = thread_id
|
|
365
374
|
url = SEND_MESSAGE_TO_GROUP_CHAT_URL
|
|
366
375
|
|
|
367
|
-
|
|
368
|
-
if (await r.json())["code"] == 100:
|
|
369
|
-
await self.refresh_session()
|
|
370
|
-
r = await self.session.post(url, json=data)
|
|
371
|
-
print(await r.json()) # debug
|
|
376
|
+
await self.send(url, data)
|
|
372
377
|
|
|
373
378
|
async def send_card(self, chat_id: str, card: Card):
|
|
374
|
-
await self.create_session_if_necessary()
|
|
375
379
|
data: dict[str, Any] = {"message": card.encode()}
|
|
376
380
|
if Chat.is_employee_code(chat_id):
|
|
377
381
|
data["employee_code"] = chat_id
|
|
@@ -381,28 +385,21 @@ class Bot:
|
|
|
381
385
|
data["group_id"] = group_id
|
|
382
386
|
data["message"]["thread_id"] = thread_id
|
|
383
387
|
url = SEND_MESSAGE_TO_GROUP_CHAT_URL
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
await self.refresh_session()
|
|
387
|
-
r = await self.session.post(url, json=data)
|
|
388
|
-
print(await r.json()) # debug
|
|
388
|
+
|
|
389
|
+
await self.send(url, data)
|
|
389
390
|
|
|
390
391
|
async def get_user_by_emails(self, emails: list[str]) -> list[User]:
|
|
391
|
-
await self.create_session_if_necessary()
|
|
392
392
|
data = {"emails": emails}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
employees: list[Any] = (
|
|
396
|
-
print(employees) # debug
|
|
393
|
+
url = GET_EMPLOYEE_CODE_WITH_EMAIL
|
|
394
|
+
result = await self.send(url, data)
|
|
395
|
+
employees: list[Any] = (result)["employees"]
|
|
397
396
|
|
|
398
397
|
if employees[0]["code"] == 3000:
|
|
399
398
|
return []
|
|
400
399
|
|
|
401
400
|
users: list[User] = []
|
|
402
401
|
for employee in employees:
|
|
403
|
-
seatalk_id =
|
|
404
|
-
0,
|
|
405
|
-
) # employee['seatalk_id'] this api doesn't provide seatalk_id...
|
|
402
|
+
seatalk_id = "Unknown" # employee['seatalk_id'] this api doesn't provide seatalk_id...
|
|
406
403
|
employee_code = employee["employee_code"]
|
|
407
404
|
email = employee["email"]
|
|
408
405
|
user = User(seatalk_id, employee_code, email)
|
|
@@ -5,19 +5,16 @@ from typing import Any, Callable, override
|
|
|
5
5
|
import asyncio
|
|
6
6
|
from collections.abc import Coroutine
|
|
7
7
|
import abc
|
|
8
|
-
import hashlib
|
|
9
|
-
import json
|
|
10
8
|
import re
|
|
11
9
|
|
|
12
|
-
import
|
|
13
|
-
|
|
10
|
+
from seatalk.ext.updater import Updater, WebhookUpdater, WebsocketUpdater
|
|
11
|
+
|
|
14
12
|
|
|
15
13
|
from .context import CallbackContext
|
|
16
14
|
from .jobs import JobQueue
|
|
17
15
|
from ..core import (
|
|
18
16
|
Bot,
|
|
19
17
|
Update,
|
|
20
|
-
EVENT_VERIFICATION,
|
|
21
18
|
)
|
|
22
19
|
|
|
23
20
|
# Re-export symbols
|
|
@@ -113,8 +110,6 @@ class Persistence:
|
|
|
113
110
|
|
|
114
111
|
class Application:
|
|
115
112
|
handlers: dict[int, list[BaseHandler]] = {}
|
|
116
|
-
persistence: Persistence | None
|
|
117
|
-
signing_secret: bytes | None = None
|
|
118
113
|
|
|
119
114
|
def __init__(
|
|
120
115
|
self,
|
|
@@ -123,14 +118,15 @@ class Application:
|
|
|
123
118
|
app_name: str,
|
|
124
119
|
persistence: Persistence | None = None,
|
|
125
120
|
):
|
|
121
|
+
self.app_id: str = app_id
|
|
122
|
+
self.app_secret: str = app_secret
|
|
126
123
|
self.bot: Bot = Bot(app_id, app_secret)
|
|
127
124
|
self.persistence: Persistence | None = persistence
|
|
128
125
|
self.app_name: str = app_name
|
|
129
126
|
|
|
130
127
|
self.job_queue: JobQueue = JobQueue(self.bot)
|
|
131
128
|
|
|
132
|
-
self.
|
|
133
|
-
self._setup()
|
|
129
|
+
self.token: str | None = None
|
|
134
130
|
|
|
135
131
|
def add_handler(self, handler: BaseHandler, group: int = 0):
|
|
136
132
|
handler.app_name = self.app_name # inject app name
|
|
@@ -140,69 +136,48 @@ class Application:
|
|
|
140
136
|
handler.app_name = self.app_name # inject app name
|
|
141
137
|
self.handlers[group].remove(handler)
|
|
142
138
|
|
|
143
|
-
def
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
assert update.effective_user is not None
|
|
176
|
-
context.chat_data = self.persistence.chat_data.setdefault(
|
|
177
|
-
update.effective_chat.chat_id, {}
|
|
178
|
-
)
|
|
179
|
-
context.user_data = self.persistence.user_data.setdefault(
|
|
180
|
-
update.effective_user.user_id, {}
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
for group, handlers in sorted(self.handlers.items()):
|
|
184
|
-
for handler in handlers:
|
|
185
|
-
if handler.check_update(update):
|
|
186
|
-
print("Handler", handler, "matched for update", update)
|
|
187
|
-
callback = handler.callback
|
|
188
|
-
break
|
|
189
|
-
else:
|
|
190
|
-
print("No handler matched for update", update)
|
|
191
|
-
|
|
192
|
-
if callback:
|
|
193
|
-
# create task instead of awaiting, we need to respond in time, otherwise seatalk will retry
|
|
194
|
-
_ = asyncio.create_task(callback(update, context))
|
|
195
|
-
|
|
196
|
-
return aiohttp.web.Response()
|
|
197
|
-
|
|
198
|
-
_ = self.server.add_routes([aiohttp.web.post("/", handle)])
|
|
139
|
+
def handle_update(self, update: Update):
|
|
140
|
+
callback = None
|
|
141
|
+
context = CallbackContext(self.bot)
|
|
142
|
+
if self.persistence:
|
|
143
|
+
context.bot_data = self.persistence.bot_data
|
|
144
|
+
assert update.effective_chat is not None
|
|
145
|
+
assert update.effective_user is not None
|
|
146
|
+
context.chat_data = self.persistence.chat_data.setdefault(
|
|
147
|
+
update.effective_chat.chat_id, {}
|
|
148
|
+
)
|
|
149
|
+
context.user_data = self.persistence.user_data.setdefault(
|
|
150
|
+
update.effective_user.user_id, {}
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
for group, handlers in sorted(self.handlers.items()):
|
|
154
|
+
for handler in handlers:
|
|
155
|
+
if handler.check_update(update):
|
|
156
|
+
print("Handler", handler, "matched for update", update)
|
|
157
|
+
callback = handler.callback
|
|
158
|
+
break
|
|
159
|
+
else:
|
|
160
|
+
print("No handler matched for update", update)
|
|
161
|
+
|
|
162
|
+
if callback:
|
|
163
|
+
# create task instead of awaiting, we need to respond in time, otherwise seatalk will retry
|
|
164
|
+
_ = asyncio.create_task(callback(update, context))
|
|
165
|
+
|
|
166
|
+
async def run(self, updater: Updater):
|
|
167
|
+
await updater.setup()
|
|
168
|
+
_ = self.job_queue.start() # don't await
|
|
169
|
+
async for update in updater.get_updates():
|
|
170
|
+
self.handle_update(update)
|
|
199
171
|
|
|
200
172
|
def run_webhook(
|
|
201
173
|
self, signing_secret: bytes, listen: str = "127.0.0.1", port: int = 80
|
|
202
174
|
):
|
|
203
|
-
|
|
204
|
-
asyncio.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
self.
|
|
208
|
-
|
|
175
|
+
updater = WebhookUpdater(signing_secret, listen, port)
|
|
176
|
+
asyncio.run(self.run(updater))
|
|
177
|
+
|
|
178
|
+
def run_websocket(self):
|
|
179
|
+
updater = WebsocketUpdater(app_id=self.app_id, app_secret=self.app_secret)
|
|
180
|
+
# loop = asyncio.new_event_loop()
|
|
181
|
+
# asyncio.set_event_loop(loop)
|
|
182
|
+
# loop.run_until_complete(self.run(updater))
|
|
183
|
+
asyncio.run(self.run(updater))
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# pyright: reportAny=false
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import abc
|
|
5
|
+
import asyncio
|
|
6
|
+
from dataclasses import MISSING, dataclass, field, fields, is_dataclass
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any, AsyncGenerator, Self, get_type_hints
|
|
10
|
+
import uuid
|
|
11
|
+
|
|
12
|
+
import aiohttp
|
|
13
|
+
import aiohttp.web
|
|
14
|
+
import websockets
|
|
15
|
+
from websockets.asyncio.client import ClientConnection
|
|
16
|
+
|
|
17
|
+
from seatalk.core import EVENT_VERIFICATION, Update
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Updater:
|
|
21
|
+
@abc.abstractmethod
|
|
22
|
+
async def setup(self): ...
|
|
23
|
+
|
|
24
|
+
@abc.abstractmethod
|
|
25
|
+
async def get_updates(self) -> AsyncGenerator[Update, None]:
|
|
26
|
+
raise NotImplementedError("get_updates must be implemented by subclasses")
|
|
27
|
+
yield # pyright: ignore
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WebhookUpdater(Updater):
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
signing_secret: bytes,
|
|
34
|
+
listen: str = "127.0.0.1",
|
|
35
|
+
port: int = 80,
|
|
36
|
+
):
|
|
37
|
+
self.signing_secret = signing_secret
|
|
38
|
+
self.listen = listen
|
|
39
|
+
self.port = port
|
|
40
|
+
|
|
41
|
+
self.queue: asyncio.Queue[Update] = asyncio.Queue()
|
|
42
|
+
|
|
43
|
+
async def setup(self):
|
|
44
|
+
server = aiohttp.web.Application()
|
|
45
|
+
|
|
46
|
+
async def handle(request: aiohttp.web.Request) -> aiohttp.web.Response:
|
|
47
|
+
assert self.signing_secret is not None
|
|
48
|
+
|
|
49
|
+
def is_valid_signature(
|
|
50
|
+
signing_secret: bytes, body: bytes, signature: str
|
|
51
|
+
) -> bool:
|
|
52
|
+
# ref: https://open.seatalk.io/docs/server-apis-event-callback
|
|
53
|
+
return hashlib.sha256(body + signing_secret).hexdigest() == signature
|
|
54
|
+
|
|
55
|
+
body: bytes = await request.read()
|
|
56
|
+
signature: str = request.headers.get("signature", "")
|
|
57
|
+
# 1. validate the signature
|
|
58
|
+
if not is_valid_signature(self.signing_secret, body, signature):
|
|
59
|
+
return aiohttp.web.Response()
|
|
60
|
+
# 2. handle events
|
|
61
|
+
data: dict[str, Any] = json.loads(body)
|
|
62
|
+
|
|
63
|
+
if data["event_type"] == EVENT_VERIFICATION:
|
|
64
|
+
data = {"seatalk_challenge": data["event"]["seatalk_challenge"]}
|
|
65
|
+
return aiohttp.web.json_response(data)
|
|
66
|
+
|
|
67
|
+
update = Update.from_data(data)
|
|
68
|
+
if not update:
|
|
69
|
+
print("Failed to parse update from data:", data)
|
|
70
|
+
return aiohttp.web.Response()
|
|
71
|
+
|
|
72
|
+
self.queue.put_nowait(update)
|
|
73
|
+
|
|
74
|
+
return aiohttp.web.Response()
|
|
75
|
+
|
|
76
|
+
server.add_routes([aiohttp.web.post("/", handle)])
|
|
77
|
+
|
|
78
|
+
# aiohttp.web.run_app(server, host=listen, port=port, loop=loop)
|
|
79
|
+
runner = aiohttp.web.AppRunner(server)
|
|
80
|
+
await runner.setup()
|
|
81
|
+
site = aiohttp.web.TCPSite(
|
|
82
|
+
runner,
|
|
83
|
+
host=self.listen,
|
|
84
|
+
port=self.port,
|
|
85
|
+
)
|
|
86
|
+
await site.start()
|
|
87
|
+
|
|
88
|
+
async def get_updates(self) -> AsyncGenerator[Update, None]:
|
|
89
|
+
while True:
|
|
90
|
+
update = await self.queue.get()
|
|
91
|
+
yield update
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
CODE_OK = 0
|
|
95
|
+
COMMAND_REGISTER = "register"
|
|
96
|
+
DEFAULT_WEB_SOCKET_URL = "wss://ws-openapi.haiserve.com/ws/bot"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Serializable:
|
|
100
|
+
@classmethod
|
|
101
|
+
def from_dict(cls, data: Any) -> Self:
|
|
102
|
+
assert is_dataclass(cls), f"{cls.__name__} must be a dataclass"
|
|
103
|
+
|
|
104
|
+
data = data if isinstance(data, dict) else {}
|
|
105
|
+
hints = get_type_hints(cls)
|
|
106
|
+
|
|
107
|
+
kwargs: dict[str, Any] = {}
|
|
108
|
+
for field in fields(cls):
|
|
109
|
+
value = data.get(field.name)
|
|
110
|
+
|
|
111
|
+
# resolved version of what field.type is trying to represent
|
|
112
|
+
annotation = hints[field.name]
|
|
113
|
+
|
|
114
|
+
if isinstance(value, dict) and issubclass(annotation, Serializable):
|
|
115
|
+
value = annotation.from_dict(value)
|
|
116
|
+
elif value is None:
|
|
117
|
+
assert field.default is not MISSING
|
|
118
|
+
value = field.default
|
|
119
|
+
|
|
120
|
+
if annotation is not Any:
|
|
121
|
+
assert isinstance(value, annotation)
|
|
122
|
+
kwargs[field.name] = value
|
|
123
|
+
|
|
124
|
+
return cls(**kwargs)
|
|
125
|
+
|
|
126
|
+
def to_dict(self) -> dict[str, Any]:
|
|
127
|
+
if not is_dataclass(self):
|
|
128
|
+
raise TypeError(f"{type(self).__name__} must be a dataclass")
|
|
129
|
+
|
|
130
|
+
result: dict[str, Any] = {}
|
|
131
|
+
for field in fields(self):
|
|
132
|
+
value = getattr(self, field.name)
|
|
133
|
+
if isinstance(value, Serializable):
|
|
134
|
+
value = value.to_dict()
|
|
135
|
+
if value:
|
|
136
|
+
result[field.name] = value
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class Header(Serializable):
|
|
142
|
+
app_id: str = ""
|
|
143
|
+
app_secret: str = ""
|
|
144
|
+
token: str = ""
|
|
145
|
+
sid: str = ""
|
|
146
|
+
callback_id: str = ""
|
|
147
|
+
rid: str = ""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class Envelope(Serializable):
|
|
152
|
+
cmd: str
|
|
153
|
+
header: Header = field(default_factory=Header)
|
|
154
|
+
data: Any = None
|
|
155
|
+
code: int = 0
|
|
156
|
+
message: str = ""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class WebsocketUpdater(Updater):
|
|
160
|
+
def __init__(self, app_id: str, app_secret: str):
|
|
161
|
+
self.app_id: str = app_id
|
|
162
|
+
self.app_secret: str = app_secret
|
|
163
|
+
self._ws: ClientConnection | None = None
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def ws(self) -> ClientConnection:
|
|
167
|
+
if self._ws is None:
|
|
168
|
+
raise ValueError("WebSocket is not connected")
|
|
169
|
+
return self._ws
|
|
170
|
+
|
|
171
|
+
async def ws_send(self, envelope: Envelope):
|
|
172
|
+
if not envelope.header.rid:
|
|
173
|
+
envelope.header.rid = uuid.uuid4().hex
|
|
174
|
+
payload = json.dumps(
|
|
175
|
+
envelope.to_dict(), ensure_ascii=False, separators=(",", ":")
|
|
176
|
+
)
|
|
177
|
+
assert self.ws is not None
|
|
178
|
+
await self.ws.send(payload)
|
|
179
|
+
|
|
180
|
+
async def ws_connect(self):
|
|
181
|
+
self._ws = await websockets.connect(
|
|
182
|
+
DEFAULT_WEB_SOCKET_URL,
|
|
183
|
+
# additional_headers=self.request_headers,
|
|
184
|
+
open_timeout=15.0,
|
|
185
|
+
)
|
|
186
|
+
print("[ws] connected")
|
|
187
|
+
await self.ws_send(
|
|
188
|
+
Envelope(
|
|
189
|
+
cmd=COMMAND_REGISTER,
|
|
190
|
+
header=Header(app_id=self.app_id, app_secret=self.app_secret),
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
assert self.ws is not None
|
|
195
|
+
message = await self.ws.recv()
|
|
196
|
+
try:
|
|
197
|
+
env = Envelope.from_dict(json.loads(message))
|
|
198
|
+
except Exception as exc:
|
|
199
|
+
raise ValueError("invalid json in register phase: %s" % exc)
|
|
200
|
+
if env.cmd != COMMAND_REGISTER:
|
|
201
|
+
raise ValueError(
|
|
202
|
+
'expected cmd "%s" first, got "%s" (%s)'
|
|
203
|
+
% (COMMAND_REGISTER, env.cmd, env.message)
|
|
204
|
+
)
|
|
205
|
+
if env.code != CODE_OK:
|
|
206
|
+
raise ValueError(
|
|
207
|
+
"register rejected: code=%d msg=%s" % (env.code, env.message)
|
|
208
|
+
)
|
|
209
|
+
if not env.header.token:
|
|
210
|
+
raise ValueError("register ok but empty token")
|
|
211
|
+
self.token = env.header.token
|
|
212
|
+
# self.ping_interval = (
|
|
213
|
+
# float(data.get("heartbeat_interval") or 0) or self.ping_interval
|
|
214
|
+
# )
|
|
215
|
+
# print(
|
|
216
|
+
# f"[ws] registered token={self._token} heartbeat_interval={self.ping_interval}"
|
|
217
|
+
# )
|
|
218
|
+
|
|
219
|
+
async def ping_loop(self):
|
|
220
|
+
assert self.token is not None
|
|
221
|
+
while True:
|
|
222
|
+
await self.ws_send(Envelope(cmd="ping", header=Header(token=self.token)))
|
|
223
|
+
# await asyncio.sleep(self.ping_interval)
|
|
224
|
+
await asyncio.sleep(10)
|
|
225
|
+
|
|
226
|
+
async def setup(self):
|
|
227
|
+
await self.ws_connect()
|
|
228
|
+
ping_task = asyncio.create_task(self.ping_loop())
|
|
229
|
+
|
|
230
|
+
async def get_updates(self) -> AsyncGenerator[Update, None]:
|
|
231
|
+
await self.setup()
|
|
232
|
+
print("[ws] waiting for updates...")
|
|
233
|
+
assert self.token is not None
|
|
234
|
+
async for message in self.ws:
|
|
235
|
+
print("[ws] received message:", message)
|
|
236
|
+
try:
|
|
237
|
+
env = Envelope.from_dict(json.loads(message))
|
|
238
|
+
except Exception as exc:
|
|
239
|
+
print(f"[ws] invalid frame: {exc}")
|
|
240
|
+
continue
|
|
241
|
+
if env.cmd == "event" and env.header.callback_id:
|
|
242
|
+
await self.ws_send(
|
|
243
|
+
Envelope(
|
|
244
|
+
cmd="ack",
|
|
245
|
+
header=Header(
|
|
246
|
+
token=self.token, callback_id=env.header.callback_id
|
|
247
|
+
),
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
update = Update.from_data(env.data)
|
|
251
|
+
if not update:
|
|
252
|
+
print(f"[ws] failed to parse update from data: {env.data}")
|
|
253
|
+
continue
|
|
254
|
+
yield update
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|