satori-python-client 0.15.2__tar.gz → 0.16.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_client-0.15.2 → satori_python_client-0.16.0}/.mina/client.toml +2 -2
- {satori_python_client-0.15.2 → satori_python_client-0.16.0}/PKG-INFO +5 -5
- {satori_python_client-0.15.2 → satori_python_client-0.16.0}/README.md +2 -2
- {satori_python_client-0.15.2 → satori_python_client-0.16.0}/pyproject.toml +3 -3
- {satori_python_client-0.15.2 → satori_python_client-0.16.0}/src/satori/client/__init__.py +17 -18
- {satori_python_client-0.15.2 → satori_python_client-0.16.0}/src/satori/client/account.py +20 -17
- {satori_python_client-0.15.2 → satori_python_client-0.16.0}/src/satori/client/account.pyi +33 -11
- satori_python_client-0.16.0/src/satori/client/config.py +67 -0
- satori_python_client-0.16.0/src/satori/client/network/base.py +35 -0
- satori_python_client-0.16.0/src/satori/client/network/webhook.py +111 -0
- {satori_python_client-0.15.2 → satori_python_client-0.16.0}/src/satori/client/network/websocket.py +44 -35
- {satori_python_client-0.15.2 → satori_python_client-0.16.0}/src/satori/client/protocol.py +40 -12
- satori_python_client-0.15.2/src/satori/client/network/base.py +0 -56
- satori_python_client-0.15.2/src/satori/client/network/webhook.py +0 -131
- {satori_python_client-0.15.2 → satori_python_client-0.16.0}/LICENSE +0 -0
- {satori_python_client-0.15.2 → satori_python_client-0.16.0}/src/satori/client/network/__init__.py +0 -0
- {satori_python_client-0.15.2 → satori_python_client-0.16.0}/src/satori/client/network/util.py +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
includes = ["src/satori/client"]
|
|
2
|
-
raw-dependencies = ["satori-python-core >= 0.
|
|
2
|
+
raw-dependencies = ["satori-python-core >= 0.16.0"]
|
|
3
3
|
|
|
4
4
|
[project]
|
|
5
5
|
name = "satori-python-client"
|
|
@@ -15,7 +15,7 @@ dependencies = [
|
|
|
15
15
|
description = "Satori Protocol SDK for python, specify client part"
|
|
16
16
|
license = {text = "MIT"}
|
|
17
17
|
readme = "README.md"
|
|
18
|
-
requires-python = ">=3.
|
|
18
|
+
requires-python = ">=3.9"
|
|
19
19
|
classifiers = [
|
|
20
20
|
"Typing :: Typed",
|
|
21
21
|
"Development Status :: 4 - Beta",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: satori-python-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.0
|
|
4
4
|
Summary: Satori Protocol SDK for python, specify client part
|
|
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,11 +16,11 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
16
16
|
Classifier: Operating System :: OS Independent
|
|
17
17
|
Project-URL: Homepage, https://github.com/RF-Tar-Railt/satori-python
|
|
18
18
|
Project-URL: Repository, https://github.com/RF-Tar-Railt/satori-python
|
|
19
|
-
Requires-Python: >=3.
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
20
|
Requires-Dist: aiohttp>=3.9.3
|
|
21
21
|
Requires-Dist: launart>=0.8.2
|
|
22
22
|
Requires-Dist: graia-amnesia>=0.9.0
|
|
23
|
-
Requires-Dist: satori-python-core>=0.
|
|
23
|
+
Requires-Dist: satori-python-core>=0.16.0
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
|
|
26
26
|
# satori-python
|
|
@@ -75,9 +75,9 @@ pip install satori-python-server
|
|
|
75
75
|
客户端:
|
|
76
76
|
|
|
77
77
|
```python
|
|
78
|
-
from satori import EventType
|
|
78
|
+
from satori import EventType
|
|
79
79
|
from satori.event import MessageEvent
|
|
80
|
-
from satori.client import Account, App
|
|
80
|
+
from satori.client import Account, App, WebsocketsInfo
|
|
81
81
|
|
|
82
82
|
app = App(WebsocketsInfo(port=5140))
|
|
83
83
|
|
|
@@ -50,9 +50,9 @@ pip install satori-python-server
|
|
|
50
50
|
客户端:
|
|
51
51
|
|
|
52
52
|
```python
|
|
53
|
-
from satori import EventType
|
|
53
|
+
from satori import EventType
|
|
54
54
|
from satori.event import MessageEvent
|
|
55
|
-
from satori.client import Account, App
|
|
55
|
+
from satori.client import Account, App, WebsocketsInfo
|
|
56
56
|
|
|
57
57
|
app = App(WebsocketsInfo(port=5140))
|
|
58
58
|
|
|
@@ -8,11 +8,11 @@ dependencies = [
|
|
|
8
8
|
"aiohttp>=3.9.3",
|
|
9
9
|
"launart>=0.8.2",
|
|
10
10
|
"graia-amnesia>=0.9.0",
|
|
11
|
-
"satori-python-core >= 0.
|
|
11
|
+
"satori-python-core >= 0.16.0",
|
|
12
12
|
]
|
|
13
13
|
description = "Satori Protocol SDK for python, specify client part"
|
|
14
14
|
readme = "README.md"
|
|
15
|
-
requires-python = ">=3.
|
|
15
|
+
requires-python = ">=3.9"
|
|
16
16
|
classifiers = [
|
|
17
17
|
"Typing :: Typed",
|
|
18
18
|
"Development Status :: 4 - Beta",
|
|
@@ -24,7 +24,7 @@ classifiers = [
|
|
|
24
24
|
"Programming Language :: Python :: 3.12",
|
|
25
25
|
"Operating System :: OS Independent",
|
|
26
26
|
]
|
|
27
|
-
version = "0.
|
|
27
|
+
version = "0.16.0"
|
|
28
28
|
|
|
29
29
|
[project.license]
|
|
30
30
|
text = "MIT"
|
|
@@ -14,12 +14,14 @@ from launart import Launart, Service, any_completed
|
|
|
14
14
|
from loguru import logger
|
|
15
15
|
|
|
16
16
|
from satori import event as events
|
|
17
|
-
from satori.config import Config, WebhookInfo, WebsocketsInfo
|
|
18
17
|
from satori.const import EventType
|
|
19
18
|
from satori.model import Event, LoginStatus
|
|
20
19
|
|
|
21
20
|
from .account import Account as Account
|
|
22
21
|
from .account import ApiInfo as ApiInfo
|
|
22
|
+
from .config import Config
|
|
23
|
+
from .config import WebhookInfo as WebhookInfo
|
|
24
|
+
from .config import WebsocketsInfo as WebsocketsInfo
|
|
23
25
|
from .network.base import BaseNetwork as BaseNetwork
|
|
24
26
|
from .network.webhook import WebhookNetwork
|
|
25
27
|
from .network.websocket import WsNetwork
|
|
@@ -187,16 +189,15 @@ class App(Service):
|
|
|
187
189
|
async def post(self, event: Event, conn: BaseNetwork):
|
|
188
190
|
if not self.event_callbacks:
|
|
189
191
|
return
|
|
190
|
-
|
|
191
|
-
if
|
|
192
|
+
login_sn = f"{event.login.sn}@{id(conn)}"
|
|
193
|
+
if login_sn not in self.accounts:
|
|
192
194
|
if event.type == EventType.LOGIN_ADDED:
|
|
193
195
|
if TYPE_CHECKING:
|
|
194
196
|
assert isinstance(event, events.LoginEvent)
|
|
195
197
|
account = Account(
|
|
196
|
-
event.platform_,
|
|
197
|
-
event.self_id_,
|
|
198
198
|
event.login,
|
|
199
199
|
conn.config,
|
|
200
|
+
conn.proxy_urls,
|
|
200
201
|
self.default_api_cls,
|
|
201
202
|
)
|
|
202
203
|
logger.info(f"account added: {account}")
|
|
@@ -205,26 +206,23 @@ class App(Service):
|
|
|
205
206
|
if event.login.status == LoginStatus.ONLINE
|
|
206
207
|
else account.connected.clear()
|
|
207
208
|
)
|
|
208
|
-
self.accounts[
|
|
209
|
-
conn.accounts[
|
|
210
|
-
await self.account_update(account,
|
|
211
|
-
await self.account_update(account, LoginStatus.ONLINE)
|
|
209
|
+
self.accounts[login_sn] = account
|
|
210
|
+
conn.accounts[login_sn] = account
|
|
211
|
+
await self.account_update(account, event.login.status)
|
|
212
212
|
elif event.type == EventType.LOGIN_UPDATED:
|
|
213
213
|
if TYPE_CHECKING:
|
|
214
214
|
assert isinstance(event, events.LoginEvent)
|
|
215
215
|
if event.login.status == LoginStatus.ONLINE:
|
|
216
216
|
account = Account(
|
|
217
|
-
event.platform_,
|
|
218
|
-
event.self_id_,
|
|
219
217
|
event.login,
|
|
220
218
|
conn.config,
|
|
219
|
+
conn.proxy_urls,
|
|
221
220
|
self.default_api_cls,
|
|
222
221
|
)
|
|
223
222
|
logger.info(f"account added: {account}")
|
|
224
223
|
account.connected.set()
|
|
225
|
-
self.accounts[
|
|
226
|
-
conn.accounts[
|
|
227
|
-
await self.account_update(account, LoginStatus.CONNECT)
|
|
224
|
+
self.accounts[login_sn] = account
|
|
225
|
+
conn.accounts[login_sn] = account
|
|
228
226
|
await self.account_update(account, LoginStatus.ONLINE)
|
|
229
227
|
else:
|
|
230
228
|
logger.warning(f"Received event for unknown account: {event}")
|
|
@@ -233,7 +231,7 @@ class App(Service):
|
|
|
233
231
|
logger.warning(f"Received event for unknown account: {event}")
|
|
234
232
|
return
|
|
235
233
|
else:
|
|
236
|
-
account = self.accounts[
|
|
234
|
+
account = self.accounts[login_sn]
|
|
237
235
|
if event.type == EventType.LOGIN_UPDATED:
|
|
238
236
|
if TYPE_CHECKING:
|
|
239
237
|
assert isinstance(event, events.LoginEvent)
|
|
@@ -243,6 +241,7 @@ class App(Service):
|
|
|
243
241
|
if event.login.status in (LoginStatus.ONLINE, LoginStatus.CONNECT)
|
|
244
242
|
else account.connected.clear()
|
|
245
243
|
)
|
|
244
|
+
await self.account_update(account, event.login.status)
|
|
246
245
|
|
|
247
246
|
await asyncio.gather(*(callback(account, event) for callback in self.event_callbacks))
|
|
248
247
|
|
|
@@ -251,9 +250,9 @@ class App(Service):
|
|
|
251
250
|
assert isinstance(event, events.LoginEvent)
|
|
252
251
|
logger.info(f"account removed: {account}")
|
|
253
252
|
account.connected.clear()
|
|
254
|
-
await self.account_update(account, LoginStatus.
|
|
255
|
-
del self.accounts[
|
|
256
|
-
del conn.accounts[
|
|
253
|
+
await self.account_update(account, LoginStatus.OFFLINE)
|
|
254
|
+
del self.accounts[login_sn]
|
|
255
|
+
del conn.accounts[login_sn]
|
|
257
256
|
|
|
258
257
|
async def launch(self, manager: Launart):
|
|
259
258
|
for conn in self.connections:
|
|
@@ -6,7 +6,7 @@ from typing import Generic, TypeVar
|
|
|
6
6
|
|
|
7
7
|
from yarl import URL
|
|
8
8
|
|
|
9
|
-
from satori.model import
|
|
9
|
+
from satori.model import Login
|
|
10
10
|
|
|
11
11
|
from .protocol import ApiProtocol
|
|
12
12
|
|
|
@@ -33,44 +33,47 @@ class ApiInfo:
|
|
|
33
33
|
class Account(Generic[TP]):
|
|
34
34
|
def __init__(
|
|
35
35
|
self,
|
|
36
|
-
|
|
37
|
-
self_id: str,
|
|
38
|
-
self_info: LoginType,
|
|
36
|
+
login: Login,
|
|
39
37
|
config: ApiInfo,
|
|
38
|
+
proxy_urls: list[str],
|
|
40
39
|
protocol_cls: type[TP] = ApiProtocol,
|
|
41
40
|
):
|
|
42
|
-
self.
|
|
43
|
-
self.
|
|
44
|
-
self.self_info =
|
|
41
|
+
self.sn = login.sn
|
|
42
|
+
self.adapter = login.adapter
|
|
43
|
+
self.self_info = login
|
|
45
44
|
self.config = config
|
|
45
|
+
self.proxy_urls = proxy_urls
|
|
46
46
|
self.protocol = protocol_cls(self) # type: ignore
|
|
47
47
|
self.connected = asyncio.Event()
|
|
48
48
|
|
|
49
|
+
@property
|
|
50
|
+
def platform(self):
|
|
51
|
+
return self.self_info.platform or "satori"
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def self_id(self):
|
|
55
|
+
return self.self_info.id
|
|
56
|
+
|
|
49
57
|
def custom(
|
|
50
58
|
self, config: ApiInfo | None = None, protocol_cls: type[TP1] = ApiProtocol, **kwargs
|
|
51
59
|
) -> "Account[TP1]":
|
|
52
60
|
return Account(
|
|
53
|
-
self.platform,
|
|
54
|
-
self.self_id,
|
|
55
61
|
self.self_info,
|
|
56
62
|
config or (ApiInfo(**kwargs) if kwargs else self.config),
|
|
63
|
+
self.proxy_urls,
|
|
57
64
|
protocol_cls, # type: ignore
|
|
58
65
|
)
|
|
59
66
|
|
|
60
|
-
@property
|
|
61
|
-
def identity(self):
|
|
62
|
-
return f"{self.platform}/{self.self_id}"
|
|
63
|
-
|
|
64
67
|
def ensure_url(self, url: str) -> URL:
|
|
65
68
|
"""确定链接形式。
|
|
66
69
|
|
|
67
70
|
若链接符合以下条件之一,则返回链接的代理形式 ({host}/{path}/{version}/proxy/{url}):
|
|
68
|
-
- 链接以 "
|
|
69
|
-
- 链接开头出现在
|
|
71
|
+
- 链接以 "internal:" 开头
|
|
72
|
+
- 链接开头出现在 proxy_urls 中的某一项
|
|
70
73
|
"""
|
|
71
|
-
if url.startswith("
|
|
74
|
+
if url.startswith("internal:"):
|
|
72
75
|
return self.config.api_base / "proxy" / url.lstrip("/")
|
|
73
|
-
for proxy_url in self.
|
|
76
|
+
for proxy_url in self.proxy_urls:
|
|
74
77
|
if url.startswith(proxy_url):
|
|
75
78
|
return self.config.api_base / "proxy" / url.lstrip("/")
|
|
76
79
|
return URL(url)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from collections.abc import Iterable
|
|
3
3
|
from typing import Any, Generic, Protocol, TypeVar, overload
|
|
4
|
+
from typing_extensions import deprecated
|
|
4
5
|
|
|
5
6
|
from yarl import URL
|
|
6
7
|
|
|
@@ -10,10 +11,11 @@ from satori.model import (
|
|
|
10
11
|
Direction,
|
|
11
12
|
Event,
|
|
12
13
|
Guild,
|
|
13
|
-
|
|
14
|
+
Login,
|
|
14
15
|
Member,
|
|
15
16
|
MessageObject,
|
|
16
17
|
MessageReceipt,
|
|
18
|
+
Meta,
|
|
17
19
|
Order,
|
|
18
20
|
PageDequeResult,
|
|
19
21
|
PageResult,
|
|
@@ -39,23 +41,25 @@ class ApiInfo(Api):
|
|
|
39
41
|
): ...
|
|
40
42
|
|
|
41
43
|
class Account(Generic[TP]):
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
self_info:
|
|
44
|
+
sn: str
|
|
45
|
+
adapter: str
|
|
46
|
+
self_info: Login
|
|
47
|
+
proxy_urls: list[str]
|
|
45
48
|
config: Api
|
|
46
49
|
protocol: TP
|
|
47
50
|
connected: asyncio.Event
|
|
48
51
|
|
|
49
52
|
def __init__(
|
|
50
53
|
self,
|
|
51
|
-
|
|
52
|
-
self_id: str,
|
|
53
|
-
self_info: LoginType,
|
|
54
|
+
login: Login,
|
|
54
55
|
config: Api,
|
|
56
|
+
proxy_urls: list[str],
|
|
55
57
|
protocol_cls: type[TP] = ApiProtocol,
|
|
56
58
|
): ...
|
|
57
59
|
@property
|
|
58
|
-
def
|
|
60
|
+
def platform(self) -> str: ...
|
|
61
|
+
@property
|
|
62
|
+
def self_id(self) -> str: ...
|
|
59
63
|
@overload
|
|
60
64
|
def custom(self, config: Api, protocol_cls: type[TP1] = ApiProtocol) -> Account[TP1]: ...
|
|
61
65
|
@overload
|
|
@@ -496,7 +500,7 @@ class Account(Generic[TP]):
|
|
|
496
500
|
PageResult[User]: `User` 的分页列表
|
|
497
501
|
"""
|
|
498
502
|
|
|
499
|
-
async def login_get(self) ->
|
|
503
|
+
async def login_get(self) -> Login:
|
|
500
504
|
"""获取当前登录信息。返回一个 `Login` 对象。
|
|
501
505
|
|
|
502
506
|
Returns:
|
|
@@ -535,21 +539,36 @@ class Account(Generic[TP]):
|
|
|
535
539
|
None: 该方法无返回值
|
|
536
540
|
"""
|
|
537
541
|
|
|
538
|
-
async def internal(self, action: str, **kwargs) -> Any:
|
|
542
|
+
async def internal(self, action: str, method: str = "POST", **kwargs) -> Any:
|
|
539
543
|
"""内部接口调用。
|
|
540
544
|
|
|
541
545
|
Args:
|
|
542
546
|
action (str): 内部接口名称
|
|
547
|
+
method (str, optional): 请求方法,默认为 POST
|
|
543
548
|
**kwargs: 参数
|
|
544
549
|
"""
|
|
545
550
|
|
|
546
|
-
async def
|
|
551
|
+
async def meta_get(self) -> Meta:
|
|
552
|
+
"""获取元信息。返回一个 `Meta` 对象。
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
Meta: `Meta` 对象
|
|
556
|
+
"""
|
|
557
|
+
|
|
558
|
+
@deprecated("Use `meta_get` instead")
|
|
559
|
+
async def admin_login_list(self) -> list[Login]:
|
|
547
560
|
"""获取登录信息列表。返回一个 `Login` 对象构成的数组。
|
|
548
561
|
|
|
549
562
|
Returns:
|
|
550
563
|
list[Login]: `Login` 对象构成的数组
|
|
551
564
|
"""
|
|
552
565
|
|
|
566
|
+
async def webhook_create(self, url: str, token: str | None = None):
|
|
567
|
+
"""创建 Webhook。"""
|
|
568
|
+
|
|
569
|
+
async def webhook_delete(self, url: str):
|
|
570
|
+
"""删除 Webhook。"""
|
|
571
|
+
|
|
553
572
|
@overload
|
|
554
573
|
async def upload_create(self, *uploads: Upload) -> list[str]: ...
|
|
555
574
|
@overload
|
|
@@ -564,3 +583,6 @@ class Account(Generic[TP]):
|
|
|
564
583
|
|
|
565
584
|
async def download(self, url: str):
|
|
566
585
|
"""访问内部链接。"""
|
|
586
|
+
|
|
587
|
+
async def request_internal(self, url: str, method: str = "GET", **kwargs) -> dict:
|
|
588
|
+
"""访问内部链接。"""
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from yarl import URL
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Config:
|
|
8
|
+
@property
|
|
9
|
+
def identity(self) -> str:
|
|
10
|
+
raise NotImplementedError
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def token(self) -> Optional[str]:
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def api_base(self) -> URL:
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class WebsocketsInfo(Config):
|
|
23
|
+
host: str = "localhost"
|
|
24
|
+
port: int = 5140
|
|
25
|
+
path: str = ""
|
|
26
|
+
token: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
def __post_init__(self):
|
|
29
|
+
if self.path and not self.path.startswith("/"):
|
|
30
|
+
self.path = f"/{self.path}"
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def identity(self):
|
|
34
|
+
return f"{self.host}:{self.port}"
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def api_base(self):
|
|
38
|
+
return URL(f"http://{self.host}:{self.port}{self.path}") / "v1"
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def ws_base(self):
|
|
42
|
+
return URL(f"ws://{self.host}:{self.port}{self.path}") / "v1"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class WebhookInfo(Config):
|
|
47
|
+
host: str = "127.0.0.1"
|
|
48
|
+
port: int = 8080
|
|
49
|
+
path: str = "v1/events"
|
|
50
|
+
token: Optional[str] = None
|
|
51
|
+
server_host: str = "localhost"
|
|
52
|
+
server_port: int = 5140
|
|
53
|
+
server_path: str = ""
|
|
54
|
+
|
|
55
|
+
def __post_init__(self):
|
|
56
|
+
if self.path and not self.path.startswith("/"):
|
|
57
|
+
self.path = f"/{self.path}"
|
|
58
|
+
if self.server_path and not self.server_path.startswith("/"):
|
|
59
|
+
self.server_path = f"/{self.server_path}"
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def identity(self):
|
|
63
|
+
return f"{self.host}:{self.port}{self.path}"
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def api_base(self):
|
|
67
|
+
return URL(f"http://{self.server_host}:{self.server_port}{self.server_path}") / "v1"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import TYPE_CHECKING, Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
from launart import Service
|
|
7
|
+
|
|
8
|
+
from ..config import Config as Config
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .. import App
|
|
12
|
+
|
|
13
|
+
TConfig = TypeVar("TConfig", bound=Config)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseNetwork(Generic[TConfig], Service):
|
|
17
|
+
close_signal: asyncio.Event
|
|
18
|
+
sequence: int
|
|
19
|
+
|
|
20
|
+
def __init__(self, app: App, config: TConfig):
|
|
21
|
+
super().__init__()
|
|
22
|
+
self.app = app
|
|
23
|
+
self.config = config
|
|
24
|
+
self.accounts = {}
|
|
25
|
+
self.close_signal = asyncio.Event()
|
|
26
|
+
self.sequence = -1
|
|
27
|
+
self.proxy_urls = []
|
|
28
|
+
|
|
29
|
+
async def wait_for_available(self): ...
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def alive(self) -> bool: ...
|
|
33
|
+
|
|
34
|
+
async def connection_closed(self):
|
|
35
|
+
self.close_signal.set()
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from aiohttp import web
|
|
6
|
+
from launart.manager import Launart
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
from satori.model import Event, LoginStatus, MetaPayload, Opcode
|
|
10
|
+
|
|
11
|
+
from ..account import Account
|
|
12
|
+
from ..config import WebhookInfo as WebhookInfo
|
|
13
|
+
from .base import BaseNetwork
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WebhookNetwork(BaseNetwork[WebhookInfo]):
|
|
17
|
+
required: set[str] = set()
|
|
18
|
+
stages: set[str] = {"preparing", "blocking", "cleanup"}
|
|
19
|
+
wsgi: web.Application | None = None
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def id(self):
|
|
23
|
+
return f"satori/network/webhook/{self.config.identity}#{id(self)}"
|
|
24
|
+
|
|
25
|
+
async def handle_request(self, req: web.Request):
|
|
26
|
+
header = req.headers
|
|
27
|
+
auth = header["Authorization"]
|
|
28
|
+
if not auth.startswith("Bearer"):
|
|
29
|
+
return web.Response(status=401)
|
|
30
|
+
token = auth.split(" ", 1)[1]
|
|
31
|
+
if self.config.token and self.config.token != token:
|
|
32
|
+
return web.Response(status=401)
|
|
33
|
+
op_code = int(header.get("Satori-OpCode", "0"))
|
|
34
|
+
body = await req.json()
|
|
35
|
+
if op_code == Opcode.META:
|
|
36
|
+
payload = MetaPayload.parse(body)
|
|
37
|
+
self.proxy_urls = payload.proxy_urls
|
|
38
|
+
return web.Response()
|
|
39
|
+
if op_code != Opcode.EVENT:
|
|
40
|
+
return web.Response(status=202)
|
|
41
|
+
# if "X-Platform" in header and "X-Self-ID" in header:
|
|
42
|
+
# platform = header["X-Platform"]
|
|
43
|
+
# self_id = header["X-Self-ID"]
|
|
44
|
+
# elif "Satori-Platform" in header and "Satori-Login-ID" in header:
|
|
45
|
+
# platform = header["Satori-Platform"]
|
|
46
|
+
# self_id = header["Satori-Login-ID"]
|
|
47
|
+
# else:
|
|
48
|
+
# return web.Response(status=400)
|
|
49
|
+
try:
|
|
50
|
+
event = Event.parse(body)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
if (
|
|
53
|
+
"self_id" in body
|
|
54
|
+
or ("login" in body and "self_id" in body["login"])
|
|
55
|
+
or ("login" in body and "user" in body["login"] and "self_id" in body["login"]["user"])
|
|
56
|
+
):
|
|
57
|
+
logger.warning(f"Failed to parse event: {body}\nCaused by {e!r}")
|
|
58
|
+
else:
|
|
59
|
+
logger.trace(f"Failed to parse event: {body}\nCaused by {e!r}")
|
|
60
|
+
return web.Response(status=500, reason=f"Failed to parse event caused by {e!r}")
|
|
61
|
+
else:
|
|
62
|
+
logger.trace(f"Received event: {event}")
|
|
63
|
+
self.sequence = event.sn
|
|
64
|
+
login_sn = f"{event.login.sn}@{id(self)}"
|
|
65
|
+
if login_sn in self.app.accounts:
|
|
66
|
+
account = self.app.accounts[login_sn]
|
|
67
|
+
account.connected.set()
|
|
68
|
+
account.config = self.config
|
|
69
|
+
else:
|
|
70
|
+
account = Account(event.login, self.config, self.proxy_urls)
|
|
71
|
+
logger.info(f"account registered: {account}")
|
|
72
|
+
account.connected.set()
|
|
73
|
+
self.app.accounts[login_sn] = account
|
|
74
|
+
self.accounts[login_sn] = account
|
|
75
|
+
await self.app.account_update(account, LoginStatus.ONLINE)
|
|
76
|
+
asyncio.create_task(self.app.post(event, self))
|
|
77
|
+
return web.Response()
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def alive(self):
|
|
81
|
+
return self.wsgi is not None
|
|
82
|
+
|
|
83
|
+
async def wait_for_available(self):
|
|
84
|
+
await self.status.wait_for_available()
|
|
85
|
+
|
|
86
|
+
async def launch(self, manager: Launart):
|
|
87
|
+
async with self.stage("preparing"):
|
|
88
|
+
logger.info(f"starting server on {self.config.identity}")
|
|
89
|
+
self.wsgi = web.Application(logger=logger) # type: ignore
|
|
90
|
+
self.wsgi.router.freeze = lambda: None # monkey patch
|
|
91
|
+
self.wsgi.router.add_post(self.config.path, self.handle_request)
|
|
92
|
+
runner = web.AppRunner(self.wsgi)
|
|
93
|
+
await runner.setup()
|
|
94
|
+
site = web.TCPSite(runner, self.config.host, self.config.port)
|
|
95
|
+
|
|
96
|
+
async with self.stage("blocking"):
|
|
97
|
+
await site.start()
|
|
98
|
+
await manager.status.wait_for_sigexit()
|
|
99
|
+
logger.info(f"{self.id} Webhook server exiting...")
|
|
100
|
+
self.close_signal.set()
|
|
101
|
+
for v in list(self.app.accounts.values()):
|
|
102
|
+
if (identity := f"{v.sn}@{id(self)}") in self.accounts:
|
|
103
|
+
v.connected.clear()
|
|
104
|
+
await self.app.account_update(v, LoginStatus.OFFLINE)
|
|
105
|
+
del self.app.accounts[identity]
|
|
106
|
+
del self.accounts[identity]
|
|
107
|
+
|
|
108
|
+
async with self.stage("cleanup"):
|
|
109
|
+
await site.stop()
|
|
110
|
+
await self.wsgi.shutdown()
|
|
111
|
+
await self.wsgi.cleanup()
|
{satori_python_client-0.15.2 → satori_python_client-0.16.0}/src/satori/client/network/websocket.py
RENAMED
|
@@ -10,10 +10,10 @@ from launart.manager import Launart
|
|
|
10
10
|
from launart.utilles import any_completed
|
|
11
11
|
from loguru import logger
|
|
12
12
|
|
|
13
|
-
from satori.
|
|
14
|
-
from satori.model import Login, LoginPreview, LoginStatus, Opcode
|
|
13
|
+
from satori.model import Event, Identify, LoginStatus, Opcode, Ready
|
|
15
14
|
|
|
16
15
|
from ..account import Account
|
|
16
|
+
from ..config import WebsocketsInfo as WebsocketsInfo
|
|
17
17
|
from .base import BaseNetwork
|
|
18
18
|
|
|
19
19
|
|
|
@@ -27,6 +27,26 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
|
|
|
27
27
|
|
|
28
28
|
connection: aiohttp.ClientWebSocketResponse | None = None
|
|
29
29
|
|
|
30
|
+
def post_event(self, body: dict):
|
|
31
|
+
async def event_parse_task(raw: dict):
|
|
32
|
+
try:
|
|
33
|
+
event = Event.parse(raw)
|
|
34
|
+
except Exception as e:
|
|
35
|
+
if (
|
|
36
|
+
"self_id" in raw
|
|
37
|
+
or ("login" in raw and "self_id" in raw["login"])
|
|
38
|
+
or ("login" in raw and "user" in raw["login"] and "self_id" in raw["login"]["user"])
|
|
39
|
+
):
|
|
40
|
+
logger.warning(f"Failed to parse event: {raw}\nCaused by {e!r}")
|
|
41
|
+
else:
|
|
42
|
+
logger.trace(f"Failed to parse event: {raw}\nCaused by {e!r}")
|
|
43
|
+
else:
|
|
44
|
+
logger.trace(f"Received event: {event}")
|
|
45
|
+
self.sequence = event.sn
|
|
46
|
+
await self.app.post(event, self)
|
|
47
|
+
|
|
48
|
+
return asyncio.create_task(event_parse_task(body))
|
|
49
|
+
|
|
30
50
|
async def message_receive(self):
|
|
31
51
|
if self.connection is None:
|
|
32
52
|
raise RuntimeError("connection is not established")
|
|
@@ -40,7 +60,7 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
|
|
|
40
60
|
logger.trace(f"Received payload: {data}")
|
|
41
61
|
if data["op"] == Opcode.EVENT:
|
|
42
62
|
self.post_event(data["body"])
|
|
43
|
-
elif data["op"] >
|
|
63
|
+
elif data["op"] > 5:
|
|
44
64
|
logger.warning(f"Received unknown event: {data}")
|
|
45
65
|
continue
|
|
46
66
|
else:
|
|
@@ -63,18 +83,13 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
|
|
|
63
83
|
"""鉴权连接"""
|
|
64
84
|
if not self.connection:
|
|
65
85
|
raise RuntimeError("connection is not established")
|
|
66
|
-
payload =
|
|
67
|
-
"op": Opcode.IDENTIFY,
|
|
68
|
-
"body": {
|
|
69
|
-
"token": self.config.token,
|
|
70
|
-
},
|
|
71
|
-
}
|
|
86
|
+
payload = Identify(self.config.token)
|
|
72
87
|
if self.sequence > -1:
|
|
73
|
-
payload
|
|
88
|
+
payload.sn = self.sequence
|
|
74
89
|
try:
|
|
75
|
-
await self.send(payload)
|
|
90
|
+
await self.send({"op": Opcode.IDENTIFY.value, "body": payload.dump()})
|
|
76
91
|
except Exception as e:
|
|
77
|
-
logger.error(f"Error while sending IDENTIFY event: {e}")
|
|
92
|
+
logger.error(f"Error while sending IDENTIFY event: {e!r}")
|
|
78
93
|
return False
|
|
79
94
|
|
|
80
95
|
resp = await self.connection.receive()
|
|
@@ -85,32 +100,24 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
|
|
|
85
100
|
if data["op"] != Opcode.READY:
|
|
86
101
|
logger.error(f"Received unexpected payload: {data}")
|
|
87
102
|
return False
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
account = self.app.accounts[identity]
|
|
97
|
-
self.accounts[identity] = account
|
|
98
|
-
if not obj.status or obj.status == LoginStatus.ONLINE:
|
|
103
|
+
ready = Ready.parse(data["body"])
|
|
104
|
+
self.proxy_urls = ready.proxy_urls
|
|
105
|
+
for login in ready.logins:
|
|
106
|
+
login_sn = f"{login.sn}@{id(self)}"
|
|
107
|
+
if login_sn in self.app.accounts:
|
|
108
|
+
account = self.app.accounts[login_sn]
|
|
109
|
+
self.accounts[login_sn] = account
|
|
110
|
+
if login.status == LoginStatus.ONLINE:
|
|
99
111
|
account.connected.set()
|
|
100
112
|
else:
|
|
101
113
|
account.connected.clear()
|
|
102
114
|
account.config = self.config
|
|
103
115
|
else:
|
|
104
|
-
account = Account(
|
|
116
|
+
account = Account(login, self.config, ready.proxy_urls, self.app.default_api_cls)
|
|
105
117
|
logger.info(f"account registered: {account}")
|
|
106
|
-
(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
else account.connected.clear()
|
|
110
|
-
)
|
|
111
|
-
self.app.accounts[identity] = account
|
|
112
|
-
self.accounts[identity] = account
|
|
113
|
-
await self.app.account_update(account, LoginStatus.CONNECT)
|
|
118
|
+
(account.connected.set() if login.status == LoginStatus.ONLINE else account.connected.clear())
|
|
119
|
+
self.app.accounts[login_sn] = account
|
|
120
|
+
self.accounts[login_sn] = account
|
|
114
121
|
await self.app.account_update(account, LoginStatus.ONLINE)
|
|
115
122
|
if not self.accounts:
|
|
116
123
|
logger.warning(f"No account available for {self.config}")
|
|
@@ -151,9 +158,11 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
|
|
|
151
158
|
self.close_signal.set()
|
|
152
159
|
self.connection = None
|
|
153
160
|
for v in list(self.app.accounts.values()):
|
|
154
|
-
if v.
|
|
161
|
+
if (identity := f"{v.sn}@{id(self)}") in self.accounts:
|
|
155
162
|
v.connected.clear()
|
|
156
|
-
|
|
163
|
+
await self.app.account_update(v, LoginStatus.OFFLINE)
|
|
164
|
+
del self.app.accounts[identity]
|
|
165
|
+
del self.accounts[identity]
|
|
157
166
|
return
|
|
158
167
|
if close_task in done:
|
|
159
168
|
receiver_task.cancel()
|
|
@@ -162,7 +171,7 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
|
|
|
162
171
|
logger.debug(f"Unregistering satori account {k}...")
|
|
163
172
|
account = self.app.accounts[k]
|
|
164
173
|
account.connected.clear()
|
|
165
|
-
await self.app.account_update(account, LoginStatus.
|
|
174
|
+
await self.app.account_update(account, LoginStatus.RECONNECT)
|
|
166
175
|
self.accounts.clear()
|
|
167
176
|
await asyncio.sleep(5)
|
|
168
177
|
logger.info(f"{self} Reconnecting...")
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from collections.abc import Iterable
|
|
4
4
|
from typing import TYPE_CHECKING, Any, cast, overload
|
|
5
|
+
from typing_extensions import deprecated
|
|
5
6
|
|
|
6
7
|
from aiohttp import FormData
|
|
7
8
|
from graia.amnesia.builtins.aiohttp import AiohttpClientService
|
|
@@ -15,11 +16,10 @@ from satori.model import (
|
|
|
15
16
|
Event,
|
|
16
17
|
Guild,
|
|
17
18
|
Login,
|
|
18
|
-
LoginPreview,
|
|
19
|
-
LoginType,
|
|
20
19
|
Member,
|
|
21
20
|
MessageObject,
|
|
22
21
|
MessageReceipt,
|
|
22
|
+
Meta,
|
|
23
23
|
Order,
|
|
24
24
|
PageDequeResult,
|
|
25
25
|
PageResult,
|
|
@@ -39,14 +39,23 @@ class ApiProtocol:
|
|
|
39
39
|
self.account = account
|
|
40
40
|
|
|
41
41
|
async def download(self, url: str):
|
|
42
|
-
"""
|
|
42
|
+
"""访问资源链接。"""
|
|
43
43
|
endpoint = self.account.ensure_url(url)
|
|
44
44
|
aio = Launart.current().get_component(AiohttpClientService)
|
|
45
45
|
async with aio.session.get(endpoint) as resp:
|
|
46
46
|
await validate_response(resp, noreturn=True)
|
|
47
47
|
return await resp.read()
|
|
48
48
|
|
|
49
|
-
async def
|
|
49
|
+
async def request_internal(self, url: str, method: str = "GET", **kwargs) -> dict:
|
|
50
|
+
"""访问内部链接。"""
|
|
51
|
+
endpoint = self.account.ensure_url(url)
|
|
52
|
+
aio = Launart.current().get_component(AiohttpClientService)
|
|
53
|
+
async with aio.session.request(method, endpoint, **kwargs) as resp:
|
|
54
|
+
return await validate_response(resp)
|
|
55
|
+
|
|
56
|
+
async def call_api(
|
|
57
|
+
self, action: str | Api, params: dict | None = None, multipart: bool = False, method: str = "POST"
|
|
58
|
+
) -> dict:
|
|
50
59
|
endpoint = self.account.config.api_base / (action.value if isinstance(action, Api) else action)
|
|
51
60
|
headers = {
|
|
52
61
|
"Content-Type": "application/json",
|
|
@@ -73,7 +82,8 @@ class ApiProtocol:
|
|
|
73
82
|
headers=headers,
|
|
74
83
|
) as resp:
|
|
75
84
|
return await validate_response(resp)
|
|
76
|
-
async with aio.session.
|
|
85
|
+
async with aio.session.request(
|
|
86
|
+
method,
|
|
77
87
|
endpoint,
|
|
78
88
|
json=params or {},
|
|
79
89
|
headers=headers,
|
|
@@ -677,14 +687,14 @@ class ApiProtocol:
|
|
|
677
687
|
)
|
|
678
688
|
return PageResult.parse(res, User.parse)
|
|
679
689
|
|
|
680
|
-
async def login_get(self) ->
|
|
690
|
+
async def login_get(self) -> Login:
|
|
681
691
|
"""获取当前登录信息。返回一个 `Login` 对象。
|
|
682
692
|
|
|
683
693
|
Returns:
|
|
684
694
|
Login: `Login` 对象
|
|
685
695
|
"""
|
|
686
696
|
res = await self.call_api(Api.LOGIN_GET, {})
|
|
687
|
-
return
|
|
697
|
+
return Login.parse(res)
|
|
688
698
|
|
|
689
699
|
async def user_get(self, user_id: str) -> User:
|
|
690
700
|
"""获取用户信息。返回一个 `User` 对象。
|
|
@@ -726,23 +736,41 @@ class ApiProtocol:
|
|
|
726
736
|
{"message_id": request_id, "approve": approve, "comment": comment},
|
|
727
737
|
)
|
|
728
738
|
|
|
729
|
-
async def internal(self, action: str, **kwargs) -> Any:
|
|
739
|
+
async def internal(self, action: str, method: str = "POST", **kwargs) -> Any:
|
|
730
740
|
"""内部接口调用。
|
|
731
741
|
|
|
732
742
|
Args:
|
|
733
743
|
action (str): 内部接口名称
|
|
744
|
+
method (str, optional): 请求方法,默认为 POST
|
|
734
745
|
**kwargs: 参数
|
|
735
746
|
"""
|
|
736
|
-
return await self.call_api(f"internal/{action}", kwargs)
|
|
747
|
+
return await self.call_api(f"internal/{action}", kwargs, method=method)
|
|
737
748
|
|
|
738
|
-
async def
|
|
749
|
+
async def meta_get(self) -> Meta:
|
|
750
|
+
"""获取元信息。返回一个 `Meta` 对象。
|
|
751
|
+
|
|
752
|
+
Returns:
|
|
753
|
+
Meta: `Meta` 对象
|
|
754
|
+
"""
|
|
755
|
+
res = await self.call_api("meta")
|
|
756
|
+
return Meta.parse(res)
|
|
757
|
+
|
|
758
|
+
@deprecated("Use `meta_get` instead")
|
|
759
|
+
async def admin_login_list(self) -> list[Login]:
|
|
739
760
|
"""获取登录信息列表。返回一个 `Login` 对象构成的数组。
|
|
740
761
|
|
|
741
762
|
Returns:
|
|
742
763
|
list[Login]: `Login` 对象构成的数组
|
|
743
764
|
"""
|
|
744
|
-
|
|
745
|
-
|
|
765
|
+
return (await self.meta_get()).logins
|
|
766
|
+
|
|
767
|
+
async def webhook_create(self, url: str, token: str | None = None):
|
|
768
|
+
"""创建 Webhook。"""
|
|
769
|
+
await self.call_api("meta/webhook.create", {"url": url, "token": token})
|
|
770
|
+
|
|
771
|
+
async def webhook_delete(self, url: str):
|
|
772
|
+
"""删除 Webhook。"""
|
|
773
|
+
await self.call_api("meta/webhook.delete", {"url": url})
|
|
746
774
|
|
|
747
775
|
@overload
|
|
748
776
|
async def upload_create(self, *uploads: Upload) -> list[str]: ...
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
from typing import TYPE_CHECKING, Generic, TypeVar
|
|
5
|
-
|
|
6
|
-
from launart import Service
|
|
7
|
-
from loguru import logger
|
|
8
|
-
|
|
9
|
-
from satori.config import Config as Config
|
|
10
|
-
from satori.model import Event
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
from .. import App
|
|
14
|
-
|
|
15
|
-
TConfig = TypeVar("TConfig", bound=Config)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class BaseNetwork(Generic[TConfig], Service):
|
|
19
|
-
close_signal: asyncio.Event
|
|
20
|
-
sequence: int
|
|
21
|
-
|
|
22
|
-
def __init__(self, app: App, config: TConfig):
|
|
23
|
-
super().__init__()
|
|
24
|
-
self.app = app
|
|
25
|
-
self.config = config
|
|
26
|
-
self.accounts = {}
|
|
27
|
-
self.close_signal = asyncio.Event()
|
|
28
|
-
self.sequence = -1
|
|
29
|
-
|
|
30
|
-
async def wait_for_available(self): ...
|
|
31
|
-
|
|
32
|
-
@property
|
|
33
|
-
def alive(self) -> bool: ...
|
|
34
|
-
|
|
35
|
-
async def connection_closed(self):
|
|
36
|
-
self.close_signal.set()
|
|
37
|
-
|
|
38
|
-
def post_event(self, body: dict):
|
|
39
|
-
async def event_parse_task(raw: dict):
|
|
40
|
-
try:
|
|
41
|
-
event = Event.parse(raw)
|
|
42
|
-
except Exception as e:
|
|
43
|
-
if (
|
|
44
|
-
"self_id" in raw
|
|
45
|
-
or ("login" in raw and "self_id" in raw["login"])
|
|
46
|
-
or ("login" in raw and "user" in raw["login"] and "self_id" in raw["login"]["user"])
|
|
47
|
-
):
|
|
48
|
-
logger.warning(f"Failed to parse event: {raw}\nCaused by {e!r}")
|
|
49
|
-
else:
|
|
50
|
-
logger.trace(f"Failed to parse event: {raw}\nCaused by {e!r}")
|
|
51
|
-
else:
|
|
52
|
-
logger.trace(f"Received event: {event}")
|
|
53
|
-
self.sequence = event.id
|
|
54
|
-
await self.app.post(event, self)
|
|
55
|
-
|
|
56
|
-
return asyncio.create_task(event_parse_task(body))
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
|
|
5
|
-
from aiohttp import web
|
|
6
|
-
from graia.amnesia.builtins.aiohttp import AiohttpClientService
|
|
7
|
-
from launart.manager import Launart
|
|
8
|
-
from launart.utilles import any_completed
|
|
9
|
-
from loguru import logger
|
|
10
|
-
|
|
11
|
-
from satori.config import WebhookInfo as WebhookInfo
|
|
12
|
-
from satori.model import Login, LoginPreview, LoginStatus, Opcode
|
|
13
|
-
|
|
14
|
-
from ..account import Account
|
|
15
|
-
from .base import BaseNetwork
|
|
16
|
-
from .util import validate_response
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class WebhookNetwork(BaseNetwork[WebhookInfo]):
|
|
20
|
-
required: set[str] = set()
|
|
21
|
-
stages: set[str] = {"preparing", "blocking", "cleanup"}
|
|
22
|
-
wsgi: web.Application | None = None
|
|
23
|
-
|
|
24
|
-
@property
|
|
25
|
-
def id(self):
|
|
26
|
-
return f"satori/network/webhook/{self.config.identity}#{id(self)}"
|
|
27
|
-
|
|
28
|
-
async def handle_request(self, req: web.Request):
|
|
29
|
-
header = req.headers
|
|
30
|
-
auth = header["Authorization"]
|
|
31
|
-
if not auth.startswith("Bearer"):
|
|
32
|
-
return web.Response(status=401)
|
|
33
|
-
token = auth.split(" ", 1)[1]
|
|
34
|
-
if self.config.token and self.config.token != token:
|
|
35
|
-
return web.Response(status=401)
|
|
36
|
-
if "X-Platform" in header and "X-Self-ID" in header:
|
|
37
|
-
platform = header["X-Platform"]
|
|
38
|
-
self_id = header["X-Self-ID"]
|
|
39
|
-
elif "Satori-Platform" in header and "Satori-Login-ID" in header:
|
|
40
|
-
platform = header["Satori-Platform"]
|
|
41
|
-
self_id = header["Satori-Login-ID"]
|
|
42
|
-
else:
|
|
43
|
-
return web.Response(status=400)
|
|
44
|
-
identity = f"{platform}/{self_id}"
|
|
45
|
-
if identity in self.app.accounts:
|
|
46
|
-
account = self.app.accounts[identity]
|
|
47
|
-
self.accounts[identity] = account
|
|
48
|
-
account.connected.set()
|
|
49
|
-
account.config = self.config
|
|
50
|
-
else:
|
|
51
|
-
assert self.manager
|
|
52
|
-
aio = self.manager.get_component(AiohttpClientService)
|
|
53
|
-
async with aio.session.post(self.config.api_base / "admin/login.list") as resp:
|
|
54
|
-
logins = [
|
|
55
|
-
LoginPreview.parse(i) if "user" in i else Login.parse(i)
|
|
56
|
-
for i in await validate_response(resp)
|
|
57
|
-
]
|
|
58
|
-
login = next(
|
|
59
|
-
(i for i in logins if i.id == self_id and i.platform == platform),
|
|
60
|
-
Login(LoginStatus.CONNECT, self_id=self_id, platform=platform),
|
|
61
|
-
)
|
|
62
|
-
account = Account(platform, self_id, login, self.config, self.app.default_api_cls)
|
|
63
|
-
logger.info(f"account registered: {account}")
|
|
64
|
-
account.connected.set()
|
|
65
|
-
self.app.accounts[identity] = account
|
|
66
|
-
self.accounts[identity] = account
|
|
67
|
-
await self.app.account_update(account, LoginStatus.CONNECT)
|
|
68
|
-
await self.app.account_update(account, LoginStatus.ONLINE)
|
|
69
|
-
data = await req.json()
|
|
70
|
-
op = data["op"]
|
|
71
|
-
if op != Opcode.EVENT:
|
|
72
|
-
return web.Response(status=202)
|
|
73
|
-
logger.trace(f"Received payload: {data}")
|
|
74
|
-
self.post_event(data["body"])
|
|
75
|
-
return web.Response()
|
|
76
|
-
|
|
77
|
-
@property
|
|
78
|
-
def alive(self):
|
|
79
|
-
return self.wsgi is not None
|
|
80
|
-
|
|
81
|
-
async def wait_for_available(self):
|
|
82
|
-
await self.status.wait_for_available()
|
|
83
|
-
|
|
84
|
-
async def daemon(self, manager: Launart, site: web.TCPSite):
|
|
85
|
-
while not manager.status.exiting:
|
|
86
|
-
await site.start()
|
|
87
|
-
self.close_signal.clear()
|
|
88
|
-
close_task = asyncio.create_task(self.close_signal.wait())
|
|
89
|
-
sigexit_task = asyncio.create_task(manager.status.wait_for_sigexit())
|
|
90
|
-
done, pending = await any_completed(
|
|
91
|
-
sigexit_task,
|
|
92
|
-
close_task,
|
|
93
|
-
)
|
|
94
|
-
if sigexit_task in done:
|
|
95
|
-
logger.info(f"{self.id} Webhook server exiting...")
|
|
96
|
-
self.close_signal.set()
|
|
97
|
-
for v in list(self.app.accounts.values()):
|
|
98
|
-
if v.identity in self.accounts:
|
|
99
|
-
v.connected.clear()
|
|
100
|
-
del self.accounts[v.identity]
|
|
101
|
-
return
|
|
102
|
-
if close_task in done:
|
|
103
|
-
await site.stop()
|
|
104
|
-
logger.warning(f"{self.id} Connection closed by server, will reconnect in 5 seconds...")
|
|
105
|
-
for k in self.accounts.keys():
|
|
106
|
-
logger.debug(f"Unregistering satori account {k}...")
|
|
107
|
-
account = self.app.accounts[k]
|
|
108
|
-
account.connected.clear()
|
|
109
|
-
await self.app.account_update(account, LoginStatus.DISCONNECT)
|
|
110
|
-
self.accounts.clear()
|
|
111
|
-
await asyncio.sleep(5)
|
|
112
|
-
logger.info(f"{self} Reconnecting...")
|
|
113
|
-
continue
|
|
114
|
-
|
|
115
|
-
async def launch(self, manager: Launart):
|
|
116
|
-
async with self.stage("preparing"):
|
|
117
|
-
logger.info(f"starting server on {self.config.identity}")
|
|
118
|
-
self.wsgi = web.Application(logger=logger) # type: ignore
|
|
119
|
-
self.wsgi.router.freeze = lambda: None # monkey patch
|
|
120
|
-
self.wsgi.router.add_post(self.config.path, self.handle_request)
|
|
121
|
-
runner = web.AppRunner(self.wsgi)
|
|
122
|
-
await runner.setup()
|
|
123
|
-
site = web.TCPSite(runner, self.config.host, self.config.port)
|
|
124
|
-
|
|
125
|
-
async with self.stage("blocking"):
|
|
126
|
-
await self.daemon(manager, site)
|
|
127
|
-
|
|
128
|
-
async with self.stage("cleanup"):
|
|
129
|
-
await site.stop()
|
|
130
|
-
await self.wsgi.shutdown()
|
|
131
|
-
await self.wsgi.cleanup()
|
|
File without changes
|
{satori_python_client-0.15.2 → satori_python_client-0.16.0}/src/satori/client/network/__init__.py
RENAMED
|
File without changes
|
{satori_python_client-0.15.2 → satori_python_client-0.16.0}/src/satori/client/network/util.py
RENAMED
|
File without changes
|