satori-python 0.16.3__tar.gz → 0.16.5__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-0.16.3 → satori_python-0.16.5}/PKG-INFO +15 -1
- {satori_python-0.16.3 → satori_python-0.16.5}/README.md +14 -0
- {satori_python-0.16.3 → satori_python-0.16.5}/pyproject.toml +2 -1
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/__init__.py +1 -1
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/client/__init__.py +27 -0
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/client/network/base.py +1 -1
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/element.py +5 -2
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/model.py +1 -1
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/parser.py +2 -2
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/server/__init__.py +80 -21
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/server/route.py +6 -6
- {satori_python-0.16.3 → satori_python-0.16.5}/LICENSE +0 -0
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/client/account.py +0 -0
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/client/account.pyi +0 -0
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/client/config.py +0 -0
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/client/network/__init__.py +0 -0
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/client/network/util.py +0 -0
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/client/network/webhook.py +0 -0
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/client/network/websocket.py +0 -0
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/client/protocol.py +0 -0
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/const.py +0 -0
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/event.py +0 -0
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/exception.py +0 -0
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/server/adapter.py +0 -0
- /satori_python-0.16.3/src/satori/server/conection.py → /satori_python-0.16.5/src/satori/server/connection.py +0 -0
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/server/formdata.py +0 -0
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/server/model.py +0 -0
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/server/utils.py +0 -0
- {satori_python-0.16.3 → satori_python-0.16.5}/src/satori/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: satori-python
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.5
|
|
4
4
|
Summary: Satori Protocol SDK for python
|
|
5
5
|
Home-page: https://github.com/RF-Tar-Railt/satori-python
|
|
6
6
|
Author-Email: RF-Tar-Railt <rf_tar_railt@qq.com>
|
|
@@ -75,6 +75,20 @@ pip install satori-python-client
|
|
|
75
75
|
pip install satori-python-server
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
### 官方适配器
|
|
79
|
+
|
|
80
|
+
| 适配器 | 安装 | 路径 |
|
|
81
|
+
|------------|----------------------------------------------|--------------------------------------------------------------------|
|
|
82
|
+
| Satori | `pip install satori-python-adapter-satori` | satori.adapters.satori |
|
|
83
|
+
| OneBot V11 | `pip install satori-python-adapter-onebot11` | satori.adapters.onebot11.forward, satori.adapters.onebot11.reverse |
|
|
84
|
+
| Console | `pip install satori-python-adapter-console` | satori.adapters.console |
|
|
85
|
+
|
|
86
|
+
### 社区适配器
|
|
87
|
+
|
|
88
|
+
| 适配器 | 安装 | 路径 |
|
|
89
|
+
|-------------------|-----------------------|--------------|
|
|
90
|
+
| nekobox(Lagrange) | `pip install nekobox` | nekobox.main |
|
|
91
|
+
|
|
78
92
|
## 使用
|
|
79
93
|
|
|
80
94
|
客户端:
|
|
@@ -45,6 +45,20 @@ pip install satori-python-client
|
|
|
45
45
|
pip install satori-python-server
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
### 官方适配器
|
|
49
|
+
|
|
50
|
+
| 适配器 | 安装 | 路径 |
|
|
51
|
+
|------------|----------------------------------------------|--------------------------------------------------------------------|
|
|
52
|
+
| Satori | `pip install satori-python-adapter-satori` | satori.adapters.satori |
|
|
53
|
+
| OneBot V11 | `pip install satori-python-adapter-onebot11` | satori.adapters.onebot11.forward, satori.adapters.onebot11.reverse |
|
|
54
|
+
| Console | `pip install satori-python-adapter-console` | satori.adapters.console |
|
|
55
|
+
|
|
56
|
+
### 社区适配器
|
|
57
|
+
|
|
58
|
+
| 适配器 | 安装 | 路径 |
|
|
59
|
+
|-------------------|-----------------------|--------------|
|
|
60
|
+
| nekobox(Lagrange) | `pip install nekobox` | nekobox.main |
|
|
61
|
+
|
|
48
62
|
## 使用
|
|
49
63
|
|
|
50
64
|
客户端:
|
|
@@ -29,7 +29,7 @@ classifiers = [
|
|
|
29
29
|
"Programming Language :: Python :: 3.12",
|
|
30
30
|
"Operating System :: OS Independent",
|
|
31
31
|
]
|
|
32
|
-
version = "0.16.
|
|
32
|
+
version = "0.16.5"
|
|
33
33
|
|
|
34
34
|
[project.license]
|
|
35
35
|
text = "MIT"
|
|
@@ -54,6 +54,7 @@ dev = [
|
|
|
54
54
|
"fix-future-annotations>=0.5.0",
|
|
55
55
|
"mina-build<0.6,>=0.5.1",
|
|
56
56
|
"pdm-mina>=0.3.2",
|
|
57
|
+
"nonechat<0.7.0,>=0.6.0",
|
|
57
58
|
]
|
|
58
59
|
|
|
59
60
|
[tool.pdm.build]
|
|
@@ -35,6 +35,28 @@ MAPPING: dict[type[Config], type[BaseNetwork]] = {
|
|
|
35
35
|
WebsocketsInfo: WsNetwork,
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
_app: App | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_accounts() -> dict[str, Account]:
|
|
42
|
+
if _app is None:
|
|
43
|
+
raise RuntimeError("App instance is not initialized.")
|
|
44
|
+
return _app.accounts
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_app() -> App:
|
|
48
|
+
"""Get the current App instance."""
|
|
49
|
+
if _app is None:
|
|
50
|
+
raise RuntimeError("App instance is not initialized.")
|
|
51
|
+
return _app
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_account(self_id: str) -> Account:
|
|
55
|
+
"""Get an account by its self_id."""
|
|
56
|
+
if _app is None:
|
|
57
|
+
raise RuntimeError("App instance is not initialized.")
|
|
58
|
+
return _app.get_account(self_id)
|
|
59
|
+
|
|
38
60
|
|
|
39
61
|
class App(Service):
|
|
40
62
|
id = "satori-python.client"
|
|
@@ -51,6 +73,10 @@ class App(Service):
|
|
|
51
73
|
MAPPING[tc] = tn
|
|
52
74
|
|
|
53
75
|
def __init__(self, *configs: Config, default_api_cls: type[ApiProtocol] = ApiProtocol):
|
|
76
|
+
global _app
|
|
77
|
+
|
|
78
|
+
if _app is not None:
|
|
79
|
+
raise RuntimeError("App instance already exists. Only one App instance is allowed.")
|
|
54
80
|
self.accounts = {}
|
|
55
81
|
self.connections = []
|
|
56
82
|
self.event_callbacks = []
|
|
@@ -59,6 +85,7 @@ class App(Service):
|
|
|
59
85
|
for config in configs:
|
|
60
86
|
self.apply(config)
|
|
61
87
|
self.default_api_cls = default_api_cls
|
|
88
|
+
_app = self
|
|
62
89
|
|
|
63
90
|
def apply(self, config: Config):
|
|
64
91
|
try:
|
|
@@ -180,6 +180,7 @@ class Resource(Element):
|
|
|
180
180
|
raw: Optional[Union[bytes, BytesIO]] = None,
|
|
181
181
|
mime: Optional[str] = None,
|
|
182
182
|
name: Optional[str] = None,
|
|
183
|
+
duration: Optional[float] = None,
|
|
183
184
|
poster: Optional[str] = None,
|
|
184
185
|
extra: Optional[dict[str, Any]] = None,
|
|
185
186
|
cache: Optional[bool] = None,
|
|
@@ -198,6 +199,8 @@ class Resource(Element):
|
|
|
198
199
|
raise ValueError(f"{cls} need at least one of url, path and raw")
|
|
199
200
|
if name is not None:
|
|
200
201
|
data["title"] = name
|
|
202
|
+
if duration is not None and cls is Audio:
|
|
203
|
+
data["duration"] = duration
|
|
201
204
|
if poster is not None and cls in (Video, Audio, File):
|
|
202
205
|
data["poster"] = poster
|
|
203
206
|
if cache is not None:
|
|
@@ -231,7 +234,7 @@ class Image(Resource):
|
|
|
231
234
|
class Audio(Resource):
|
|
232
235
|
"""<audio> 元素用于表示语音。"""
|
|
233
236
|
|
|
234
|
-
duration: Optional[
|
|
237
|
+
duration: Optional[float] = None
|
|
235
238
|
poster: Optional[str] = None
|
|
236
239
|
|
|
237
240
|
__names__ = ("src", "title", "duration", "poster")
|
|
@@ -243,7 +246,7 @@ class Video(Resource):
|
|
|
243
246
|
|
|
244
247
|
width: Optional[int] = None
|
|
245
248
|
height: Optional[int] = None
|
|
246
|
-
duration: Optional[
|
|
249
|
+
duration: Optional[float] = None
|
|
247
250
|
poster: Optional[str] = None
|
|
248
251
|
|
|
249
252
|
__names__ = ("src", "title", "width", "height", "duration", "poster")
|
|
@@ -358,9 +358,9 @@ def parse(src: str, context: Optional[dict] = None):
|
|
|
358
358
|
def parse_content(source: str, _start: bool, _end: bool):
|
|
359
359
|
source = unescape(source)
|
|
360
360
|
if _start:
|
|
361
|
-
source = re.sub(r"^\s*\n\s*", "", source, re.MULTILINE)
|
|
361
|
+
source = re.sub(r"^\s*\n\s*", "", source, flags=re.MULTILINE)
|
|
362
362
|
if _end:
|
|
363
|
-
source = re.sub(r"\s*\n\s*$", "", source, re.MULTILINE)
|
|
363
|
+
source = re.sub(r"\s*\n\s*$", "", source, flags=re.MULTILINE)
|
|
364
364
|
push_text(source)
|
|
365
365
|
|
|
366
366
|
tag_pat = tag_pat2 if context is not None else tag_pat1
|
|
@@ -8,30 +8,29 @@ import secrets
|
|
|
8
8
|
import signal
|
|
9
9
|
import threading
|
|
10
10
|
import urllib.parse
|
|
11
|
-
from collections.abc import Iterable
|
|
11
|
+
from collections.abc import Awaitable, 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, Callable, TypeVar
|
|
18
18
|
|
|
19
19
|
import aiohttp
|
|
20
20
|
from creart import it
|
|
21
|
-
from graia.amnesia.builtins.
|
|
21
|
+
from graia.amnesia.builtins.aiohttp import AiohttpClientService
|
|
22
|
+
from graia.amnesia.builtins.asgi import UvicornASGIService, asgitypes
|
|
22
23
|
from launart import Launart, Service, any_completed
|
|
23
24
|
from loguru import logger
|
|
24
25
|
from starlette.applications import Starlette
|
|
25
26
|
from starlette.datastructures import FormData as FormData
|
|
26
27
|
from starlette.requests import Request as StarletteRequest
|
|
27
|
-
from starlette.responses import
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
StreamingResponse,
|
|
34
|
-
)
|
|
28
|
+
from starlette.responses import FileResponse as FileResponse
|
|
29
|
+
from starlette.responses import HTMLResponse as HTMLResponse
|
|
30
|
+
from starlette.responses import JSONResponse as JSONResponse
|
|
31
|
+
from starlette.responses import PlainTextResponse as PlainTextResponse
|
|
32
|
+
from starlette.responses import Response as Response
|
|
33
|
+
from starlette.responses import StreamingResponse as StreamingResponse
|
|
35
34
|
from starlette.routing import Route, WebSocketRoute
|
|
36
35
|
from starlette.staticfiles import StaticFiles
|
|
37
36
|
from starlette.websockets import WebSocket, WebSocketDisconnect
|
|
@@ -42,7 +41,7 @@ from satori.model import Event, Meta, ModelBase, Opcode
|
|
|
42
41
|
|
|
43
42
|
from .. import EventType
|
|
44
43
|
from .adapter import Adapter as Adapter
|
|
45
|
-
from .
|
|
44
|
+
from .connection import WebsocketConnection
|
|
46
45
|
from .formdata import parse_content_disposition as parse_content_disposition
|
|
47
46
|
from .model import Provider as Provider
|
|
48
47
|
from .model import Request as Request
|
|
@@ -52,6 +51,10 @@ from .route import RouteCall as RouteCall
|
|
|
52
51
|
from .route import RouterMixin as RouterMixin
|
|
53
52
|
from .utils import Deque
|
|
54
53
|
|
|
54
|
+
_T_endpoint = TypeVar("_T_endpoint", bound=Callable[[StarletteRequest], Awaitable[Response] | Response])
|
|
55
|
+
_T_ws_endpoint = TypeVar("_T_ws_endpoint", bound=Callable[[WebSocket], Awaitable[None]])
|
|
56
|
+
StarletteResponse = Response
|
|
57
|
+
|
|
55
58
|
|
|
56
59
|
async def _request_handler(
|
|
57
60
|
action: str, request: StarletteRequest, func: RouteCall, platform: str, self_id: str
|
|
@@ -115,13 +118,12 @@ class Server(Service, RouterMixin):
|
|
|
115
118
|
stream_chunk_size: int = 64 * 1024,
|
|
116
119
|
):
|
|
117
120
|
self.connections = []
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
self.host = host
|
|
122
|
+
self.port = port
|
|
120
123
|
self.version = version
|
|
121
124
|
self.path = path
|
|
122
125
|
if self.path and not self.path.startswith("/"):
|
|
123
126
|
self.path = f"/{self.path}"
|
|
124
|
-
self.url_base = f"http://{host}:{port}{self.path}/{version}"
|
|
125
127
|
self.token = token
|
|
126
128
|
self._adapters = []
|
|
127
129
|
self.providers = []
|
|
@@ -134,8 +136,59 @@ class Server(Service, RouterMixin):
|
|
|
134
136
|
self.stream_threshold = stream_threshold
|
|
135
137
|
self.stream_chunk_size = stream_chunk_size
|
|
136
138
|
self.resources: dict[str, Path] = {}
|
|
139
|
+
self.app = Starlette()
|
|
137
140
|
super().__init__()
|
|
138
141
|
|
|
142
|
+
def replace_app(self, app: asgitypes.ASGI3Application):
|
|
143
|
+
"""替换当前的 Starlette 应用"""
|
|
144
|
+
self.app = app
|
|
145
|
+
|
|
146
|
+
def asgi_route(
|
|
147
|
+
self,
|
|
148
|
+
path: str,
|
|
149
|
+
methods: list[str] | None = None,
|
|
150
|
+
name: str | None = None,
|
|
151
|
+
include_in_schema: bool = True,
|
|
152
|
+
) -> Callable[[_T_endpoint], _T_endpoint]:
|
|
153
|
+
"""注册一个 ASGI 路由
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
path (str): 路由路径
|
|
157
|
+
methods (list[str], optional): 支持的 HTTP 方法,默认为 None,表示 ["GET"]
|
|
158
|
+
name (str, optional): 路由名称,默认为 None
|
|
159
|
+
include_in_schema (bool, optional): 是否包含在 OpenAPI 文档中,默认为 True
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
def wrapper(endpoint: _T_endpoint, /) -> _T_endpoint:
|
|
163
|
+
self.app.add_route(
|
|
164
|
+
path, endpoint, methods=methods, name=name, include_in_schema=include_in_schema
|
|
165
|
+
)
|
|
166
|
+
return endpoint
|
|
167
|
+
|
|
168
|
+
return wrapper
|
|
169
|
+
|
|
170
|
+
def asgi_websocket_route(
|
|
171
|
+
self,
|
|
172
|
+
path: str,
|
|
173
|
+
name: str | None = None,
|
|
174
|
+
) -> Callable[[_T_ws_endpoint], _T_ws_endpoint]:
|
|
175
|
+
"""注册一个 ASGI WebSocket 路由
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
path (str): 路由路径
|
|
179
|
+
name (str, optional): 路由名称,默认为 None
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def wrapper(endpoint: _T_ws_endpoint, /) -> _T_ws_endpoint:
|
|
183
|
+
self.app.add_websocket_route(path, endpoint, name=name)
|
|
184
|
+
return endpoint
|
|
185
|
+
|
|
186
|
+
return wrapper
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def url_base(self):
|
|
190
|
+
return f"http://{self.host}:{self.port}{self.path}/{self.version}"
|
|
191
|
+
|
|
139
192
|
def apply(self, item: Provider | Router | Adapter):
|
|
140
193
|
if isinstance(item, Adapter):
|
|
141
194
|
item.ensure_server(self)
|
|
@@ -157,9 +210,11 @@ class Server(Service, RouterMixin):
|
|
|
157
210
|
self._event_cache.append(event)
|
|
158
211
|
self._sequence += 1
|
|
159
212
|
for connection in self.connections:
|
|
213
|
+
if not connection.alive:
|
|
214
|
+
continue
|
|
160
215
|
try:
|
|
161
216
|
await connection.send({"op": Opcode.EVENT, "body": event.dump()})
|
|
162
|
-
except WebSocketDisconnect:
|
|
217
|
+
except (WebSocketDisconnect, RuntimeError):
|
|
163
218
|
break
|
|
164
219
|
except Exception as e:
|
|
165
220
|
print_exc()
|
|
@@ -287,13 +342,14 @@ class Server(Service, RouterMixin):
|
|
|
287
342
|
file = Path(self._tempdir.name) / path[5:]
|
|
288
343
|
if file.exists():
|
|
289
344
|
return FileResponse(file)
|
|
290
|
-
raise FileNotFoundError(f"{path[5:]} not found")
|
|
291
345
|
assert request is not None
|
|
292
346
|
for provider in self.providers:
|
|
293
347
|
if provider.ensure(platform, self_id):
|
|
294
348
|
return await provider.handle_internal(
|
|
295
349
|
Request(request, "internal", {}, platform=platform, self_id=self_id), path
|
|
296
350
|
)
|
|
351
|
+
if path.startswith("_tmp"):
|
|
352
|
+
raise FileNotFoundError(f"File not found: {path[5:]}")
|
|
297
353
|
raise NotImplementedError(f"Login with {platform}:{self_id} not found")
|
|
298
354
|
raise TypeError(f"Invalid internal url: {url}")
|
|
299
355
|
|
|
@@ -382,8 +438,8 @@ class Server(Service, RouterMixin):
|
|
|
382
438
|
|
|
383
439
|
async with self.stage("preparing"):
|
|
384
440
|
asgi_service = manager.get_component(UvicornASGIService)
|
|
385
|
-
app
|
|
386
|
-
|
|
441
|
+
self.app.routes.extend(
|
|
442
|
+
[
|
|
387
443
|
*chain.from_iterable(ada.get_routes() for ada in self._adapters),
|
|
388
444
|
WebSocketRoute(f"{self.path}/{self.version}/events", self.websocket_server_handler),
|
|
389
445
|
Route(
|
|
@@ -414,8 +470,8 @@ class Server(Service, RouterMixin):
|
|
|
414
470
|
]
|
|
415
471
|
)
|
|
416
472
|
for path, file in self.resources.items():
|
|
417
|
-
app.mount(path, StaticFiles(directory=file.parent, html=file.suffix == ".html"))
|
|
418
|
-
asgi_service.middleware.mounts[""] = app # type: ignore
|
|
473
|
+
self.app.mount(path, StaticFiles(directory=file.parent, html=file.suffix == ".html"))
|
|
474
|
+
asgi_service.middleware.mounts[""] = self.app # type: ignore
|
|
419
475
|
|
|
420
476
|
async def event_task(_provider: Provider):
|
|
421
477
|
async for event in _provider.publisher():
|
|
@@ -457,7 +513,10 @@ class Server(Service, RouterMixin):
|
|
|
457
513
|
):
|
|
458
514
|
if manager is None:
|
|
459
515
|
manager = it(Launart)
|
|
516
|
+
manager.add_component(UvicornASGIService(self.host, self.port))
|
|
460
517
|
manager.add_component(self)
|
|
518
|
+
with suppress(ValueError):
|
|
519
|
+
manager.add_component(AiohttpClientService())
|
|
461
520
|
manager.launch_blocking(loop=loop, stop_signal=stop_signal)
|
|
462
521
|
|
|
463
522
|
async def run_async(
|
|
@@ -88,20 +88,20 @@ class ChannelListParam(TypedDict):
|
|
|
88
88
|
CHANNEL_LIST: TypeAlias = RouteCall[ChannelListParam, Union[PageResult[Channel], dict[str, Any]]]
|
|
89
89
|
|
|
90
90
|
|
|
91
|
-
class
|
|
91
|
+
class ChannelCreateParam(TypedDict):
|
|
92
92
|
guild_id: str
|
|
93
93
|
data: dict
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
CHANNEL_CREATE: TypeAlias = RouteCall[
|
|
96
|
+
CHANNEL_CREATE: TypeAlias = RouteCall[ChannelCreateParam, Union[Channel, dict[str, Any]]]
|
|
97
97
|
|
|
98
98
|
|
|
99
|
-
class
|
|
99
|
+
class ChannelUpdateParam(TypedDict):
|
|
100
100
|
channel_id: str
|
|
101
101
|
data: dict
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
CHANNEL_UPDATE: TypeAlias = RouteCall[
|
|
104
|
+
CHANNEL_UPDATE: TypeAlias = RouteCall[ChannelUpdateParam, None]
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
class ChannelMuteParam(TypedDict):
|
|
@@ -261,7 +261,7 @@ FRIEND_LIST: TypeAlias = RouteCall[FriendListParam, Union[PageResult[User], dict
|
|
|
261
261
|
class ApproveParam(TypedDict):
|
|
262
262
|
message_id: str
|
|
263
263
|
approve: bool
|
|
264
|
-
comment: str
|
|
264
|
+
comment: NotRequired[str]
|
|
265
265
|
|
|
266
266
|
|
|
267
267
|
APPROVE: TypeAlias = RouteCall[ApproveParam, None]
|
|
@@ -402,7 +402,7 @@ class RouterMixin:
|
|
|
402
402
|
def route(self, path: str) -> Callable[[INTERAL], INTERAL]: ...
|
|
403
403
|
|
|
404
404
|
def route(self, path: Union[str, Api]) -> Callable[[RouteCall], RouteCall]:
|
|
405
|
-
"""
|
|
405
|
+
"""注册一个 Satori 路由
|
|
406
406
|
|
|
407
407
|
Args:
|
|
408
408
|
path (str | Api): 路由路径;若 path 不属于 Api,则会被认为是内部接口
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|