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.
- {satori_python-0.16.7 → satori_python-0.17.0}/PKG-INFO +6 -4
- {satori_python-0.16.7 → satori_python-0.17.0}/pyproject.toml +17 -11
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/__init__.py +2 -1
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/__init__.py +9 -20
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/account.py +7 -11
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/account.pyi +13 -11
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/config.py +13 -20
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/network/util.py +2 -2
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/network/webhook.py +4 -3
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/network/websocket.py +23 -26
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/protocol.py +12 -7
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/element.py +152 -120
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/model.py +78 -74
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/parser.py +21 -28
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/server/__init__.py +33 -23
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/server/adapter.py +4 -8
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/server/connection.py +5 -3
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/server/model.py +5 -8
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/server/route.py +29 -45
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/server/utils.py +3 -0
- satori_python-0.17.0/src/satori/utils.py +33 -0
- satori_python-0.16.7/src/satori/utils.py +0 -17
- {satori_python-0.16.7 → satori_python-0.17.0}/LICENSE +0 -0
- {satori_python-0.16.7 → satori_python-0.17.0}/README.md +0 -0
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/network/__init__.py +0 -0
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/client/network/base.py +0 -0
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/const.py +0 -0
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/event.py +0 -0
- {satori_python-0.16.7 → satori_python-0.17.0}/src/satori/exception.py +0 -0
- {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.
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
[
|
|
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 =
|
|
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 =
|
|
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 =
|
|
94
|
-
target-version = "
|
|
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
|
-
"
|
|
120
|
+
"UP038",
|
|
115
121
|
]
|
|
116
122
|
|
|
117
123
|
[tool.pyright]
|
|
118
124
|
pythonPlatform = "All"
|
|
119
|
-
pythonVersion = "3.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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) ->
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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"
|
|
45
|
-
|
|
46
|
-
|
|
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 =
|
|
60
|
-
logger.trace(f"Received payload: {data}")
|
|
55
|
+
data: dict = decode(cast(str, msg.data))
|
|
61
56
|
if data["op"] == Opcode.EVENT:
|
|
62
|
-
self.
|
|
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.
|
|
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.
|
|
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=
|
|
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
|
-
|
|
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
|
|
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
|
|
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 则表示删除自己的表态。
|