satori-python 1.3.2__py3-none-any.whl → 1.3.3__py3-none-any.whl
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/__init__.py +1 -1
- satori/client/__init__.py +28 -22
- satori/client/network/websocket.py +13 -8
- satori/client/protocol.py +9 -4
- satori/element.py +125 -52
- satori/model.py +77 -44
- satori/parser.py +56 -16
- satori/py.typed +0 -0
- satori/server/__init__.py +7 -9
- satori/utils.py +6 -0
- {satori_python-1.3.2.dist-info → satori_python-1.3.3.dist-info}/METADATA +3 -3
- {satori_python-1.3.2.dist-info → satori_python-1.3.3.dist-info}/RECORD +14 -13
- {satori_python-1.3.2.dist-info → satori_python-1.3.3.dist-info}/WHEEL +0 -0
- {satori_python-1.3.2.dist-info → satori_python-1.3.3.dist-info}/licenses/LICENSE +0 -0
satori/__init__.py
CHANGED
satori/client/__init__.py
CHANGED
|
@@ -232,7 +232,8 @@ class App(Service):
|
|
|
232
232
|
task.cancel()
|
|
233
233
|
|
|
234
234
|
async def post(self, event: Event, conn: BaseNetwork):
|
|
235
|
-
|
|
235
|
+
ev_type = event.type
|
|
236
|
+
if ev_type == EventType.LOGIN_ADDED:
|
|
236
237
|
if TYPE_CHECKING:
|
|
237
238
|
assert isinstance(event, events.LoginEvent)
|
|
238
239
|
login = event.login
|
|
@@ -251,7 +252,7 @@ class App(Service):
|
|
|
251
252
|
self.accounts[login_sn] = account
|
|
252
253
|
conn.accounts[login_sn] = account
|
|
253
254
|
await self.account_update(account, login.status)
|
|
254
|
-
elif
|
|
255
|
+
elif ev_type == EventType.LOGIN_UPDATED:
|
|
255
256
|
if TYPE_CHECKING:
|
|
256
257
|
assert isinstance(event, events.LoginEvent)
|
|
257
258
|
login = event.login
|
|
@@ -260,21 +261,20 @@ class App(Service):
|
|
|
260
261
|
return
|
|
261
262
|
login_sn = f"{login.platform}_{login.user.id}@{id(conn):x}"
|
|
262
263
|
if login_sn not in self.accounts:
|
|
263
|
-
if login.status
|
|
264
|
-
account = Account(
|
|
265
|
-
login,
|
|
266
|
-
conn.config,
|
|
267
|
-
conn.proxy_urls,
|
|
268
|
-
self.default_api_cls,
|
|
269
|
-
)
|
|
270
|
-
logger.info(f"account added: {account}")
|
|
271
|
-
account.connected.set()
|
|
272
|
-
self.accounts[login_sn] = account
|
|
273
|
-
conn.accounts[login_sn] = account
|
|
274
|
-
await self.account_update(account, LoginStatus.ONLINE)
|
|
275
|
-
else:
|
|
264
|
+
if login.status != LoginStatus.ONLINE:
|
|
276
265
|
logger.warning(f"Received event for unknown account: {event}")
|
|
277
266
|
return
|
|
267
|
+
account = Account(
|
|
268
|
+
login,
|
|
269
|
+
conn.config,
|
|
270
|
+
conn.proxy_urls,
|
|
271
|
+
self.default_api_cls,
|
|
272
|
+
)
|
|
273
|
+
logger.info(f"account added: {account}")
|
|
274
|
+
account.connected.set()
|
|
275
|
+
self.accounts[login_sn] = account
|
|
276
|
+
conn.accounts[login_sn] = account
|
|
277
|
+
await self.account_update(account, LoginStatus.ONLINE)
|
|
278
278
|
else:
|
|
279
279
|
account = self.accounts[login_sn]
|
|
280
280
|
account.self_info = login
|
|
@@ -285,7 +285,7 @@ class App(Service):
|
|
|
285
285
|
else account.connected.clear()
|
|
286
286
|
)
|
|
287
287
|
await self.account_update(account, login.status)
|
|
288
|
-
elif
|
|
288
|
+
elif ev_type == EventType.LOGIN_REMOVED:
|
|
289
289
|
if TYPE_CHECKING:
|
|
290
290
|
assert isinstance(event, events.LoginEvent)
|
|
291
291
|
login = event.login
|
|
@@ -305,12 +305,18 @@ class App(Service):
|
|
|
305
305
|
account = self.accounts[login_sn]
|
|
306
306
|
|
|
307
307
|
if self.event_callbacks:
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
308
|
+
if len(self.event_callbacks) == 1:
|
|
309
|
+
try:
|
|
310
|
+
await self.event_callbacks[0](account, event)
|
|
311
|
+
except Exception:
|
|
312
|
+
traceback.print_exc()
|
|
313
|
+
else:
|
|
314
|
+
task = asyncio.gather(*(callback(account, event) for callback in self.event_callbacks))
|
|
315
|
+
try:
|
|
316
|
+
await task
|
|
317
|
+
except Exception:
|
|
318
|
+
traceback.print_exc()
|
|
319
|
+
task.cancel()
|
|
314
320
|
|
|
315
321
|
if event.type == EventType.LOGIN_REMOVED:
|
|
316
322
|
logger.info(f"account removed: {account}")
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
from contextlib import suppress
|
|
5
4
|
from typing import cast
|
|
6
5
|
|
|
7
6
|
import aiohttp
|
|
@@ -47,12 +46,18 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
|
|
|
47
46
|
if self.connection is None:
|
|
48
47
|
raise RuntimeError("connection is not established")
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
while True:
|
|
50
|
+
msg = await self.connection.receive()
|
|
51
|
+
if msg.type in {
|
|
52
|
+
aiohttp.WSMsgType.CLOSE,
|
|
53
|
+
aiohttp.WSMsgType.ERROR,
|
|
54
|
+
aiohttp.WSMsgType.CLOSING,
|
|
55
|
+
aiohttp.WSMsgType.CLOSED,
|
|
56
|
+
}:
|
|
57
|
+
await self.connection_closed()
|
|
53
58
|
return
|
|
54
59
|
elif msg.type == aiohttp.WSMsgType.TEXT:
|
|
55
|
-
data: dict = decode(
|
|
60
|
+
data: dict = decode(msg.data)
|
|
56
61
|
if data["op"] == Opcode.EVENT:
|
|
57
62
|
asyncio.create_task(self.event_parse_task(data["body"]))
|
|
58
63
|
elif data["op"] == Opcode.META:
|
|
@@ -65,8 +70,6 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
|
|
|
65
70
|
else:
|
|
66
71
|
logger.trace(f"Received payload: {data}")
|
|
67
72
|
continue
|
|
68
|
-
else:
|
|
69
|
-
await self.connection_closed()
|
|
70
73
|
|
|
71
74
|
async def send(self, payload: dict):
|
|
72
75
|
if self.connection is None:
|
|
@@ -131,8 +134,10 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
|
|
|
131
134
|
async def _heartbeat(self):
|
|
132
135
|
"""心跳"""
|
|
133
136
|
while True:
|
|
134
|
-
|
|
137
|
+
try:
|
|
135
138
|
await self.send({"op": 1})
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.error(f"Error while sending heartbeat: {e!r}")
|
|
136
141
|
await asyncio.sleep(9)
|
|
137
142
|
|
|
138
143
|
async def daemon(self, manager: Launart, session: aiohttp.ClientSession):
|
satori/client/protocol.py
CHANGED
|
@@ -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 ClientSession, ClientTimeout, FormData
|
|
7
|
+
from aiohttp import BytesPayload, ClientSession, ClientTimeout, FormData
|
|
8
8
|
from graia.amnesia.builtins.aiohttp import AiohttpClientService
|
|
9
9
|
from launart import Launart
|
|
10
10
|
|
|
@@ -14,6 +14,7 @@ from satori.model import (
|
|
|
14
14
|
Channel,
|
|
15
15
|
Direction,
|
|
16
16
|
Event,
|
|
17
|
+
Friend,
|
|
17
18
|
Guild,
|
|
18
19
|
IterablePageResult,
|
|
19
20
|
Login,
|
|
@@ -28,8 +29,8 @@ from satori.model import (
|
|
|
28
29
|
Upload,
|
|
29
30
|
User,
|
|
30
31
|
)
|
|
32
|
+
from satori.utils import encode_bytes
|
|
31
33
|
|
|
32
|
-
from .. import Friend
|
|
33
34
|
from .network.util import validate_response
|
|
34
35
|
|
|
35
36
|
if TYPE_CHECKING:
|
|
@@ -63,7 +64,7 @@ class ApiProtocol:
|
|
|
63
64
|
async def call_api(
|
|
64
65
|
self, action: str | Api, params: dict | None = None, multipart: bool = False, method: str = "POST"
|
|
65
66
|
) -> dict:
|
|
66
|
-
endpoint = self.account.config.api_base
|
|
67
|
+
endpoint = f"{self.account.config.api_base!s}/{action.value if isinstance(action, Api) else action}"
|
|
67
68
|
headers = {
|
|
68
69
|
"Content-Type": "application/json",
|
|
69
70
|
"Authorization": f"Bearer {self.account.config.token or ''}",
|
|
@@ -93,7 +94,11 @@ class ApiProtocol:
|
|
|
93
94
|
async with self.session.request(
|
|
94
95
|
method,
|
|
95
96
|
endpoint,
|
|
96
|
-
|
|
97
|
+
data=BytesPayload(
|
|
98
|
+
encode_bytes(params or {}),
|
|
99
|
+
content_type="application/json",
|
|
100
|
+
encoding="utf-8",
|
|
101
|
+
),
|
|
97
102
|
headers=headers,
|
|
98
103
|
timeout=self.timeout,
|
|
99
104
|
) as resp:
|
satori/element.py
CHANGED
|
@@ -4,7 +4,7 @@ from dataclasses import InitVar, dataclass, field
|
|
|
4
4
|
from io import BytesIO
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from types import UnionType
|
|
7
|
-
from typing import Any, ClassVar, Final, TypeVar, Union, final, get_args, get_origin, overload
|
|
7
|
+
from typing import Any, ClassVar, Final, Literal, TypeVar, Union, final, get_args, get_origin, overload
|
|
8
8
|
from typing_extensions import Self, override
|
|
9
9
|
|
|
10
10
|
from ._vendor.fleep import get
|
|
@@ -17,7 +17,7 @@ TE = TypeVar("TE", bound="Element")
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def conv_bool(v: str) -> bool:
|
|
20
|
-
if v.lower() not in
|
|
20
|
+
if v.lower() not in {"true", "false"}:
|
|
21
21
|
raise ValueError(v)
|
|
22
22
|
return v.lower() == "true"
|
|
23
23
|
|
|
@@ -28,13 +28,17 @@ class Element:
|
|
|
28
28
|
_children: list["Element"] = field(init=False, default_factory=list)
|
|
29
29
|
|
|
30
30
|
__names__: ClassVar[tuple[str, ...]]
|
|
31
|
-
__convert_fields__: ClassVar[dict[str, Callable[[str], Any]]]
|
|
31
|
+
__convert_fields__: ClassVar[dict[str, Literal[True] | Callable[[str], Any]]]
|
|
32
|
+
__unpack_names__: ClassVar[frozenset[str]]
|
|
32
33
|
|
|
33
34
|
def __init_subclass__(cls, **kwargs):
|
|
34
|
-
|
|
35
|
+
convert_fields = {}
|
|
36
|
+
for base in cls.__mro__:
|
|
37
|
+
if hasattr(base, "__convert_fields__"):
|
|
38
|
+
convert_fields.update(base.__convert_fields__)
|
|
35
39
|
annotations = cls.__annotations__
|
|
36
40
|
for name, typ in annotations.items():
|
|
37
|
-
if name.startswith("_"):
|
|
41
|
+
if name.startswith("_") or isinstance(typ, InitVar):
|
|
38
42
|
continue
|
|
39
43
|
# _type = get_args(typ)[0] if hasattr(typ, "__origin__") else typ
|
|
40
44
|
orig = get_origin(typ)
|
|
@@ -46,39 +50,49 @@ class Element:
|
|
|
46
50
|
_type = args[0]
|
|
47
51
|
else:
|
|
48
52
|
_type = typ
|
|
49
|
-
if _type is
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
if _type is bool:
|
|
54
|
+
convert_fields[name] = conv_bool
|
|
55
|
+
elif _type in (list, dict):
|
|
56
|
+
convert_fields[name] = decode
|
|
57
|
+
else:
|
|
58
|
+
convert_fields[name] = True if _type is str else _type
|
|
59
|
+
cls.__convert_fields__ = convert_fields
|
|
60
|
+
names = getattr(cls, "__names__", None)
|
|
61
|
+
for base in cls.__mro__:
|
|
62
|
+
if parent_names := getattr(base, "__names__", None):
|
|
63
|
+
if names is None:
|
|
64
|
+
names = parent_names
|
|
54
65
|
else:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
@property
|
|
58
|
-
def children(self) -> list["Element"]:
|
|
59
|
-
return self._children
|
|
60
|
-
|
|
61
|
-
@property
|
|
62
|
-
def tag(self) -> str:
|
|
63
|
-
return self.__class__.__name__.lower()
|
|
66
|
+
names = names + parent_names
|
|
67
|
+
cls.__unpack_names__ = frozenset(names if names is not None else annotations.keys())
|
|
64
68
|
|
|
65
69
|
@classmethod
|
|
66
70
|
def unpack(cls, attrs: dict[str, Any]):
|
|
67
71
|
data = {}
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
args = {}
|
|
73
|
+
convert_fields = cls.__convert_fields__
|
|
74
|
+
names = cls.__unpack_names__
|
|
75
|
+
for name in convert_fields:
|
|
70
76
|
if name not in attrs:
|
|
71
77
|
continue
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
else:
|
|
78
|
+
convert = convert_fields[name]
|
|
79
|
+
if convert is True:
|
|
75
80
|
data[name] = attrs[name]
|
|
76
|
-
|
|
81
|
+
else:
|
|
82
|
+
data[name] = convert(attrs[name])
|
|
83
|
+
if name in names:
|
|
84
|
+
args[name] = data[name]
|
|
85
|
+
obj = cls(**args) # type: ignore
|
|
77
86
|
obj._attrs.update(data)
|
|
78
87
|
return obj
|
|
79
88
|
|
|
80
|
-
|
|
81
|
-
|
|
89
|
+
@property
|
|
90
|
+
def children(self) -> list["Element"]:
|
|
91
|
+
return self._children
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def tag(self) -> str:
|
|
95
|
+
return self.__class__.__name__.lower()
|
|
82
96
|
|
|
83
97
|
def attributes(self) -> str:
|
|
84
98
|
def _attr(key: str, value: Any):
|
|
@@ -94,6 +108,8 @@ class Element:
|
|
|
94
108
|
return "".join(_attr(k, v) for k, v in self._attrs.items())
|
|
95
109
|
|
|
96
110
|
def dumps(self, strip: bool = False) -> str:
|
|
111
|
+
if not self._attrs:
|
|
112
|
+
self._attrs = {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
|
|
97
113
|
if self.tag == "text" and "text" in self._attrs:
|
|
98
114
|
return self._attrs["text"] if strip else escape(self._attrs["text"])
|
|
99
115
|
inner = "".join(c.dumps(strip) for c in self._children)
|
|
@@ -124,6 +140,9 @@ class Element:
|
|
|
124
140
|
def __getitem__(self, key: str) -> Any:
|
|
125
141
|
return self._attrs[key]
|
|
126
142
|
|
|
143
|
+
def raw(self) -> RawElement:
|
|
144
|
+
return RawElement(self.tag, self._attrs, [c.raw() for c in self._children])
|
|
145
|
+
|
|
127
146
|
|
|
128
147
|
@dataclass(repr=False)
|
|
129
148
|
class Text(Element):
|
|
@@ -131,6 +150,13 @@ class Text(Element):
|
|
|
131
150
|
|
|
132
151
|
text: str
|
|
133
152
|
|
|
153
|
+
@override
|
|
154
|
+
@classmethod
|
|
155
|
+
def unpack(cls, attrs: dict[str, Any]):
|
|
156
|
+
obj = cls(attrs["text"])
|
|
157
|
+
obj._attrs["text"] = attrs["text"]
|
|
158
|
+
return obj
|
|
159
|
+
|
|
134
160
|
@override
|
|
135
161
|
def dumps(self, strip: bool = False) -> str:
|
|
136
162
|
return self.text if strip else escape(self.text)
|
|
@@ -145,6 +171,12 @@ class At(Element):
|
|
|
145
171
|
role: str | None = None
|
|
146
172
|
type: str | None = None
|
|
147
173
|
|
|
174
|
+
@classmethod
|
|
175
|
+
def unpack(cls, attrs: dict[str, Any]):
|
|
176
|
+
obj = cls(attrs.get("id"), attrs.get("name"), attrs.get("role"), attrs.get("type"))
|
|
177
|
+
obj._attrs.update(attrs)
|
|
178
|
+
return obj
|
|
179
|
+
|
|
148
180
|
@staticmethod
|
|
149
181
|
def role_(
|
|
150
182
|
role: str,
|
|
@@ -164,6 +196,12 @@ class Emoji(Element):
|
|
|
164
196
|
id: str
|
|
165
197
|
name: str | None = None
|
|
166
198
|
|
|
199
|
+
@classmethod
|
|
200
|
+
def unpack(cls, attrs: dict[str, Any]):
|
|
201
|
+
obj = cls(attrs["id"], attrs.get("name"))
|
|
202
|
+
obj._attrs.update(attrs)
|
|
203
|
+
return obj
|
|
204
|
+
|
|
167
205
|
def to_model(self):
|
|
168
206
|
from .model import EmojiObject
|
|
169
207
|
|
|
@@ -177,6 +215,12 @@ class Sharp(Element):
|
|
|
177
215
|
id: str
|
|
178
216
|
name: str | None = None
|
|
179
217
|
|
|
218
|
+
@classmethod
|
|
219
|
+
def unpack(cls, attrs: dict[str, Any]):
|
|
220
|
+
obj = cls(attrs["id"], attrs.get("name"))
|
|
221
|
+
obj._attrs.update(attrs)
|
|
222
|
+
return obj
|
|
223
|
+
|
|
180
224
|
|
|
181
225
|
@dataclass(repr=False)
|
|
182
226
|
class Link(Element):
|
|
@@ -184,6 +228,12 @@ class Link(Element):
|
|
|
184
228
|
|
|
185
229
|
href: str
|
|
186
230
|
|
|
231
|
+
@classmethod
|
|
232
|
+
def unpack(cls, attrs: dict[str, Any]):
|
|
233
|
+
obj = cls(attrs["href"])
|
|
234
|
+
obj._attrs.update(attrs)
|
|
235
|
+
return obj
|
|
236
|
+
|
|
187
237
|
def __post_call__(self):
|
|
188
238
|
if not self._children:
|
|
189
239
|
return
|
|
@@ -255,7 +305,6 @@ class Resource(Element):
|
|
|
255
305
|
return cls(**data)
|
|
256
306
|
|
|
257
307
|
def __post_init__(self, extra: dict[str, Any] | None = None):
|
|
258
|
-
super().__post_init__()
|
|
259
308
|
if extra:
|
|
260
309
|
self._attrs.update(extra)
|
|
261
310
|
|
|
@@ -267,7 +316,7 @@ class Image(Resource):
|
|
|
267
316
|
width: int | None = None
|
|
268
317
|
height: int | None = None
|
|
269
318
|
|
|
270
|
-
__names__ = ("
|
|
319
|
+
__names__ = ("width", "height")
|
|
271
320
|
|
|
272
321
|
@property
|
|
273
322
|
@override
|
|
@@ -282,7 +331,7 @@ class Audio(Resource):
|
|
|
282
331
|
duration: float | None = None
|
|
283
332
|
poster: str | None = None
|
|
284
333
|
|
|
285
|
-
__names__ = ("
|
|
334
|
+
__names__ = ("duration", "poster")
|
|
286
335
|
|
|
287
336
|
|
|
288
337
|
@dataclass(repr=False)
|
|
@@ -294,7 +343,7 @@ class Video(Resource):
|
|
|
294
343
|
duration: float | None = None
|
|
295
344
|
poster: str | None = None
|
|
296
345
|
|
|
297
|
-
__names__ = ("
|
|
346
|
+
__names__ = ("width", "height", "duration", "poster")
|
|
298
347
|
|
|
299
348
|
|
|
300
349
|
@dataclass(repr=False)
|
|
@@ -303,7 +352,7 @@ class File(Resource):
|
|
|
303
352
|
|
|
304
353
|
poster: str | None = None
|
|
305
354
|
|
|
306
|
-
__names__ = ("
|
|
355
|
+
__names__ = ("poster",)
|
|
307
356
|
|
|
308
357
|
|
|
309
358
|
@dataclass(init=False, repr=False)
|
|
@@ -312,6 +361,12 @@ class Style(Element):
|
|
|
312
361
|
|
|
313
362
|
__names__ = ()
|
|
314
363
|
|
|
364
|
+
@classmethod
|
|
365
|
+
def unpack(cls, attrs: dict[str, Any]):
|
|
366
|
+
obj = cls()
|
|
367
|
+
obj._attrs.update(attrs)
|
|
368
|
+
return obj
|
|
369
|
+
|
|
315
370
|
def __init__(self, *text: "str | Text | Style"):
|
|
316
371
|
super().__init__()
|
|
317
372
|
self.__call__(*text)
|
|
@@ -422,6 +477,12 @@ class Message(Element):
|
|
|
422
477
|
id: str | None
|
|
423
478
|
forward: bool | None
|
|
424
479
|
|
|
480
|
+
@classmethod
|
|
481
|
+
def unpack(cls, attrs: dict[str, Any]):
|
|
482
|
+
obj = cls(attrs.get("id"), conv_bool(attrs["forward"]) if "forward" in attrs else None)
|
|
483
|
+
obj._attrs.update(attrs)
|
|
484
|
+
return obj
|
|
485
|
+
|
|
425
486
|
def __init__(
|
|
426
487
|
self,
|
|
427
488
|
id: str | None = None,
|
|
@@ -451,6 +512,12 @@ class Author(Element):
|
|
|
451
512
|
name: str | None = None
|
|
452
513
|
avatar: str | None = None
|
|
453
514
|
|
|
515
|
+
@classmethod
|
|
516
|
+
def unpack(cls, attrs: dict[str, Any]):
|
|
517
|
+
obj = cls(attrs["id"], attrs.get("name"), attrs.get("avatar"))
|
|
518
|
+
obj._attrs.update(attrs)
|
|
519
|
+
return obj
|
|
520
|
+
|
|
454
521
|
|
|
455
522
|
@dataclass(repr=False)
|
|
456
523
|
class Button(Element):
|
|
@@ -462,6 +529,12 @@ class Button(Element):
|
|
|
462
529
|
text: str | None = None
|
|
463
530
|
theme: str | None = None
|
|
464
531
|
|
|
532
|
+
@classmethod
|
|
533
|
+
def unpack(cls, attrs: dict[str, Any]):
|
|
534
|
+
obj = cls(attrs["type"], attrs.get("id"), attrs.get("href"), attrs.get("text"), attrs.get("theme"))
|
|
535
|
+
obj._attrs.update(attrs)
|
|
536
|
+
return obj
|
|
537
|
+
|
|
465
538
|
@classmethod
|
|
466
539
|
def action(cls, button_id: str, theme: str | None = None):
|
|
467
540
|
return Button("action", id=button_id, theme=theme)
|
|
@@ -533,7 +606,7 @@ class Raw(Element):
|
|
|
533
606
|
|
|
534
607
|
@override
|
|
535
608
|
def dumps(self, strip: bool = False):
|
|
536
|
-
return self.content
|
|
609
|
+
return self.content
|
|
537
610
|
|
|
538
611
|
|
|
539
612
|
def register_element(cls: type[TE], tag: str | None = None) -> type[TE]:
|
|
@@ -547,7 +620,7 @@ def register_element(cls: type[TE], tag: str | None = None) -> type[TE]:
|
|
|
547
620
|
return cls
|
|
548
621
|
|
|
549
622
|
|
|
550
|
-
|
|
623
|
+
COMMON_TYPE_MAP = {
|
|
551
624
|
"text": Text,
|
|
552
625
|
"at": At,
|
|
553
626
|
"emoji": Emoji,
|
|
@@ -558,6 +631,9 @@ ELEMENT_TYPE_MAP = {
|
|
|
558
631
|
"video": Video,
|
|
559
632
|
"file": File,
|
|
560
633
|
"author": Author,
|
|
634
|
+
"button": Button,
|
|
635
|
+
"a": Link,
|
|
636
|
+
"link": Link,
|
|
561
637
|
}
|
|
562
638
|
|
|
563
639
|
STYLE_TYPE_MAP = {
|
|
@@ -586,35 +662,32 @@ STYLE_TYPE_MAP = {
|
|
|
586
662
|
"br": Br,
|
|
587
663
|
}
|
|
588
664
|
|
|
665
|
+
ELEMENT_TYPE_MAP = {**COMMON_TYPE_MAP, **STYLE_TYPE_MAP}
|
|
666
|
+
|
|
589
667
|
|
|
590
668
|
def transform(elements: list[RawElement]) -> list[Element]:
|
|
591
669
|
msg = []
|
|
592
670
|
for elem in elements:
|
|
593
671
|
tag = elem.tag()
|
|
594
672
|
if tag in ELEMENT_TYPE_MAP:
|
|
595
|
-
|
|
596
|
-
msg.append(seg_cls.unpack(elem.attrs)(*transform(elem.children)))
|
|
597
|
-
elif tag in ("a", "link"):
|
|
598
|
-
link = Link.unpack(elem.attrs)
|
|
599
|
-
if elem.children:
|
|
600
|
-
link(*transform(elem.children))
|
|
601
|
-
msg.append(link)
|
|
602
|
-
elif tag == "button":
|
|
603
|
-
button = Button.unpack(elem.attrs)
|
|
673
|
+
seg = ELEMENT_TYPE_MAP[tag].unpack(elem.attrs)
|
|
604
674
|
if elem.children:
|
|
605
|
-
|
|
606
|
-
msg.append(
|
|
607
|
-
elif tag in STYLE_TYPE_MAP:
|
|
608
|
-
seg_cls = STYLE_TYPE_MAP[tag]
|
|
609
|
-
msg.append(seg_cls.unpack(elem.attrs)(*transform(elem.children)))
|
|
610
|
-
elif tag in ("br", "newline"):
|
|
611
|
-
msg.append(Br())
|
|
675
|
+
seg(*transform(elem.children))
|
|
676
|
+
msg.append(seg)
|
|
612
677
|
elif tag == "message":
|
|
613
678
|
msg.append(Message.unpack(elem.attrs)(*transform(elem.children)))
|
|
614
679
|
elif tag == "quote":
|
|
615
|
-
|
|
680
|
+
quot = Quote.unpack(elem.attrs)
|
|
681
|
+
if elem.children:
|
|
682
|
+
quot(*transform(elem.children))
|
|
683
|
+
msg.append(quot)
|
|
684
|
+
elif tag in ("br", "newline"):
|
|
685
|
+
msg.append(Br())
|
|
616
686
|
else:
|
|
617
|
-
|
|
687
|
+
custom = Custom(tag, elem.attrs)
|
|
688
|
+
if elem.children:
|
|
689
|
+
custom(*transform(elem.children))
|
|
690
|
+
msg.append(custom)
|
|
618
691
|
return msg
|
|
619
692
|
|
|
620
693
|
|
satori/model.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import mimetypes
|
|
2
2
|
from collections.abc import AsyncIterable, Awaitable, Callable
|
|
3
|
-
from dataclasses import
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from enum import IntEnum
|
|
6
6
|
from os import PathLike
|
|
@@ -13,16 +13,20 @@ from .parser import Element as RawElement
|
|
|
13
13
|
from .parser import parse
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
@dataclass
|
|
16
|
+
@dataclass(slots=True)
|
|
17
17
|
class ModelBase:
|
|
18
18
|
__converter__: ClassVar[dict[str, Callable[[Any], Any]]] = {}
|
|
19
19
|
_raw_data: dict[str, Any] = field(init=False, default_factory=dict, repr=False, compare=False, hash=False)
|
|
20
20
|
|
|
21
|
+
@classmethod
|
|
22
|
+
def before_parse(cls, raw: dict):
|
|
23
|
+
pass
|
|
24
|
+
|
|
21
25
|
@classmethod
|
|
22
26
|
def parse(cls: type[Self], raw: dict) -> Self:
|
|
23
|
-
fs: dict[str, Field] = cls.__dataclass_fields__
|
|
24
27
|
data = {}
|
|
25
|
-
|
|
28
|
+
cls.before_parse(data)
|
|
29
|
+
for name in cls.__dataclass_fields__:
|
|
26
30
|
if name in raw:
|
|
27
31
|
if name in cls.__converter__:
|
|
28
32
|
data[name] = cls.__converter__[name](raw[name])
|
|
@@ -32,6 +36,42 @@ class ModelBase:
|
|
|
32
36
|
obj._raw_data = raw
|
|
33
37
|
return obj
|
|
34
38
|
|
|
39
|
+
def __init_subclass__(cls, **kwargs):
|
|
40
|
+
has_converter = False
|
|
41
|
+
keys = set()
|
|
42
|
+
for c in cls.__mro__:
|
|
43
|
+
if c is Generic:
|
|
44
|
+
Generic.__init_subclass__.__func__(cls, **kwargs)
|
|
45
|
+
if getattr(c, "__converter__", None):
|
|
46
|
+
has_converter = True
|
|
47
|
+
keys.update(getattr(c, "__annotations__", {}).keys())
|
|
48
|
+
keys = frozenset(k for k in keys if not k.startswith("_"))
|
|
49
|
+
|
|
50
|
+
def parse1(cls_: type[Self], raw: dict, _keys=keys) -> Self:
|
|
51
|
+
data = {k: v for k, v in raw.items() if k in _keys}
|
|
52
|
+
obj = cls_(**data) # type: ignore
|
|
53
|
+
obj._raw_data = raw
|
|
54
|
+
return obj
|
|
55
|
+
|
|
56
|
+
def parse2(cls_: type[Self], raw: dict, _keys=keys) -> Self:
|
|
57
|
+
data = {}
|
|
58
|
+
cls_.before_parse(data)
|
|
59
|
+
for name in _keys:
|
|
60
|
+
if name in raw:
|
|
61
|
+
if name in cls_.__converter__:
|
|
62
|
+
data[name] = cls_.__converter__[name](raw[name])
|
|
63
|
+
else:
|
|
64
|
+
data[name] = raw[name]
|
|
65
|
+
obj = cls_(**data) # type: ignore
|
|
66
|
+
obj._raw_data = raw
|
|
67
|
+
return obj
|
|
68
|
+
|
|
69
|
+
if "parse" not in cls.__dict__:
|
|
70
|
+
if has_converter:
|
|
71
|
+
cls.parse = classmethod(parse2) # type: ignore
|
|
72
|
+
else:
|
|
73
|
+
cls.parse = classmethod(parse1) # type: ignore
|
|
74
|
+
|
|
35
75
|
def dump(self) -> dict:
|
|
36
76
|
raise NotImplementedError
|
|
37
77
|
|
|
@@ -43,7 +83,7 @@ class ChannelType(IntEnum):
|
|
|
43
83
|
VOICE = 3
|
|
44
84
|
|
|
45
85
|
|
|
46
|
-
@dataclass
|
|
86
|
+
@dataclass(slots=True)
|
|
47
87
|
class Channel(ModelBase):
|
|
48
88
|
id: str
|
|
49
89
|
type: ChannelType = ChannelType.TEXT
|
|
@@ -61,7 +101,7 @@ class Channel(ModelBase):
|
|
|
61
101
|
return res
|
|
62
102
|
|
|
63
103
|
|
|
64
|
-
@dataclass
|
|
104
|
+
@dataclass(slots=True)
|
|
65
105
|
class Guild(ModelBase):
|
|
66
106
|
id: str
|
|
67
107
|
name: str | None = None
|
|
@@ -76,7 +116,7 @@ class Guild(ModelBase):
|
|
|
76
116
|
return res
|
|
77
117
|
|
|
78
118
|
|
|
79
|
-
@dataclass
|
|
119
|
+
@dataclass(slots=True)
|
|
80
120
|
class User(ModelBase):
|
|
81
121
|
id: str
|
|
82
122
|
name: str | None = None
|
|
@@ -97,7 +137,7 @@ class User(ModelBase):
|
|
|
97
137
|
return res
|
|
98
138
|
|
|
99
139
|
|
|
100
|
-
@dataclass
|
|
140
|
+
@dataclass(slots=True)
|
|
101
141
|
class Friend(ModelBase):
|
|
102
142
|
user: User | None = None
|
|
103
143
|
nick: str | None = None
|
|
@@ -117,7 +157,7 @@ class Friend(ModelBase):
|
|
|
117
157
|
return res
|
|
118
158
|
|
|
119
159
|
|
|
120
|
-
@dataclass
|
|
160
|
+
@dataclass(slots=True)
|
|
121
161
|
class Role(ModelBase):
|
|
122
162
|
id: str
|
|
123
163
|
name: str | None = None
|
|
@@ -126,7 +166,7 @@ class Role(ModelBase):
|
|
|
126
166
|
def parse(cls, raw: str | dict):
|
|
127
167
|
if isinstance(raw, str):
|
|
128
168
|
return cls(id=raw)
|
|
129
|
-
return
|
|
169
|
+
return cls(raw["id"], raw.get("name"))
|
|
130
170
|
|
|
131
171
|
def dump(self):
|
|
132
172
|
res = {"id": self.id}
|
|
@@ -135,7 +175,7 @@ class Role(ModelBase):
|
|
|
135
175
|
return res
|
|
136
176
|
|
|
137
177
|
|
|
138
|
-
@dataclass
|
|
178
|
+
@dataclass(slots=True)
|
|
139
179
|
class Member(ModelBase):
|
|
140
180
|
user: User | None = None
|
|
141
181
|
nick: str | None = None
|
|
@@ -177,11 +217,11 @@ class LoginStatus(IntEnum):
|
|
|
177
217
|
"""正在重新连接"""
|
|
178
218
|
|
|
179
219
|
|
|
180
|
-
@dataclass
|
|
220
|
+
@dataclass(slots=True, kw_only=True)
|
|
181
221
|
class Login(ModelBase):
|
|
182
|
-
sn: int
|
|
183
|
-
status: LoginStatus
|
|
184
|
-
adapter: str
|
|
222
|
+
sn: int = 0
|
|
223
|
+
status: LoginStatus = LoginStatus.ONLINE
|
|
224
|
+
adapter: str = "satori"
|
|
185
225
|
platform: str
|
|
186
226
|
user: User
|
|
187
227
|
features: list[str] = field(default_factory=list)
|
|
@@ -203,29 +243,22 @@ class Login(ModelBase):
|
|
|
203
243
|
return res
|
|
204
244
|
|
|
205
245
|
@classmethod
|
|
206
|
-
def
|
|
246
|
+
def before_parse(cls, raw: dict):
|
|
207
247
|
if "self_id" in raw and "user" not in raw:
|
|
208
248
|
raw["user"] = {"id": raw["self_id"]}
|
|
209
|
-
if "sn" not in raw:
|
|
210
|
-
raw["sn"] = 0
|
|
211
|
-
if "adapter" not in raw:
|
|
212
|
-
raw["adapter"] = "satori"
|
|
213
|
-
if "status" not in raw:
|
|
214
|
-
raw["status"] = LoginStatus.ONLINE
|
|
215
|
-
return super().parse(raw)
|
|
216
249
|
|
|
217
250
|
@property
|
|
218
251
|
def id(self) -> str:
|
|
219
252
|
return self.user.id
|
|
220
253
|
|
|
221
254
|
|
|
222
|
-
@dataclass
|
|
255
|
+
@dataclass(slots=True)
|
|
223
256
|
class LoginPartial(Login):
|
|
224
257
|
platform: str | None = None
|
|
225
258
|
user: User | None = None
|
|
226
259
|
|
|
227
260
|
|
|
228
|
-
@dataclass
|
|
261
|
+
@dataclass(slots=True)
|
|
229
262
|
class ArgvInteraction(ModelBase):
|
|
230
263
|
name: str
|
|
231
264
|
arguments: list
|
|
@@ -235,7 +268,7 @@ class ArgvInteraction(ModelBase):
|
|
|
235
268
|
return {"name": self.name, "arguments": self.arguments, "options": self.options}
|
|
236
269
|
|
|
237
270
|
|
|
238
|
-
@dataclass
|
|
271
|
+
@dataclass(slots=True)
|
|
239
272
|
class ButtonInteraction(ModelBase):
|
|
240
273
|
id: str
|
|
241
274
|
data: str | None = None
|
|
@@ -262,7 +295,7 @@ class Opcode(IntEnum):
|
|
|
262
295
|
"""元信息更新 (接收)"""
|
|
263
296
|
|
|
264
297
|
|
|
265
|
-
@dataclass
|
|
298
|
+
@dataclass(slots=True)
|
|
266
299
|
class Identify(ModelBase):
|
|
267
300
|
token: str | None = None
|
|
268
301
|
sn: int | None = None
|
|
@@ -271,7 +304,7 @@ class Identify(ModelBase):
|
|
|
271
304
|
def parse(cls, raw: dict):
|
|
272
305
|
if "sequence" in raw and "sn" not in raw:
|
|
273
306
|
raw["sn"] = raw["sequence"]
|
|
274
|
-
return
|
|
307
|
+
return cls(token=raw.get("token"), sn=raw.get("sn"))
|
|
275
308
|
|
|
276
309
|
@property
|
|
277
310
|
def sequence(self) -> int | None:
|
|
@@ -281,7 +314,7 @@ class Identify(ModelBase):
|
|
|
281
314
|
return {k: v for k, v in (("token", self.token), ("sn", self.sn)) if v is not None}
|
|
282
315
|
|
|
283
316
|
|
|
284
|
-
@dataclass
|
|
317
|
+
@dataclass(slots=True)
|
|
285
318
|
class Ready(ModelBase):
|
|
286
319
|
logins: list[LoginPartial]
|
|
287
320
|
proxy_urls: list[str] = field(default_factory=list)
|
|
@@ -292,7 +325,7 @@ class Ready(ModelBase):
|
|
|
292
325
|
return {"logins": [login.dump() for login in self.logins], "proxy_urls": self.proxy_urls}
|
|
293
326
|
|
|
294
327
|
|
|
295
|
-
@dataclass
|
|
328
|
+
@dataclass(slots=True)
|
|
296
329
|
class MetaPayload(ModelBase):
|
|
297
330
|
"""Meta 信令"""
|
|
298
331
|
|
|
@@ -302,7 +335,7 @@ class MetaPayload(ModelBase):
|
|
|
302
335
|
return {"proxy_urls": self.proxy_urls}
|
|
303
336
|
|
|
304
337
|
|
|
305
|
-
@dataclass
|
|
338
|
+
@dataclass(slots=True)
|
|
306
339
|
class Meta(ModelBase):
|
|
307
340
|
"""Meta 数据"""
|
|
308
341
|
|
|
@@ -315,7 +348,7 @@ class Meta(ModelBase):
|
|
|
315
348
|
return {"logins": [login.dump() for login in self.logins], "proxy_urls": self.proxy_urls}
|
|
316
349
|
|
|
317
350
|
|
|
318
|
-
@dataclass
|
|
351
|
+
@dataclass(slots=True)
|
|
319
352
|
class EmojiObject(ModelBase):
|
|
320
353
|
id: str
|
|
321
354
|
name: str | None = None
|
|
@@ -330,7 +363,7 @@ class EmojiObject(ModelBase):
|
|
|
330
363
|
return Emoji(self.id, self.name)
|
|
331
364
|
|
|
332
365
|
|
|
333
|
-
@dataclass
|
|
366
|
+
@dataclass(slots=True)
|
|
334
367
|
class MessageObject(ModelBase):
|
|
335
368
|
id: str
|
|
336
369
|
content: str = ""
|
|
@@ -342,6 +375,8 @@ class MessageObject(ModelBase):
|
|
|
342
375
|
updated_at: datetime | None = None
|
|
343
376
|
referrer: dict | None = None
|
|
344
377
|
|
|
378
|
+
_parsed_message: list[Element] | None = field(init=False, default=None, repr=False, compare=False, hash=False)
|
|
379
|
+
|
|
345
380
|
@classmethod
|
|
346
381
|
def from_elements(
|
|
347
382
|
cls,
|
|
@@ -361,10 +396,10 @@ class MessageObject(ModelBase):
|
|
|
361
396
|
|
|
362
397
|
@property
|
|
363
398
|
def message(self) -> list[Element]:
|
|
364
|
-
if
|
|
399
|
+
if self._parsed_message is not None:
|
|
365
400
|
return self._parsed_message
|
|
366
|
-
self._parsed_message = transform(parse(self.content))
|
|
367
|
-
return
|
|
401
|
+
self._parsed_message = msg = transform(parse(self.content))
|
|
402
|
+
return msg
|
|
368
403
|
|
|
369
404
|
@message.setter
|
|
370
405
|
def message(self, value: list[Element]):
|
|
@@ -372,11 +407,10 @@ class MessageObject(ModelBase):
|
|
|
372
407
|
self.content = "".join(str(i) for i in value)
|
|
373
408
|
|
|
374
409
|
@classmethod
|
|
375
|
-
def
|
|
410
|
+
def before_parse(cls, raw: dict):
|
|
376
411
|
if "elements" in raw and "content" not in raw:
|
|
377
412
|
content = [RawElement(*item.values()) for item in raw["elements"]]
|
|
378
413
|
raw["content"] = "".join(str(i) for i in content)
|
|
379
|
-
return super().parse(raw)
|
|
380
414
|
|
|
381
415
|
__converter__ = {
|
|
382
416
|
"channel": Channel.parse,
|
|
@@ -406,7 +440,7 @@ class MessageObject(ModelBase):
|
|
|
406
440
|
return res
|
|
407
441
|
|
|
408
442
|
|
|
409
|
-
@dataclass
|
|
443
|
+
@dataclass(slots=True)
|
|
410
444
|
class Event(ModelBase):
|
|
411
445
|
type: str
|
|
412
446
|
timestamp: datetime
|
|
@@ -444,7 +478,7 @@ class Event(ModelBase):
|
|
|
444
478
|
}
|
|
445
479
|
|
|
446
480
|
@classmethod
|
|
447
|
-
def
|
|
481
|
+
def before_parse(cls, raw: dict):
|
|
448
482
|
if "id" in raw and "sn" not in raw:
|
|
449
483
|
raw["sn"] = raw["id"]
|
|
450
484
|
if "platform" in raw and "self_id" in raw and "login" not in raw:
|
|
@@ -458,7 +492,6 @@ class Event(ModelBase):
|
|
|
458
492
|
if "login" not in raw:
|
|
459
493
|
raw["login"] = {"sn": 0, "status": LoginStatus.ONLINE, "platform": raw.get("platform", "unknown")}
|
|
460
494
|
raw["login"]["user"] = {"id": raw["self_id"]}
|
|
461
|
-
return super().parse(raw)
|
|
462
495
|
|
|
463
496
|
@property
|
|
464
497
|
def platform(self):
|
|
@@ -509,7 +542,7 @@ class Event(ModelBase):
|
|
|
509
542
|
T = TypeVar("T", bound=ModelBase)
|
|
510
543
|
|
|
511
544
|
|
|
512
|
-
@dataclass
|
|
545
|
+
@dataclass(slots=True)
|
|
513
546
|
class PageResult(ModelBase, Generic[T]):
|
|
514
547
|
data: list[T]
|
|
515
548
|
next: str | None = None
|
|
@@ -526,7 +559,7 @@ class PageResult(ModelBase, Generic[T]):
|
|
|
526
559
|
return res
|
|
527
560
|
|
|
528
561
|
|
|
529
|
-
@dataclass
|
|
562
|
+
@dataclass(slots=True)
|
|
530
563
|
class PageDequeResult(PageResult[T]):
|
|
531
564
|
prev: str | None = None
|
|
532
565
|
|
|
@@ -569,7 +602,7 @@ Direction: TypeAlias = Literal["before", "after", "around"]
|
|
|
569
602
|
Order: TypeAlias = Literal["asc", "desc"]
|
|
570
603
|
|
|
571
604
|
|
|
572
|
-
@dataclass
|
|
605
|
+
@dataclass(slots=True)
|
|
573
606
|
class Upload:
|
|
574
607
|
file: bytes | IO[bytes] | PathLike
|
|
575
608
|
mimetype: str = "image/png"
|
satori/parser.py
CHANGED
|
@@ -12,27 +12,37 @@ def escape(text: str, inline: bool = False) -> str:
|
|
|
12
12
|
return result.replace('"', """) if inline else result
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
uc_escape_pat1 = re.compile(r"&#(\d+);")
|
|
16
|
+
uc_escape_pat2 = re.compile(r"&#x([0-9a-f]+);")
|
|
17
|
+
uc_escape_pat3 = re.compile(r"&(amp|#38|#x26);")
|
|
18
|
+
|
|
19
|
+
|
|
15
20
|
def unescape(text: str) -> str:
|
|
16
21
|
result = text.replace("<", "<").replace(">", ">").replace(""", '"')
|
|
17
|
-
result =
|
|
18
|
-
result =
|
|
19
|
-
return
|
|
22
|
+
result = uc_escape_pat1.sub(lambda m: m[0] if m[1] == "38" else chr(int(m[1])), result)
|
|
23
|
+
result = uc_escape_pat2.sub(lambda m: m[0] if m[1] == "26" else chr(int(m[1], 16)), result)
|
|
24
|
+
return uc_escape_pat3.sub("&", result)
|
|
20
25
|
|
|
21
26
|
|
|
22
27
|
def uncapitalize(source: str) -> str:
|
|
23
|
-
return source[
|
|
28
|
+
return source[:1].lower() + source[1:]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
camel_pat = re.compile(r"[_-][a-z]")
|
|
32
|
+
param_pat = re.compile(r".[A-Z]+")
|
|
33
|
+
snake_pat = re.compile(r".[A-Z]")
|
|
24
34
|
|
|
25
35
|
|
|
26
36
|
def camel_case(source: str) -> str:
|
|
27
|
-
return
|
|
37
|
+
return camel_pat.sub(lambda mat: mat[0][1:].upper(), source)
|
|
28
38
|
|
|
29
39
|
|
|
30
40
|
def param_case(source: str) -> str:
|
|
31
|
-
return
|
|
41
|
+
return param_pat.sub(lambda mat: mat[0][0] + "-" + mat[0][1:].lower(), uncapitalize(source).replace("_", "-"))
|
|
32
42
|
|
|
33
43
|
|
|
34
44
|
def snake_case(source: str) -> str:
|
|
35
|
-
return
|
|
45
|
+
return snake_pat.sub(lambda mat: mat[0][0] + "_" + mat[0][1:].lower(), uncapitalize(source).replace("-", "_"))
|
|
36
46
|
|
|
37
47
|
|
|
38
48
|
def ensure_list(value: T | list[T] | None) -> list[T]:
|
|
@@ -49,9 +59,9 @@ def make_element(content: Union[str, bool, int, float, "Element"]) -> Optional["
|
|
|
49
59
|
if isinstance(content, Element):
|
|
50
60
|
return content
|
|
51
61
|
if isinstance(content, (bool, int, float)):
|
|
52
|
-
return Element(type="text", attrs={"text": str(content)})
|
|
62
|
+
return Element.parse(type="text", attrs={"text": str(content)})
|
|
53
63
|
if isinstance(content, str) and content:
|
|
54
|
-
return Element(type="text", attrs={"text": content})
|
|
64
|
+
return Element.parse(type="text", attrs={"text": content})
|
|
55
65
|
if content is not None:
|
|
56
66
|
raise ValueError(f"Invalid content: {content!r}")
|
|
57
67
|
|
|
@@ -68,7 +78,9 @@ class Element:
|
|
|
68
78
|
type: str
|
|
69
79
|
attrs: dict[str, Any]
|
|
70
80
|
children: list["Element"]
|
|
71
|
-
source: str | None
|
|
81
|
+
source: str | None
|
|
82
|
+
|
|
83
|
+
__slots__ = ("type", "attrs", "children", "source")
|
|
72
84
|
|
|
73
85
|
def __init__(
|
|
74
86
|
self,
|
|
@@ -99,6 +111,31 @@ class Element:
|
|
|
99
111
|
elif not self.attrs:
|
|
100
112
|
self.attrs["text"] = ""
|
|
101
113
|
|
|
114
|
+
@classmethod
|
|
115
|
+
def parse(
|
|
116
|
+
cls, type: str, attrs: dict[str, Any], children: list["Element"] | None = None, source: str | None = None
|
|
117
|
+
) -> "Element":
|
|
118
|
+
elem = cls.__new__(cls)
|
|
119
|
+
elem.type = type
|
|
120
|
+
elem.attrs = {}
|
|
121
|
+
elem.children = []
|
|
122
|
+
elem.source = source
|
|
123
|
+
for k, v in attrs.items():
|
|
124
|
+
if v is None:
|
|
125
|
+
continue
|
|
126
|
+
if k == "children":
|
|
127
|
+
elem.children.extend(ensure_list(v))
|
|
128
|
+
else:
|
|
129
|
+
elem.attrs[camel_case(k)] = v
|
|
130
|
+
if children:
|
|
131
|
+
elem.children.extend(children)
|
|
132
|
+
if type == "text":
|
|
133
|
+
if "content" in elem.attrs:
|
|
134
|
+
elem.attrs["text"] = elem.attrs.pop("content")
|
|
135
|
+
elif not elem.attrs:
|
|
136
|
+
elem.attrs["text"] = ""
|
|
137
|
+
return elem
|
|
138
|
+
|
|
102
139
|
def tag(self):
|
|
103
140
|
if self.type == "component":
|
|
104
141
|
if is_ := self.attrs.get("is"):
|
|
@@ -234,6 +271,9 @@ tag_pat2 = re.compile(
|
|
|
234
271
|
attr_pat1 = re.compile(r"([^\s=]+)(?:=\"(?P<value1>[^\"]*)\"|='(?P<value2>[^']*)')?", re.S)
|
|
235
272
|
attr_pat2 = re.compile(r"([^\s=]+)(?:=\"(?P<value1>[^\"]*)\"|='(?P<value2>[^']*)'|=\{(?P<curly>[^\}]+)\})?", re.S)
|
|
236
273
|
|
|
274
|
+
space_pat1 = re.compile(r"^\s*\n\s*", re.MULTILINE)
|
|
275
|
+
space_pat2 = re.compile(r"\s*\n\s*$", re.MULTILINE)
|
|
276
|
+
|
|
237
277
|
|
|
238
278
|
class Position(IntEnum):
|
|
239
279
|
OPEN = 0
|
|
@@ -242,7 +282,7 @@ class Position(IntEnum):
|
|
|
242
282
|
CONTINUE = 3
|
|
243
283
|
|
|
244
284
|
|
|
245
|
-
@dataclass
|
|
285
|
+
@dataclass(slots=True)
|
|
246
286
|
class Token:
|
|
247
287
|
type: Literal["angle", "curly"]
|
|
248
288
|
name: str
|
|
@@ -299,7 +339,7 @@ def parse_tokens(tokens: list[str | Token], context: dict | None = None) -> list
|
|
|
299
339
|
result: list[Element] = []
|
|
300
340
|
for token in tokens:
|
|
301
341
|
if isinstance(token, str):
|
|
302
|
-
result.append(Element(type="text", attrs={"text": token}))
|
|
342
|
+
result.append(Element.parse(type="text", attrs={"text": token}))
|
|
303
343
|
elif token.type == "angle":
|
|
304
344
|
attrs = {}
|
|
305
345
|
attr_pat = attr_pat2 if context is not None else attr_pat1
|
|
@@ -318,10 +358,10 @@ def parse_tokens(tokens: list[str | Token], context: dict | None = None) -> list
|
|
|
318
358
|
attrs[key] = True
|
|
319
359
|
token.extra = token.extra[mat.end() :]
|
|
320
360
|
result.append(
|
|
321
|
-
Element(
|
|
361
|
+
Element.parse(
|
|
322
362
|
token.name,
|
|
323
363
|
attrs,
|
|
324
|
-
|
|
364
|
+
parse_tokens(token.children["default"], context) if token.children else [],
|
|
325
365
|
)
|
|
326
366
|
)
|
|
327
367
|
elif not token.name:
|
|
@@ -351,9 +391,9 @@ def parse(src: str, context: dict | None = None):
|
|
|
351
391
|
def parse_content(source: str, _start: bool, _end: bool):
|
|
352
392
|
source = unescape(source)
|
|
353
393
|
if _start:
|
|
354
|
-
source =
|
|
394
|
+
source = space_pat1.sub("", source)
|
|
355
395
|
if _end:
|
|
356
|
-
source =
|
|
396
|
+
source = space_pat2.sub("", source)
|
|
357
397
|
push_text(source)
|
|
358
398
|
|
|
359
399
|
tag_pat = tag_pat2 if context is not None else tag_pat1
|
satori/py.typed
ADDED
|
File without changes
|
satori/server/__init__.py
CHANGED
|
@@ -71,7 +71,7 @@ StarletteRequest.json = _json
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
async def _request_handler(action: str, request: StarletteRequest, func: RouteCall, platform: str, self_id: str):
|
|
74
|
-
if action == Api.UPLOAD_CREATE
|
|
74
|
+
if action == Api.UPLOAD_CREATE:
|
|
75
75
|
async with request.form() as form:
|
|
76
76
|
res = await func(
|
|
77
77
|
Request(
|
|
@@ -314,12 +314,10 @@ class Server(Service, RouterMixin):
|
|
|
314
314
|
if not self._adapters and not self.routes:
|
|
315
315
|
return Response(status_code=404, content=request.path_params["method"])
|
|
316
316
|
action = request.path_params["action"]
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
platform
|
|
320
|
-
|
|
321
|
-
return Response(status_code=401, content="Missing header X-Self-ID or Satori-User-ID")
|
|
322
|
-
self_id: str = request.headers.get("X-Self-ID") or request.headers.get("Satori-User-ID") # type: ignore
|
|
317
|
+
platform = request.headers.get("Satori-Platform")
|
|
318
|
+
self_id = request.headers.get("Satori-User-ID")
|
|
319
|
+
if platform is None or self_id is None:
|
|
320
|
+
return Response(status_code=401, content="Missing header Satori-Platform or Satori-User-ID")
|
|
323
321
|
|
|
324
322
|
for _router in self._adapters:
|
|
325
323
|
if action in _router.routes:
|
|
@@ -440,7 +438,7 @@ class Server(Service, RouterMixin):
|
|
|
440
438
|
for provider in self.providers:
|
|
441
439
|
logins.extend(await provider.get_logins())
|
|
442
440
|
proxy_urls.extend(provider.proxy_urls())
|
|
443
|
-
return JSONResponse(content=Meta(logins, proxy_urls).dump())
|
|
441
|
+
return JSONResponse(content=Meta(logins=logins, proxy_urls=proxy_urls).dump())
|
|
444
442
|
|
|
445
443
|
async def webhook_create_handler(self, request: StarletteRequest):
|
|
446
444
|
body = await request.json()
|
|
@@ -556,7 +554,7 @@ class Server(Service, RouterMixin):
|
|
|
556
554
|
stop_signal: Iterable[signal.Signals] = (signal.SIGINT,),
|
|
557
555
|
):
|
|
558
556
|
if manager is None:
|
|
559
|
-
manager = it(Launart)
|
|
557
|
+
manager = manager or it(Launart)
|
|
560
558
|
manager.add_component(self.asgi_service)
|
|
561
559
|
manager.add_component(self)
|
|
562
560
|
with suppress(ValueError):
|
satori/utils.py
CHANGED
|
@@ -24,10 +24,16 @@ try:
|
|
|
24
24
|
def encode(obj):
|
|
25
25
|
return encoder.encode(obj).decode()
|
|
26
26
|
|
|
27
|
+
def encode_bytes(obj):
|
|
28
|
+
return encoder.encode(obj)
|
|
29
|
+
|
|
27
30
|
except ImportError:
|
|
28
31
|
import json
|
|
29
32
|
|
|
30
33
|
def encode(obj):
|
|
31
34
|
return json.dumps(obj, separators=(",", ":"), ensure_ascii=False)
|
|
32
35
|
|
|
36
|
+
def encode_bytes(obj):
|
|
37
|
+
return json.dumps(obj, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
|
38
|
+
|
|
33
39
|
decode = json.loads
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: satori-python
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.3
|
|
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>
|
|
@@ -38,11 +38,11 @@ Description-Content-Type: text/markdown
|
|
|
38
38
|
[](https://pypi.org/project/satori-python)
|
|
39
39
|
[](https://www.python.org/)
|
|
40
40
|
|
|
41
|
-
基于 [Satori](https://satori.chat/zh-CN/) 协议的 Python 开发工具包
|
|
41
|
+
基于 [Satori](https://satori.chat/zh-CN/protocol) 协议的 Python 开发工具包
|
|
42
42
|
|
|
43
43
|
## 协议介绍
|
|
44
44
|
|
|
45
|
-
[Satori Protocol](https://satori.chat/zh-CN/)
|
|
45
|
+
[Satori Protocol](https://satori.chat/zh-CN/protocol)
|
|
46
46
|
|
|
47
47
|
### 协议端
|
|
48
48
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
satori/__init__.py,sha256=
|
|
1
|
+
satori/__init__.py,sha256=Uo9vtxXg7Smr-CCrMc4CNzHYIoXdqE2x8w_eGmcZBhI,1888
|
|
2
2
|
satori/_vendor/fleep.py,sha256=_zKP7iY3mMQr0rC5KbgkbkMorT3KVLeIsWPLIUa0Y34,16347
|
|
3
|
-
satori/client/__init__.py,sha256=
|
|
3
|
+
satori/client/__init__.py,sha256=1HLiYmYZoScymOzTuOVZht99eREXMUT09F8PZzvAMPA,14484
|
|
4
4
|
satori/client/account.py,sha256=HP7BJWkN69SB9ldNIjHxWlXcrmB-IPnTnYqm3lBMvl8,2742
|
|
5
5
|
satori/client/account.pyi,sha256=ha632NYjlQDU65cjmYiP3eoPJnb82tMXuApLlUBwUKE,18744
|
|
6
6
|
satori/client/config.py,sha256=iHBazxFDDE_-Qeyra2DVxZvQRCYqEoDrrzEVJjig93Q,1858
|
|
@@ -8,23 +8,24 @@ satori/client/network/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
|
|
|
8
8
|
satori/client/network/base.py,sha256=0ajEhclVpVch-HMWLknoicexNe_keWW0dbLtboAtdtE,817
|
|
9
9
|
satori/client/network/util.py,sha256=WfVTjmYxj4EjrbR8r3zm7RJZjxmGQwC7yjZtXQT6SWE,1246
|
|
10
10
|
satori/client/network/webhook.py,sha256=N1zcJGx-Ijwtc0dIsPWzdnJSLi0ydQTengHBxtbbC2E,5326
|
|
11
|
-
satori/client/network/websocket.py,sha256=
|
|
12
|
-
satori/client/protocol.py,sha256=
|
|
11
|
+
satori/client/network/websocket.py,sha256=oZtiFLywP_Rw-_I-hl32nShCTTQ1XKlv0BhWJj53x9Y,8476
|
|
12
|
+
satori/client/protocol.py,sha256=hXrtgh-ypiF1W-dTl5I4LBWg8P2VTNKYQWFq8aRCeeo,28577
|
|
13
13
|
satori/const.py,sha256=6TQAIlgNq7fHLiadxGUU-mzkffOcpRrEvq5yrWYMuGc,2738
|
|
14
|
-
satori/element.py,sha256=
|
|
14
|
+
satori/element.py,sha256=Ya0iDwMpgmfdluHWEYEhbaC9fdRZuYRJfjMwLM4vb_E,20707
|
|
15
15
|
satori/event.py,sha256=yC6rKaa7JbeOZ4Un1rX-Isvm3EcR7e782nxYf1j6JdM,1060
|
|
16
16
|
satori/exception.py,sha256=edKeuLRZAQxyCdw1_XCgOtv5tIHkfHw0Die9B818_HM,545
|
|
17
|
-
satori/model.py,sha256=
|
|
18
|
-
satori/parser.py,sha256=
|
|
19
|
-
satori/
|
|
17
|
+
satori/model.py,sha256=U3Ad78zjIATMgEQ-dpNtA60eungIU3L_AAJMq_EhzHU,17439
|
|
18
|
+
satori/parser.py,sha256=EyKitj39OewGhZuNIvrd8nWJiL5EJAVy7mtcv8Zk6v0,14658
|
|
19
|
+
satori/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
satori/server/__init__.py,sha256=qvbPHSbIiOQGJV_D3yfEKupqOqMKwzrLixkjy3AMgqs,23820
|
|
20
21
|
satori/server/adapter.py,sha256=-eIkj1TvZ5LJ6jrRoobvea_MlrqksY3HiZ8sDYY3wP8,1600
|
|
21
22
|
satori/server/connection.py,sha256=28sDNT9QmTJknCRQAMB1jzxcRiG3Tw_TO0AFlCMNr5g,1439
|
|
22
23
|
satori/server/formdata.py,sha256=lHLTatf6117eC4U7FX77PjGvchEPiZGmVWnpe9tceh4,437
|
|
23
24
|
satori/server/model.py,sha256=zzjNa3iBFkd8crZIVTrEisPaB0g8JsxV4fA2rRU0maw,1105
|
|
24
25
|
satori/server/route.py,sha256=kpelAX0kYQbJncp5biWi8pWRcEIez9m9sJN8NF3sjn4,10654
|
|
25
26
|
satori/server/utils.py,sha256=hMNqLzbzcwyLJO-sGncO5kuviiW5Y6k34fj4AS2iVWQ,910
|
|
26
|
-
satori/utils.py,sha256=
|
|
27
|
-
satori_python-1.3.
|
|
28
|
-
satori_python-1.3.
|
|
29
|
-
satori_python-1.3.
|
|
30
|
-
satori_python-1.3.
|
|
27
|
+
satori/utils.py,sha256=N8HEw-DOXxOBwFHmmXgqPdMh6AXjiGR0ZGa1zBPH6EI,822
|
|
28
|
+
satori_python-1.3.3.dist-info/METADATA,sha256=VqEeAYKbYM1fbB1hr-ugFBSrI0VjQntpUpnKaqCVdL8,5511
|
|
29
|
+
satori_python-1.3.3.dist-info/WHEEL,sha256=rSwsxJWe3vzyR5HCwjWXQruDgschpei4h_giTm0dJVE,90
|
|
30
|
+
satori_python-1.3.3.dist-info/licenses/LICENSE,sha256=2EDswKd1M1648judap8BEEwp3Jz6IpfFP2wYVdU0Y2o,1069
|
|
31
|
+
satori_python-1.3.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|