satori-python 0.17.6__tar.gz → 0.18.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.17.6 → satori_python-0.18.0}/PKG-INFO +6 -3
- {satori_python-0.17.6 → satori_python-0.18.0}/README.md +4 -2
- {satori_python-0.17.6 → satori_python-0.18.0}/pyproject.toml +2 -1
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/__init__.py +4 -1
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/client/__init__.py +29 -7
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/client/account.py +4 -1
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/client/account.pyi +12 -8
- satori_python-0.18.0/src/satori/client/network/util.py +44 -0
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/client/network/webhook.py +2 -2
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/client/network/websocket.py +2 -2
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/client/protocol.py +29 -22
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/const.py +8 -3
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/element.py +1 -1
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/event.py +4 -0
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/exception.py +7 -0
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/model.py +18 -38
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/server/__init__.py +12 -3
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/server/route.py +4 -3
- satori_python-0.17.6/src/satori/client/network/util.py +0 -42
- {satori_python-0.17.6 → satori_python-0.18.0}/LICENSE +0 -0
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/_vendor/fleep.py +0 -0
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/client/config.py +0 -0
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/client/network/__init__.py +0 -0
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/client/network/base.py +0 -0
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/parser.py +0 -0
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/server/adapter.py +0 -0
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/server/connection.py +0 -0
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/server/formdata.py +0 -0
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/server/model.py +0 -0
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/server/utils.py +0 -0
- {satori_python-0.17.6 → satori_python-0.18.0}/src/satori/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: satori-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.18.0
|
|
4
4
|
Summary: Satori Protocol SDK for python
|
|
5
5
|
Home-page: https://github.com/RF-Tar-Railt/satori-python
|
|
6
6
|
Author-Email: RF-Tar-Railt <rf_tar_railt@qq.com>
|
|
@@ -26,6 +26,7 @@ Requires-Dist: yarl>=1.9.4
|
|
|
26
26
|
Requires-Dist: python-multipart>=0.0.9
|
|
27
27
|
Requires-Dist: websockets>=15.0.1
|
|
28
28
|
Requires-Dist: starlette>=0.40.0
|
|
29
|
+
Requires-Dist: cryptography>=46.0.4
|
|
29
30
|
Requires-Dist: msgspec>=0.19.0; extra == "msgspec"
|
|
30
31
|
Provides-Extra: msgspec
|
|
31
32
|
Description-Content-Type: text/markdown
|
|
@@ -37,17 +38,18 @@ Description-Content-Type: text/markdown
|
|
|
37
38
|
[](https://pypi.org/project/satori-python)
|
|
38
39
|
[](https://www.python.org/)
|
|
39
40
|
|
|
40
|
-
基于 [Satori](https://satori.
|
|
41
|
+
基于 [Satori](https://satori.chat/zh-CN/) 协议的 Python 开发工具包
|
|
41
42
|
|
|
42
43
|
## 协议介绍
|
|
43
44
|
|
|
44
|
-
[Satori Protocol](https://satori.
|
|
45
|
+
[Satori Protocol](https://satori.chat/zh-CN/)
|
|
45
46
|
|
|
46
47
|
### 协议端
|
|
47
48
|
|
|
48
49
|
目前提供了 `satori` 协议实现的有:
|
|
49
50
|
|
|
50
51
|
- [Chronocat](https://chronocat.vercel.app)
|
|
52
|
+
- [LLBot](https://www.llonebot.com/guide/introduction)
|
|
51
53
|
- [nekobox](https://github.com/wyapx/nekobox)
|
|
52
54
|
- Koishi (搭配 `@koishijs/plugin-server`)
|
|
53
55
|
|
|
@@ -85,6 +87,7 @@ pip install satori-python-server
|
|
|
85
87
|
| OneBot V11 | `pip install satori-python-adapter-onebot11` | satori.adapters.onebot11.forward, satori.adapters.onebot11.reverse |
|
|
86
88
|
| Console | `pip install satori-python-adapter-console` | satori.adapters.console |
|
|
87
89
|
| Milky | `pip install satori-python-adapter-milky` | satori.adapters.milky.main, satori.adapters.milky.webhook |
|
|
90
|
+
| QQ | `pip install satori-python-adapter-qq` | satori.adapters.milky.main, satori.adapters.milky.websocket |
|
|
88
91
|
|
|
89
92
|
### 社区适配器
|
|
90
93
|
|
|
@@ -5,17 +5,18 @@
|
|
|
5
5
|
[](https://pypi.org/project/satori-python)
|
|
6
6
|
[](https://www.python.org/)
|
|
7
7
|
|
|
8
|
-
基于 [Satori](https://satori.
|
|
8
|
+
基于 [Satori](https://satori.chat/zh-CN/) 协议的 Python 开发工具包
|
|
9
9
|
|
|
10
10
|
## 协议介绍
|
|
11
11
|
|
|
12
|
-
[Satori Protocol](https://satori.
|
|
12
|
+
[Satori Protocol](https://satori.chat/zh-CN/)
|
|
13
13
|
|
|
14
14
|
### 协议端
|
|
15
15
|
|
|
16
16
|
目前提供了 `satori` 协议实现的有:
|
|
17
17
|
|
|
18
18
|
- [Chronocat](https://chronocat.vercel.app)
|
|
19
|
+
- [LLBot](https://www.llonebot.com/guide/introduction)
|
|
19
20
|
- [nekobox](https://github.com/wyapx/nekobox)
|
|
20
21
|
- Koishi (搭配 `@koishijs/plugin-server`)
|
|
21
22
|
|
|
@@ -53,6 +54,7 @@ pip install satori-python-server
|
|
|
53
54
|
| OneBot V11 | `pip install satori-python-adapter-onebot11` | satori.adapters.onebot11.forward, satori.adapters.onebot11.reverse |
|
|
54
55
|
| Console | `pip install satori-python-adapter-console` | satori.adapters.console |
|
|
55
56
|
| Milky | `pip install satori-python-adapter-milky` | satori.adapters.milky.main, satori.adapters.milky.webhook |
|
|
57
|
+
| QQ | `pip install satori-python-adapter-qq` | satori.adapters.milky.main, satori.adapters.milky.websocket |
|
|
56
58
|
|
|
57
59
|
### 社区适配器
|
|
58
60
|
|
|
@@ -15,6 +15,7 @@ dependencies = [
|
|
|
15
15
|
"python-multipart>=0.0.9",
|
|
16
16
|
"websockets>=15.0.1",
|
|
17
17
|
"starlette>=0.40.0",
|
|
18
|
+
"cryptography>=46.0.4",
|
|
18
19
|
]
|
|
19
20
|
requires-python = ">=3.10,<4.0"
|
|
20
21
|
readme = "README.md"
|
|
@@ -29,7 +30,7 @@ classifiers = [
|
|
|
29
30
|
"Programming Language :: Python :: 3.12",
|
|
30
31
|
"Operating System :: OS Independent",
|
|
31
32
|
]
|
|
32
|
-
version = "0.
|
|
33
|
+
version = "0.18.0"
|
|
33
34
|
|
|
34
35
|
[project.license]
|
|
35
36
|
text = "MIT"
|
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
import functools
|
|
5
5
|
import signal
|
|
6
6
|
import threading
|
|
7
|
+
import traceback
|
|
7
8
|
from collections.abc import Awaitable, Callable, Iterable
|
|
8
9
|
from functools import wraps
|
|
9
10
|
from typing import TYPE_CHECKING, Any, Literal, TypeVar, overload
|
|
@@ -101,7 +102,9 @@ class App(Service):
|
|
|
101
102
|
self.event_callbacks.append(callback)
|
|
102
103
|
|
|
103
104
|
@overload
|
|
104
|
-
def register_on(
|
|
105
|
+
def register_on(
|
|
106
|
+
self, event_type: Literal[EventType.FRIEND_ADDED, EventType.FRIEND_REMOVED, EventType.FRIEND_REQUEST]
|
|
107
|
+
) -> Callable[
|
|
105
108
|
[Callable[[Account, events.UserEvent], Awaitable[Any]]],
|
|
106
109
|
Callable[[Account, events.UserEvent], Awaitable[Any]],
|
|
107
110
|
]: ...
|
|
@@ -117,6 +120,15 @@ class App(Service):
|
|
|
117
120
|
Callable[[Account, events.GuildEvent], Awaitable[Any]],
|
|
118
121
|
]: ...
|
|
119
122
|
|
|
123
|
+
@overload
|
|
124
|
+
def register_on(
|
|
125
|
+
self,
|
|
126
|
+
event_type: Literal[EventType.CHANNEL_ADDED, EventType.CHANNEL_REMOVED, EventType.CHANNEL_UPDATED],
|
|
127
|
+
) -> Callable[
|
|
128
|
+
[Callable[[Account, events.ChannelEvent], Awaitable[Any]]],
|
|
129
|
+
Callable[[Account, events.ChannelEvent], Awaitable[Any]],
|
|
130
|
+
]: ...
|
|
131
|
+
|
|
120
132
|
@overload
|
|
121
133
|
def register_on(
|
|
122
134
|
self,
|
|
@@ -203,7 +215,12 @@ class App(Service):
|
|
|
203
215
|
|
|
204
216
|
async def account_update(self, account: Account, state: LoginStatus):
|
|
205
217
|
if self.lifecycle_callbacks:
|
|
206
|
-
|
|
218
|
+
task = asyncio.gather(*(callback(account, state) for callback in self.lifecycle_callbacks))
|
|
219
|
+
try:
|
|
220
|
+
await task
|
|
221
|
+
except Exception:
|
|
222
|
+
traceback.print_exc()
|
|
223
|
+
task.cancel()
|
|
207
224
|
|
|
208
225
|
async def post(self, event: Event, conn: BaseNetwork):
|
|
209
226
|
if event.type == EventType.LOGIN_ADDED:
|
|
@@ -213,7 +230,7 @@ class App(Service):
|
|
|
213
230
|
if not login.user:
|
|
214
231
|
logger.warning(f"Received login-added event without user info: {login}")
|
|
215
232
|
return
|
|
216
|
-
login_sn = f"{login.user.id}@{id(conn):x}"
|
|
233
|
+
login_sn = f"{login.platform}_{login.user.id}@{id(conn):x}"
|
|
217
234
|
account = Account(
|
|
218
235
|
login,
|
|
219
236
|
conn.config,
|
|
@@ -232,7 +249,7 @@ class App(Service):
|
|
|
232
249
|
if not login.user:
|
|
233
250
|
logger.warning(f"Received login-updated event without user info: {login}")
|
|
234
251
|
return
|
|
235
|
-
login_sn = f"{login.user.id}@{id(conn):x}"
|
|
252
|
+
login_sn = f"{login.platform}_{login.user.id}@{id(conn):x}"
|
|
236
253
|
if login_sn not in self.accounts:
|
|
237
254
|
if login.status == LoginStatus.ONLINE:
|
|
238
255
|
account = Account(
|
|
@@ -266,20 +283,25 @@ class App(Service):
|
|
|
266
283
|
if not login.user:
|
|
267
284
|
logger.warning(f"Received login-removed event without user info: {login}")
|
|
268
285
|
return
|
|
269
|
-
login_sn = f"{login.user.id}@{id(conn):x}"
|
|
286
|
+
login_sn = f"{login.platform}_{login.user.id}@{id(conn):x}"
|
|
270
287
|
if login_sn not in self.accounts:
|
|
271
288
|
logger.warning(f"Received event for unknown account: {event}")
|
|
272
289
|
return
|
|
273
290
|
account = self.accounts[login_sn]
|
|
274
291
|
else:
|
|
275
|
-
login_sn = f"{event.login.user.id}@{id(conn):x}"
|
|
292
|
+
login_sn = f"{event.login.platform}_{event.login.user.id}@{id(conn):x}"
|
|
276
293
|
if login_sn not in self.accounts:
|
|
277
294
|
logger.warning(f"Received event for unknown account: {event}")
|
|
278
295
|
return
|
|
279
296
|
account = self.accounts[login_sn]
|
|
280
297
|
|
|
281
298
|
if self.event_callbacks:
|
|
282
|
-
|
|
299
|
+
task = asyncio.gather(*(callback(account, event) for callback in self.event_callbacks))
|
|
300
|
+
try:
|
|
301
|
+
await task
|
|
302
|
+
except Exception:
|
|
303
|
+
traceback.print_exc()
|
|
304
|
+
task.cancel()
|
|
283
305
|
|
|
284
306
|
if event.type == EventType.LOGIN_REMOVED:
|
|
285
307
|
logger.info(f"account removed: {account}")
|
|
@@ -72,7 +72,10 @@ class Account(Generic[TP]):
|
|
|
72
72
|
for proxy_url in self.proxy_urls:
|
|
73
73
|
if url.startswith(proxy_url):
|
|
74
74
|
return self.config.api_base / "proxy" / url.lstrip("/")
|
|
75
|
-
|
|
75
|
+
ans = URL(url)
|
|
76
|
+
if not ans.scheme:
|
|
77
|
+
ans = URL(f"http://{url}")
|
|
78
|
+
return ans
|
|
76
79
|
|
|
77
80
|
def __repr__(self):
|
|
78
81
|
return f"<Account {self.self_id} ({self.platform})>"
|
|
@@ -15,7 +15,6 @@ from satori.model import (
|
|
|
15
15
|
Login,
|
|
16
16
|
Member,
|
|
17
17
|
MessageObject,
|
|
18
|
-
MessageReceipt,
|
|
19
18
|
Meta,
|
|
20
19
|
Order,
|
|
21
20
|
PageDequeResult,
|
|
@@ -80,7 +79,7 @@ class Account(Generic[TP]):
|
|
|
80
79
|
- 链接开头出现在 self_info.proxy_urls 中的某一项
|
|
81
80
|
"""
|
|
82
81
|
|
|
83
|
-
async def send(self, event: Event, message: str | Iterable[str | Element]) -> list[
|
|
82
|
+
async def send(self, event: Event, message: str | Iterable[str | Element]) -> list[MessageObject]:
|
|
84
83
|
"""发送消息。返回一个 `MessageObject` 对象构成的数组。
|
|
85
84
|
|
|
86
85
|
Args:
|
|
@@ -95,26 +94,28 @@ class Account(Generic[TP]):
|
|
|
95
94
|
"""
|
|
96
95
|
|
|
97
96
|
async def send_message(
|
|
98
|
-
self, channel: str | Channel, message: str | Iterable[str | Element]
|
|
99
|
-
) -> list[
|
|
97
|
+
self, channel: str | Channel, message: str | Iterable[str | Element], referrer: dict[str, Any] | None = None
|
|
98
|
+
) -> list[MessageObject]:
|
|
100
99
|
"""发送消息。返回一个 `MessageObject` 对象构成的数组。
|
|
101
100
|
|
|
102
101
|
Args:
|
|
103
102
|
channel (str | Channel): 要发送的频道 ID
|
|
104
103
|
message (str | Iterable[str | Element]): 要发送的消息
|
|
104
|
+
referrer (dict[str, Any] | None, optional): 消息来源信息,默认为 None
|
|
105
105
|
|
|
106
106
|
Returns:
|
|
107
107
|
list[MessageObject]: `MessageObject` 对象构成的数组
|
|
108
108
|
"""
|
|
109
109
|
|
|
110
110
|
async def send_private_message(
|
|
111
|
-
self, user: str | User, message: str | Iterable[str | Element]
|
|
112
|
-
) -> list[
|
|
111
|
+
self, user: str | User, message: str | Iterable[str | Element], referrer: dict[str, Any] | None = None
|
|
112
|
+
) -> list[MessageObject]:
|
|
113
113
|
"""发送私聊消息。返回一个 `MessageObject` 对象构成的数组。
|
|
114
114
|
|
|
115
115
|
Args:
|
|
116
116
|
user (str | User): 要发送的用户 ID
|
|
117
117
|
message (str | Iterable[str | Element]): 要发送的消息
|
|
118
|
+
referrer (dict[str, Any] | None, optional): 消息来源信息,默认为 None
|
|
118
119
|
|
|
119
120
|
Returns:
|
|
120
121
|
list[MessageObject]: `MessageObject` 对象构成的数组
|
|
@@ -134,12 +135,15 @@ class Account(Generic[TP]):
|
|
|
134
135
|
None: 该方法无返回值
|
|
135
136
|
"""
|
|
136
137
|
|
|
137
|
-
async def message_create(
|
|
138
|
+
async def message_create(
|
|
139
|
+
self, channel_id: str, content: str, referrer: dict[str, Any] | None = None
|
|
140
|
+
) -> list[MessageObject]:
|
|
138
141
|
"""发送消息。返回一个 `MessageObject` 对象构成的数组。
|
|
139
142
|
|
|
140
143
|
Args:
|
|
141
144
|
channel_id (str): 频道 ID
|
|
142
145
|
content (str): 消息内容
|
|
146
|
+
referrer (dict[str, Any] | None, optional): 消息来源信息,默认为 None
|
|
143
147
|
|
|
144
148
|
Returns:
|
|
145
149
|
list[MessageObject]: `MessageObject` 对象构成的数组
|
|
@@ -583,7 +587,7 @@ class Account(Generic[TP]):
|
|
|
583
587
|
"""
|
|
584
588
|
upload = upload_create
|
|
585
589
|
|
|
586
|
-
async def download(self, url: str):
|
|
590
|
+
async def download(self, url: str) -> bytes:
|
|
587
591
|
"""访问内部链接。"""
|
|
588
592
|
|
|
589
593
|
async def request_internal(self, url: str, method: str = "GET", **kwargs) -> dict:
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import Literal, overload
|
|
2
|
+
|
|
3
|
+
from aiohttp import ClientResponse
|
|
4
|
+
|
|
5
|
+
from satori.exception import (
|
|
6
|
+
BadRequestException,
|
|
7
|
+
ForbiddenException,
|
|
8
|
+
MethodNotAllowedException,
|
|
9
|
+
NotFoundException,
|
|
10
|
+
ServerException,
|
|
11
|
+
UnauthorizedException,
|
|
12
|
+
)
|
|
13
|
+
from satori.utils import decode
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@overload
|
|
17
|
+
async def validate_response(resp: ClientResponse) -> dict: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@overload
|
|
21
|
+
async def validate_response(resp: ClientResponse, noreturn: Literal[True]) -> None: ...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def validate_response(resp: ClientResponse, noreturn=False):
|
|
25
|
+
match resp.status:
|
|
26
|
+
case x if 200 <= x < 300:
|
|
27
|
+
if noreturn:
|
|
28
|
+
return
|
|
29
|
+
content = await resp.text()
|
|
30
|
+
return decode(content) if content else {}
|
|
31
|
+
case 400:
|
|
32
|
+
raise BadRequestException(await resp.text())
|
|
33
|
+
case 401:
|
|
34
|
+
raise UnauthorizedException(await resp.text())
|
|
35
|
+
case 403:
|
|
36
|
+
raise ForbiddenException(await resp.text())
|
|
37
|
+
case 404:
|
|
38
|
+
raise NotFoundException(await resp.text())
|
|
39
|
+
case 405:
|
|
40
|
+
raise MethodNotAllowedException(await resp.text())
|
|
41
|
+
case x if x >= 500:
|
|
42
|
+
raise ServerException(await resp.text())
|
|
43
|
+
case _:
|
|
44
|
+
resp.raise_for_status()
|
|
@@ -105,7 +105,7 @@ class WebhookNetwork(BaseNetwork[WebhookInfo]):
|
|
|
105
105
|
for login in meta.logins:
|
|
106
106
|
if not login.user:
|
|
107
107
|
continue
|
|
108
|
-
login_sn = f"{login.user.id}@{id(self):x}"
|
|
108
|
+
login_sn = f"{login.platform}_{login.user.id}@{id(self):x}"
|
|
109
109
|
account = Account(login, self.config, meta.proxy_urls, self.app.default_api_cls)
|
|
110
110
|
logger.info(f"account registered: {account}")
|
|
111
111
|
(account.connected.set() if login.status == LoginStatus.ONLINE else account.connected.clear())
|
|
@@ -117,7 +117,7 @@ class WebhookNetwork(BaseNetwork[WebhookInfo]):
|
|
|
117
117
|
logger.info(f"{self.id} Webhook server exiting...")
|
|
118
118
|
self.close_signal.set()
|
|
119
119
|
for v in list(self.app.accounts.values()):
|
|
120
|
-
if (identity := f"{v.self_id}@{id(self):x}") in self.accounts:
|
|
120
|
+
if (identity := f"{v.platform}_{v.self_id}@{id(self):x}") in self.accounts:
|
|
121
121
|
v.connected.clear()
|
|
122
122
|
await self.app.account_update(v, LoginStatus.OFFLINE)
|
|
123
123
|
del self.app.accounts[identity]
|
|
@@ -107,7 +107,7 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
|
|
|
107
107
|
for login in ready.logins:
|
|
108
108
|
if not login.user:
|
|
109
109
|
continue
|
|
110
|
-
login_sn = f"{login.user.id}@{id(self):x}"
|
|
110
|
+
login_sn = f"{login.platform}_{login.user.id}@{id(self):x}"
|
|
111
111
|
if login_sn in self.app.accounts:
|
|
112
112
|
account = self.app.accounts[login_sn]
|
|
113
113
|
self.accounts[login_sn] = account
|
|
@@ -162,7 +162,7 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
|
|
|
162
162
|
self.close_signal.set()
|
|
163
163
|
self.connection = None
|
|
164
164
|
for v in list(self.app.accounts.values()):
|
|
165
|
-
if (identity := f"{v.self_id}@{id(self):x}") in self.accounts:
|
|
165
|
+
if (identity := f"{v.platform}_{v.self_id}@{id(self):x}") in self.accounts:
|
|
166
166
|
v.connected.clear()
|
|
167
167
|
await self.app.account_update(v, LoginStatus.OFFLINE)
|
|
168
168
|
del self.app.accounts[identity]
|
|
@@ -20,7 +20,6 @@ from satori.model import (
|
|
|
20
20
|
LoginPartial,
|
|
21
21
|
Member,
|
|
22
22
|
MessageObject,
|
|
23
|
-
MessageReceipt,
|
|
24
23
|
Meta,
|
|
25
24
|
Order,
|
|
26
25
|
PageDequeResult,
|
|
@@ -45,7 +44,7 @@ class ApiProtocol:
|
|
|
45
44
|
self.session = ClientSession()
|
|
46
45
|
self.timeout = ClientTimeout(self.account.config.timeout or 300)
|
|
47
46
|
|
|
48
|
-
async def download(self, url: str):
|
|
47
|
+
async def download(self, url: str) -> bytes:
|
|
49
48
|
"""访问资源链接。"""
|
|
50
49
|
endpoint = self.account.ensure_url(url)
|
|
51
50
|
aio = Launart.current().get_component(AiohttpClientService)
|
|
@@ -99,54 +98,58 @@ class ApiProtocol:
|
|
|
99
98
|
) as resp:
|
|
100
99
|
return await validate_response(resp)
|
|
101
100
|
|
|
102
|
-
async def send(self, event: Event, message: str | Iterable[str | Element]) -> list[
|
|
103
|
-
"""发送消息。返回一个 `
|
|
101
|
+
async def send(self, event: Event, message: str | Iterable[str | Element]) -> list[MessageObject]:
|
|
102
|
+
"""发送消息。返回一个 `MessageObject` 对象构成的数组。
|
|
104
103
|
|
|
105
104
|
Args:
|
|
106
105
|
event (Event): 当前事件(上下文)
|
|
107
106
|
message (str | Iterable[str | Element]): 要发送的消息
|
|
108
107
|
|
|
109
108
|
Returns:
|
|
110
|
-
list[
|
|
109
|
+
list[MessageObject]: `MessageObject` 对象构成的数组
|
|
111
110
|
|
|
112
111
|
Raises:
|
|
113
112
|
RuntimeError: 传入的事件缺少 `channel` 对象
|
|
114
113
|
"""
|
|
115
114
|
if not event.channel:
|
|
116
115
|
raise RuntimeError("Event cannot be replied to!")
|
|
117
|
-
return await self.send_message(event.channel.id, message)
|
|
116
|
+
return await self.send_message(event.channel.id, message, event.referrer)
|
|
118
117
|
|
|
119
118
|
async def send_message(
|
|
120
|
-
self, channel: str | Channel, message: str | Iterable[str | Element]
|
|
121
|
-
) -> list[
|
|
122
|
-
"""发送消息。返回一个 `
|
|
119
|
+
self, channel: str | Channel, message: str | Iterable[str | Element], referrer: dict[str, Any] | None = None
|
|
120
|
+
) -> list[MessageObject]:
|
|
121
|
+
"""发送消息。返回一个 `MessageObject` 对象构成的数组。
|
|
123
122
|
|
|
124
123
|
Args:
|
|
125
124
|
channel (str | Channel): 要发送的频道 ID
|
|
126
125
|
message (str | Iterable[str | Element]): 要发送的消息
|
|
126
|
+
referrer (dict[str, Any] | None): 消息来源信息,默认为 None
|
|
127
127
|
|
|
128
128
|
Returns:
|
|
129
|
-
list[
|
|
129
|
+
list[MessageObject]: `MessageObject` 对象构成的数组
|
|
130
130
|
"""
|
|
131
131
|
channel_id = channel.id if isinstance(channel, Channel) else channel
|
|
132
132
|
msg = message if isinstance(message, str) else "".join(str(i) for i in message)
|
|
133
|
-
return await self.message_create(channel_id=channel_id, content=msg)
|
|
133
|
+
return await self.message_create(channel_id=channel_id, content=msg, referrer=referrer)
|
|
134
134
|
|
|
135
135
|
async def send_private_message(
|
|
136
|
-
self, user: str | User, message: str | Iterable[str | Element]
|
|
137
|
-
) -> list[
|
|
138
|
-
"""发送私聊消息。返回一个 `
|
|
136
|
+
self, user: str | User, message: str | Iterable[str | Element], referrer: dict[str, Any] | None = None
|
|
137
|
+
) -> list[MessageObject]:
|
|
138
|
+
"""发送私聊消息。返回一个 `MessageObject` 对象构成的数组。
|
|
139
139
|
|
|
140
140
|
Args:
|
|
141
141
|
user (str | User): 要发送的用户 ID
|
|
142
142
|
message (str | Iterable[str | Element]): 要发送的消息
|
|
143
|
+
referrer (dict[str, Any] | None): 消息来源信息,默认为 None
|
|
143
144
|
|
|
144
145
|
Returns:
|
|
145
|
-
list[
|
|
146
|
+
list[MessageObject]: `MessageObject` 对象构成的数组
|
|
146
147
|
"""
|
|
147
148
|
user_id = user.id if isinstance(user, User) else user
|
|
148
149
|
channel = await self.user_channel_create(user_id=user_id)
|
|
149
|
-
return await self.message_create(
|
|
150
|
+
return await self.message_create(
|
|
151
|
+
channel_id=channel.id, content="".join(str(i) for i in message), referrer=referrer
|
|
152
|
+
)
|
|
150
153
|
|
|
151
154
|
async def update_message(
|
|
152
155
|
self, channel: str | Channel, message_id: str, message: str | Iterable[str | Element]
|
|
@@ -169,22 +172,25 @@ class ApiProtocol:
|
|
|
169
172
|
content=msg,
|
|
170
173
|
)
|
|
171
174
|
|
|
172
|
-
async def message_create(
|
|
173
|
-
|
|
175
|
+
async def message_create(
|
|
176
|
+
self, channel_id: str, content: str, referrer: dict[str, Any] | None = None
|
|
177
|
+
) -> list[MessageObject]:
|
|
178
|
+
"""发送消息。返回一个 `MessageObject` 对象构成的数组。
|
|
174
179
|
|
|
175
180
|
Args:
|
|
176
181
|
channel_id (str): 频道 ID
|
|
177
182
|
content (str): 消息内容
|
|
183
|
+
referrer (dict[str, Any] | None): 消息来源信息,默认为 None
|
|
178
184
|
|
|
179
185
|
Returns:
|
|
180
|
-
list[
|
|
186
|
+
list[MessageObject]: `MessageObject` 对象构成的数组
|
|
181
187
|
"""
|
|
182
188
|
res = await self.call_api(
|
|
183
189
|
Api.MESSAGE_CREATE,
|
|
184
|
-
{"channel_id": channel_id, "content": content},
|
|
190
|
+
{"channel_id": channel_id, "content": content, "referrer": referrer},
|
|
185
191
|
)
|
|
186
192
|
res = cast("list[dict]", res)
|
|
187
|
-
return [
|
|
193
|
+
return [MessageObject.parse(i) for i in res]
|
|
188
194
|
|
|
189
195
|
async def message_get(self, channel_id: str, message_id: str) -> MessageObject:
|
|
190
196
|
"""获取特定消息。返回一个 `MessageObject` 对象。
|
|
@@ -793,7 +799,8 @@ class ApiProtocol:
|
|
|
793
799
|
Returns:
|
|
794
800
|
list[Login]: `Login` 对象构成的数组
|
|
795
801
|
"""
|
|
796
|
-
|
|
802
|
+
res = await self.call_api("admin/login.list")
|
|
803
|
+
return [LoginPartial.parse(i) for i in res]
|
|
797
804
|
|
|
798
805
|
async def webhook_create(self, url: str, token: str | None = None):
|
|
799
806
|
"""创建 Webhook。"""
|
|
@@ -48,18 +48,23 @@ class Api(str, Enum):
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
class EventType(str, Enum):
|
|
51
|
+
FRIEND_ADDED = "friend-added"
|
|
52
|
+
FRIEND_REMOVED = "friend-removed"
|
|
51
53
|
FRIEND_REQUEST = "friend-request"
|
|
52
54
|
GUILD_ADDED = "guild-added"
|
|
55
|
+
GUILD_UPDATED = "guild-updated"
|
|
56
|
+
GUILD_REMOVED = "guild-removed"
|
|
57
|
+
GUILD_REQUEST = "guild-request"
|
|
58
|
+
CHANNEL_ADDED = "channel-added"
|
|
59
|
+
CHANNEL_UPDATED = "channel-updated"
|
|
60
|
+
CHANNEL_REMOVED = "channel-removed"
|
|
53
61
|
GUILD_MEMBER_ADDED = "guild-member-added"
|
|
54
62
|
GUILD_MEMBER_REMOVED = "guild-member-removed"
|
|
55
63
|
GUILD_MEMBER_REQUEST = "guild-member-request"
|
|
56
64
|
GUILD_MEMBER_UPDATED = "guild-member-updated"
|
|
57
|
-
GUILD_REMOVED = "guild-removed"
|
|
58
|
-
GUILD_REQUEST = "guild-request"
|
|
59
65
|
GUILD_ROLE_CREATED = "guild-role-created"
|
|
60
66
|
GUILD_ROLE_DELETED = "guild-role-deleted"
|
|
61
67
|
GUILD_ROLE_UPDATED = "guild-role-updated"
|
|
62
|
-
GUILD_UPDATED = "guild-updated"
|
|
63
68
|
LOGIN_ADDED = "login-added"
|
|
64
69
|
LOGIN_REMOVED = "login-removed"
|
|
65
70
|
LOGIN_UPDATED = "login-updated"
|
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
class ActionFailed(Exception):
|
|
2
|
+
CODE = 400
|
|
2
3
|
pass
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class BadRequestException(ActionFailed):
|
|
7
|
+
CODE = 400
|
|
6
8
|
pass
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class UnauthorizedException(ActionFailed):
|
|
12
|
+
CODE = 401
|
|
10
13
|
pass
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
class ForbiddenException(ActionFailed):
|
|
17
|
+
CODE = 403
|
|
14
18
|
pass
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
class NotFoundException(ActionFailed):
|
|
22
|
+
CODE = 404
|
|
18
23
|
pass
|
|
19
24
|
|
|
20
25
|
|
|
21
26
|
class MethodNotAllowedException(ActionFailed):
|
|
27
|
+
CODE = 405
|
|
22
28
|
pass
|
|
23
29
|
|
|
24
30
|
|
|
25
31
|
class ServerException(ActionFailed):
|
|
32
|
+
CODE = 500
|
|
26
33
|
pass
|
|
27
34
|
|
|
28
35
|
|
|
@@ -205,9 +205,13 @@ class ArgvInteraction(ModelBase):
|
|
|
205
205
|
@dataclass
|
|
206
206
|
class ButtonInteraction(ModelBase):
|
|
207
207
|
id: str
|
|
208
|
+
data: str | None = None
|
|
208
209
|
|
|
209
210
|
def dump(self):
|
|
210
|
-
|
|
211
|
+
res = {"id": self.id}
|
|
212
|
+
if self.data:
|
|
213
|
+
res["data"] = self.data
|
|
214
|
+
return res
|
|
211
215
|
|
|
212
216
|
|
|
213
217
|
class Opcode(IntEnum):
|
|
@@ -281,13 +285,14 @@ class Meta(ModelBase):
|
|
|
281
285
|
@dataclass
|
|
282
286
|
class MessageObject(ModelBase):
|
|
283
287
|
id: str
|
|
284
|
-
content: str
|
|
288
|
+
content: str = ""
|
|
285
289
|
channel: Channel | None = None
|
|
286
290
|
guild: Guild | None = None
|
|
287
291
|
member: Member | None = None
|
|
288
292
|
user: User | None = None
|
|
289
293
|
created_at: datetime | None = None
|
|
290
294
|
updated_at: datetime | None = None
|
|
295
|
+
referrer: dict | None = None
|
|
291
296
|
|
|
292
297
|
@classmethod
|
|
293
298
|
def from_elements(
|
|
@@ -300,8 +305,9 @@ class MessageObject(ModelBase):
|
|
|
300
305
|
user: User | None = None,
|
|
301
306
|
created_at: datetime | None = None,
|
|
302
307
|
updated_at: datetime | None = None,
|
|
308
|
+
referrer: dict | None = None,
|
|
303
309
|
):
|
|
304
|
-
obj = cls(id, "".join(str(i) for i in content), channel, guild, member, user, created_at, updated_at)
|
|
310
|
+
obj = cls(id, "".join(str(i) for i in content), channel, guild, member, user, created_at, updated_at, referrer)
|
|
305
311
|
obj._parsed_message = content
|
|
306
312
|
return obj
|
|
307
313
|
|
|
@@ -347,41 +353,8 @@ class MessageObject(ModelBase):
|
|
|
347
353
|
res["created_at"] = int(self.created_at.timestamp() * 1000)
|
|
348
354
|
if self.updated_at:
|
|
349
355
|
res["updated_at"] = int(self.updated_at.timestamp() * 1000)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
@dataclass
|
|
354
|
-
class MessageReceipt(ModelBase):
|
|
355
|
-
id: str
|
|
356
|
-
content: str | None = None
|
|
357
|
-
|
|
358
|
-
@classmethod
|
|
359
|
-
def from_elements(
|
|
360
|
-
cls,
|
|
361
|
-
id: str,
|
|
362
|
-
content: list[Element] | None = None,
|
|
363
|
-
):
|
|
364
|
-
return cls(id, "".join(str(i) for i in content) if content else None)
|
|
365
|
-
|
|
366
|
-
@property
|
|
367
|
-
def message(self) -> list[Element] | None:
|
|
368
|
-
return transform(parse(self.content)) if self.content else None
|
|
369
|
-
|
|
370
|
-
@message.setter
|
|
371
|
-
def message(self, value: list[Element] | None):
|
|
372
|
-
self.content = "".join(str(i) for i in value) if value else None
|
|
373
|
-
|
|
374
|
-
@classmethod
|
|
375
|
-
def parse(cls, raw: dict):
|
|
376
|
-
if "elements" in raw and "content" not in raw:
|
|
377
|
-
content = [RawElement(*item.values()) for item in raw["elements"]]
|
|
378
|
-
raw["content"] = "".join(str(i) for i in content)
|
|
379
|
-
return super().parse(raw)
|
|
380
|
-
|
|
381
|
-
def dump(self):
|
|
382
|
-
res = {"id": self.id}
|
|
383
|
-
if self.content:
|
|
384
|
-
res["content"] = self.content
|
|
356
|
+
if self.referrer:
|
|
357
|
+
res["referrer"] = self.referrer
|
|
385
358
|
return res
|
|
386
359
|
|
|
387
360
|
|
|
@@ -399,6 +372,7 @@ class Event(ModelBase):
|
|
|
399
372
|
operator: User | None = None
|
|
400
373
|
role: Role | None = None
|
|
401
374
|
user: User | None = None
|
|
375
|
+
referrer: dict | None = None
|
|
402
376
|
|
|
403
377
|
_type: str | None = None
|
|
404
378
|
_data: dict | None = None
|
|
@@ -430,6 +404,10 @@ class Event(ModelBase):
|
|
|
430
404
|
"user": {"id": raw["self_id"]},
|
|
431
405
|
"status": LoginStatus.ONLINE,
|
|
432
406
|
}
|
|
407
|
+
if "self_id" in raw and not raw.get("login", {}).get("user"):
|
|
408
|
+
if "login" not in raw:
|
|
409
|
+
raw["login"] = {"sn": 0, "status": LoginStatus.ONLINE, "platform": raw.get("platform", "unknown")}
|
|
410
|
+
raw["login"]["user"] = {"id": raw["self_id"]}
|
|
433
411
|
return super().parse(raw)
|
|
434
412
|
|
|
435
413
|
@property
|
|
@@ -467,6 +445,8 @@ class Event(ModelBase):
|
|
|
467
445
|
res["role"] = self.role.dump()
|
|
468
446
|
if self.user:
|
|
469
447
|
res["user"] = self.user.dump()
|
|
448
|
+
if self.referrer:
|
|
449
|
+
res["referrer"] = self.referrer
|
|
470
450
|
if self._type:
|
|
471
451
|
res["_type"] = self._type
|
|
472
452
|
if self._data:
|
|
@@ -40,6 +40,7 @@ from starlette.websockets import WebSocket, WebSocketDisconnect
|
|
|
40
40
|
from yarl import URL
|
|
41
41
|
|
|
42
42
|
from satori.const import Api, EventType
|
|
43
|
+
from satori.exception import ActionFailed
|
|
43
44
|
from satori.model import Event, Meta, ModelBase, Opcode
|
|
44
45
|
from satori.utils import decode
|
|
45
46
|
|
|
@@ -96,6 +97,9 @@ async def _request_handler(action: str, request: StarletteRequest, func: RouteCa
|
|
|
96
97
|
self_id=self_id,
|
|
97
98
|
)
|
|
98
99
|
)
|
|
100
|
+
except ActionFailed as ae:
|
|
101
|
+
logger.warning(ae)
|
|
102
|
+
return Response(status_code=ae.CODE, content=str(ae))
|
|
99
103
|
except Exception as e:
|
|
100
104
|
logger.error(e)
|
|
101
105
|
return Response(status_code=500, content=str(e))
|
|
@@ -139,7 +143,6 @@ class Server(Service, RouterMixin):
|
|
|
139
143
|
self.port = port
|
|
140
144
|
self.version = version
|
|
141
145
|
self.path = path
|
|
142
|
-
self.uvicorn_options = uvicorn_options
|
|
143
146
|
if self.path and not self.path.startswith("/"):
|
|
144
147
|
self.path = f"/{self.path}"
|
|
145
148
|
if (self.host == "0.0.0.0" or self.host == "::") and not token:
|
|
@@ -157,9 +160,13 @@ class Server(Service, RouterMixin):
|
|
|
157
160
|
self.stream_chunk_size = stream_chunk_size
|
|
158
161
|
self.resources: dict[str, Path] = {}
|
|
159
162
|
self.app = Starlette()
|
|
160
|
-
self.asgi_service = UvicornASGIService(self.host, self.port, options=
|
|
163
|
+
self.asgi_service = UvicornASGIService(self.host, self.port, options=uvicorn_options)
|
|
161
164
|
super().__init__()
|
|
162
165
|
|
|
166
|
+
@property
|
|
167
|
+
def uvicorn_options(self) -> UvicornOptions:
|
|
168
|
+
return self.asgi_service.options
|
|
169
|
+
|
|
163
170
|
def replace_app(self, app: ASGIApp | asgitypes.ASGI3Application):
|
|
164
171
|
"""替换当前的 Starlette 应用"""
|
|
165
172
|
self.app = app
|
|
@@ -332,7 +339,9 @@ class Server(Service, RouterMixin):
|
|
|
332
339
|
else:
|
|
333
340
|
continue
|
|
334
341
|
return await _request_handler(action, request, func, platform, self_id)
|
|
335
|
-
return Response(
|
|
342
|
+
return Response(
|
|
343
|
+
status_code=404, content=f"Action {action!r} is not supported in current platform {platform!r}."
|
|
344
|
+
)
|
|
336
345
|
|
|
337
346
|
async def proxy_url_handler(self, request: StarletteRequest):
|
|
338
347
|
url = request.path_params["internal_url"]
|
|
@@ -36,6 +36,7 @@ INTERAL: TypeAlias = RouteCall[Any, ModelBase | list[ModelBase] | dict[str, Any]
|
|
|
36
36
|
class MessageParam(TypedDict):
|
|
37
37
|
channel_id: str
|
|
38
38
|
content: str
|
|
39
|
+
referrer: NotRequired[dict[str, Any]]
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
MESSAGE_CREATE: TypeAlias = RouteCall[MessageParam, list[MessageObject] | list[dict[str, Any]]]
|
|
@@ -179,7 +180,7 @@ GUILD_ROLE_LIST: TypeAlias = RouteCall[GuildXXXListParam, PageResult[Role] | dic
|
|
|
179
180
|
|
|
180
181
|
|
|
181
182
|
class GuildRoleCreateParam(TypedDict):
|
|
182
|
-
|
|
183
|
+
guild_id: str
|
|
183
184
|
role: dict
|
|
184
185
|
|
|
185
186
|
|
|
@@ -187,7 +188,7 @@ GUILD_ROLE_CREATE: TypeAlias = RouteCall[GuildRoleCreateParam, Role | dict[str,
|
|
|
187
188
|
|
|
188
189
|
|
|
189
190
|
class GuildRoleUpdateParam(TypedDict):
|
|
190
|
-
|
|
191
|
+
guild_id: str
|
|
191
192
|
role_id: str
|
|
192
193
|
role: dict
|
|
193
194
|
|
|
@@ -196,7 +197,7 @@ GUILD_ROLE_UPDATE: TypeAlias = RouteCall[GuildRoleUpdateParam, None]
|
|
|
196
197
|
|
|
197
198
|
|
|
198
199
|
class GuildRoleDeleteParam(TypedDict):
|
|
199
|
-
|
|
200
|
+
guild_id: str
|
|
200
201
|
role_id: str
|
|
201
202
|
|
|
202
203
|
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
from typing import Literal, overload
|
|
2
|
-
|
|
3
|
-
from aiohttp import ClientResponse
|
|
4
|
-
|
|
5
|
-
from satori.exception import (
|
|
6
|
-
BadRequestException,
|
|
7
|
-
ForbiddenException,
|
|
8
|
-
MethodNotAllowedException,
|
|
9
|
-
NotFoundException,
|
|
10
|
-
ServerException,
|
|
11
|
-
UnauthorizedException,
|
|
12
|
-
)
|
|
13
|
-
from satori.utils import decode
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@overload
|
|
17
|
-
async def validate_response(resp: ClientResponse) -> dict: ...
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@overload
|
|
21
|
-
async def validate_response(resp: ClientResponse, noreturn: Literal[True]) -> None: ...
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
async def validate_response(resp: ClientResponse, noreturn=False):
|
|
25
|
-
if 200 <= resp.status < 300:
|
|
26
|
-
if noreturn:
|
|
27
|
-
return
|
|
28
|
-
return decode(content) if (content := await resp.text()) else {}
|
|
29
|
-
elif resp.status == 400:
|
|
30
|
-
raise BadRequestException(await resp.text())
|
|
31
|
-
elif resp.status == 401:
|
|
32
|
-
raise UnauthorizedException(await resp.text())
|
|
33
|
-
elif resp.status == 403:
|
|
34
|
-
raise ForbiddenException(await resp.text())
|
|
35
|
-
elif resp.status == 404:
|
|
36
|
-
raise NotFoundException(await resp.text())
|
|
37
|
-
elif resp.status == 405:
|
|
38
|
-
raise MethodNotAllowedException(await resp.text())
|
|
39
|
-
elif resp.status >= 500:
|
|
40
|
-
raise ServerException(await resp.text())
|
|
41
|
-
else:
|
|
42
|
-
resp.raise_for_status()
|
|
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
|