satori-python-server 0.15.1__tar.gz → 0.16.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.15.0"]
2
+ raw-dependencies = ["satori-python-core >= 0.16.0"]
3
3
 
4
4
  [project]
5
5
  name = "satori-python-server"
@@ -18,7 +18,7 @@ dependencies = [
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.8"
21
+ requires-python = ">=3.9"
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.15.1
3
+ Version: 0.16.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>
@@ -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: >=3.8
19
+ Requires-Python: >=3.9
20
20
  Requires-Dist: aiohttp>=3.9.3
21
21
  Requires-Dist: launart>=0.8.2
22
22
  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.15.0
26
+ Requires-Dist: satori-python-core>=0.16.0
27
27
  Description-Content-Type: text/markdown
28
28
 
29
29
  # satori-python
@@ -78,9 +78,9 @@ pip install satori-python-server
78
78
  客户端:
79
79
 
80
80
  ```python
81
- from satori import EventType, WebsocketsInfo
81
+ from satori import EventType
82
82
  from satori.event import MessageEvent
83
- from satori.client import Account, App
83
+ from satori.client import Account, App, WebsocketsInfo
84
84
 
85
85
  app = App(WebsocketsInfo(port=5140))
86
86
 
@@ -50,9 +50,9 @@ pip install satori-python-server
50
50
  客户端:
51
51
 
52
52
  ```python
53
- from satori import EventType, WebsocketsInfo
53
+ from satori import EventType
54
54
  from satori.event import MessageEvent
55
- from satori.client import Account, App
55
+ from satori.client import Account, App, WebsocketsInfo
56
56
 
57
57
  app = App(WebsocketsInfo(port=5140))
58
58
 
@@ -11,11 +11,11 @@ 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.15.0",
14
+ "satori-python-core >= 0.16.0",
15
15
  ]
16
16
  description = "Satori Protocol SDK for python, specify server part"
17
17
  readme = "README.md"
18
- requires-python = ">=3.8"
18
+ requires-python = ">=3.9"
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.15.1"
30
+ version = "0.16.0"
31
31
 
32
32
  [project.license]
33
33
  text = "MIT"
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import functools
5
5
  import mimetypes
6
+ import re
6
7
  import secrets
7
8
  import signal
8
9
  import threading
@@ -13,7 +14,7 @@ from itertools import chain
13
14
  from pathlib import Path
14
15
  from tempfile import TemporaryDirectory
15
16
  from traceback import print_exc
16
- from typing import Any, cast
17
+ from typing import Any
17
18
 
18
19
  import aiohttp
19
20
  from creart import it
@@ -23,36 +24,44 @@ from loguru import logger
23
24
  from starlette.applications import Starlette
24
25
  from starlette.datastructures import FormData as FormData
25
26
  from starlette.requests import Request as StarletteRequest
26
- from starlette.responses import FileResponse, JSONResponse, Response, StreamingResponse
27
+ from starlette.responses import (
28
+ FileResponse,
29
+ HTMLResponse,
30
+ JSONResponse,
31
+ PlainTextResponse,
32
+ Response,
33
+ StreamingResponse,
34
+ )
27
35
  from starlette.routing import Route, WebSocketRoute
28
36
  from starlette.staticfiles import StaticFiles
29
37
  from starlette.websockets import WebSocket, WebSocketDisconnect
30
38
  from yarl import URL
31
39
 
32
- from satori.config import WebhookInfo
33
40
  from satori.const import Api
34
- from satori.model import Event, ModelBase, Opcode
41
+ from satori.model import Event, Meta, ModelBase, Opcode
35
42
 
43
+ from .. import EventType
36
44
  from .adapter import Adapter as Adapter
37
45
  from .conection import WebsocketConnection
38
46
  from .formdata import parse_content_disposition as parse_content_disposition
39
47
  from .model import Provider as Provider
40
48
  from .model import Request as Request
41
49
  from .model import Router as Router
50
+ from .model import WebhookEndpoint as WebhookEndpoint
42
51
  from .route import RouteCall as RouteCall
