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.
@@ -1,5 +1,5 @@
1
1
  includes = ["src/satori/server"]
2
- raw-dependencies = ["satori-python-core >= 0.11.4"]
2
+ raw-dependencies = ["satori-python-core >= 0.15.0"]
3
3
 
4
4
  [project]
5
5
  name = "satori-python-server"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: satori-python-server
3
- Version: 0.14.4
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.11.4
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 的框架
@@ -16,6 +16,7 @@
16
16
  目前提供了 `satori` 协议实现的有:
17
17
 
18
18
  - [Chronocat](https://chronocat.vercel.app)
19
+ - [nekobox](https://github.com/wyapx/nekobox)
19
20
  - Koishi (搭配 `@koishijs/plugin-server`)
20
21
 
21
22
  ### 使用该 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.11.4",
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.14.4"
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.websockets import WebSocket
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.platform,
145
- "X-Self-ID": event.self_id,
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
- logins.extend(await provider.get_logins())
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 header")
203
- platform = request.headers["X-Platform"]
204
- if "X-Self-ID" not in request.headers:
205
- return Response(status_code=401, content="Missing X-Self-ID header")
206
- self_id = request.headers["X-Self-ID"]
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
- pr = urllib.parse.urlparse(url.replace(":/", "://", 1).replace(":///", "://", 1))
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.read_bytes()
266
+ return file
254
267
  raise FileNotFoundError(f"{filename} not found")
255
- platform = pr.netloc
256
- _, self_id, path = pr.path.split("/", 2)
257
- for provider in self.providers:
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
- for provider in self.providers:
261
- for proxy_url_pf in self.proxy_url_mapping[provider.id]:
262
- if url.startswith(proxy_url_pf):
263
- return await provider.download_proxied(proxy_url_pf, url)
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, Login
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[Login]: ...
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
- Login,
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[Login, dict[str, Any]]]
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)