satori-python-server 0.16.7__tar.gz → 0.17.1__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.
- {satori_python_server-0.16.7 → satori_python_server-0.17.1}/.mina/server.toml +5 -5
- {satori_python_server-0.16.7 → satori_python_server-0.17.1}/PKG-INFO +5 -5
- {satori_python_server-0.16.7 → satori_python_server-0.17.1}/pyproject.toml +13 -12
- {satori_python_server-0.16.7 → satori_python_server-0.17.1}/src/satori/server/__init__.py +35 -25
- {satori_python_server-0.16.7 → satori_python_server-0.17.1}/src/satori/server/adapter.py +4 -8
- {satori_python_server-0.16.7 → satori_python_server-0.17.1}/src/satori/server/connection.py +6 -4
- {satori_python_server-0.16.7 → satori_python_server-0.17.1}/src/satori/server/model.py +5 -8
- {satori_python_server-0.16.7 → satori_python_server-0.17.1}/src/satori/server/route.py +29 -45
- {satori_python_server-0.16.7 → satori_python_server-0.17.1}/src/satori/server/utils.py +3 -0
- {satori_python_server-0.16.7 → satori_python_server-0.17.1}/LICENSE +0 -0
- {satori_python_server-0.16.7 → satori_python_server-0.17.1}/README.md +0 -0
- {satori_python_server-0.16.7 → satori_python_server-0.17.1}/src/satori/server/formdata.py +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
includes = ["src/satori/server"]
|
|
2
|
-
raw-dependencies = ["satori-python-core >= 0.
|
|
2
|
+
raw-dependencies = ["satori-python-core >= 0.17.0"]
|
|
3
3
|
|
|
4
4
|
[project]
|
|
5
5
|
name = "satori-python-server"
|
|
@@ -10,15 +10,15 @@ authors = [
|
|
|
10
10
|
dependencies = [
|
|
11
11
|
"aiohttp",
|
|
12
12
|
"launart",
|
|
13
|
-
"graia-amnesia",
|
|
14
|
-
"starlette",
|
|
15
|
-
"
|
|
13
|
+
"graia-amnesia[uvicorn]",
|
|
14
|
+
"starlette[python-multipart]",
|
|
15
|
+
"websockets",
|
|
16
16
|
"python-multipart",
|
|
17
17
|
]
|
|
18
18
|
description = "Satori Protocol SDK for python, specify server part"
|
|
19
19
|
license = {text = "MIT"}
|
|
20
20
|
readme = "README.md"
|
|
21
|
-
requires-python = ">=3.
|
|
21
|
+
requires-python = ">=3.10,<4.0"
|
|
22
22
|
classifiers = [
|
|
23
23
|
"Typing :: Typed",
|
|
24
24
|
"Development Status :: 4 - Beta",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: satori-python-server
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.17.1
|
|
4
4
|
Summary: Satori Protocol SDK for python, specify server part
|
|
5
5
|
Home-page: https://github.com/RF-Tar-Railt/satori-python
|
|
6
6
|
Author-Email: RF-Tar-Railt <rf_tar_railt@qq.com>
|
|
@@ -16,14 +16,14 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
16
16
|
Classifier: Operating System :: OS Independent
|
|
17
17
|
Project-URL: Homepage, https://github.com/RF-Tar-Railt/satori-python
|
|
18
18
|
Project-URL: Repository, https://github.com/RF-Tar-Railt/satori-python
|
|
19
|
-
Requires-Python:
|
|
19
|
+
Requires-Python: <4.0,>=3.10
|
|
20
20
|
Requires-Dist: aiohttp>=3.9.3
|
|
21
21
|
Requires-Dist: launart>=0.8.2
|
|
22
|
-
Requires-Dist: graia-amnesia
|
|
22
|
+
Requires-Dist: graia-amnesia[uvicorn]<0.12.0,>=0.11.0
|
|
23
23
|
Requires-Dist: starlette[python-multipart]>=0.37.2
|
|
24
|
-
Requires-Dist:
|
|
24
|
+
Requires-Dist: websockets>=15.0.1
|
|
25
25
|
Requires-Dist: python-multipart>=0.0.9
|
|
26
|
-
Requires-Dist: satori-python-core>=0.
|
|
26
|
+
Requires-Dist: satori-python-core>=0.17.0
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
|
|
29
29
|
# satori-python
|
|
@@ -7,15 +7,15 @@ authors = [
|
|
|
7
7
|
dependencies = [
|
|
8
8
|
"aiohttp>=3.9.3",
|
|
9
9
|
"launart>=0.8.2",
|
|
10
|
-
"graia-amnesia
|
|
10
|
+
"graia-amnesia[uvicorn]<0.12.0,>=0.11.0",
|
|
11
11
|
"starlette[python-multipart]>=0.37.2",
|
|
12
|
-
"
|
|
12
|
+
"websockets>=15.0.1",
|
|
13
13
|
"python-multipart>=0.0.9",
|
|
14
|
-
"satori-python-core >= 0.
|
|
14
|
+
"satori-python-core >= 0.17.0",
|
|
15
15
|
]
|
|
16
16
|
description = "Satori Protocol SDK for python, specify server part"
|
|
17
17
|
readme = "README.md"
|
|
18
|
-
requires-python = ">=3.
|
|
18
|
+
requires-python = ">=3.10,<4.0"
|
|
19
19
|
classifiers = [
|
|
20
20
|
"Typing :: Typed",
|
|
21
21
|
"Development Status :: 4 - Beta",
|
|
@@ -27,7 +27,7 @@ classifiers = [
|
|
|
27
27
|
"Programming Language :: Python :: 3.12",
|
|
28
28
|
"Operating System :: OS Independent",
|
|
29
29
|
]
|
|
30
|
-
version = "0.
|
|
30
|
+
version = "0.17.1"
|
|
31
31
|
|
|
32
32
|
[project.license]
|
|
33
33
|
text = "MIT"
|
|
@@ -43,7 +43,7 @@ requires = [
|
|
|
43
43
|
]
|
|
44
44
|
build-backend = "mina.backend"
|
|
45
45
|
|
|
46
|
-
[
|
|
46
|
+
[dependency-groups]
|
|
47
47
|
dev = [
|
|
48
48
|
"isort>=5.13.2",
|
|
49
49
|
"black>=24.4.0",
|
|
@@ -53,6 +53,7 @@ dev = [
|
|
|
53
53
|
"mina-build<0.6,>=0.5.1",
|
|
54
54
|
"pdm-mina>=0.3.2",
|
|
55
55
|
"nonechat<0.7.0,>=0.6.0",
|
|
56
|
+
"uvicorn[standard]>=0.35.0",
|
|
56
57
|
]
|
|
57
58
|
|
|
58
59
|
[tool.pdm.build]
|
|
@@ -76,21 +77,21 @@ source = "file"
|
|
|
76
77
|
path = "src/satori/__init__.py"
|
|
77
78
|
|
|
78
79
|
[tool.black]
|
|
79
|
-
line-length =
|
|
80
|
+
line-length = 120
|
|
80
81
|
include = "\\.pyi?$"
|
|
81
82
|
extend-exclude = ""
|
|
82
83
|
|
|
83
84
|
[tool.isort]
|
|
84
85
|
profile = "black"
|
|
85
|
-
line_length =
|
|
86
|
+
line_length = 120
|
|
86
87
|
skip_gitignore = true
|
|
87
88
|
extra_standard_library = [
|
|
88
89
|
"typing_extensions",
|
|
89
90
|
]
|
|
90
91
|
|
|
91
92
|
[tool.ruff]
|
|
92
|
-
line-length =
|
|
93
|
-
target-version = "
|
|
93
|
+
line-length = 120
|
|
94
|
+
target-version = "py310"
|
|
94
95
|
exclude = [
|
|
95
96
|
"exam.py",
|
|
96
97
|
]
|
|
@@ -110,12 +111,12 @@ ignore = [
|
|
|
110
111
|
"F403",
|
|
111
112
|
"F405",
|
|
112
113
|
"C901",
|
|
113
|
-
"
|
|
114
|
+
"UP038",
|
|
114
115
|
]
|
|
115
116
|
|
|
116
117
|
[tool.pyright]
|
|
117
118
|
pythonPlatform = "All"
|
|
118
|
-
pythonVersion = "3.
|
|
119
|
+
pythonVersion = "3.10"
|
|
119
120
|
typeCheckingMode = "basic"
|
|
120
121
|
reportShadowedImports = false
|
|
121
122
|
disableBytesTypePromotions = true
|
|
@@ -8,18 +8,20 @@ import secrets
|
|
|
8
8
|
import signal
|
|
9
9
|
import threading
|
|
10
10
|
import urllib.parse
|
|
11
|
-
from collections.abc import Awaitable, Iterable
|
|
11
|
+
from collections.abc import Awaitable, Callable, Iterable
|
|
12
12
|
from contextlib import suppress
|
|
13
13
|
from itertools import chain
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from tempfile import TemporaryDirectory
|
|
16
16
|
from traceback import print_exc
|
|
17
|
-
from typing import Any,
|
|
17
|
+
from typing import Any, TypeVar
|
|
18
18
|
|
|
19
19
|
import aiohttp
|
|
20
|
+
from aiohttp import ClientTimeout
|
|
20
21
|
from creart import it
|
|
21
22
|
from graia.amnesia.builtins.aiohttp import AiohttpClientService
|
|
22
|
-
from graia.amnesia.builtins.asgi import
|
|
23
|
+
from graia.amnesia.builtins.asgi import asgitypes
|
|
24
|
+
from graia.amnesia.builtins.asgi.uvicorn import UvicornASGIService, UvicornOptions
|
|
23
25
|
from launart import Launart, Service, any_completed
|
|
24
26
|
from loguru import logger
|
|
25
27
|
from starlette.applications import Starlette
|
|
@@ -39,6 +41,7 @@ from yarl import URL
|
|
|
39
41
|
|
|
40
42
|
from satori.const import Api
|
|
41
43
|
from satori.model import Event, Meta, ModelBase, Opcode
|
|
44
|
+
from satori.utils import decode
|
|
42
45
|
|
|
43
46
|
from .. import EventType
|
|
44
47
|
from .adapter import Adapter as Adapter
|
|
@@ -57,9 +60,7 @@ _T_ws_endpoint = TypeVar("_T_ws_endpoint", bound=Callable[[WebSocket], Awaitable
|
|
|
57
60
|
StarletteResponse = Response
|
|
58
61
|
|
|
59
62
|
|
|
60
|
-
async def _request_handler(
|
|
61
|
-
action: str, request: StarletteRequest, func: RouteCall, platform: str, self_id: str
|
|
62
|
-
):
|
|
63
|
+
async def _request_handler(action: str, request: StarletteRequest, func: RouteCall, platform: str, self_id: str):
|
|
63
64
|
if action == Api.UPLOAD_CREATE.value:
|
|
64
65
|
async with request.form() as form:
|
|
65
66
|
res = await func(
|
|
@@ -77,7 +78,7 @@ async def _request_handler(
|
|
|
77
78
|
Request(
|
|
78
79
|
request,
|
|
79
80
|
action,
|
|
80
|
-
await request.
|
|
81
|
+
decode(await request.body()),
|
|
81
82
|
platform=platform,
|
|
82
83
|
self_id=self_id,
|
|
83
84
|
)
|
|
@@ -115,6 +116,8 @@ class Server(Service, RouterMixin):
|
|
|
115
116
|
version: str = "v1",
|
|
116
117
|
token: str | None = None,
|
|
117
118
|
webhooks: list[WebhookEndpoint] | None = None,
|
|
119
|
+
uvicorn_options: UvicornOptions | None = None,
|
|
120
|
+
*,
|
|
118
121
|
stream_threshold: int = 16 * 1024 * 1024,
|
|
119
122
|
stream_chunk_size: int = 64 * 1024,
|
|
120
123
|
):
|
|
@@ -123,8 +126,11 @@ class Server(Service, RouterMixin):
|
|
|
123
126
|
self.port = port
|
|
124
127
|
self.version = version
|
|
125
128
|
self.path = path
|
|
129
|
+
self.uvicorn_options = uvicorn_options
|
|
126
130
|
if self.path and not self.path.startswith("/"):
|
|
127
131
|
self.path = f"/{self.path}"
|
|
132
|
+
if (self.host == "0.0.0.0" or self.host == "::") and not token:
|
|
133
|
+
raise ValueError("Token is required when the server is exposed to the public network")
|
|
128
134
|
self.token = token
|
|
129
135
|
self._adapters = []
|
|
130
136
|
self.providers = []
|
|
@@ -138,6 +144,7 @@ class Server(Service, RouterMixin):
|
|
|
138
144
|
self.stream_chunk_size = stream_chunk_size
|
|
139
145
|
self.resources: dict[str, Path] = {}
|
|
140
146
|
self.app = Starlette()
|
|
147
|
+
self.asgi_service = UvicornASGIService(self.host, self.port, options=self.uvicorn_options)
|
|
141
148
|
super().__init__()
|
|
142
149
|
|
|
143
150
|
def replace_app(self, app: ASGIApp | asgitypes.ASGI3Application):
|
|
@@ -161,9 +168,7 @@ class Server(Service, RouterMixin):
|
|
|
161
168
|
"""
|
|
162
169
|
|
|
163
170
|
def wrapper(endpoint: _T_endpoint, /) -> _T_endpoint:
|
|
164
|
-
self.app.add_route(
|
|
165
|
-
path, endpoint, methods=methods, name=name, include_in_schema=include_in_schema
|
|
166
|
-
)
|
|
171
|
+
self.app.add_route(path, endpoint, methods=methods, name=name, include_in_schema=include_in_schema)
|
|
167
172
|
return endpoint
|
|
168
173
|
|
|
169
174
|
return wrapper
|
|
@@ -206,7 +211,7 @@ class Server(Service, RouterMixin):
|
|
|
206
211
|
"""在指定路径挂载静态文件"""
|
|
207
212
|
self.resources[route_path] = file
|
|
208
213
|
|
|
209
|
-
async def
|
|
214
|
+
async def post(self, event: Event):
|
|
210
215
|
event.sn = self._sequence
|
|
211
216
|
self._event_cache.append(event)
|
|
212
217
|
self._sequence += 1
|
|
@@ -230,6 +235,7 @@ class Server(Service, RouterMixin):
|
|
|
230
235
|
"Satori-OpCode": str(Opcode.EVENT.value),
|
|
231
236
|
},
|
|
232
237
|
json=event.dump(),
|
|
238
|
+
timeout=ClientTimeout(hook.timeout or 300),
|
|
233
239
|
) as resp:
|
|
234
240
|
resp.raise_for_status()
|
|
235
241
|
except Exception as e:
|
|
@@ -239,7 +245,7 @@ class Server(Service, RouterMixin):
|
|
|
239
245
|
async def websocket_server_handler(self, ws: WebSocket):
|
|
240
246
|
await ws.accept()
|
|
241
247
|
connection = WebsocketConnection(ws)
|
|
242
|
-
identity = await ws.
|
|
248
|
+
identity = decode(await ws.receive_text())
|
|
243
249
|
if not isinstance(identity, dict) or identity.get("op") != Opcode.IDENTIFY:
|
|
244
250
|
return await ws.close(code=3000, reason="Unauthorized")
|
|
245
251
|
body = identity["body"]
|
|
@@ -258,7 +264,7 @@ class Server(Service, RouterMixin):
|
|
|
258
264
|
{"op": Opcode.READY, "body": {"logins": [lo.dump() for lo in logins], "proxy_urls": proxy_urls}}
|
|
259
265
|
)
|
|
260
266
|
self.connections.append(connection)
|
|
261
|
-
logger.debug(f"New connection: {id(connection)}")
|
|
267
|
+
logger.debug(f"New connection: {id(connection):x}")
|
|
262
268
|
heartbeat_task = asyncio.create_task(connection.heartbeat())
|
|
263
269
|
close_task = asyncio.create_task(connection.close_signal.wait())
|
|
264
270
|
try:
|
|
@@ -275,7 +281,7 @@ class Server(Service, RouterMixin):
|
|
|
275
281
|
await any_completed(heartbeat_task, close_task)
|
|
276
282
|
finally:
|
|
277
283
|
await connection.connection_closed()
|
|
278
|
-
logger.debug(f"Connection closed: {id(connection)}")
|
|
284
|
+
logger.debug(f"Connection closed: {id(connection):x}")
|
|
279
285
|
heartbeat_task.cancel()
|
|
280
286
|
close_task.cancel()
|
|
281
287
|
self.connections.remove(connection)
|
|
@@ -311,8 +317,7 @@ class Server(Service, RouterMixin):
|
|
|
311
317
|
resp = await self.fetch_proxy(url, request)
|
|
312
318
|
# if content size > stream_limit, use streaming response
|
|
313
319
|
if (
|
|
314
|
-
isinstance(resp, (PlainTextResponse, HTMLResponse, JSONResponse))
|
|
315
|
-
or resp.__class__ is Response
|
|
320
|
+
isinstance(resp, (PlainTextResponse, HTMLResponse, JSONResponse)) or resp.__class__ is Response
|
|
316
321
|
) and len(resp.body) > self.stream_threshold:
|
|
317
322
|
|
|
318
323
|
async def iter_content(body: bytes):
|
|
@@ -402,7 +407,7 @@ class Server(Service, RouterMixin):
|
|
|
402
407
|
return JSONResponse(content=Meta(logins, proxy_urls).dump())
|
|
403
408
|
|
|
404
409
|
async def webhook_create_handler(self, request: StarletteRequest):
|
|
405
|
-
body = await request.
|
|
410
|
+
body = decode(await request.body())
|
|
406
411
|
url = body["url"]
|
|
407
412
|
token = body.get("token")
|
|
408
413
|
self.webhooks.append(WebhookEndpoint(url, token))
|
|
@@ -422,7 +427,7 @@ class Server(Service, RouterMixin):
|
|
|
422
427
|
return Response()
|
|
423
428
|
|
|
424
429
|
async def webhook_delete_handler(self, request: StarletteRequest):
|
|
425
|
-
body = await request.
|
|
430
|
+
body = decode(await request.body())
|
|
426
431
|
url = body["url"]
|
|
427
432
|
for endpoint in self.webhooks:
|
|
428
433
|
if endpoint.url == url:
|
|
@@ -438,7 +443,6 @@ class Server(Service, RouterMixin):
|
|
|
438
443
|
self.routes[Api.UPLOAD_CREATE.value] = self._default_upload_create_handler
|
|
439
444
|
|
|
440
445
|
async with self.stage("preparing"):
|
|
441
|
-
asgi_service = manager.get_component(UvicornASGIService)
|
|
442
446
|
self.app.routes.extend(
|
|
443
447
|
[
|
|
444
448
|
*chain.from_iterable(ada.get_routes() for ada in self._adapters),
|
|
@@ -472,11 +476,13 @@ class Server(Service, RouterMixin):
|
|
|
472
476
|
)
|
|
473
477
|
for path, file in self.resources.items():
|
|
474
478
|
self.app.mount(path, StaticFiles(directory=file.parent, html=file.suffix == ".html"))
|
|
475
|
-
asgi_service.middleware.mounts[""] = self.app # type: ignore
|
|
479
|
+
self.asgi_service.middleware.mounts[""] = self.app # type: ignore
|
|
476
480
|
|
|
477
481
|
async def event_task(_provider: Provider):
|
|
478
|
-
async for event in _provider.publisher():
|
|
479
|
-
await self.
|
|
482
|
+
async for event in _provider.publisher(): # type: ignore
|
|
483
|
+
await self.post(event)
|
|
484
|
+
|
|
485
|
+
event_tasks = [event_task(_provider) for _provider in self.providers if hasattr(_provider, "publisher")]
|
|
480
486
|
|
|
481
487
|
async with self.stage("blocking"):
|
|
482
488
|
proxy_urls = []
|
|
@@ -491,17 +497,18 @@ class Server(Service, RouterMixin):
|
|
|
491
497
|
"Satori-OpCode": str(Opcode.META.value),
|
|
492
498
|
},
|
|
493
499
|
json={"proxy_urls": proxy_urls},
|
|
500
|
+
timeout=ClientTimeout(hook.timeout or 300),
|
|
494
501
|
) as resp:
|
|
495
502
|
resp.raise_for_status()
|
|
496
503
|
await any_completed(
|
|
497
504
|
manager.status.wait_for_sigexit(),
|
|
498
|
-
*
|
|
505
|
+
*event_tasks,
|
|
499
506
|
*(_adapter.status.wait_for("blocking-completed") for _adapter in self._adapters),
|
|
500
507
|
)
|
|
501
508
|
|
|
502
509
|
async with self.stage("cleanup"):
|
|
503
510
|
with suppress(KeyError):
|
|
504
|
-
del asgi_service.middleware.mounts[""]
|
|
511
|
+
del self.asgi_service.middleware.mounts[""]
|
|
505
512
|
await self.session.close()
|
|
506
513
|
self._tempdir.cleanup()
|
|
507
514
|
|
|
@@ -514,7 +521,7 @@ class Server(Service, RouterMixin):
|
|
|
514
521
|
):
|
|
515
522
|
if manager is None:
|
|
516
523
|
manager = it(Launart)
|
|
517
|
-
manager.add_component(
|
|
524
|
+
manager.add_component(self.asgi_service)
|
|
518
525
|
manager.add_component(self)
|
|
519
526
|
with suppress(ValueError):
|
|
520
527
|
manager.add_component(AiohttpClientService())
|
|
@@ -527,7 +534,10 @@ class Server(Service, RouterMixin):
|
|
|
527
534
|
):
|
|
528
535
|
if manager is None:
|
|
529
536
|
manager = it(Launart)
|
|
537
|
+
manager.add_component(self.asgi_service)
|
|
530
538
|
manager.add_component(self)
|
|
539
|
+
with suppress(ValueError):
|
|
540
|
+
manager.add_component(AiohttpClientService())
|
|
531
541
|
handled_signals: dict[signal.Signals, Any] = {}
|
|
532
542
|
launch_task = asyncio.create_task(manager.launch(), name="amnesia-launch")
|
|
533
543
|
signal_handler = functools.partial(manager._on_sys_signal, main_task=launch_task)
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
from abc import abstractmethod
|
|
2
|
-
from
|
|
3
|
-
from typing import TYPE_CHECKING, Optional, Union
|
|
2
|
+
from typing import TYPE_CHECKING, Union
|
|
4
3
|
|
|
5
4
|
from launart import Service
|
|
6
5
|
from starlette.responses import Response
|
|
7
6
|
from starlette.routing import BaseRoute
|
|
8
7
|
|
|
9
|
-
from satori.model import
|
|
8
|
+
from satori.model import Login, LoginPartial
|
|
10
9
|
|
|
11
10
|
from .model import Request
|
|
12
11
|
from .route import RouterMixin
|
|
@@ -16,7 +15,7 @@ if TYPE_CHECKING:
|
|
|
16
15
|
from . import Server
|
|
17
16
|
|
|
18
17
|
|
|
19
|
-
LoginType = Union[Login, LoginPartial]
|
|
18
|
+
LoginType = Union[Login, LoginPartial] # noqa: UP007
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
class Adapter(Service, RouterMixin):
|
|
@@ -25,9 +24,6 @@ class Adapter(Service, RouterMixin):
|
|
|
25
24
|
@abstractmethod
|
|
26
25
|
def get_platform(self) -> str: ...
|
|
27
26
|
|
|
28
|
-
@abstractmethod
|
|
29
|
-
def publisher(self) -> AsyncIterator[Event]: ...
|
|
30
|
-
|
|
31
27
|
@abstractmethod
|
|
32
28
|
def ensure(self, platform: str, self_id: str) -> bool: ...
|
|
33
29
|
|
|
@@ -38,7 +34,7 @@ class Adapter(Service, RouterMixin):
|
|
|
38
34
|
@abstractmethod
|
|
39
35
|
async def handle_internal(self, request: Request, path: str) -> Response: ...
|
|
40
36
|
|
|
41
|
-
async def handle_proxied(self, prefix: str, url: str) ->
|
|
37
|
+
async def handle_proxied(self, prefix: str, url: str) -> Response | None:
|
|
42
38
|
async with self.server.session.get(url, ssl=ctx) as resp:
|
|
43
39
|
return Response(await resp.read())
|
|
44
40
|
|
|
@@ -6,6 +6,7 @@ from loguru import logger
|
|
|
6
6
|
from starlette.websockets import WebSocket, WebSocketDisconnect
|
|
7
7
|
|
|
8
8
|
from satori.model import Opcode
|
|
9
|
+
from satori.utils import decode, encode
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class WebsocketConnection:
|
|
@@ -22,12 +23,13 @@ class WebsocketConnection:
|
|
|
22
23
|
async def heartbeat(self):
|
|
23
24
|
while True:
|
|
24
25
|
try:
|
|
25
|
-
msg = await asyncio.wait_for(self.connection.
|
|
26
|
+
msg = await asyncio.wait_for(self.connection.receive_text(), timeout=12)
|
|
27
|
+
msg = decode(msg)
|
|
26
28
|
if not isinstance(msg, dict) or msg.get("op") != Opcode.PING:
|
|
27
29
|
continue
|
|
28
|
-
await self.connection.
|
|
30
|
+
await self.connection.send_text(encode({"op": Opcode.PONG}))
|
|
29
31
|
except asyncio.TimeoutError:
|
|
30
|
-
logger.warning(f"Connection {id(self)} heartbeat timeout, closing connection.")
|
|
32
|
+
logger.warning(f"Connection {id(self):x} heartbeat timeout, closing connection.")
|
|
31
33
|
await self.connection.close()
|
|
32
34
|
await self.connection_closed()
|
|
33
35
|
break
|
|
@@ -41,4 +43,4 @@ class WebsocketConnection:
|
|
|
41
43
|
return
|
|
42
44
|
|
|
43
45
|
async def send(self, payload: dict) -> None:
|
|
44
|
-
return await self.connection.
|
|
46
|
+
return await self.connection.send_text(encode(payload))
|
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
from collections.abc import AsyncIterator
|
|
2
1
|
from dataclasses import dataclass
|
|
3
|
-
from typing import TYPE_CHECKING, Any, Generic,
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeVar, runtime_checkable
|
|
4
3
|
|
|
5
4
|
from starlette.requests import Request as StarletteRequest
|
|
6
5
|
from starlette.responses import Response
|
|
7
6
|
|
|
8
7
|
from satori.const import Api
|
|
9
|
-
from satori.model import
|
|
8
|
+
from satori.model import Login
|
|
10
9
|
|
|
11
10
|
if TYPE_CHECKING:
|
|
12
11
|
from .route import RouteCall
|
|
13
12
|
|
|
14
|
-
JsonType = Union[list, dict, str, int, bool, float, None]
|
|
15
13
|
TA = TypeVar("TA", str, Api)
|
|
16
14
|
TP = TypeVar("TP")
|
|
17
15
|
|
|
@@ -27,8 +25,6 @@ class Request(Generic[TP]):
|
|
|
27
25
|
|
|
28
26
|
@runtime_checkable
|
|
29
27
|
class Provider(Protocol):
|
|
30
|
-
def publisher(self) -> AsyncIterator[Event]: ...
|
|
31
|
-
|
|
32
28
|
async def get_logins(self) -> list[Login]: ...
|
|
33
29
|
|
|
34
30
|
@staticmethod
|
|
@@ -38,7 +34,7 @@ class Provider(Protocol):
|
|
|
38
34
|
|
|
39
35
|
async def handle_internal(self, request: Request, path: str) -> Response: ...
|
|
40
36
|
|
|
41
|
-
async def handle_proxied(self, prefix: str, url: str) ->
|
|
37
|
+
async def handle_proxied(self, prefix: str, url: str) -> Response | None: ...
|
|
42
38
|
|
|
43
39
|
|
|
44
40
|
@runtime_checkable
|
|
@@ -49,4 +45,5 @@ class Router(Protocol):
|
|
|
49
45
|
@dataclass
|
|
50
46
|
class WebhookEndpoint:
|
|
51
47
|
url: str
|
|
52
|
-
token:
|
|
48
|
+
token: str | None = None
|
|
49
|
+
timeout: float | None = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
from collections.abc import Awaitable
|
|
2
|
-
from typing import Any,
|
|
3
|
-
from typing_extensions import NotRequired,
|
|
1
|
+
from collections.abc import Awaitable, Callable
|
|
2
|
+
from typing import Any, Literal, Protocol, TypeAlias, TypeVar, overload
|
|
3
|
+
from typing_extensions import NotRequired, TypedDict
|
|
4
4
|
|
|
5
5
|
from starlette.datastructures import FormData
|
|
6
6
|
|
|
@@ -30,9 +30,7 @@ class RouteCall(Protocol[T, R]):
|
|
|
30
30
|
def __call__(self, request: Request[T]) -> Awaitable[R]: ...
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
INTERAL: TypeAlias = RouteCall[
|
|
34
|
-
Any, Union[ModelBase, list[ModelBase], dict[str, Any], list[dict[str, Any]], None]
|
|
35
|
-
]
|
|
33
|
+
INTERAL: TypeAlias = RouteCall[Any, ModelBase | list[ModelBase] | dict[str, Any] | list[dict[str, Any]] | None]
|
|
36
34
|
|
|
37
35
|
|
|
38
36
|
class MessageParam(TypedDict):
|
|
@@ -40,7 +38,7 @@ class MessageParam(TypedDict):
|
|
|
40
38
|
content: str
|
|
41
39
|
|
|
42
40
|
|
|
43
|
-
MESSAGE_CREATE: TypeAlias = RouteCall[MessageParam,
|
|
41
|
+
MESSAGE_CREATE: TypeAlias = RouteCall[MessageParam, list[MessageObject] | list[dict[str, Any]]]
|
|
44
42
|
|
|
45
43
|
|
|
46
44
|
class MessageOpParam(TypedDict):
|
|
@@ -48,7 +46,7 @@ class MessageOpParam(TypedDict):
|
|
|
48
46
|
message_id: str
|
|
49
47
|
|
|
50
48
|
|
|
51
|
-
MESSAGE_GET: TypeAlias = RouteCall[MessageOpParam,
|
|
49
|
+
MESSAGE_GET: TypeAlias = RouteCall[MessageOpParam, MessageObject | dict[str, Any]]
|
|
52
50
|
MESSAGE_DELETE: TypeAlias = RouteCall[MessageOpParam, None]
|
|
53
51
|
|
|
54
52
|
|
|
@@ -69,14 +67,14 @@ class MessageListParam(TypedDict):
|
|
|
69
67
|
order: NotRequired[Order]
|
|
70
68
|
|
|
71
69
|
|
|
72
|
-
MESSAGE_LIST: TypeAlias = RouteCall[MessageListParam,
|
|
70
|
+
MESSAGE_LIST: TypeAlias = RouteCall[MessageListParam, PageDequeResult[MessageObject] | dict[str, Any]]
|
|
73
71
|
|
|
74
72
|
|
|
75
73
|
class ChannelParam(TypedDict):
|
|
76
74
|
channel_id: str
|
|
77
75
|
|
|
78
76
|
|
|
79
|
-
CHANNEL_GET: TypeAlias = RouteCall[ChannelParam,
|
|
77
|
+
CHANNEL_GET: TypeAlias = RouteCall[ChannelParam, Channel | dict[str, Any]]
|
|
80
78
|
CHANNEL_DELETE: TypeAlias = RouteCall[ChannelParam, None]
|
|
81
79
|
|
|
82
80
|
|
|
@@ -85,7 +83,7 @@ class ChannelListParam(TypedDict):
|
|
|
85
83
|
next: NotRequired[str]
|
|
86
84
|
|
|
87
85
|
|
|
88
|
-
CHANNEL_LIST: TypeAlias = RouteCall[ChannelListParam,
|
|
86
|
+
CHANNEL_LIST: TypeAlias = RouteCall[ChannelListParam, PageResult[Channel] | dict[str, Any]]
|
|
89
87
|
|
|
90
88
|
|
|
91
89
|
class ChannelCreateParam(TypedDict):
|
|
@@ -93,7 +91,7 @@ class ChannelCreateParam(TypedDict):
|
|
|
93
91
|
data: dict
|
|
94
92
|
|
|
95
93
|
|
|
96
|
-
CHANNEL_CREATE: TypeAlias = RouteCall[ChannelCreateParam,
|
|
94
|
+
CHANNEL_CREATE: TypeAlias = RouteCall[ChannelCreateParam, Channel | dict[str, Any]]
|
|
97
95
|
|
|
98
96
|
|
|
99
97
|
class ChannelUpdateParam(TypedDict):
|
|
@@ -117,21 +115,21 @@ class UserChannelCreateParam(TypedDict):
|
|
|
117
115
|
guild_id: NotRequired[str]
|
|
118
116
|
|
|
119
117
|
|
|
120
|
-
ROUTE_USER_CHANNEL_CREATE: TypeAlias = RouteCall[UserChannelCreateParam,
|
|
118
|
+
ROUTE_USER_CHANNEL_CREATE: TypeAlias = RouteCall[UserChannelCreateParam, Channel | dict[str, Any]]
|
|
121
119
|
|
|
122
120
|
|
|
123
121
|
class GuildGetParam(TypedDict):
|
|
124
122
|
guild_id: str
|
|
125
123
|
|
|
126
124
|
|
|
127
|
-
GUILD_GET: TypeAlias = RouteCall[GuildGetParam,
|
|
125
|
+
GUILD_GET: TypeAlias = RouteCall[GuildGetParam, Guild | dict[str, Any]]
|
|
128
126
|
|
|
129
127
|
|
|
130
128
|
class GuildListParam(TypedDict):
|
|
131
129
|
next: NotRequired[str]
|
|
132
130
|
|
|
133
131
|
|
|
134
|
-
GUILD_LIST: TypeAlias = RouteCall[GuildListParam,
|
|
132
|
+
GUILD_LIST: TypeAlias = RouteCall[GuildListParam, PageResult[Guild] | dict[str, Any]]
|
|
135
133
|
|
|
136
134
|
|
|
137
135
|
class GuildMemberGetParam(TypedDict):
|
|
@@ -139,7 +137,7 @@ class GuildMemberGetParam(TypedDict):
|
|
|
139
137
|
user_id: str
|
|
140
138
|
|
|
141
139
|
|
|
142
|
-
GUILD_MEMBER_GET: TypeAlias = RouteCall[GuildMemberGetParam,
|
|
140
|
+
GUILD_MEMBER_GET: TypeAlias = RouteCall[GuildMemberGetParam, Member | dict[str, Any]]
|
|
143
141
|
|
|
144
142
|
|
|
145
143
|
class GuildXXXListParam(TypedDict):
|
|
@@ -147,7 +145,7 @@ class GuildXXXListParam(TypedDict):
|
|
|
147
145
|
next: NotRequired[str]
|
|
148
146
|
|
|
149
147
|
|
|
150
|
-
GUILD_MEMBER_LIST: TypeAlias = RouteCall[GuildXXXListParam,
|
|
148
|
+
GUILD_MEMBER_LIST: TypeAlias = RouteCall[GuildXXXListParam, PageResult[Member] | dict[str, Any]]
|
|
151
149
|
|
|
152
150
|
|
|
153
151
|
class GuildMemberKickParam(TypedDict):
|
|
@@ -177,7 +175,7 @@ class GuildMemberRoleParam(TypedDict):
|
|
|
177
175
|
GUILD_MEMBER_ROLE_SET: TypeAlias = RouteCall[GuildMemberRoleParam, None]
|
|
178
176
|
GUILD_MEMBER_ROLE_UNSET: TypeAlias = RouteCall[GuildMemberRoleParam, None]
|
|
179
177
|
|
|
180
|
-
GUILD_ROLE_LIST: TypeAlias = RouteCall[GuildXXXListParam,
|
|
178
|
+
GUILD_ROLE_LIST: TypeAlias = RouteCall[GuildXXXListParam, PageResult[Role] | dict[str, Any]]
|
|
181
179
|
|
|
182
180
|
|
|
183
181
|
class GuildRoleCreateParam(TypedDict):
|
|
@@ -185,7 +183,7 @@ class GuildRoleCreateParam(TypedDict):
|
|
|
185
183
|
role: dict
|
|
186
184
|
|
|
187
185
|
|
|
188
|
-
GUILD_ROLE_CREATE: TypeAlias = RouteCall[GuildRoleCreateParam,
|
|
186
|
+
GUILD_ROLE_CREATE: TypeAlias = RouteCall[GuildRoleCreateParam, Role | dict[str, Any]]
|
|
189
187
|
|
|
190
188
|
|
|
191
189
|
class GuildRoleUpdateParam(TypedDict):
|
|
@@ -240,22 +238,22 @@ class ReactionListParam(TypedDict):
|
|
|
240
238
|
next: NotRequired[str]
|
|
241
239
|
|
|
242
240
|
|
|
243
|
-
REACTION_LIST: TypeAlias = RouteCall[ReactionListParam,
|
|
244
|
-
LOGIN_GET: TypeAlias = RouteCall[Any,
|
|
241
|
+
REACTION_LIST: TypeAlias = RouteCall[ReactionListParam, PageResult[User] | dict[str, Any]]
|
|
242
|
+
LOGIN_GET: TypeAlias = RouteCall[Any, Login | dict[str, Any]]
|
|
245
243
|
|
|
246
244
|
|
|
247
245
|
class UserGetParam(TypedDict):
|
|
248
246
|
user_id: str
|
|
249
247
|
|
|
250
248
|
|
|
251
|
-
USER_GET: TypeAlias = RouteCall[UserGetParam,
|
|
249
|
+
USER_GET: TypeAlias = RouteCall[UserGetParam, User | dict[str, Any]]
|
|
252
250
|
|
|
253
251
|
|
|
254
252
|
class FriendListParam(TypedDict):
|
|
255
253
|
next: NotRequired[str]
|
|
256
254
|
|
|
257
255
|
|
|
258
|
-
FRIEND_LIST: TypeAlias = RouteCall[FriendListParam,
|
|
256
|
+
FRIEND_LIST: TypeAlias = RouteCall[FriendListParam, PageResult[User] | dict[str, Any]]
|
|
259
257
|
|
|
260
258
|
|
|
261
259
|
class ApproveParam(TypedDict):
|
|
@@ -321,24 +319,16 @@ class RouterMixin:
|
|
|
321
319
|
def route(self, path: Literal[Api.GUILD_APPROVE]) -> Callable[[APPROVE], APPROVE]: ...
|
|
322
320
|
|
|
323
321
|
@overload
|
|
324
|
-
def route(
|
|
325
|
-
self, path: Literal[Api.GUILD_MEMBER_LIST]
|
|
326
|
-
) -> Callable[[GUILD_MEMBER_LIST], GUILD_MEMBER_LIST]: ...
|
|
322
|
+
def route(self, path: Literal[Api.GUILD_MEMBER_LIST]) -> Callable[[GUILD_MEMBER_LIST], GUILD_MEMBER_LIST]: ...
|
|
327
323
|
|
|
328
324
|
@overload
|
|
329
|
-
def route(
|
|
330
|
-
self, path: Literal[Api.GUILD_MEMBER_GET]
|
|
331
|
-
) -> Callable[[GUILD_MEMBER_GET], GUILD_MEMBER_GET]: ...
|
|
325
|
+
def route(self, path: Literal[Api.GUILD_MEMBER_GET]) -> Callable[[GUILD_MEMBER_GET], GUILD_MEMBER_GET]: ...
|
|
332
326
|
|
|
333
327
|
@overload
|
|
334
|
-
def route(
|
|
335
|
-
self, path: Literal[Api.GUILD_MEMBER_KICK]
|
|
336
|
-
) -> Callable[[GUILD_MEMBER_KICK], GUILD_MEMBER_KICK]: ...
|
|
328
|
+
def route(self, path: Literal[Api.GUILD_MEMBER_KICK]) -> Callable[[GUILD_MEMBER_KICK], GUILD_MEMBER_KICK]: ...
|
|
337
329
|
|
|
338
330
|
@overload
|
|
339
|
-
def route(
|
|
340
|
-
self, path: Literal[Api.GUILD_MEMBER_MUTE]
|
|
341
|
-
) -> Callable[[GUILD_MEMBER_MUTE], GUILD_MEMBER_MUTE]: ...
|
|
331
|
+
def route(self, path: Literal[Api.GUILD_MEMBER_MUTE]) -> Callable[[GUILD_MEMBER_MUTE], GUILD_MEMBER_MUTE]: ...
|
|
342
332
|
|
|
343
333
|
@overload
|
|
344
334
|
def route(self, path: Literal[Api.GUILD_MEMBER_APPROVE]) -> Callable[[APPROVE], APPROVE]: ...
|
|
@@ -357,19 +347,13 @@ class RouterMixin:
|
|
|
357
347
|
def route(self, path: Literal[Api.GUILD_ROLE_LIST]) -> Callable[[GUILD_ROLE_LIST], GUILD_ROLE_LIST]: ...
|
|
358
348
|
|
|
359
349
|
@overload
|
|
360
|
-
def route(
|
|
361
|
-
self, path: Literal[Api.GUILD_ROLE_CREATE]
|
|
362
|
-
) -> Callable[[GUILD_ROLE_CREATE], GUILD_ROLE_CREATE]: ...
|
|
350
|
+
def route(self, path: Literal[Api.GUILD_ROLE_CREATE]) -> Callable[[GUILD_ROLE_CREATE], GUILD_ROLE_CREATE]: ...
|
|
363
351
|
|
|
364
352
|
@overload
|
|
365
|
-
def route(
|
|
366
|
-
self, path: Literal[Api.GUILD_ROLE_UPDATE]
|
|
367
|
-
) -> Callable[[GUILD_ROLE_UPDATE], GUILD_ROLE_UPDATE]: ...
|
|
353
|
+
def route(self, path: Literal[Api.GUILD_ROLE_UPDATE]) -> Callable[[GUILD_ROLE_UPDATE], GUILD_ROLE_UPDATE]: ...
|
|
368
354
|
|
|
369
355
|
@overload
|
|
370
|
-
def route(
|
|
371
|
-
self, path: Literal[Api.GUILD_ROLE_DELETE]
|
|
372
|
-
) -> Callable[[GUILD_ROLE_DELETE], GUILD_ROLE_DELETE]: ...
|
|
356
|
+
def route(self, path: Literal[Api.GUILD_ROLE_DELETE]) -> Callable[[GUILD_ROLE_DELETE], GUILD_ROLE_DELETE]: ...
|
|
373
357
|
|
|
374
358
|
@overload
|
|
375
359
|
def route(self, path: Literal[Api.REACTION_CREATE]) -> Callable[[REACTION_CREATE], REACTION_CREATE]: ...
|
|
@@ -401,7 +385,7 @@ class RouterMixin:
|
|
|
401
385
|
@overload
|
|
402
386
|
def route(self, path: str) -> Callable[[INTERAL], INTERAL]: ...
|
|
403
387
|
|
|
404
|
-
def route(self, path:
|
|
388
|
+
def route(self, path: str | Api) -> Callable[[RouteCall], RouteCall]:
|
|
405
389
|
"""注册一个 Satori 路由
|
|
406
390
|
|
|
407
391
|
Args:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|