satori-python 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.
Files changed (30) hide show
  1. {satori_python-0.16.7 → satori_python-0.17.1}/PKG-INFO +6 -4
  2. {satori_python-0.16.7 → satori_python-0.17.1}/pyproject.toml +17 -11
  3. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/__init__.py +2 -1
  4. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/__init__.py +13 -24
  5. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/account.py +7 -11
  6. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/account.pyi +13 -11
  7. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/config.py +13 -20
  8. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/network/util.py +2 -2
  9. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/network/webhook.py +7 -6
  10. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/network/websocket.py +30 -33
  11. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/protocol.py +12 -7
  12. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/element.py +152 -120
  13. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/model.py +78 -74
  14. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/parser.py +21 -28
  15. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/server/__init__.py +35 -25
  16. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/server/adapter.py +4 -8
  17. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/server/connection.py +6 -4
  18. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/server/model.py +5 -8
  19. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/server/route.py +29 -45
  20. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/server/utils.py +3 -0
  21. satori_python-0.17.1/src/satori/utils.py +33 -0
  22. satori_python-0.16.7/src/satori/utils.py +0 -17
  23. {satori_python-0.16.7 → satori_python-0.17.1}/LICENSE +0 -0
  24. {satori_python-0.16.7 → satori_python-0.17.1}/README.md +0 -0
  25. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/network/__init__.py +0 -0
  26. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/network/base.py +0 -0
  27. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/const.py +0 -0
  28. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/event.py +0 -0
  29. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/exception.py +0 -0
  30. {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/server/formdata.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: satori-python
3
- Version: 0.16.7
3
+ Version: 0.17.1
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>
@@ -16,16 +16,18 @@ 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.9
19
+ Requires-Python: <4.0,>=3.10
20
20
  Requires-Dist: aiohttp>=3.9.3
21
21
  Requires-Dist: loguru>=0.7.2
22
22
  Requires-Dist: launart>=0.8.2
23
23
  Requires-Dist: typing-extensions>=4.7.0
24
- Requires-Dist: graia-amnesia>=0.9.0
24
+ Requires-Dist: graia-amnesia[uvicorn]<0.12.0,>=0.11.0
25
25
  Requires-Dist: starlette[python-multipart]>=0.37.2
26
- Requires-Dist: uvicorn[standard]>=0.28.0
27
26
  Requires-Dist: yarl>=1.9.4
28
27
  Requires-Dist: python-multipart>=0.0.9
28
+ Requires-Dist: websockets>=15.0.1
29
+ Requires-Dist: msgspec>=0.19.0; extra == "msgspec"
30
+ Provides-Extra: msgspec
29
31
  Description-Content-Type: text/markdown
30
32
 
31
33
  # satori-python
@@ -10,13 +10,13 @@ dependencies = [
10
10
  "loguru>=0.7.2",
11
11
  "launart>=0.8.2",
12
12
  "typing-extensions>=4.7.0",
13
- "graia-amnesia>=0.9.0",
13
+ "graia-amnesia[uvicorn]<0.12.0,>=0.11.0",
14
14
  "starlette[python-multipart]>=0.37.2",
15
- "uvicorn[standard]>=0.28.0",
16
15
  "yarl>=1.9.4",
17
16
  "python-multipart>=0.0.9",
17
+ "websockets>=15.0.1",
18
18
  ]
19
- requires-python = ">=3.9"
19
+ requires-python = ">=3.10,<4.0"
20
20
  readme = "README.md"
21
21
  classifiers = [
22
22
  "Typing :: Typed",
@@ -29,7 +29,7 @@ classifiers = [
29
29
  "Programming Language :: Python :: 3.12",
30
30
  "Operating System :: OS Independent",
31
31
  ]
32
- version = "0.16.7"
32
+ version = "0.17.1"
33
33
 
34
34
  [project.license]
35
35
  text = "MIT"
@@ -38,6 +38,11 @@ text = "MIT"
38
38
  homepage = "https://github.com/RF-Tar-Railt/satori-python"
39
39
  repository = "https://github.com/RF-Tar-Railt/satori-python"
40
40
 
41
+ [project.optional-dependencies]
42
+ msgspec = [
43
+ "msgspec>=0.19.0",
44
+ ]
45
+
41
46
  [build-system]
42
47
  requires = [
43
48
  "mina-build<0.6,>=0.5.1",
@@ -45,7 +50,7 @@ requires = [
45
50
  ]
46
51
  build-backend = "mina.backend"
47
52
 
48
- [tool.pdm.dev-dependencies]
53
+ [dependency-groups]
49
54
  dev = [
50
55
  "isort>=5.13.2",
51
56
  "black>=24.4.0",
@@ -55,6 +60,7 @@ dev = [
55
60
  "mina-build<0.6,>=0.5.1",
56
61
  "pdm-mina>=0.3.2",
57
62
  "nonechat<0.7.0,>=0.6.0",
63
+ "uvicorn[standard]>=0.35.0",
58
64
  ]
59
65
 
60
66
  [tool.pdm.build]
@@ -77,21 +83,21 @@ source = "file"
77
83
  path = "src/satori/__init__.py"
78
84
 
79
85
  [tool.black]
80
- line-length = 110
86
+ line-length = 120
81
87
  include = "\\.pyi?$"
82
88
  extend-exclude = ""
83
89
 
84
90
  [tool.isort]
85
91
  profile = "black"
86
- line_length = 110
92
+ line_length = 120
87
93
  skip_gitignore = true
88
94
  extra_standard_library = [
89
95
  "typing_extensions",
90
96
  ]
91
97
 
92
98
  [tool.ruff]
93
- line-length = 110
94
- target-version = "py39"
99
+ line-length = 120
100
+ target-version = "py310"
95
101
  exclude = [
96
102
  "exam.py",
97
103
  ]
@@ -111,12 +117,12 @@ ignore = [
111
117
  "F403",
112
118
  "F405",
113
119
  "C901",
114
- "UP037",
120
+ "UP038",
115
121
  ]
116
122
 
117
123
  [tool.pyright]
118
124
  pythonPlatform = "All"
119
- pythonVersion = "3.9"
125
+ pythonVersion = "3.10"
120
126
  typeCheckingMode = "basic"
121
127
  reportShadowedImports = false
122
128
  disableBytesTypePromotions = true
@@ -23,6 +23,7 @@ from .element import Superscript as Superscript
23
23
  from .element import Text as Text
24
24
  from .element import Underline as Underline
25
25
  from .element import Video as Video
26
+ from .element import register_element as register_element
26
27
  from .element import select as select
27
28
  from .element import transform as transform
28
29
  from .model import ArgvInteraction as ArgvInteraction
@@ -41,4 +42,4 @@ from .model import Role as Role
41
42
  from .model import Upload as Upload
42
43
  from .model import User as User
43
44
 
44
- __version__ = "0.16.7"
45
+ __version__ = "0.17.1"
@@ -4,9 +4,9 @@ import asyncio
4
4
  import functools
5
5
  import signal
6
6
  import threading
7
- from collections.abc import Awaitable, Iterable
7
+ from collections.abc import Awaitable, Callable, Iterable
8
8
  from functools import wraps
9
- from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, overload
9
+ from typing import TYPE_CHECKING, Any, Literal, TypeVar, overload
10
10
 
11
11
  from creart import it
12
12
  from graia.amnesia.builtins.aiohttp import AiohttpClientService
@@ -75,9 +75,7 @@ class App(Service):
75
75
  def register_config(cls, tc: type[TConfig], tn: type[BaseNetwork[TConfig]]):
76
76
  MAPPING[tc] = tn
77
77
 
78
- def __init__(
79
- self, *configs: Config, default_api_cls: type[ApiProtocol] = ApiProtocol, main_app: bool = True
80
- ):
78
+ def __init__(self, *configs: Config, default_api_cls: type[ApiProtocol] = ApiProtocol, main_app: bool = True):
81
79
  global _app
82
80
 
83
81
  if _app is not None and main_app:
@@ -136,9 +134,7 @@ class App(Service):
136
134
  @overload
137
135
  def register_on(
138
136
  self,
139
- event_type: Literal[
140
- EventType.GUILD_ROLE_CREATED, EventType.GUILD_ROLE_DELETED, EventType.GUILD_ROLE_UPDATED
141
- ],
137
+ event_type: Literal[EventType.GUILD_ROLE_CREATED, EventType.GUILD_ROLE_DELETED, EventType.GUILD_ROLE_UPDATED],
142
138
  ) -> Callable[
143
139
  [Callable[[Account, events.GuildRoleEvent], Awaitable[Any]]],
144
140
  Callable[[Account, events.GuildRoleEvent], Awaitable[Any]],
@@ -162,9 +158,7 @@ class App(Service):
162
158
  ]: ...
163
159
 
164
160
  @overload
165
- def register_on(
166
- self, event_type: Literal[EventType.REACTION_ADDED, EventType.REACTION_REMOVED]
167
- ) -> Callable[
161
+ def register_on(self, event_type: Literal[EventType.REACTION_ADDED, EventType.REACTION_REMOVED]) -> Callable[
168
162
  [Callable[[Account, events.ReactionEvent], Awaitable[Any]]],
169
163
  Callable[[Account, events.ReactionEvent], Awaitable[Any]],
170
164
  ]: ...
@@ -190,14 +184,10 @@ class App(Service):
190
184
  @overload
191
185
  def register_on(
192
186
  self, event_type: str
193
- ) -> Callable[
194
- [Callable[[Account, Event], Awaitable[Any]]], Callable[[Account, Event], Awaitable[Any]]
195
- ]: ...
187
+ ) -> Callable[[Callable[[Account, Event], Awaitable[Any]]], Callable[[Account, Event], Awaitable[Any]]]: ...
196
188
 
197
189
  def register_on(self, event_type: str | EventType):
198
- def decorator(
199
- func: Callable[[Account, Any], Awaitable[Any]], /
200
- ) -> Callable[[Account, Any], Awaitable[Any]]:
190
+ def decorator(func: Callable[[Account, Any], Awaitable[Any]], /) -> Callable[[Account, Any], Awaitable[Any]]:
201
191
  @wraps(func)
202
192
  async def wrapper(account: Account, event: Event) -> Any:
203
193
  if event.type == event_type:
@@ -216,8 +206,6 @@ class App(Service):
216
206
  await asyncio.gather(*(callback(account, state) for callback in self.lifecycle_callbacks))
217
207
 
218
208
  async def post(self, event: Event, conn: BaseNetwork):
219
- if not self.event_callbacks:
220
- return
221
209
  if event.type == EventType.LOGIN_ADDED:
222
210
  if TYPE_CHECKING:
223
211
  assert isinstance(event, events.LoginEvent)
@@ -225,7 +213,7 @@ class App(Service):
225
213
  if not login.user:
226
214
  logger.warning(f"Received login-added event without user info: {login}")
227
215
  return
228
- login_sn = f"{login.user.id}@{id(conn)}"
216
+ login_sn = f"{login.user.id}@{id(conn):x}"
229
217
  account = Account(
230
218
  login,
231
219
  conn.config,
@@ -244,7 +232,7 @@ class App(Service):
244
232
  if not login.user:
245
233
  logger.warning(f"Received login-updated event without user info: {login}")
246
234
  return
247
- login_sn = f"{login.user.id}@{id(conn)}"
235
+ login_sn = f"{login.user.id}@{id(conn):x}"
248
236
  if login_sn not in self.accounts:
249
237
  if login.status == LoginStatus.ONLINE:
250
238
  account = Account(
@@ -278,19 +266,20 @@ class App(Service):
278
266
  if not login.user:
279
267
  logger.warning(f"Received login-removed event without user info: {login}")
280
268
  return
281
- login_sn = f"{login.user.id}@{id(conn)}"
269
+ login_sn = f"{login.user.id}@{id(conn):x}"
282
270
  if login_sn not in self.accounts:
283
271
  logger.warning(f"Received event for unknown account: {event}")
284
272
  return
285
273
  account = self.accounts[login_sn]
286
274
  else:
287
- login_sn = f"{event.login.user.id}@{id(conn)}"
275
+ login_sn = f"{event.login.user.id}@{id(conn):x}"
288
276
  if login_sn not in self.accounts:
289
277
  logger.warning(f"Received event for unknown account: {event}")
290
278
  return
291
279
  account = self.accounts[login_sn]
292
280
 
293
- await asyncio.gather(*(callback(account, event) for callback in self.event_callbacks))
281
+ if self.event_callbacks:
282
+ await asyncio.gather(*(callback(account, event) for callback in self.event_callbacks))
294
283
 
295
284
  if event.type == EventType.LOGIN_REMOVED:
296
285
  logger.info(f"account removed: {account}")
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  from dataclasses import dataclass
5
- from typing import Generic, TypeVar
5
+ from typing_extensions import Generic, TypeVar # noqa: UP035
6
6
 
7
7
  from yarl import URL
8
8
 
@@ -10,8 +10,8 @@ from satori.model import Login
10
10
 
11
11
  from .protocol import ApiProtocol
12
12
 
13
- TP = TypeVar("TP", bound="ApiProtocol")
14
- TP1 = TypeVar("TP1", bound="ApiProtocol")
13
+ TP = TypeVar("TP", bound="ApiProtocol", default=ApiProtocol)
14
+ TP1 = TypeVar("TP1", bound="ApiProtocol", default=ApiProtocol)
15
15
 
16
16
 
17
17
  @dataclass
@@ -20,14 +20,12 @@ class ApiInfo:
20
20
  port: int = 5140
21
21
  path: str = ""
22
22
  token: str | None = None
23
+ timeout: float | None = None
23
24
 
24
25
  def __post_init__(self):
25
26
  if self.path and not self.path.startswith("/"):
26
27
  self.path = f"/{self.path}"
27
-
28
- @property
29
- def api_base(self):
30
- return URL(f"http://{self.host}:{self.port}{self.path}") / "v1"
28
+ self.api_base = URL(f"http://{self.host}:{self.port}{self.path}") / "v1"
31
29
 
32
30
 
33
31
  class Account(Generic[TP]):
@@ -53,14 +51,12 @@ class Account(Generic[TP]):
53
51
  def self_id(self):
54
52
  return self.self_info.user.id
55
53
 
56
- def custom(
57
- self, config: ApiInfo | None = None, protocol_cls: type[TP1] = ApiProtocol, **kwargs
58
- ) -> "Account[TP1]":
54
+ def custom(self, config: ApiInfo | None = None, protocol_cls: type[TP1] = ApiProtocol, **kwargs) -> Account[TP1]:
59
55
  return Account(
60
56
  self.self_info,
61
57
  config or (ApiInfo(**kwargs) if kwargs else self.config),
62
58
  self.proxy_urls,
63
- protocol_cls, # type: ignore
59
+ protocol_cls,
64
60
  )
65
61
 
66
62
  def ensure_url(self, url: str) -> URL:
@@ -1,7 +1,7 @@
1
1
  import asyncio
2
2
  from collections.abc import Iterable
3
- from typing import Any, Generic, Protocol, TypeVar, overload
4
- from typing_extensions import deprecated
3
+ from typing import Any, Protocol, overload
4
+ from typing_extensions import Generic, TypeVar, deprecated # noqa: UP035
5
5
 
6
6
  from yarl import URL
7
7
 
@@ -26,18 +26,22 @@ from satori.model import (
26
26
 
27
27
  from .protocol import ApiProtocol
28
28
 
29
- TP = TypeVar("TP", bound="ApiProtocol")
30
- TP1 = TypeVar("TP1", bound="ApiProtocol")
29
+ TP = TypeVar("TP", bound="ApiProtocol", default=ApiProtocol)
30
+ TP1 = TypeVar("TP1", bound="ApiProtocol", default=ApiProtocol)
31
31
 
32
32
  class Api(Protocol):
33
33
  token: str | None = None
34
-
35
- @property
36
- def api_base(self) -> URL: ...
34
+ timeout: float | None = None
35
+ api_base: URL
37
36
 
38
37
  class ApiInfo(Api):
39
38
  def __init__(
40
- self, host: str = "localhost", port: int = 5140, path: str = "", token: str | None = None
39
+ self,
40
+ host: str = "localhost",
41
+ port: int = 5140,
42
+ path: str = "",
43
+ token: str | None = None,
44
+ timeout: float | None = None,
41
45
  ): ...
42
46
 
43
47
  class Account(Generic[TP]):
@@ -453,9 +457,7 @@ class Account(Generic[TP]):
453
457
  None: 该方法无返回值
454
458
  """
455
459
 
456
- async def reaction_delete(
457
- self, channel_id: str, message_id: str, emoji: str, user_id: str | None = None
458
- ) -> None:
460
+ async def reaction_delete(self, channel_id: str, message_id: str, emoji: str, user_id: str | None = None) -> None:
459
461
  """从特定消息删除某个用户添加的特定表态。
460
462
 
461
463
  如果没有传入用户 ID 则表示删除自己的表态。
@@ -1,5 +1,4 @@
1
1
  from dataclasses import dataclass
2
- from typing import Optional
3
2
 
4
3
  from yarl import URL
5
4
 
@@ -10,7 +9,7 @@ class Config:
10
9
  raise NotImplementedError
11
10
 
12
11
  @property
13
- def token(self) -> Optional[str]:
12
+ def token(self) -> str | None:
14
13
  raise NotImplementedError
15
14
 
16
15
  @property
@@ -23,19 +22,16 @@ class WebsocketsInfo(Config):
23
22
  host: str = "localhost"
24
23
  port: int = 5140
25
24
  path: str = ""
26
- token: Optional[str] = None
25
+ token: str | None = None
26
+ timeout: float | None = None
27
+ identity: str = None # type: ignore
28
+ api_base: URL = None # type: ignore
27
29
 
28
30
  def __post_init__(self):
29
31
  if self.path and not self.path.startswith("/"):
30
32
  self.path = f"/{self.path}"
31
-
32
- @property
33
- def identity(self):
34
- return f"{self.host}:{self.port}"
35
-
36
- @property
37
- def api_base(self):
38
- return URL(f"http://{self.host}:{self.port}{self.path}") / "v1"
33
+ self.identity = f"{self.host}:{self.port}"
34
+ self.api_base = URL(f"http://{self.host}:{self.port}{self.path}") / "v1"
39
35
 
40
36
  @property
41
37
  def ws_base(self):
@@ -47,21 +43,18 @@ class WebhookInfo(Config):
47
43
  host: str = "127.0.0.1"
48
44
  port: int = 8080
49
45
  path: str = "v1/events"
50
- token: Optional[str] = None
46
+ token: str | None = None
51
47
  server_host: str = "localhost"
52
48
  server_port: int = 5140
53
49
  server_path: str = ""
50
+ timeout: float | None = None
51
+ identity: str = None # type: ignore
52
+ api_base: URL = None # type: ignore
54
53
 
55
54
  def __post_init__(self):
56
55
  if self.path and not self.path.startswith("/"):
57
56
  self.path = f"/{self.path}"
58
57
  if self.server_path and not self.server_path.startswith("/"):
59
58
  self.server_path = f"/{self.server_path}"
60
-
61
- @property
62
- def identity(self):
63
- return f"{self.host}:{self.port}{self.path}"
64
-
65
- @property
66
- def api_base(self):
67
- return URL(f"http://{self.server_host}:{self.server_port}{self.server_path}") / "v1"
59
+ self.identity = f"{self.host}:{self.port}{self.path}"
60
+ self.api_base = URL(f"http://{self.server_host}:{self.server_port}{self.server_path}") / "v1"
@@ -1,4 +1,3 @@
1
- import json
2
1
  from typing import Literal, overload
3
2
 
4
3
  from aiohttp import ClientResponse
@@ -11,6 +10,7 @@ from satori.exception import (
11
10
  ServerException,
12
11
  UnauthorizedException,
13
12
  )
13
+ from satori.utils import decode
14
14
 
15
15
 
16
16
  @overload
@@ -25,7 +25,7 @@ async def validate_response(resp: ClientResponse, noreturn=False):
25
25
  if 200 <= resp.status < 300:
26
26
  if noreturn:
27
27
  return
28
- return json.loads(content) if (content := await resp.text()) else {}
28
+ return decode(content) if (content := await resp.text()) else {}
29
29
  elif resp.status == 400:
30
30
  raise BadRequestException(await resp.text())
31
31
  elif resp.status == 401:
@@ -2,12 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
 
5
- from aiohttp import web
5
+ from aiohttp import ClientTimeout, web
6
6
  from graia.amnesia.builtins.aiohttp import AiohttpClientService
7
7
  from launart.manager import Launart
8
8
  from loguru import logger
9
9
 
10
10
  from satori.model import Event, LoginStatus, Meta, MetaPayload, Opcode
11
+ from satori.utils import decode
11
12
 
12
13
  from ..account import Account
13
14
  from ..config import WebhookInfo as WebhookInfo
@@ -22,7 +23,7 @@ class WebhookNetwork(BaseNetwork[WebhookInfo]):
22
23
 
23
24
  @property
24
25
  def id(self):
25
- return f"satori/network/webhook/{self.config.identity}#{id(self)}"
26
+ return f"satori/net/wh/{self.config.identity}#{id(self):x}"
26
27
 
27
28
  async def handle_request(self, req: web.Request):
28
29
  header = req.headers
@@ -33,7 +34,7 @@ class WebhookNetwork(BaseNetwork[WebhookInfo]):
33
34
  if self.config.token and self.config.token != token:
34
35
  return web.Response(status=401)
35
36
  op_code = int(header.get("Satori-OpCode", "0"))
36
- body = await req.json()
37
+ body = decode(await req.text())
37
38
  if op_code == Opcode.META:
38
39
  payload = MetaPayload.parse(body)
39
40
  self.proxy_urls = payload.proxy_urls
@@ -63,7 +64,6 @@ class WebhookNetwork(BaseNetwork[WebhookInfo]):
63
64
  logger.trace(f"Failed to parse event: {body}\nCaused by {e!r}")
64
65
  return web.Response(status=500, reason=f"Failed to parse event caused by {e!r}")
65
66
  else:
66
- logger.trace(f"Received event: {event}")
67
67
  self.sequence = event.sn
68
68
  asyncio.create_task(self.app.post(event, self))
69
69
  return web.Response()
@@ -97,6 +97,7 @@ class WebhookNetwork(BaseNetwork[WebhookInfo]):
97
97
  endpoint,
98
98
  json={},
99
99
  headers=headers,
100
+ timeout=ClientTimeout(total=self.config.timeout or 300),
100
101
  ) as resp:
101
102
  data = await validate_response(resp)
102
103
  meta = Meta.parse(data)
@@ -104,7 +105,7 @@ class WebhookNetwork(BaseNetwork[WebhookInfo]):
104
105
  for login in meta.logins:
105
106
  if not login.user:
106
107
  continue
107
- login_sn = f"{login.user.id}@{id(self)}"
108
+ login_sn = f"{login.user.id}@{id(self):x}"
108
109
  account = Account(login, self.config, meta.proxy_urls, self.app.default_api_cls)
109
110
  logger.info(f"account registered: {account}")
110
111
  (account.connected.set() if login.status == LoginStatus.ONLINE else account.connected.clear())
@@ -116,7 +117,7 @@ class WebhookNetwork(BaseNetwork[WebhookInfo]):
116
117
  logger.info(f"{self.id} Webhook server exiting...")
117
118
  self.close_signal.set()
118
119
  for v in list(self.app.accounts.values()):
119
- if (identity := f"{v.self_id}@{id(self)}") in self.accounts:
120
+ if (identity := f"{v.self_id}@{id(self):x}") in self.accounts:
120
121
  v.connected.clear()
121
122
  await self.app.account_update(v, LoginStatus.OFFLINE)
122
123
  del self.app.accounts[identity]
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import json
5
4
  from contextlib import suppress
6
5
  from typing import cast
7
6
 
@@ -11,6 +10,7 @@ from launart.utilles import any_completed
11
10
  from loguru import logger
12
11
 
13
12
  from satori.model import Event, Identify, LoginStatus, MetaPayload, Opcode, Ready
13
+ from satori.utils import decode, encode
14
14
 
15
15
  from ..account import Account
16
16
  from ..config import WebsocketsInfo as WebsocketsInfo
@@ -23,29 +23,25 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
23
23
 
24
24
  @property
25
25
  def id(self):
26
- return f"satori/network/ws/{self.config.identity}#{id(self)}"
26
+ return f"satori/net/ws/{self.config.identity}#{id(self):x}"
27
27
 
28
28
  connection: aiohttp.ClientWebSocketResponse | None = None
29
29
 
30
- def post_event(self, body: dict):
31
- async def event_parse_task(raw: dict):
32
- try:
33
- event = Event.parse(raw)
34
- except Exception as e:
35
- if (
36
- "self_id" in raw
37
- or ("login" in raw and "self_id" in raw["login"])
38
- or ("login" in raw and "user" in raw["login"] and "self_id" in raw["login"]["user"])
39
- ):
40
- logger.warning(f"Failed to parse event: {raw}\nCaused by {e!r}")
41
- else:
42
- logger.trace(f"Failed to parse event: {raw}\nCaused by {e!r}")
30
+ async def event_parse_task(self, raw: dict):
31
+ try:
32
+ event = Event.parse(raw)
33
+ except Exception as e:
34
+ if (
35
+ "self_id" in raw
36
+ or ("login" in raw and "self_id" in raw["login"])
37
+ or ("login" in raw and "user" in raw["login"] and "self_id" in raw["login"]["user"])
38
+ ):
39
+ logger.warning(f"Failed to parse event: {raw}\nCaused by {e!r}")
43
40
  else:
44
- logger.trace(f"Received event: {event}")
45
- self.sequence = event.sn
46
- await self.app.post(event, self)
47
-
48
- return asyncio.create_task(event_parse_task(body))
41
+ logger.trace(f"Failed to parse event: {raw}\nCaused by {e!r}")
42
+ else:
43
+ self.sequence = event.sn
44
+ await self.app.post(event, self)
49
45
 
50
46
  async def message_receive(self):
51
47
  if self.connection is None:
@@ -56,10 +52,9 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
56
52
  self.close_signal.set()
57
53
  return
58
54
  elif msg.type == aiohttp.WSMsgType.TEXT:
59
- data: dict = json.loads(cast(str, msg.data))
60
- logger.trace(f"Received payload: {data}")
55
+ data: dict = decode(cast(str, msg.data))
61
56
  if data["op"] == Opcode.EVENT:
62
- self.post_event(data["body"])
57
+ asyncio.create_task(self.event_parse_task(data["body"]))
63
58
  elif data["op"] == Opcode.META:
64
59
  payload = MetaPayload.parse(data["body"])
65
60
  self.proxy_urls = payload.proxy_urls
@@ -67,6 +62,8 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
67
62
  account.proxy_urls = payload.proxy_urls.copy()
68
63
  elif data["op"] > 5:
69
64
  logger.warning(f"Received unknown event: {data}")
65
+ else:
66
+ logger.trace(f"Received payload: {data}")
70
67
  continue
71
68
  else:
72
69
  await self.connection_closed()
@@ -75,7 +72,7 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
75
72
  if self.connection is None:
76
73
  raise RuntimeError("connection is not established")
77
74
 
78
- await self.connection.send_json(payload)
75
+ await self.connection.send_str(encode(payload))
79
76
 
80
77
  @property
81
78
  def alive(self):
@@ -88,7 +85,7 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
88
85
  """鉴权连接"""
89
86
  if not self.connection:
90
87
  raise RuntimeError("connection is not established")
91
- payload = Identify(self.config.token)
88
+ payload = Identify(token=self.config.token)
92
89
  if self.sequence > -1:
93
90
  payload.sn = self.sequence
94
91
  try:
@@ -101,7 +98,7 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
101
98
  if resp.type != aiohttp.WSMsgType.TEXT:
102
99
  logger.error(f"Received unexpected payload: {resp}")
103
100
  return False
104
- data = resp.json()
101
+ data = decode(cast(str, resp.data))
105
102
  if data["op"] != Opcode.READY:
106
103
  logger.error(f"Received unexpected payload: {data}")
107
104
  return False
@@ -110,7 +107,7 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
110
107
  for login in ready.logins:
111
108
  if not login.user:
112
109
  continue
113
- login_sn = f"{login.user.id}@{id(self)}"
110
+ login_sn = f"{login.user.id}@{id(self):x}"
114
111
  if login_sn in self.app.accounts:
115
112
  account = self.app.accounts[login_sn]
116
113
  self.accounts[login_sn] = account
@@ -141,7 +138,7 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
141
138
  async def daemon(self, manager: Launart, session: aiohttp.ClientSession):
142
139
  while not manager.status.exiting:
143
140
  try:
144
- async with session.ws_connect(self.config.ws_base / "events", timeout=30) as self.connection:
141
+ async with session.ws_connect(self.config.ws_base / "events", timeout=300) as self.connection:
145
142
  logger.debug(f"{self.id} Websocket client connected")
146
143
  self.close_signal.clear()
147
144
  result = await self._authenticate()
@@ -165,7 +162,7 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
165
162
  self.close_signal.set()
166
163
  self.connection = None
167
164
  for v in list(self.app.accounts.values()):
168
- if (identity := f"{v.self_id}@{id(self)}") in self.accounts:
165
+ if (identity := f"{v.self_id}@{id(self):x}") in self.accounts:
169
166
  v.connected.clear()
170
167
  await self.app.account_update(v, LoginStatus.OFFLINE)
171
168
  del self.app.accounts[identity]
@@ -173,7 +170,7 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
173
170
  return
174
171
  if close_task in done:
175
172
  receiver_task.cancel()
176
- logger.warning(f"{self} Connection closed by server, will reconnect in 5 seconds...")
173
+ logger.warning(f"{self.id} Connection closed by server, will reconnect in 5 seconds...")
177
174
  for k in self.accounts.keys():
178
175
  logger.debug(f"Unregistering satori account {k}...")
179
176
  account = self.app.accounts[k]
@@ -181,12 +178,12 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
181
178
  await self.app.account_update(account, LoginStatus.RECONNECT)
182
179
  self.accounts.clear()
183
180
  await asyncio.sleep(5)
184
- logger.info(f"{self} Reconnecting...")
181
+ logger.info(f"{self.id} Reconnecting...")
185
182
  continue
186
183
  except Exception as e:
187
- logger.error(f"{self} Error while connecting: {e}")
184
+ logger.error(f"{self.id} Error while connecting: {e}")
188
185
  await asyncio.sleep(5)
189
- logger.info(f"{self} Reconnecting...")
186
+ logger.info(f"{self.id} Reconnecting...")
190
187
 
191
188
  async def launch(self, manager: Launart):
192
189
  async with self.stage("preparing"):