satori-python 0.13.3__tar.gz → 0.14.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.13.3 → satori_python-0.14.0}/PKG-INFO +1 -1
- {satori_python-0.13.3 → satori_python-0.14.0}/pyproject.toml +3 -3
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/__init__.py +1 -1
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/client/__init__.py +2 -1
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/client/account.pyi +2 -1
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/client/network/websocket.py +2 -3
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/client/protocol.py +2 -1
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/element.py +20 -20
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/model.py +11 -11
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/parser.py +20 -19
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/server/__init__.py +16 -3
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/server/adapter.py +5 -4
- satori_python-0.14.0/src/satori/server/conection.py +44 -0
- satori_python-0.14.0/src/satori/server/deque.py +33 -0
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/server/model.py +4 -5
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/server/route.py +22 -21
- satori_python-0.13.3/src/satori/server/conection.py +0 -40
- {satori_python-0.13.3 → satori_python-0.14.0}/LICENSE +0 -0
- {satori_python-0.13.3 → satori_python-0.14.0}/README.md +0 -0
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/client/account.py +0 -0
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/client/network/__init__.py +0 -0
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/client/network/base.py +0 -0
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/client/network/util.py +0 -0
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/client/network/webhook.py +0 -0
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/config.py +0 -0
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/const.py +0 -0
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/event.py +0 -0
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/exception.py +0 -0
- {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/server/formdata.py +0 -0
|
@@ -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.14.0"
|
|
33
33
|
|
|
34
34
|
[project.license]
|
|
35
35
|
text = "MIT"
|
|
@@ -86,7 +86,7 @@ extra_standard_library = [
|
|
|
86
86
|
|
|
87
87
|
[tool.ruff]
|
|
88
88
|
line-length = 110
|
|
89
|
-
target-version = "
|
|
89
|
+
target-version = "py39"
|
|
90
90
|
exclude = [
|
|
91
91
|
"exam.py",
|
|
92
92
|
]
|
|
@@ -111,7 +111,7 @@ ignore = [
|
|
|
111
111
|
|
|
112
112
|
[tool.pyright]
|
|
113
113
|
pythonPlatform = "All"
|
|
114
|
-
pythonVersion = "3.
|
|
114
|
+
pythonVersion = "3.9"
|
|
115
115
|
typeCheckingMode = "basic"
|
|
116
116
|
reportShadowedImports = false
|
|
117
117
|
disableBytesTypePromotions = true
|
|
@@ -4,8 +4,9 @@ import asyncio
|
|
|
4
4
|
import functools
|
|
5
5
|
import signal
|
|
6
6
|
import threading
|
|
7
|
+
from collections.abc import Awaitable, Iterable
|
|
7
8
|
from functools import wraps
|
|
8
|
-
from typing import TYPE_CHECKING, Any,
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, overload
|
|
9
10
|
|
|
10
11
|
from creart import it
|
|
11
12
|
from graia.amnesia.builtins.aiohttp import AiohttpClientService
|
|
@@ -119,9 +119,8 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
|
|
|
119
119
|
async def _heartbeat(self):
|
|
120
120
|
"""心跳"""
|
|
121
121
|
while True:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
await self.send({"op": 1})
|
|
122
|
+
with suppress(Exception):
|
|
123
|
+
await self.send({"op": 1})
|
|
125
124
|
await asyncio.sleep(9)
|
|
126
125
|
|
|
127
126
|
async def daemon(self, manager: Launart, session: aiohttp.ClientSession):
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from typing import TYPE_CHECKING, Any, cast, overload
|
|
4
5
|
|
|
5
6
|
from aiohttp import FormData
|
|
6
7
|
from graia.amnesia.builtins.aiohttp import AiohttpClientService
|
|
@@ -2,7 +2,7 @@ from base64 import b64encode
|
|
|
2
2
|
from dataclasses import InitVar, dataclass, field, fields
|
|
3
3
|
from io import BytesIO
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Any, ClassVar,
|
|
5
|
+
from typing import Any, ClassVar, Optional, TypeVar, Union, get_args, overload
|
|
6
6
|
from typing_extensions import override
|
|
7
7
|
|
|
8
8
|
from .parser import Element as RawElement
|
|
@@ -14,13 +14,13 @@ TE = TypeVar("TE", bound="Element")
|
|
|
14
14
|
|
|
15
15
|
@dataclass(repr=False)
|
|
16
16
|
class Element:
|
|
17
|
-
_attrs:
|
|
18
|
-
_children:
|
|
17
|
+
_attrs: dict[str, Any] = field(init=False, default_factory=dict)
|
|
18
|
+
_children: list["Element"] = field(init=False, default_factory=list)
|
|
19
19
|
|
|
20
|
-
__names__: ClassVar[
|
|
20
|
+
__names__: ClassVar[tuple[str, ...]]
|
|
21
21
|
|
|
22
22
|
@property
|
|
23
|
-
def children(self) ->
|
|
23
|
+
def children(self) -> list["Element"]:
|
|
24
24
|
return self._children
|
|
25
25
|
|
|
26
26
|
@property
|
|
@@ -28,7 +28,7 @@ class Element:
|
|
|
28
28
|
return self.__class__.__name__.lower()
|
|
29
29
|
|
|
30
30
|
@classmethod
|
|
31
|
-
def unpack(cls, attrs:
|
|
31
|
+
def unpack(cls, attrs: dict[str, Any]):
|
|
32
32
|
obj = cls(**{k: v for k, v in attrs.items() if k in cls.__names__}) # type: ignore
|
|
33
33
|
obj._attrs.update({k: v for k, v in attrs.items() if k not in cls.__names__})
|
|
34
34
|
return obj
|
|
@@ -166,7 +166,7 @@ class Link(Element):
|
|
|
166
166
|
class Resource(Element):
|
|
167
167
|
src: str
|
|
168
168
|
title: Optional[str] = None
|
|
169
|
-
extra: InitVar[Optional[
|
|
169
|
+
extra: InitVar[Optional[dict[str, Any]]] = None
|
|
170
170
|
cache: Optional[bool] = None
|
|
171
171
|
timeout: Optional[int] = None
|
|
172
172
|
|
|
@@ -181,19 +181,19 @@ class Resource(Element):
|
|
|
181
181
|
mime: Optional[str] = None,
|
|
182
182
|
name: Optional[str] = None,
|
|
183
183
|
poster: Optional[str] = None,
|
|
184
|
-
extra: Optional[
|
|
184
|
+
extra: Optional[dict[str, Any]] = None,
|
|
185
185
|
cache: Optional[bool] = None,
|
|
186
186
|
timeout: Optional[int] = None,
|
|
187
187
|
**kwargs,
|
|
188
188
|
):
|
|
189
|
-
data:
|
|
189
|
+
data: dict[str, Any] = {"extra": extra or kwargs}
|
|
190
190
|
if url is not None:
|
|
191
|
-
data
|
|
191
|
+
data |= {"src": url}
|
|
192
192
|
elif path:
|
|
193
|
-
data
|
|
193
|
+
data |= {"src": Path(path).as_uri()}
|
|
194
194
|
elif raw and mime:
|
|
195
195
|
bd = raw.getvalue() if isinstance(raw, BytesIO) else raw
|
|
196
|
-
data
|
|
196
|
+
data |= {"src": f"data:{mime};base64,{b64encode(bd).decode('utf-8')}"}
|
|
197
197
|
else:
|
|
198
198
|
raise ValueError(f"{cls} need at least one of url, path and raw")
|
|
199
199
|
if name is not None:
|
|
@@ -206,7 +206,7 @@ class Resource(Element):
|
|
|
206
206
|
data["timeout"] = timeout
|
|
207
207
|
return cls(**data)
|
|
208
208
|
|
|
209
|
-
def __post_init__(self, extra: Optional[
|
|
209
|
+
def __post_init__(self, extra: Optional[dict[str, Any]] = None):
|
|
210
210
|
super().__post_init__()
|
|
211
211
|
if extra:
|
|
212
212
|
self._attrs.update(extra)
|
|
@@ -380,7 +380,7 @@ class Message(Element):
|
|
|
380
380
|
self,
|
|
381
381
|
id: Optional[str] = None,
|
|
382
382
|
forward: Optional[bool] = None,
|
|
383
|
-
content: Optional[
|
|
383
|
+
content: Optional[list[Union[str, Element]]] = None,
|
|
384
384
|
):
|
|
385
385
|
self.id = id
|
|
386
386
|
self.forward = forward
|
|
@@ -464,8 +464,8 @@ class Custom(Element):
|
|
|
464
464
|
def __init__(
|
|
465
465
|
self,
|
|
466
466
|
type: str,
|
|
467
|
-
attrs: Optional[
|
|
468
|
-
children: Optional[
|
|
467
|
+
attrs: Optional[dict[str, Any]] = None,
|
|
468
|
+
children: Optional[list[Union[str, Element]]] = None,
|
|
469
469
|
):
|
|
470
470
|
self.type = type
|
|
471
471
|
super().__init__()
|
|
@@ -536,7 +536,7 @@ STYLE_TYPE_MAP = {
|
|
|
536
536
|
}
|
|
537
537
|
|
|
538
538
|
|
|
539
|
-
def transform(elements:
|
|
539
|
+
def transform(elements: list[RawElement]) -> list[Element]:
|
|
540
540
|
msg = []
|
|
541
541
|
for elem in elements:
|
|
542
542
|
tag = elem.tag()
|
|
@@ -568,14 +568,14 @@ def transform(elements: List[RawElement]) -> List[Element]:
|
|
|
568
568
|
|
|
569
569
|
|
|
570
570
|
@overload
|
|
571
|
-
def select(elements: Union[Element,
|
|
571
|
+
def select(elements: Union[Element, list[Element]], query: type[TE]) -> list[TE]: ...
|
|
572
572
|
|
|
573
573
|
|
|
574
574
|
@overload
|
|
575
|
-
def select(elements: Union[Element,
|
|
575
|
+
def select(elements: Union[Element, list[Element]], query: str) -> list[Element]: ...
|
|
576
576
|
|
|
577
577
|
|
|
578
|
-
def select(elements: Union[Element,
|
|
578
|
+
def select(elements: Union[Element, list[Element]], query: Union[type[TE], str]):
|
|
579
579
|
if not elements:
|
|
580
580
|
return []
|
|
581
581
|
if isinstance(elements, Element):
|
|
@@ -4,7 +4,7 @@ from datetime import datetime
|
|
|
4
4
|
from enum import IntEnum
|
|
5
5
|
from os import PathLike
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import IO, Any, Callable, ClassVar,
|
|
7
|
+
from typing import IO, Any, Callable, ClassVar, Generic, Literal, Optional, TypeVar, Union
|
|
8
8
|
from typing_extensions import TypeAlias
|
|
9
9
|
|
|
10
10
|
from .element import Element, transform
|
|
@@ -14,7 +14,7 @@ from .parser import parse
|
|
|
14
14
|
|
|
15
15
|
@dataclass
|
|
16
16
|
class ModelBase:
|
|
17
|
-
__converter__: ClassVar[
|
|
17
|
+
__converter__: ClassVar[dict[str, Callable[[Any], Any]]] = {}
|
|
18
18
|
|
|
19
19
|
@classmethod
|
|
20
20
|
def parse(cls, raw: dict):
|
|
@@ -81,7 +81,7 @@ class User(ModelBase):
|
|
|
81
81
|
is_bot: Optional[bool] = None
|
|
82
82
|
|
|
83
83
|
def dump(self):
|
|
84
|
-
res:
|
|
84
|
+
res: dict[str, Any] = {"id": self.id}
|
|
85
85
|
if self.name:
|
|
86
86
|
res["name"] = self.name
|
|
87
87
|
if self.nick:
|
|
@@ -141,13 +141,13 @@ class Login(ModelBase):
|
|
|
141
141
|
user: Optional[User] = None
|
|
142
142
|
self_id: Optional[str] = None
|
|
143
143
|
platform: Optional[str] = None
|
|
144
|
-
features:
|
|
145
|
-
proxy_urls:
|
|
144
|
+
features: list[str] = field(default_factory=list)
|
|
145
|
+
proxy_urls: list[str] = field(default_factory=list)
|
|
146
146
|
|
|
147
147
|
__converter__ = {"user": User.parse, "status": LoginStatus}
|
|
148
148
|
|
|
149
149
|
def dump(self):
|
|
150
|
-
res:
|
|
150
|
+
res: dict[str, Any] = {
|
|
151
151
|
"status": self.status.value,
|
|
152
152
|
"features": self.features,
|
|
153
153
|
"proxy_urls": self.proxy_urls,
|
|
@@ -195,7 +195,7 @@ class Identify(ModelBase):
|
|
|
195
195
|
|
|
196
196
|
@dataclass
|
|
197
197
|
class Ready(ModelBase):
|
|
198
|
-
logins:
|
|
198
|
+
logins: list[Login]
|
|
199
199
|
|
|
200
200
|
|
|
201
201
|
@dataclass
|
|
@@ -213,7 +213,7 @@ class MessageObject(ModelBase):
|
|
|
213
213
|
def from_elements(
|
|
214
214
|
cls,
|
|
215
215
|
id: str,
|
|
216
|
-
content:
|
|
216
|
+
content: list[Element],
|
|
217
217
|
channel: Optional[Channel] = None,
|
|
218
218
|
guild: Optional[Guild] = None,
|
|
219
219
|
member: Optional[Member] = None,
|
|
@@ -224,7 +224,7 @@ class MessageObject(ModelBase):
|
|
|
224
224
|
return cls(id, "".join(str(i) for i in content), channel, guild, member, user, created_at, updated_at)
|
|
225
225
|
|
|
226
226
|
@property
|
|
227
|
-
def message(self) ->
|
|
227
|
+
def message(self) -> list[Element]:
|
|
228
228
|
return transform(parse(self.content))
|
|
229
229
|
|
|
230
230
|
@classmethod
|
|
@@ -244,7 +244,7 @@ class MessageObject(ModelBase):
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
def dump(self):
|
|
247
|
-
res:
|
|
247
|
+
res: dict[str, Any] = {"id": self.id, "content": self.content}
|
|
248
248
|
if self.channel:
|
|
249
249
|
res["channel"] = self.channel.dump()
|
|
250
250
|
if self.guild:
|
|
@@ -335,7 +335,7 @@ T = TypeVar("T", bound=ModelBase)
|
|
|
335
335
|
|
|
336
336
|
@dataclass
|
|
337
337
|
class PageResult(ModelBase, Generic[T]):
|
|
338
|
-
data:
|
|
338
|
+
data: list[T]
|
|
339
339
|
next: Optional[str] = None
|
|
340
340
|
|
|
341
341
|
@classmethod
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import re
|
|
2
|
+
from collections.abc import Iterable
|
|
2
3
|
from dataclasses import dataclass, field
|
|
3
4
|
from enum import IntEnum
|
|
4
|
-
from typing import Any, Callable,
|
|
5
|
+
from typing import Any, Callable, Literal, Optional, TypedDict, TypeVar, Union, cast
|
|
5
6
|
from typing_extensions import TypeAlias
|
|
6
7
|
|
|
7
8
|
T = TypeVar("T")
|
|
@@ -39,13 +40,13 @@ def snake_case(source: str) -> str:
|
|
|
39
40
|
)
|
|
40
41
|
|
|
41
42
|
|
|
42
|
-
def ensure_list(value: Union[T,
|
|
43
|
+
def ensure_list(value: Union[T, list[T], None]) -> list[T]:
|
|
43
44
|
return value if isinstance(value, list) else [value] if value else []
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
S = TypeVar("S")
|
|
47
|
-
Fragment: TypeAlias = Union[str, "Element",
|
|
48
|
-
Render: TypeAlias = Callable[[dict,
|
|
48
|
+
Fragment: TypeAlias = Union[str, "Element", list[Union[str, "Element"]]]
|
|
49
|
+
Render: TypeAlias = Callable[[dict, list["Element"], S], T]
|
|
49
50
|
Visitor: TypeAlias = Callable[["Element", S], T]
|
|
50
51
|
|
|
51
52
|
|
|
@@ -60,7 +61,7 @@ def make_element(content: Union[str, bool, int, float, "Element"]) -> Optional["
|
|
|
60
61
|
raise ValueError(f"Invalid content: {content!r}")
|
|
61
62
|
|
|
62
63
|
|
|
63
|
-
def make_elements(content: Fragment) ->
|
|
64
|
+
def make_elements(content: Fragment) -> list["Element"]:
|
|
64
65
|
if isinstance(content, list):
|
|
65
66
|
res = [make_element(c) for c in content]
|
|
66
67
|
else:
|
|
@@ -70,14 +71,14 @@ def make_elements(content: Fragment) -> List["Element"]:
|
|
|
70
71
|
|
|
71
72
|
class Element:
|
|
72
73
|
type: str
|
|
73
|
-
attrs:
|
|
74
|
-
children:
|
|
74
|
+
attrs: dict[str, Any]
|
|
75
|
+
children: list["Element"]
|
|
75
76
|
source: Optional[str] = None
|
|
76
77
|
|
|
77
78
|
def __init__(
|
|
78
79
|
self,
|
|
79
80
|
type: Union[str, Render[Fragment, Any]],
|
|
80
|
-
attrs: Optional[
|
|
81
|
+
attrs: Optional[dict[str, Any]] = None,
|
|
81
82
|
*children: Fragment,
|
|
82
83
|
) -> None:
|
|
83
84
|
self.attrs = {}
|
|
@@ -154,8 +155,8 @@ class Selector:
|
|
|
154
155
|
comb_pat = re.compile(" *([ >+~]) *")
|
|
155
156
|
|
|
156
157
|
|
|
157
|
-
def parse_selector(input: str) ->
|
|
158
|
-
def _quert(query: str) ->
|
|
158
|
+
def parse_selector(input: str) -> list[list[Selector]]:
|
|
159
|
+
def _quert(query: str) -> list[Selector]:
|
|
159
160
|
selectors = []
|
|
160
161
|
combinator = " "
|
|
161
162
|
while mat := comb_pat.search(query):
|
|
@@ -173,7 +174,7 @@ def parse_selector(input: str) -> List[List[Selector]]:
|
|
|
173
174
|
return [_quert(q) for q in input.split(",")]
|
|
174
175
|
|
|
175
176
|
|
|
176
|
-
def select(source: Union[str,
|
|
177
|
+
def select(source: Union[str, list[Element]], query: Union[str, list[list[Selector]]]) -> list[Element]:
|
|
177
178
|
if not source or not query:
|
|
178
179
|
return []
|
|
179
180
|
if isinstance(source, str):
|
|
@@ -182,10 +183,10 @@ def select(source: Union[str, List[Element]], query: Union[str, List[List[Select
|
|
|
182
183
|
query = parse_selector(query)
|
|
183
184
|
if not query:
|
|
184
185
|
return []
|
|
185
|
-
adjacent:
|
|
186
|
+
adjacent: list[list[Selector]] = []
|
|
186
187
|
results = []
|
|
187
188
|
for index, elem in enumerate(source):
|
|
188
|
-
inner:
|
|
189
|
+
inner: list[list[Selector]] = []
|
|
189
190
|
local = [*query, *adjacent]
|
|
190
191
|
adjacent = []
|
|
191
192
|
matched = False
|
|
@@ -255,7 +256,7 @@ class Token:
|
|
|
255
256
|
positon: Position
|
|
256
257
|
source: str
|
|
257
258
|
extra: str
|
|
258
|
-
children:
|
|
259
|
+
children: dict[str, list[Union[str, "Token"]]] = field(default_factory=dict)
|
|
259
260
|
|
|
260
261
|
|
|
261
262
|
class StackItem(TypedDict):
|
|
@@ -263,8 +264,8 @@ class StackItem(TypedDict):
|
|
|
263
264
|
slot: str
|
|
264
265
|
|
|
265
266
|
|
|
266
|
-
def fold_tokens(tokens:
|
|
267
|
-
stack:
|
|
267
|
+
def fold_tokens(tokens: list[Union[str, Token]]) -> list[Union[str, Token]]:
|
|
268
|
+
stack: list[StackItem] = [
|
|
268
269
|
{
|
|
269
270
|
"token": Token(
|
|
270
271
|
type="angle",
|
|
@@ -301,8 +302,8 @@ def fold_tokens(tokens: List[Union[str, Token]]) -> List[Union[str, Token]]:
|
|
|
301
302
|
return stack[-1]["token"].children["default"]
|
|
302
303
|
|
|
303
304
|
|
|
304
|
-
def parse_tokens(tokens:
|
|
305
|
-
result:
|
|
305
|
+
def parse_tokens(tokens: list[Union[str, Token]], context: Optional[dict] = None) -> list[Element]:
|
|
306
|
+
result: list[Element] = []
|
|
306
307
|
for token in tokens:
|
|
307
308
|
if isinstance(token, str):
|
|
308
309
|
result.append(Element(type="text", attrs={"text": token}))
|
|
@@ -348,7 +349,7 @@ def parse_tokens(tokens: List[Union[str, Token]], context: Optional[dict] = None
|
|
|
348
349
|
|
|
349
350
|
|
|
350
351
|
def parse(src: str, context: Optional[dict] = None):
|
|
351
|
-
tokens:
|
|
352
|
+
tokens: list[Union[str, Token]] = []
|
|
352
353
|
|
|
353
354
|
def push_text(text: str):
|
|
354
355
|
if text:
|
|
@@ -7,11 +7,12 @@ import secrets
|
|
|
7
7
|
import signal
|
|
8
8
|
import threading
|
|
9
9
|
import urllib.parse
|
|
10
|
+
from collections.abc import Iterable
|
|
10
11
|
from contextlib import suppress
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from tempfile import TemporaryDirectory
|
|
13
14
|
from traceback import print_exc
|
|
14
|
-
from typing import Any,
|
|
15
|
+
from typing import Any, cast
|
|
15
16
|
|
|
16
17
|
import aiohttp
|
|
17
18
|
from creart import it
|
|
@@ -32,6 +33,7 @@ from satori.model import Event, ModelBase, Opcode
|
|
|
32
33
|
|
|
33
34
|
from .adapter import Adapter as Adapter
|
|
34
35
|
from .conection import WebsocketConnection
|
|
36
|
+
from .deque import Deque
|
|
35
37
|
from .formdata import parse_content_disposition
|
|
36
38
|
from .model import Provider as Provider
|
|
37
39
|
from .model import Request as Request
|
|
@@ -99,6 +101,8 @@ class Server(Service, RouterMixin):
|
|
|
99
101
|
self.webhooks = webhooks or []
|
|
100
102
|
self._tempdir = TemporaryDirectory()
|
|
101
103
|
self.proxy_url_mapping = {}
|
|
104
|
+
self._sequence = 0
|
|
105
|
+
self._event_cache = Deque(maxlen=100)
|
|
102
106
|
super().__init__()
|
|
103
107
|
|
|
104
108
|
def apply(self, item: Provider | Router | Adapter):
|
|
@@ -117,6 +121,9 @@ class Server(Service, RouterMixin):
|
|
|
117
121
|
raise TypeError(f"Unknown config type: {item}")
|
|
118
122
|
|
|
119
123
|
async def event_callback(self, event: Event):
|
|
124
|
+
event.id = self._sequence
|
|
125
|
+
self._event_cache.append(event)
|
|
126
|
+
self._sequence += 1
|
|
120
127
|
for connection in self.connections:
|
|
121
128
|
try:
|
|
122
129
|
await connection.send({"op": Opcode.EVENT, "body": event.dump()})
|
|
@@ -146,16 +153,22 @@ class Server(Service, RouterMixin):
|
|
|
146
153
|
identity = await ws.receive_json()
|
|
147
154
|
if not isinstance(identity, dict) or identity.get("op") != Opcode.IDENTIFY:
|
|
148
155
|
return await ws.close(code=3000, reason="Unauthorized")
|
|
149
|
-
|
|
156
|
+
body = identity["body"]
|
|
157
|
+
token = identity["body"].get("token")
|
|
150
158
|
logins = []
|
|
151
159
|
for provider in self.providers:
|
|
152
160
|
if not provider.authenticate(token):
|
|
153
161
|
return await ws.close(code=3000, reason="Unauthorized")
|
|
154
162
|
logins.extend(await provider.get_logins())
|
|
163
|
+
sequence = body.get("sequence", -1)
|
|
155
164
|
await connection.send({"op": Opcode.READY, "body": {"logins": [lo.dump() for lo in logins]}})
|
|
156
165
|
self.connections.append(connection)
|
|
157
|
-
|
|
166
|
+
logger.debug(f"New connection: {id(connection)}")
|
|
158
167
|
try:
|
|
168
|
+
if sequence > -1:
|
|
169
|
+
for event in self._event_cache.after(sequence):
|
|
170
|
+
await connection.send({"op": Opcode.EVENT, "body": event.dump()})
|
|
171
|
+
await asyncio.sleep(0.1)
|
|
159
172
|
await any_completed(connection.heartbeat(), connection.close_signal.wait())
|
|
160
173
|
finally:
|
|
161
174
|
self.connections.remove(connection)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from abc import abstractmethod
|
|
2
|
-
from
|
|
2
|
+
from collections.abc import AsyncIterator
|
|
3
|
+
from typing import Optional
|
|
3
4
|
|
|
4
5
|
from launart import Service
|
|
5
6
|
|
|
@@ -18,17 +19,17 @@ class Adapter(Service, RouterMixin):
|
|
|
18
19
|
def ensure(self, platform: str, self_id: str) -> bool: ...
|
|
19
20
|
|
|
20
21
|
@abstractmethod
|
|
21
|
-
def authenticate(self, token: str) -> bool: ...
|
|
22
|
+
def authenticate(self, token: Optional[str]) -> bool: ...
|
|
22
23
|
|
|
23
24
|
@staticmethod
|
|
24
|
-
def proxy_urls() ->
|
|
25
|
+
def proxy_urls() -> list[str]:
|
|
25
26
|
return []
|
|
26
27
|
|
|
27
28
|
@abstractmethod
|
|
28
29
|
async def download_uploaded(self, platform: str, self_id: str, path: str) -> bytes: ...
|
|
29
30
|
|
|
30
31
|
@abstractmethod
|
|
31
|
-
async def get_logins(self) ->
|
|
32
|
+
async def get_logins(self) -> list[Login]: ...
|
|
32
33
|
|
|
33
34
|
def __init__(self):
|
|
34
35
|
super().__init__()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
from starlette.websockets import WebSocket, WebSocketDisconnect
|
|
7
|
+
|
|
8
|
+
from satori.model import Opcode
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WebsocketConnection:
|
|
12
|
+
connection: WebSocket
|
|
13
|
+
|
|
14
|
+
def __init__(self, connection: WebSocket):
|
|
15
|
+
self.connection = connection
|
|
16
|
+
self.close_signal: asyncio.Event = asyncio.Event()
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def alive(self) -> bool:
|
|
20
|
+
return not self.close_signal.is_set()
|
|
21
|
+
|
|
22
|
+
async def heartbeat(self):
|
|
23
|
+
while True:
|
|
24
|
+
try:
|
|
25
|
+
msg = await asyncio.wait_for(self.connection.receive_json(), timeout=10)
|
|
26
|
+
if not isinstance(msg, dict) or msg.get("op") != Opcode.PING:
|
|
27
|
+
continue
|
|
28
|
+
await self.connection.send_json({"op": Opcode.PONG})
|
|
29
|
+
except asyncio.TimeoutError:
|
|
30
|
+
logger.warning(f"Connection {id(self)} heartbeat timeout, closing connection.")
|
|
31
|
+
await self.connection.close()
|
|
32
|
+
await self.connection_closed()
|
|
33
|
+
break
|
|
34
|
+
except WebSocketDisconnect:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
async def connection_closed(self):
|
|
38
|
+
self.close_signal.set()
|
|
39
|
+
|
|
40
|
+
async def wait_for_available(self):
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
async def send(self, payload: dict) -> None:
|
|
44
|
+
return await self.connection.send_json(payload)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from collections import deque
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Deque:
|
|
5
|
+
def __init__(self, maxlen: int):
|
|
6
|
+
self.data = deque(maxlen=maxlen)
|
|
7
|
+
self.offset = 0
|
|
8
|
+
|
|
9
|
+
def append(self, x):
|
|
10
|
+
if len(self.data) == self.data.maxlen:
|
|
11
|
+
self.offset += 1
|
|
12
|
+
self.data.append(x)
|
|
13
|
+
|
|
14
|
+
def __getitem__(self, i: int):
|
|
15
|
+
index = i - self.offset
|
|
16
|
+
if index < 0 or index >= len(self.data):
|
|
17
|
+
return
|
|
18
|
+
return self.data[index]
|
|
19
|
+
|
|
20
|
+
def after(self, i: int):
|
|
21
|
+
if i < self.offset:
|
|
22
|
+
i = self.offset - 1
|
|
23
|
+
return list(self.data)[i + 1 - self.offset :]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
d = Deque(3)
|
|
28
|
+
d.append(0)
|
|
29
|
+
d.append(1)
|
|
30
|
+
d.append(2)
|
|
31
|
+
d.append(3)
|
|
32
|
+
d.append(4)
|
|
33
|
+
d.append(5)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from collections.abc import AsyncIterator
|
|
3
2
|
from dataclasses import dataclass
|
|
4
|
-
from typing import TYPE_CHECKING, Any,
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Generic, Optional, Protocol, TypeVar, Union, runtime_checkable
|
|
5
4
|
|
|
6
5
|
from satori.const import Api
|
|
7
6
|
from satori.model import Event, Login
|
|
@@ -25,7 +24,7 @@ class Request(Generic[TP]):
|
|
|
25
24
|
class Provider(Protocol):
|
|
26
25
|
def publisher(self) -> AsyncIterator[Event]: ...
|
|
27
26
|
|
|
28
|
-
def authenticate(self, token: str) -> bool: ...
|
|
27
|
+
def authenticate(self, token: Optional[str]) -> bool: ...
|
|
29
28
|
|
|
30
29
|
async def get_logins(self) -> list[Login]: ...
|
|
31
30
|
|
|
@@ -39,4 +38,4 @@ class Provider(Protocol):
|
|
|
39
38
|
|
|
40
39
|
@runtime_checkable
|
|
41
40
|
class Router(Protocol):
|
|
42
|
-
routes: dict[str, RouteCall[Any, Any]]
|
|
41
|
+
routes: dict[str, "RouteCall[Any, Any]"]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
1
|
+
from collections.abc import Awaitable
|
|
2
|
+
from typing import Any, Callable, Literal, Protocol, TypeVar, Union, overload
|
|
2
3
|
from typing_extensions import NotRequired, TypeAlias, TypedDict
|
|
3
4
|
|
|
4
5
|
from starlette.datastructures import FormData
|
|
@@ -30,7 +31,7 @@ class RouteCall(Protocol[T, R]):
|
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
INTERAL: TypeAlias = RouteCall[
|
|
33
|
-
Any, Union[ModelBase,
|
|
34
|
+
Any, Union[ModelBase, list[ModelBase], dict[str, Any], list[dict[str, Any]], None]
|
|
34
35
|
]
|
|
35
36
|
|
|
36
37
|
|
|
@@ -39,7 +40,7 @@ class MessageParam(TypedDict):
|
|
|
39
40
|
content: str
|
|
40
41
|
|
|
41
42
|
|
|
42
|
-
MESSAGE_CREATE: TypeAlias = RouteCall[MessageParam, Union[
|
|
43
|
+
MESSAGE_CREATE: TypeAlias = RouteCall[MessageParam, Union[list[MessageObject], list[dict[str, Any]]]]
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
class MessageOpParam(TypedDict):
|
|
@@ -47,7 +48,7 @@ class MessageOpParam(TypedDict):
|
|
|
47
48
|
message_id: str
|
|
48
49
|
|
|
49
50
|
|
|
50
|
-
MESSAGE_GET: TypeAlias = RouteCall[MessageOpParam, Union[MessageObject,
|
|
51
|
+
MESSAGE_GET: TypeAlias = RouteCall[MessageOpParam, Union[MessageObject, dict[str, Any]]]
|
|
51
52
|
MESSAGE_DELETE: TypeAlias = RouteCall[MessageOpParam, None]
|
|
52
53
|
|
|
53
54
|
|
|
@@ -68,14 +69,14 @@ class MessageListParam(TypedDict):
|
|
|
68
69
|
order: NotRequired[Order]
|
|
69
70
|
|
|
70
71
|
|
|
71
|
-
MESSAGE_LIST: TypeAlias = RouteCall[MessageListParam, Union[PageDequeResult[MessageObject],
|
|
72
|
+
MESSAGE_LIST: TypeAlias = RouteCall[MessageListParam, Union[PageDequeResult[MessageObject], dict[str, Any]]]
|
|
72
73
|
|
|
73
74
|
|
|
74
75
|
class ChannelParam(TypedDict):
|
|
75
76
|
channel_id: str
|
|
76
77
|
|
|
77
78
|
|
|
78
|
-
CHANNEL_GET: TypeAlias = RouteCall[ChannelParam, Union[Channel,
|
|
79
|
+
CHANNEL_GET: TypeAlias = RouteCall[ChannelParam, Union[Channel, dict[str, Any]]]
|
|
79
80
|
CHANNEL_DELETE: TypeAlias = RouteCall[ChannelParam, None]
|
|
80
81
|
|
|
81
82
|
|
|
@@ -84,7 +85,7 @@ class ChannelListParam(TypedDict):
|
|
|
84
85
|
next: NotRequired[str]
|
|
85
86
|
|
|
86
87
|
|
|
87
|
-
CHANNEL_LIST: TypeAlias = RouteCall[ChannelListParam, Union[PageResult[Channel],
|
|
88
|
+
CHANNEL_LIST: TypeAlias = RouteCall[ChannelListParam, Union[PageResult[Channel], dict[str, Any]]]
|
|
88
89
|
|
|
89
90
|
|
|
90
91
|
class ChanneCreateParam(TypedDict):
|
|
@@ -92,7 +93,7 @@ class ChanneCreateParam(TypedDict):
|
|
|
92
93
|
data: dict
|
|
93
94
|
|
|
94
95
|
|
|
95
|
-
CHANNEL_CREATE: TypeAlias = RouteCall[ChanneCreateParam, Union[Channel,
|
|
96
|
+
CHANNEL_CREATE: TypeAlias = RouteCall[ChanneCreateParam, Union[Channel, dict[str, Any]]]
|
|
96
97
|
|
|
97
98
|
|
|
98
99
|
class ChanneUpdateParam(TypedDict):
|
|
@@ -116,21 +117,21 @@ class UserChannelCreateParam(TypedDict):
|
|
|
116
117
|
guild_id: NotRequired[str]
|
|
117
118
|
|
|
118
119
|
|
|
119
|
-
ROUTE_USER_CHANNEL_CREATE: TypeAlias = RouteCall[UserChannelCreateParam, Union[Channel,
|
|
120
|
+
ROUTE_USER_CHANNEL_CREATE: TypeAlias = RouteCall[UserChannelCreateParam, Union[Channel, dict[str, Any]]]
|
|
120
121
|
|
|
121
122
|
|
|
122
123
|
class GuildGetParam(TypedDict):
|
|
123
124
|
guild_id: str
|
|
124
125
|
|
|
125
126
|
|
|
126
|
-
GUILD_GET: TypeAlias = RouteCall[GuildGetParam, Union[Guild,
|
|
127
|
+
GUILD_GET: TypeAlias = RouteCall[GuildGetParam, Union[Guild, dict[str, Any]]]
|
|
127
128
|
|
|
128
129
|
|
|
129
130
|
class GuildListParam(TypedDict):
|
|
130
131
|
next: NotRequired[str]
|
|
131
132
|
|
|
132
133
|
|
|
133
|
-
GUILD_LIST: TypeAlias = RouteCall[GuildListParam, Union[PageResult[Guild],
|
|
134
|
+
GUILD_LIST: TypeAlias = RouteCall[GuildListParam, Union[PageResult[Guild], dict[str, Any]]]
|
|
134
135
|
|
|
135
136
|
|
|
136
137
|
class GuildMemberGetParam(TypedDict):
|
|
@@ -138,7 +139,7 @@ class GuildMemberGetParam(TypedDict):
|
|
|
138
139
|
user_id: str
|
|
139
140
|
|
|
140
141
|
|
|
141
|
-
GUILD_MEMBER_GET: TypeAlias = RouteCall[GuildMemberGetParam, Union[Member,
|
|
142
|
+
GUILD_MEMBER_GET: TypeAlias = RouteCall[GuildMemberGetParam, Union[Member, dict[str, Any]]]
|
|
142
143
|
|
|
143
144
|
|
|
144
145
|
class GuildXXXListParam(TypedDict):
|
|
@@ -146,7 +147,7 @@ class GuildXXXListParam(TypedDict):
|
|
|
146
147
|
next: NotRequired[str]
|
|
147
148
|
|
|
148
149
|
|
|
149
|
-
GUILD_MEMBER_LIST: TypeAlias = RouteCall[GuildXXXListParam, Union[PageResult[Member],
|
|
150
|
+
GUILD_MEMBER_LIST: TypeAlias = RouteCall[GuildXXXListParam, Union[PageResult[Member], dict[str, Any]]]
|
|
150
151
|
|
|
151
152
|
|
|
152
153
|
class GuildMemberKickParam(TypedDict):
|
|
@@ -176,7 +177,7 @@ class GuildMemberRoleParam(TypedDict):
|
|
|
176
177
|
GUILD_MEMBER_ROLE_SET: TypeAlias = RouteCall[GuildMemberRoleParam, None]
|
|
177
178
|
GUILD_MEMBER_ROLE_UNSET: TypeAlias = RouteCall[GuildMemberRoleParam, None]
|
|
178
179
|
|
|
179
|
-
GUILD_ROLE_LIST: TypeAlias = RouteCall[GuildXXXListParam, Union[PageResult[Role],
|
|
180
|
+
GUILD_ROLE_LIST: TypeAlias = RouteCall[GuildXXXListParam, Union[PageResult[Role], dict[str, Any]]]
|
|
180
181
|
|
|
181
182
|
|
|
182
183
|
class GuildRoleCreateParam(TypedDict):
|
|
@@ -184,7 +185,7 @@ class GuildRoleCreateParam(TypedDict):
|
|
|
184
185
|
role: dict
|
|
185
186
|
|
|
186
187
|
|
|
187
|
-
GUILD_ROLE_CREATE: TypeAlias = RouteCall[GuildRoleCreateParam, Union[Role,
|
|
188
|
+
GUILD_ROLE_CREATE: TypeAlias = RouteCall[GuildRoleCreateParam, Union[Role, dict[str, Any]]]
|
|
188
189
|
|
|
189
190
|
|
|
190
191
|
class GuildRoleUpdateParam(TypedDict):
|
|
@@ -239,22 +240,22 @@ class ReactionListParam(TypedDict):
|
|
|
239
240
|
next: NotRequired[str]
|
|
240
241
|
|
|
241
242
|
|
|
242
|
-
REACTION_LIST: TypeAlias = RouteCall[ReactionListParam, Union[PageResult[User],
|
|
243
|
-
LOGIN_GET: TypeAlias = RouteCall[Any, Union[Login,
|
|
243
|
+
REACTION_LIST: TypeAlias = RouteCall[ReactionListParam, Union[PageResult[User], dict[str, Any]]]
|
|
244
|
+
LOGIN_GET: TypeAlias = RouteCall[Any, Union[Login, dict[str, Any]]]
|
|
244
245
|
|
|
245
246
|
|
|
246
247
|
class UserGetParam(TypedDict):
|
|
247
248
|
user_id: str
|
|
248
249
|
|
|
249
250
|
|
|
250
|
-
USER_GET: TypeAlias = RouteCall[UserGetParam, Union[User,
|
|
251
|
+
USER_GET: TypeAlias = RouteCall[UserGetParam, Union[User, dict[str, Any]]]
|
|
251
252
|
|
|
252
253
|
|
|
253
254
|
class FriendListParam(TypedDict):
|
|
254
255
|
next: NotRequired[str]
|
|
255
256
|
|
|
256
257
|
|
|
257
|
-
FRIEND_LIST: TypeAlias = RouteCall[FriendListParam, Union[PageResult[User],
|
|
258
|
+
FRIEND_LIST: TypeAlias = RouteCall[FriendListParam, Union[PageResult[User], dict[str, Any]]]
|
|
258
259
|
|
|
259
260
|
|
|
260
261
|
class ApproveParam(TypedDict):
|
|
@@ -266,11 +267,11 @@ class ApproveParam(TypedDict):
|
|
|
266
267
|
APPROVE: TypeAlias = RouteCall[ApproveParam, None]
|
|
267
268
|
|
|
268
269
|
|
|
269
|
-
UPLOAD_CREATE: TypeAlias = RouteCall[FormData,
|
|
270
|
+
UPLOAD_CREATE: TypeAlias = RouteCall[FormData, dict[str, str]]
|
|
270
271
|
|
|
271
272
|
|
|
272
273
|
class RouterMixin:
|
|
273
|
-
routes:
|
|
274
|
+
routes: dict[str, RouteCall[Any, Any]]
|
|
274
275
|
|
|
275
276
|
@overload
|
|
276
277
|
def route(self, path: Literal[Api.MESSAGE_CREATE]) -> Callable[[MESSAGE_CREATE], MESSAGE_CREATE]: ...
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
|
|
5
|
-
from starlette.websockets import WebSocket
|
|
6
|
-
|
|
7
|
-
from satori.model import Opcode
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class WebsocketConnection:
|
|
11
|
-
connection: WebSocket
|
|
12
|
-
|
|
13
|
-
def __init__(self, connection: WebSocket):
|
|
14
|
-
self.connection = connection
|
|
15
|
-
self.close_signal: asyncio.Event = asyncio.Event()
|
|
16
|
-
|
|
17
|
-
@property
|
|
18
|
-
def id(self):
|
|
19
|
-
return self.connection.headers["X-Self-ID"]
|
|
20
|
-
|
|
21
|
-
@property
|
|
22
|
-
def alive(self) -> bool:
|
|
23
|
-
return not self.close_signal.is_set()
|
|
24
|
-
|
|
25
|
-
async def heartbeat(self):
|
|
26
|
-
async for msg in self.connection.iter_json():
|
|
27
|
-
if not isinstance(msg, dict) or msg.get("op") != Opcode.PING:
|
|
28
|
-
continue
|
|
29
|
-
await self.connection.send_json({"op": Opcode.PONG})
|
|
30
|
-
else:
|
|
31
|
-
await self.connection_closed()
|
|
32
|
-
|
|
33
|
-
async def connection_closed(self):
|
|
34
|
-
self.close_signal.set()
|
|
35
|
-
|
|
36
|
-
async def wait_for_available(self):
|
|
37
|
-
return
|
|
38
|
-
|
|
39
|
-
async def send(self, payload: dict) -> None:
|
|
40
|
-
return await self.connection.send_json(payload)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|