43
52
  from .route import RouterMixin as RouterMixin
44
53
  from .utils import Deque
45
54
 
46
55
 
47
56
  async def _request_handler(
48
- method: str, request: StarletteRequest, func: RouteCall, platform: str, self_id: str
57
+ action: str, request: StarletteRequest, func: RouteCall, platform: str, self_id: str
49
58
  ):
50
- if method == Api.UPLOAD_CREATE.value:
59
+ if action == Api.UPLOAD_CREATE.value:
51
60
  async with request.form() as form:
52
61
  res = await func(
53
62
  Request(
54
- cast(dict, request.headers.mutablecopy()),
55
- method,
63
+ request,
64
+ action,
56
65
  form,
57
66
  platform=platform,
58
67
  self_id=self_id,
@@ -62,8 +71,8 @@ async def _request_handler(
62
71
  try:
63
72
  res = await func(
64
73
  Request(
65
- cast(dict, request.headers.mutablecopy()),
66
- method,
74
+ request,
75
+ action,
67
76
  await request.json(),
68
77
  platform=platform,
69
78
  self_id=self_id,
@@ -79,6 +88,9 @@ async def _request_handler(
79
88
  return res if isinstance(res, Response) else JSONResponse(content=res)
80
89
 
81
90
 
91
+ INTERNAL_URL_PAT = re.compile("internal:(?P<platform>[^/]+)/(?P<self_id>[^/]+)/(?P<path>.+)")
92
+
93
+
82
94
  class Server(Service, RouterMixin):
83
95
  id = "satori-python.server"
84
96
  required: set[str] = {"asgi.service/uvicorn"}
@@ -98,7 +110,7 @@ class Server(Service, RouterMixin):
98
110
  path: str = "",
99
111
  version: str = "v1",
100
112
  token: str | None = None,
101
- webhooks: list[WebhookInfo] | None = None,
113
+ webhooks: list[WebhookEndpoint] | None = None,
102
114
  stream_threshold: int = 16 * 1024 * 1024,
103
115
  stream_chunk_size: int = 64 * 1024,
104
116
  ):
@@ -141,7 +153,7 @@ class Server(Service, RouterMixin):
141
153
  self.resources[route_path] = file
142
154
 
143
155
  async def event_callback(self, event: Event):
144
- event.id = self._sequence
156
+ event.sn = self._sequence
145
157
  self._event_cache.append(event)
146
158
  self._sequence += 1
147
159
  for connection in self.connections:
@@ -155,16 +167,13 @@ class Server(Service, RouterMixin):
155
167
  for hook in self.webhooks:
156
168
  try:
157
169
  async with self.session.post(
158
- URL(f"http://{hook.identity}"),
170
+ URL(hook.url),
159
171
  headers={
160
172
  "Content-Type": "application/json",
161
173
  "Authorization": f"Bearer {hook.token or ''}",
162
- "X-Platform": event.platform_,
163
- "Satori-Platform": event.platform_,
164
- "X-Self-ID": event.self_id_,
165
- "Satori-Login-ID": event.self_id_,
174
+ "Satori-OpCode": str(Opcode.EVENT.value),
166
175
  },
167
- json={"op": Opcode.EVENT, "body": event.dump()},
176
+ json=event.dump(),
168
177
  ) as resp:
169
178
  resp.raise_for_status()
170
179
  except Exception as e:
@@ -180,17 +189,18 @@ class Server(Service, RouterMixin):
180
189
  body = identity["body"]
181
190
  token = identity["body"].get("token")
182
191
  logins = []
192
+ proxy_urls = []
183
193
  if token != self.token:
184
194
  return await ws.close(code=3000, reason="Unauthorized")
185
195
  for provider in self.providers:
186
- _logins = await provider.get_logins()
187
- for _login in _logins:
188
- _login.proxy_urls.extend(provider.proxy_urls())
189
- logins.extend(_logins)
196
+ logins.extend(await provider.get_logins())
197
+ proxy_urls.extend(provider.proxy_urls())
190
198
  sequence = body.get("sequence")
191
199
  if sequence is None:
192
200
  sequence = -1
193
- await connection.send({"op": Opcode.READY, "body": {"logins": [lo.dump() for lo in logins]}})
201
+ await connection.send(
202
+ {"op": Opcode.READY, "body": {"logins": [lo.dump() for lo in logins], "proxy_urls": proxy_urls}}
203
+ )
194
204
  self.connections.append(connection)
195
205
  logger.debug(f"New connection: {id(connection)}")
196
206
  heartbeat_task = asyncio.create_task(connection.heartbeat())
@@ -198,6 +208,12 @@ class Server(Service, RouterMixin):
198
208
  try:
199
209
  if sequence > -1:
200
210
  for event in self._event_cache.after(sequence):
211
+ if event.type in (
212
+ EventType.LOGIN_ADDED,
213
+ EventType.LOGIN_REMOVED,
214
+ EventType.LOGIN_UPDATED,
215
+ ):
216
+ continue
201
217
  await connection.send({"op": Opcode.EVENT, "body": event.dump()})
202
218
  await asyncio.sleep(0.1)
203
219
  await any_completed(heartbeat_task, close_task)
@@ -208,19 +224,10 @@ class Server(Service, RouterMixin):
208
224
  close_task.cancel()
209
225
  self.connections.remove(connection)
210
226
 
211
- async def admin_login_list_handler(self, request: StarletteRequest):
212
- logins = []
213
- for provider in self.providers:
214
- _logins = await provider.get_logins()
215
- for _login in _logins:
216
- _login.proxy_urls.extend(provider.proxy_urls())
217
- logins.extend(_logins)
218
- return JSONResponse(content=[lo.dump() for lo in logins])
219
-
220
227
  async def http_server_handler(self, request: StarletteRequest):
221
228
  if not self._adapters and not self.routes:
222
229
  return Response(status_code=404, content=request.path_params["method"])
223
- method = request.path_params["method"]
230
+ action = request.path_params["action"]
224
231
  if "X-Platform" not in request.headers and "Satori-Platform" not in request.headers:
225
232
  return Response(status_code=401, content="Missing header X-Platform or Satori-Platform")
226
233
  platform: str = request.headers.get("X-Platform") or request.headers.get("Satori-Platform") # type: ignore
@@ -229,35 +236,36 @@ class Server(Service, RouterMixin):
229
236
  self_id: str = request.headers.get("X-Self-ID") or request.headers.get("Satori-Login-ID") # type: ignore
230
237
 
231
238
  for _router in self._adapters:
232
- if method not in _router.routes:
239
+ if action not in _router.routes:
233
240
  continue
234
241
  if not _router.ensure(platform, self_id):
235
242
  continue
236
- return await _request_handler(method, request, _router.routes[method], platform, self_id)
237
- if method in self.routes:
238
- return await _request_handler(method, request, self.routes[method], platform, self_id)
243
+ return await _request_handler(action, request, _router.routes[action], platform, self_id)
244
+ if action in self.routes:
245
+ return await _request_handler(action, request, self.routes[action], platform, self_id)
239
246
  for _router in self.routers:
240
- if method not in _router.routes:
247
+ if action not in _router.routes:
241
248
  continue
242
- return await _request_handler(method, request, _router.routes[method], platform, self_id)
243
- return Response(status_code=404, content=method)
249
+ return await _request_handler(action, request, _router.routes[action], platform, self_id)
250
+ return Response(status_code=404, content=action)
244
251
 
245
252
  async def proxy_url_handler(self, request: StarletteRequest):
246
- url = request.path_params["upload_url"]
253
+ url = request.path_params["internal_url"]
247
254
  try:
248
- content = await self.download(url)
249
- if isinstance(content, Path):
250
- return FileResponse(path=content)
255
+ resp = await self.fetch_proxy(url, request)
251
256
  # if content size > stream_limit, use streaming response
252
- if len(content) > self.stream_threshold:
257
+ if (
258
+ isinstance(resp, (PlainTextResponse, HTMLResponse, JSONResponse))
259
+ or resp.__class__ is Response
260
+ ) and len(resp.body) > self.stream_threshold:
253
261
 
254
262
  async def iter_content(body: bytes):
255
263
  for i in range(0, len(body), self.stream_chunk_size):
256
264
  yield body[i : i + self.stream_chunk_size]
257
265
 
258
- return StreamingResponse(content=iter_content(content))
259
- return Response(content=content)
260
- except FileNotFoundError as e404:
266
+ return StreamingResponse(content=iter_content(resp.body))
267
+ return resp
268
+ except (FileNotFoundError, NotImplementedError, AssertionError) as e404:
261
269
  return Response(status_code=404, content=str(e404))
262
270
  except ValueError as e403:
263
271
  return Response(status_code=403, content=str(e403))
@@ -267,28 +275,33 @@ class Server(Service, RouterMixin):
267
275
  logger.error(repr(e))
268
276
  return Response(status_code=500, content=repr(e))
269
277
 
270
- async def download(self, url: str):
278
+ async def fetch_proxy(self, url: str, request: StarletteRequest | None = None):
271
279
  url = url.replace(":/", "://", 1).replace(":///", "://", 1)
272
- pr = urllib.parse.urlparse(url)
273
- if pr.scheme == "upload":
274
- if pr.netloc == "temp":
275
- _, inst, filename = pr.path.split("/", 2)
276
- if inst == f"{self.id}:{id(self)}":
277
- file = Path(self._tempdir.name) / filename
280
+ url = urllib.parse.unquote(url)
281
+ if url.startswith("internal:"):
282
+ if mat := INTERNAL_URL_PAT.match(url):
283
+ platform = mat["platform"]
284
+ self_id = mat["self_id"]
285
+ path = mat["path"]
286
+ if path.startswith("_tmp"):
287
+ file = Path(self._tempdir.name) / path[5:]
278
288
  if file.exists():
279
- return file
280
- raise FileNotFoundError(f"{filename} not found")
281
- for provider in self.providers:
282
- if pr.scheme == "upload":
283
- platform = pr.netloc
284
- _, self_id, path = pr.path.split("/", 2)
285
- if provider.ensure(platform, self_id):
286
- return await provider.download_uploaded(platform, self_id, path)
289
+ return FileResponse(file)
290
+ raise FileNotFoundError(f"{path[5:]} not found")
291
+ assert request is not None
292
+ for provider in self.providers:
293
+ if provider.ensure(platform, self_id):
294
+ return await provider.handle_internal(
295
+ Request(request, "internal", {}, platform=platform, self_id=self_id), path
296
+ )
297
+ raise NotImplementedError(f"Login with {platform}:{self_id} not found")
298
+ raise TypeError(f"Invalid internal url: {url}")
287
299
 
300
+ for provider in self.providers:
288
301
  for proxy_url_pf in provider.proxy_urls():
289
302
  if not url.startswith(proxy_url_pf):
290
303
  continue
291
- resp = await provider.download_proxied(proxy_url_pf, url)
304
+ resp = await provider.handle_proxied(proxy_url_pf, url)
292
305
  if resp is None:
293
306
  continue
294
307
  return resp
@@ -317,18 +330,54 @@ class Server(Service, RouterMixin):
317
330
  with file.resolve().open("wb+") as f:
318
331
  f.write(await data.read())
319
332
 
320
- res[disp["name"]] = f"upload://temp/{self.id}:{id(self)}/{filename}"
333
+ res[disp["name"]] = f"internal:{request.platform}/{request.self_id}/_tmp/{filename}"
321
334
 
322
335
  loop = asyncio.get_running_loop()
323
336
  loop.call_later(600, file.unlink, True)
324
337
  return res
325
338
 
339
+ async def meta_get_handler(self, request: StarletteRequest):
340
+ logins = []
341
+ proxy_urls = []
342
+ for provider in self.providers:
343
+ logins.extend(await provider.get_logins())
344
+ proxy_urls.extend(provider.proxy_urls())
345
+ return JSONResponse(content=Meta(logins, proxy_urls).dump())
346
+
347
+ async def webhook_create_handler(self, request: StarletteRequest):
348
+ body = await request.json()
349
+ url = body["url"]
350
+ token = body.get("token")
351
+ self.webhooks.append(WebhookEndpoint(url, token))
352
+ proxy_urls = []
353
+ for provider in self.providers:
354
+ proxy_urls.extend(provider.proxy_urls())
355
+ async with self.session.post(
356
+ URL(url),
357
+ headers={
358
+ "Content-Type": "application/json",
359
+ "Authorization": f"Bearer {token or ''}",
360
+ "Satori-OpCode": str(Opcode.META.value),
361
+ },
362
+ json={"proxy_urls": proxy_urls},
363
+ ) as resp:
364
+ resp.raise_for_status()
365
+ return Response()
366
+
367
+ async def webhook_delete_handler(self, request: StarletteRequest):
368
+ body = await request.json()
369
+ url = body["url"]
370
+ for endpoint in self.webhooks:
371
+ if endpoint.url == url:
372
+ self.webhooks.remove(endpoint)
373
+ return Response()
374
+
326
375
  async def launch(self, manager: Launart):
327
376
  self.session = aiohttp.ClientSession()
328
377
  for _adapter in self._adapters:
329
378
  manager.add_component(_adapter)
330
379
 
331
- if Api.UPLOAD_CREATE.value not in self.routes and not self._adapters:
380
+ if Api.UPLOAD_CREATE.value not in self.routes:
332
381
  self.routes[Api.UPLOAD_CREATE.value] = self._default_upload_create_handler
333
382
 
334
383
  async with self.stage("preparing"):
@@ -338,19 +387,29 @@ class Server(Service, RouterMixin):
338
387
  *chain.from_iterable(ada.get_routes() for ada in self._adapters),
339
388
  WebSocketRoute(f"{self.path}/{self.version}/events", self.websocket_server_handler),
340
389
  Route(
341
- f"{self.path}/{self.version}/admin/login.list",
342
- self.admin_login_list_handler,
390
+ f"{self.path}/{self.version}/meta",
391
+ self.meta_get_handler,
392
+ methods=["POST"],
393
+ ),
394
+ Route(
395
+ f"{self.path}/{self.version}/meta/webhook.create",
396
+ self.webhook_create_handler,
343
397
  methods=["POST"],
344
398
  ),
345
399
  Route(
346
- f"{self.path}/{self.version}/proxy/{{upload_url:path}}",
400
+ f"{self.path}/{self.version}/meta/webhook.delete",
401
+ self.webhook_delete_handler,
402
+ methods=["POST"],
403
+ ),
404
+ Route(
405
+ f"{self.path}/{self.version}/proxy/{{internal_url:path}}",
347
406
  self.proxy_url_handler,
348
- methods=["GET"],
407
+ methods=["GET", "POST", "PUT", "DELETE"],
349
408
  ),
350
409
  Route(
351
- f"{self.path}/{self.version}/{{method:path}}",
410
+ f"{self.path}/{self.version}/{{action:path}}",
352
411
  self.http_server_handler,
353
- methods=["POST"],
412
+ methods=["GET", "POST", "PUT", "DELETE"],
354
413
  ),
355
414
  ]
356
415
  )
@@ -363,6 +422,20 @@ class Server(Service, RouterMixin):
363
422
  await self.event_callback(event)
364
423
 
365
424
  async with self.stage("blocking"):
425
+ proxy_urls = []
426
+ for provider in self.providers:
427
+ proxy_urls.extend(provider.proxy_urls())
428
+ for hook in self.webhooks:
429
+ async with self.session.post(
430
+ URL(hook.url),
431
+ headers={
432
+ "Content-Type": "application/json",
433
+ "Authorization": f"Bearer {hook.token or ''}",
434
+ "Satori-OpCode": str(Opcode.META.value),
435
+ },
436
+ json={"proxy_urls": proxy_urls},
437
+ ) as resp:
438
+ resp.raise_for_status()
366
439
  await any_completed(
367
440
  manager.status.wait_for_sigexit(),
368
441
  *(event_task(provider) for provider in self.providers),
@@ -1,11 +1,13 @@
1
1
  from abc import abstractmethod
2
2
  from collections.abc import AsyncIterator
3
- from typing import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, Optional
4
4
 
5
5
  from launart import Service
6
+ from starlette.responses import Response
6
7
  from starlette.routing import BaseRoute
7
8
 
8
- from ..model import Event, LoginType
9
+ from ..model import Event, Login
10
+ from .model import Request
9
11
  from .route import RouterMixin
10
12
  from .utils import ctx
11
13
 
@@ -30,14 +32,14 @@ class Adapter(Service, RouterMixin):
30
32
  return []
31
33
 
32
34
  @abstractmethod
33
- async def download_uploaded(self, platform: str, self_id: str, path: str) -> bytes: ...
35
+ async def handle_internal(self, request: Request, path: str) -> Response: ...
34
36
 
35
- async def download_proxied(self, prefix: str, url: str) -> bytes:
37
+ async def handle_proxied(self, prefix: str, url: str) -> Optional[Response]:
36
38
  async with self.server.session.get(url, ssl=ctx) as resp:
37
- return await resp.read()
39
+ return Response(await resp.read())
38
40
 
39
41
  @abstractmethod
40
- async def get_logins(self) -> list[LoginType]: ...
42
+ async def get_logins(self) -> list[Login]: ...
41
43
 
42
44
  def __init__(self):
43
45
  super().__init__()
@@ -2,8 +2,11 @@ from collections.abc import AsyncIterator
2
2
  from dataclasses import dataclass
3
3
  from typing import TYPE_CHECKING, Any, Generic, Optional, Protocol, TypeVar, Union, runtime_checkable
4
4
 
5
+ from starlette.requests import Request as StarletteRequest
6
+ from starlette.responses import Response
7
+
5
8
  from satori.const import Api
6
- from satori.model import Event, Login, LoginPreview, LoginType
9
+ from satori.model import Event, Login
7
10
 
8
11
  if TYPE_CHECKING:
9
12
  from .route import RouteCall
@@ -15,7 +18,7 @@ TP = TypeVar("TP")
15
18
 
16
19
  @dataclass
17
20
  class Request(Generic[TP]):
18
- headers: dict[str, Any]
21
+ origin: StarletteRequest
19
22
  action: str
20
23
  params: TP
21
24
  platform: str
@@ -26,18 +29,24 @@ class Request(Generic[TP]):
26
29
  class Provider(Protocol):
27
30
  def publisher(self) -> AsyncIterator[Event]: ...
28
31
 
29
- async def get_logins(self) -> Union[list[Login], list[LoginPreview], list[LoginType]]: ...
32
+ async def get_logins(self) -> list[Login]: ...
30
33
 
31
34
  @staticmethod
32
35
  def proxy_urls() -> list[str]: ...
33
36
 
34
37
  def ensure(self, platform: str, self_id: str) -> bool: ...
35
38
 
36
- async def download_uploaded(self, platform: str, self_id: str, path: str) -> bytes: ...
39
+ async def handle_internal(self, request: Request, path: str) -> Response: ...
37
40
 
38
- async def download_proxied(self, prefix: str, url: str) -> Optional[bytes]: ...
41
+ async def handle_proxied(self, prefix: str, url: str) -> Optional[Response]: ...
39
42
 
40
43
 
41
44
  @runtime_checkable
42
45
  class Router(Protocol):
43
46
  routes: dict[str, "RouteCall[Any, Any]"]
47
+
48
+
49
+ @dataclass
50
+ class WebhookEndpoint:
51
+ url: str
52
+ token: Optional[str] = None
@@ -8,7 +8,7 @@ from satori.model import (
8
8
  Channel,
9
9
  Direction,
10
10
  Guild,
11
- LoginType,
11
+ Login,
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[LoginType, dict[str, Any]]]
244
+ LOGIN_GET: TypeAlias = RouteCall[Any, Union[Login, dict[str, Any]]]
245
245
 
246
246
 
247
247
  class UserGetParam(TypedDict):