satori-python 0.17.7__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.7 → satori_python-0.18.0}/PKG-INFO +6 -3
- {satori_python-0.17.7 → satori_python-0.18.0}/README.md +4 -2
- {satori_python-0.17.7 → satori_python-0.18.0}/pyproject.toml +2 -1
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/__init__.py +4 -1
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/client/__init__.py +22 -6
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/client/account.py +4 -1
- {satori_python-0.17.7 → 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.7 → satori_python-0.18.0}/src/satori/client/network/webhook.py +2 -2
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/client/network/websocket.py +2 -2
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/client/protocol.py +29 -22
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/const.py +8 -3
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/element.py +1 -1
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/event.py +4 -0
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/exception.py +7 -0
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/model.py +18 -38
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/server/__init__.py +9 -2
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/server/route.py +4 -3
- satori_python-0.17.7/src/satori/client/network/util.py +0 -42
- {satori_python-0.17.7 → satori_python-0.18.0}/LICENSE +0 -0
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/_vendor/fleep.py +0 -0
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/client/config.py +0 -0
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/client/network/__init__.py +0 -0
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/client/network/base.py +0 -0
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/parser.py +0 -0
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/server/adapter.py +0 -0
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/server/connection.py +0 -0
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/server/formdata.py +0 -0
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/server/model.py +0 -0
- {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/server/utils.py +0 -0
- {satori_python-0.17.7 → 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"
|
|
@@ -102,7 +102,9 @@ class App(Service):
|
|
|
102
102
|
self.event_callbacks.append(callback)
|
|
103
103
|
|
|
104
104
|
@overload
|
|
105
|
-
def register_on(
|
|
105
|
+
def register_on(
|
|
106
|
+
self, event_type: Literal[EventType.FRIEND_ADDED, EventType.FRIEND_REMOVED, EventType.FRIEND_REQUEST]
|
|
107
|
+
) -> Callable[
|
|
106
108
|
[Callable[[Account, events.UserEvent], Awaitable[Any]]],
|
|
107
109
|
Callable[[Account, events.UserEvent], Awaitable[Any]],
|
|
108
110
|
]: ...
|
|
@@ -118,6 +120,15 @@ class App(Service):
|
|
|
118
120
|
Callable[[Account, events.GuildEvent], Awaitable[Any]],
|
|
119
121
|
]: ...
|
|
120
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
|
+
|
|
121
132
|
@overload
|
|
122
133
|
def register_on(
|
|
123
134
|
self,
|
|
@@ -204,7 +215,12 @@ class App(Service):
|
|
|
204
215
|
|
|
205
216
|
async def account_update(self, account: Account, state: LoginStatus):
|
|
206
217
|
if self.lifecycle_callbacks:
|
|
207
|
-
|
|
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()
|
|
208
224
|
|
|
209
225
|
async def post(self, event: Event, conn: BaseNetwork):
|
|
210
226
|
if event.type == EventType.LOGIN_ADDED:
|
|
@@ -214,7 +230,7 @@ class App(Service):
|
|
|
214
230
|
if not login.user:
|
|
215
231
|
logger.warning(f"Received login-added event without user info: {login}")
|
|
216
232
|
return
|
|
217
|
-
login_sn = f"{login.user.id}@{id(conn):x}"
|
|
233
|
+
login_sn = f"{login.platform}_{login.user.id}@{id(conn):x}"
|
|
218
234
|
account = Account(
|
|
219
235
|
login,
|
|
220
236
|
conn.config,
|
|
@@ -233,7 +249,7 @@ class App(Service):
|
|
|
233
249
|
if not login.user:
|
|
234
250
|
logger.warning(f"Received login-updated event without user info: {login}")
|
|
235
251
|
return
|
|
236
|
-
login_sn = f"{login.user.id}@{id(conn):x}"
|
|
252
|
+
login_sn = f"{login.platform}_{login.user.id}@{id(conn):x}"
|
|
237
253
|
if login_sn not in self.accounts:
|
|
238
254
|
if login.status == LoginStatus.ONLINE:
|
|
239
255
|
account = Account(
|
|
@@ -267,13 +283,13 @@ class App(Service):
|
|
|
267
283
|
if not login.user:
|
|
268
284
|
logger.warning(f"Received login-removed event without user info: {login}")
|
|
269
285
|
return
|
|
270
|
-
login_sn = f"{login.user.id}@{id(conn):x}"
|
|
286
|
+
login_sn = f"{login.platform}_{login.user.id}@{id(conn):x}"
|
|
271
287
|
if login_sn not in self.accounts:
|
|
272
288
|
logger.warning(f"Received event for unknown account: {event}")
|
|
273
289
|
return
|
|
274
290
|
account = self.accounts[login_sn]
|
|
275
291
|
else:
|
|
276
|
-
login_sn = f"{event.login.user.id}@{id(conn):x}"
|
|
292
|
+
login_sn = f"{event.login.platform}_{event.login.user.id}@{id(conn):x}"
|
|
277
293
|
if login_sn not in self.accounts:
|
|
278
294
|
logger.warning(f"Received event for unknown account: {event}")
|
|
279
295
|
return
|
|
@@ -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
|
|
@@ -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
|