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.
- {satori_python-0.16.7 → satori_python-0.17.1}/PKG-INFO +6 -4
- {satori_python-0.16.7 → satori_python-0.17.1}/pyproject.toml +17 -11
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/__init__.py +2 -1
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/__init__.py +13 -24
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/account.py +7 -11
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/account.pyi +13 -11
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/config.py +13 -20
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/network/util.py +2 -2
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/network/webhook.py +7 -6
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/network/websocket.py +30 -33
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/protocol.py +12 -7
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/element.py +152 -120
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/model.py +78 -74
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/parser.py +21 -28
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/server/__init__.py +35 -25
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/server/adapter.py +4 -8
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/server/connection.py +6 -4
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/server/model.py +5 -8
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/server/route.py +29 -45
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/server/utils.py +3 -0
- satori_python-0.17.1/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.1}/LICENSE +0 -0
- {satori_python-0.16.7 → satori_python-0.17.1}/README.md +0 -0
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/network/__init__.py +0 -0
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/client/network/base.py +0 -0
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/const.py +0 -0
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/event.py +0 -0
- {satori_python-0.16.7 → satori_python-0.17.1}/src/satori/exception.py +0 -0
- {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.
|
|
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:
|
|
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.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
|
-
[
|
|
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.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,
|
|
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
|
-
|
|
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
|
|
@@ -22,7 +23,7 @@ class WebhookNetwork(BaseNetwork[WebhookInfo]):
|
|
|
22
23
|
|
|
23
24
|
@property
|
|
24
25
|
def id(self):
|
|
25
|
-
return f"satori/
|
|
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.
|
|
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/
|
|
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
|
|
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
|
|
@@ -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=
|
|
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"):
|