satori-python 0.16.7__tar.gz → 0.17.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.
Files changed (30) hide show
  1. {satori_python-0.16.7 → satori_python-0.17.0}/PKG-INFO +6 -4
  2. {satori_python-0.16.7 → satori_python-0.17.0}/pyproject.toml +17 -11
  3. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/__init__.py +2 -1
  4. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/__init__.py +9 -20
  5. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/account.py +7 -11
  6. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/account.pyi +13 -11
  7. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/config.py +13 -20
  8. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/network/util.py +2 -2
  9. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/network/webhook.py +4 -3
  10. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/network/websocket.py +23 -26
  11. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/protocol.py +12 -7
  12. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/element.py +152 -120
  13. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/model.py +78 -74
  14. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/parser.py +21 -28
  15. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/server/__init__.py +33 -23
  16. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/server/adapter.py +4 -8
  17. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/server/connection.py +5 -3
  18. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/server/model.py +5 -8
  19. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/server/route.py +29 -45
  20. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/server/utils.py +3 -0
  21. satori_python-0.17.0/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.0}/LICENSE +0 -0
  24. {satori_python-0.16.7 → satori_python-0.17.0}/README.md +0 -0
  25. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/network/__init__.py +0 -0
  26. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/network/base.py +0 -0
  27. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/const.py +0 -0
  28. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/event.py +0 -0
  29. {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/exception.py +0 -0
  30. {satori_python-0.16.7 → satori_python-0.17.0}/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.0
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.0"
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.0"
@@ -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)
@@ -290,7 +278,8 @@ class App(Service):
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
@@ -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)
@@ -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
@@ -27,25 +27,21 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
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
@@ -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()
@@ -4,7 +4,7 @@ from collections.abc import Iterable
4
4
  from typing import TYPE_CHECKING, Any, cast, overload
5
5
  from typing_extensions import deprecated
6
6
 
7
- from aiohttp import FormData
7
+ from aiohttp import ClientSession, ClientTimeout, FormData
8
8
  from graia.amnesia.builtins.aiohttp import AiohttpClientService
9
9
  from launart import Launart
10
10
 
@@ -39,6 +39,11 @@ if TYPE_CHECKING:
39
39
  class ApiProtocol:
40
40
  def __init__(self, account: Account):
41
41
  self.account = account
42
+ try:
43
+ self.session = Launart.current().get_component(AiohttpClientService).session
44
+ except (LookupError, ValueError):
45
+ self.session = ClientSession()
46
+ self.timeout = ClientTimeout(self.account.config.timeout or 300)
42
47
 
43
48
  async def download(self, url: str):
44
49
  """访问资源链接。"""
@@ -67,7 +72,7 @@ class ApiProtocol:
67
72
  "Satori-Platform": self.account.platform,
68
73
  "Satori-User-ID": self.account.self_id,
69
74
  }
70
- aio = Launart.current().get_component(AiohttpClientService)
75
+
71
76
  if multipart:
72
77
  data = FormData(quote_fields=False)
73
78
  if params is None:
@@ -78,17 +83,19 @@ class ApiProtocol:
78
83
  data.add_field(k, v["value"], filename=v.get("filename"), content_type=v["content_type"])
79
84
  else:
80
85
  data.add_field(k, v)
81
- async with aio.session.post(
86
+ async with self.session.post(
82
87
  endpoint,
83
88
  data=data,
84
89
  headers=headers,
90
+ timeout=self.timeout,
85
91
  ) as resp:
86
92
  return await validate_response(resp)
87
- async with aio.session.request(
93
+ async with self.session.request(
88
94
  method,
89
95
  endpoint,
90
96
  json=params or {},
91
97
  headers=headers,
98
+ timeout=self.timeout,
92
99
  ) as resp:
93
100
  return await validate_response(resp)
94
101
 
@@ -635,9 +642,7 @@ class ApiProtocol:
635
642
  {"channel_id": channel_id, "message_id": message_id, "emoji": emoji},
636
643
  )
637
644
 
638
- async def reaction_delete(
639
- self, channel_id: str, message_id: str, emoji: str, user_id: str | None = None
640
- ) -> None:
645
+ async def reaction_delete(self, channel_id: str, message_id: str, emoji: str, user_id: str | None = None) -> None:
641
646
  """从特定消息删除某个用户添加的特定表态。
642
647
 
643
648
  如果没有传入用户 ID 则表示删除自己的表态。