satori-python-server 0.14.4__tar.gz → 0.15.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.
- {satori_python_server-0.14.4 → satori_python_server-0.15.0}/.mina/server.toml +1 -1
- {satori_python_server-0.14.4 → satori_python_server-0.15.0}/PKG-INFO +3 -2
- {satori_python_server-0.14.4 → satori_python_server-0.15.0}/README.md +1 -0
- {satori_python_server-0.14.4 → satori_python_server-0.15.0}/pyproject.toml +3 -2
- {satori_python_server-0.14.4 → satori_python_server-0.15.0}/src/satori/server/__init__.py +44 -24
- {satori_python_server-0.14.4 → satori_python_server-0.15.0}/src/satori/server/adapter.py +4 -3
- {satori_python_server-0.14.4 → satori_python_server-0.15.0}/src/satori/server/model.py +3 -6
- {satori_python_server-0.14.4 → satori_python_server-0.15.0}/src/satori/server/route.py +2 -2
- satori_python_server-0.14.4/src/satori/server/deque.py → satori_python_server-0.15.0/src/satori/server/utils.py +5 -0
- {satori_python_server-0.14.4 → satori_python_server-0.15.0}/LICENSE +0 -0
- {satori_python_server-0.14.4 → satori_python_server-0.15.0}/src/satori/server/conection.py +0 -0
- {satori_python_server-0.14.4 → satori_python_server-0.15.0}/src/satori/server/formdata.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: satori-python-server
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.0
|
|
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>
|
|
@@ -23,7 +23,7 @@ Requires-Dist: graia-amnesia>=0.9.0
|
|
|
23
23
|
Requires-Dist: starlette[python-multipart]>=0.37.2
|
|
24
24
|
Requires-Dist: uvicorn[standard]>=0.28.0
|
|
25
25
|
Requires-Dist: python-multipart>=0.0.9
|
|
26
|
-
Requires-Dist: satori-python-core>=0.
|
|
26
|
+
Requires-Dist: satori-python-core>=0.15.0
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
|
|
29
29
|
# satori-python
|
|
@@ -44,6 +44,7 @@ Description-Content-Type: text/markdown
|
|
|
44
44
|
目前提供了 `satori` 协议实现的有:
|
|
45
45
|
|
|
46
46
|
- [Chronocat](https://chronocat.vercel.app)
|
|
47
|
+
- [nekobox](https://github.com/wyapx/nekobox)
|
|
47
48
|
- Koishi (搭配 `@koishijs/plugin-server`)
|
|
48
49
|
|
|
49
50
|
### 使用该 SDK 的框架
|
|
@@ -11,7 +11,7 @@ dependencies = [
|
|
|
11
11
|
"starlette[python-multipart]>=0.37.2",
|
|
12
12
|
"uvicorn[standard]>=0.28.0",
|
|
13
13
|
"python-multipart>=0.0.9",
|
|
14
|
-
"satori-python-core >= 0.
|
|
14
|
+
"satori-python-core >= 0.15.0",
|
|
15
15
|
]
|
|
16
16
|
description = "Satori Protocol SDK for python, specify server part"
|
|
17
17
|
readme = "README.md"
|
|
@@ -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.15.0"
|
|
31
31
|
|
|
32
32
|
[project.license]
|
|
33
33
|
text = "MIT"
|
|
@@ -39,6 +39,7 @@ repository = "https://github.com/RF-Tar-Railt/satori-python"
|
|
|
39
39
|
[build-system]
|
|
40
40
|
requires = [
|
|
41
41
|
"mina-build<0.6,>=0.5.1",
|
|
42
|
+
"pdm-backend<2.4.0",
|
|
42
43
|
]
|
|
43
44
|
build-backend = "mina.backend"
|
|
44
45
|
|
|
@@ -22,9 +22,10 @@ from loguru import logger
|
|
|
22
22
|
from starlette.applications import Starlette
|
|
23
23
|
from starlette.datastructures import FormData as FormData
|
|
24
24
|
from starlette.requests import Request as StarletteRequest
|
|
25
|
-
from starlette.responses import JSONResponse, Response, StreamingResponse
|
|
25
|
+
from starlette.responses import FileResponse, JSONResponse, Response, StreamingResponse
|
|
26
26
|
from starlette.routing import Route, WebSocketRoute
|
|
27
|
-
from starlette.
|
|
27
|
+
from starlette.staticfiles import StaticFiles
|
|
28
|
+
from starlette.websockets import WebSocket, WebSocketDisconnect
|
|
28
29
|
from yarl import URL
|
|
29
30
|
|
|
30
31
|
from satori.config import WebhookInfo
|
|
@@ -33,13 +34,13 @@ from satori.model import Event, ModelBase, Opcode
|
|
|
33
34
|
|
|
34
35
|
from .adapter import Adapter as Adapter
|
|
35
36
|
from .conection import WebsocketConnection
|
|
36
|
-
from .deque import Deque
|
|
37
37
|
from .formdata import parse_content_disposition as parse_content_disposition
|
|
38
38
|
from .model import Provider as Provider
|
|
39
39
|
from .model import Request as Request
|
|
40
40
|
from .model import Router as Router
|
|
41
41
|
from .route import RouteCall as RouteCall
|
|
42
42
|
from .route import RouterMixin as RouterMixin
|
|
43
|
+
from .utils import Deque
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
async def _request_handler(method: str, request: StarletteRequest, func: RouteCall):
|
|
@@ -103,11 +104,11 @@ class Server(Service, RouterMixin):
|
|
|
103
104
|
self.routes = {}
|
|
104
105
|
self.webhooks = webhooks or []
|
|
105
106
|
self._tempdir = TemporaryDirectory()
|
|
106
|
-
self.proxy_url_mapping = {}
|
|
107
107
|
self._sequence = 0
|
|
108
108
|
self._event_cache = Deque(maxlen=100)
|
|
109
109
|
self.stream_threshold = stream_threshold
|
|
110
110
|
self.stream_chunk_size = stream_chunk_size
|
|
111
|
+
self.resources: dict[str, Path] = {}
|
|
111
112
|
super().__init__()
|
|
112
113
|
|
|
113
114
|
def apply(self, item: Provider | Router | Adapter):
|
|
@@ -115,15 +116,17 @@ class Server(Service, RouterMixin):
|
|
|
115
116
|
item.ensure_server(self)
|
|
116
117
|
self._adapters.append(item)
|
|
117
118
|
self.providers.append(item)
|
|
118
|
-
self.proxy_url_mapping[item.id] = item.proxy_urls()
|
|
119
119
|
elif isinstance(item, Provider):
|
|
120
120
|
self.providers.append(item)
|
|
121
|
-
self.proxy_url_mapping[item.id] = item.proxy_urls()
|
|
122
121
|
elif isinstance(item, Router):
|
|
123
122
|
self.routers.append(item)
|
|
124
123
|
else:
|
|
125
124
|
raise TypeError(f"Unknown config type: {item}")
|
|
126
125
|
|
|
126
|
+
def mount(self, route_path: str, file: Path):
|
|
127
|
+
"""在指定路径挂载静态文件"""
|
|
128
|
+
self.resources[route_path] = file
|
|
129
|
+
|
|
127
130
|
async def event_callback(self, event: Event):
|
|
128
131
|
event.id = self._sequence
|
|
129
132
|
self._event_cache.append(event)
|
|
@@ -131,6 +134,8 @@ class Server(Service, RouterMixin):
|
|
|
131
134
|
for connection in self.connections:
|
|
132
135
|
try:
|
|
133
136
|
await connection.send({"op": Opcode.EVENT, "body": event.dump()})
|
|
137
|
+
except WebSocketDisconnect:
|
|
138
|
+
break
|
|
134
139
|
except Exception as e:
|
|
135
140
|
print_exc()
|
|
136
141
|
logger.error(e)
|
|
@@ -141,8 +146,10 @@ class Server(Service, RouterMixin):
|
|
|
141
146
|
headers={
|
|
142
147
|
"Content-Type": "application/json",
|
|
143
148
|
"Authorization": f"Bearer {hook.token or ''}",
|
|
144
|
-
"X-Platform": event.
|
|
145
|
-
"
|
|
149
|
+
"X-Platform": event.platform_,
|
|
150
|
+
"Satori-Platform": event.platform_,
|
|
151
|
+
"X-Self-ID": event.self_id_,
|
|
152
|
+
"Satori-Login-ID": event.self_id_,
|
|
146
153
|
},
|
|
147
154
|
json={"op": Opcode.EVENT, "body": event.dump()},
|
|
148
155
|
) as resp:
|
|
@@ -191,19 +198,22 @@ class Server(Service, RouterMixin):
|
|
|
191
198
|
async def admin_login_list_handler(self, request: StarletteRequest):
|
|
192
199
|
logins = []
|
|
193
200
|
for provider in self.providers:
|
|
194
|
-
|
|
201
|
+
_logins = await provider.get_logins()
|
|
202
|
+
for _login in _logins:
|
|
203
|
+
_login.proxy_urls.extend(provider.proxy_urls())
|
|
204
|
+
logins.extend(_logins)
|
|
195
205
|
return JSONResponse(content=[lo.dump() for lo in logins])
|
|
196
206
|
|
|
197
207
|
async def http_server_handler(self, request: StarletteRequest):
|
|
198
208
|
if not self._adapters and not self.routes:
|
|
199
209
|
return Response(status_code=404, content=request.path_params["method"])
|
|
200
210
|
method = request.path_params["method"]
|
|
201
|
-
if "X-Platform" not in request.headers:
|
|
202
|
-
return Response(status_code=401, content="Missing X-Platform
|
|
203
|
-
platform = request.headers
|
|
204
|
-
if "X-Self-ID" not in request.headers:
|
|
205
|
-
return Response(status_code=401, content="Missing X-Self-ID
|
|
206
|
-
self_id = request.headers
|
|
211
|
+
if "X-Platform" not in request.headers and "Satori-Platform" not in request.headers:
|
|
212
|
+
return Response(status_code=401, content="Missing header X-Platform or Satori-Platform")
|
|
213
|
+
platform: str = request.headers.get("X-Platform") or request.headers.get("Satori-Platform") # type: ignore
|
|
214
|
+
if "X-Self-ID" not in request.headers and "Satori-Login-ID" not in request.headers:
|
|
215
|
+
return Response(status_code=401, content="Missing header X-Self-ID or Satori-Login-ID")
|
|
216
|
+
self_id: str = request.headers.get("X-Self-ID") or request.headers.get("Satori-Login-ID") # type: ignore
|
|
207
217
|
|
|
208
218
|
for _router in self._adapters:
|
|
209
219
|
if method not in _router.routes:
|
|
@@ -223,6 +233,8 @@ class Server(Service, RouterMixin):
|
|
|
223
233
|
url = request.path_params["upload_url"]
|
|
224
234
|
try:
|
|
225
235
|
content = await self.download(url)
|
|
236
|
+
if isinstance(content, Path):
|
|
237
|
+
return FileResponse(path=content)
|
|
226
238
|
# if content size > stream_limit, use streaming response
|
|
227
239
|
if len(content) > self.stream_threshold:
|
|
228
240
|
|
|
@@ -243,24 +255,30 @@ class Server(Service, RouterMixin):
|
|
|
243
255
|
return Response(status_code=500, content=repr(e))
|
|
244
256
|
|
|
245
257
|
async def download(self, url: str):
|
|
246
|
-
|
|
258
|
+
url = url.replace(":/", "://", 1).replace(":///", "://", 1)
|
|
259
|
+
pr = urllib.parse.urlparse(url)
|
|
247
260
|
if pr.scheme == "upload":
|
|
248
261
|
if pr.netloc == "temp":
|
|
249
262
|
_, inst, filename = pr.path.split("/", 2)
|
|
250
263
|
if inst == f"{self.id}:{id(self)}":
|
|
251
264
|
file = Path(self._tempdir.name) / filename
|
|
252
265
|
if file.exists():
|
|
253
|
-
return file
|
|
266
|
+
return file
|
|
254
267
|
raise FileNotFoundError(f"{filename} not found")
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
268
|
+
for provider in self.providers:
|
|
269
|
+
if pr.scheme == "upload":
|
|
270
|
+
platform = pr.netloc
|
|
271
|
+
_, self_id, path = pr.path.split("/", 2)
|
|
258
272
|
if provider.ensure(platform, self_id):
|
|
259
273
|
return await provider.download_uploaded(platform, self_id, path)
|
|
260
|
-
|
|
261
|
-
for proxy_url_pf in
|
|
262
|
-
if url.startswith(proxy_url_pf):
|
|
263
|
-
|
|
274
|
+
|
|
275
|
+
for proxy_url_pf in provider.proxy_urls():
|
|
276
|
+
if not url.startswith(proxy_url_pf):
|
|
277
|
+
continue
|
|
278
|
+
resp = await provider.download_proxied(proxy_url_pf, url)
|
|
279
|
+
if resp is None:
|
|
280
|
+
continue
|
|
281
|
+
return resp
|
|
264
282
|
raise ValueError(f"Unknown proxy url: {url}")
|
|
265
283
|
|
|
266
284
|
def get_local_file(self, url: str):
|
|
@@ -322,6 +340,8 @@ class Server(Service, RouterMixin):
|
|
|
322
340
|
),
|
|
323
341
|
]
|
|
324
342
|
)
|
|
343
|
+
for path, file in self.resources.items():
|
|
344
|
+
app.mount(path, StaticFiles(directory=file.parent, html=file.suffix == ".html"))
|
|
325
345
|
asgi_service.middleware.mounts[""] = app # type: ignore
|
|
326
346
|
|
|
327
347
|
async def event_task(_provider: Provider):
|
|
@@ -4,8 +4,9 @@ from typing import TYPE_CHECKING, Optional
|
|
|
4
4
|
|
|
5
5
|
from launart import Service
|
|
6
6
|
|
|
7
|
-
from ..model import Event,
|
|
7
|
+
from ..model import Event, LoginType
|
|
8
8
|
from .route import RouterMixin
|
|
9
|
+
from .utils import ctx
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
11
12
|
from . import Server
|
|
@@ -34,11 +35,11 @@ class Adapter(Service, RouterMixin):
|
|
|
34
35
|
async def download_uploaded(self, platform: str, self_id: str, path: str) -> bytes: ...
|
|
35
36
|
|
|
36
37
|
async def download_proxied(self, prefix: str, url: str) -> bytes:
|
|
37
|
-
async with self.server.session.get(url) as resp:
|
|
38
|
+
async with self.server.session.get(url, ssl=ctx) as resp:
|
|
38
39
|
return await resp.read()
|
|
39
40
|
|
|
40
41
|
@abstractmethod
|
|
41
|
-
async def get_logins(self) -> list[
|
|
42
|
+
async def get_logins(self) -> list[LoginType]: ...
|
|
42
43
|
|
|
43
44
|
def __init__(self):
|
|
44
45
|
super().__init__()
|
|
@@ -3,7 +3,7 @@ from dataclasses import dataclass
|
|
|
3
3
|
from typing import TYPE_CHECKING, Any, Generic, Optional, Protocol, TypeVar, Union, runtime_checkable
|
|
4
4
|
|
|
5
5
|
from satori.const import Api
|
|
6
|
-
from satori.model import Event, Login
|
|
6
|
+
from satori.model import Event, Login, LoginPreview, LoginType
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from .route import RouteCall
|
|
@@ -22,14 +22,11 @@ class Request(Generic[TP]):
|
|
|
22
22
|
|
|
23
23
|
@runtime_checkable
|
|
24
24
|
class Provider(Protocol):
|
|
25
|
-
@property
|
|
26
|
-
def id(self) -> str: ...
|
|
27
|
-
|
|
28
25
|
def publisher(self) -> AsyncIterator[Event]: ...
|
|
29
26
|
|
|
30
27
|
def authenticate(self, token: Optional[str]) -> bool: ...
|
|
31
28
|
|
|
32
|
-
async def get_logins(self) -> list[Login]: ...
|
|
29
|
+
async def get_logins(self) -> Union[list[Login], list[LoginPreview], list[LoginType]]: ...
|
|
33
30
|
|
|
34
31
|
@staticmethod
|
|
35
32
|
def proxy_urls() -> list[str]: ...
|
|
@@ -38,7 +35,7 @@ class Provider(Protocol):
|
|
|
38
35
|
|
|
39
36
|
async def download_uploaded(self, platform: str, self_id: str, path: str) -> bytes: ...
|
|
40
37
|
|
|
41
|
-
async def download_proxied(self, prefix: str, url: str) -> bytes: ...
|
|
38
|
+
async def download_proxied(self, prefix: str, url: str) -> Optional[bytes]: ...
|
|
42
39
|
|
|
43
40
|
|
|
44
41
|
@runtime_checkable
|
|
@@ -8,7 +8,7 @@ from satori.model import (
|
|
|
8
8
|
Channel,
|
|
9
9
|
Direction,
|
|
10
10
|
Guild,
|
|
11
|
-
|
|
11
|
+
LoginType,
|
|
12
12
|
Member,
|
|
13
13
|
MessageObject,
|
|
14
14
|
ModelBase,
|
|
@@ -241,7 +241,7 @@ class ReactionListParam(TypedDict):
|
|
|
241
241
|
|
|
242
242
|
|
|
243
243
|
REACTION_LIST: TypeAlias = RouteCall[ReactionListParam, Union[PageResult[User], dict[str, Any]]]
|
|
244
|
-
LOGIN_GET: TypeAlias = RouteCall[Any, Union[
|
|
244
|
+
LOGIN_GET: TypeAlias = RouteCall[Any, Union[LoginType, dict[str, Any]]]
|
|
245
245
|
|
|
246
246
|
|
|
247
247
|
class UserGetParam(TypedDict):
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import ssl
|
|
1
2
|
from collections import deque
|
|
2
3
|
|
|
3
4
|
|
|
@@ -23,6 +24,10 @@ class Deque:
|
|
|
23
24
|
return list(self.data)[i + 1 - self.offset :]
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
ctx = ssl.create_default_context()
|
|
28
|
+
ctx.set_ciphers("DEFAULT")
|
|
29
|
+
|
|
30
|
+
|
|
26
31
|
if __name__ == "__main__":
|
|
27
32
|
d = Deque(3)
|
|
28
33
|
d.append(0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|