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.
Files changed (31) hide show
  1. {satori_python-0.17.7 → satori_python-0.18.0}/PKG-INFO +6 -3
  2. {satori_python-0.17.7 → satori_python-0.18.0}/README.md +4 -2
  3. {satori_python-0.17.7 → satori_python-0.18.0}/pyproject.toml +2 -1
  4. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/__init__.py +4 -1
  5. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/client/__init__.py +22 -6
  6. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/client/account.py +4 -1
  7. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/client/account.pyi +12 -8
  8. satori_python-0.18.0/src/satori/client/network/util.py +44 -0
  9. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/client/network/webhook.py +2 -2
  10. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/client/network/websocket.py +2 -2
  11. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/client/protocol.py +29 -22
  12. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/const.py +8 -3
  13. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/element.py +1 -1
  14. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/event.py +4 -0
  15. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/exception.py +7 -0
  16. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/model.py +18 -38
  17. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/server/__init__.py +9 -2
  18. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/server/route.py +4 -3
  19. satori_python-0.17.7/src/satori/client/network/util.py +0 -42
  20. {satori_python-0.17.7 → satori_python-0.18.0}/LICENSE +0 -0
  21. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/_vendor/fleep.py +0 -0
  22. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/client/config.py +0 -0
  23. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/client/network/__init__.py +0 -0
  24. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/client/network/base.py +0 -0
  25. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/parser.py +0 -0
  26. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/server/adapter.py +0 -0
  27. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/server/connection.py +0 -0
  28. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/server/formdata.py +0 -0
  29. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/server/model.py +0 -0
  30. {satori_python-0.17.7 → satori_python-0.18.0}/src/satori/server/utils.py +0 -0
  31. {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.17.7
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
  [![PyPI](https://img.shields.io/pypi/v/satori-python)](https://pypi.org/project/satori-python)
38
39
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/satori-python)](https://www.python.org/)
39
40
 
40
- 基于 [Satori](https://satori.js.org/zh-CN/) 协议的 Python 开发工具包
41
+ 基于 [Satori](https://satori.chat/zh-CN/) 协议的 Python 开发工具包
41
42
 
42
43
  ## 协议介绍
43
44
 
44
- [Satori Protocol](https://satori.js.org/zh-CN/)
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
  [![PyPI](https://img.shields.io/pypi/v/satori-python)](https://pypi.org/project/satori-python)
6
6
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/satori-python)](https://www.python.org/)
7
7
 
8
- 基于 [Satori](https://satori.js.org/zh-CN/) 协议的 Python 开发工具包
8
+ 基于 [Satori](https://satori.chat/zh-CN/) 协议的 Python 开发工具包
9
9
 
10
10
  ## 协议介绍
11
11
 
12
- [Satori Protocol](https://satori.js.org/zh-CN/)
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.17.7"
33
+ version = "0.18.0"
33
34
 
34
35
  [project.license]
35
36
  text = "MIT"
@@ -42,4 +42,7 @@ from .model import Role as Role
42
42
  from .model import Upload as Upload
43
43
  from .model import User as User
44
44
 
45
- __version__ = "0.17.7"
45
+ __version__ = "0.18.0"
46
+
47
+
48
+ MessageReceipt = MessageObject
@@ -102,7 +102,9 @@ class App(Service):
102
102
  self.event_callbacks.append(callback)
103
103
 
104
104
  @overload
105
- def register_on(self, event_type: Literal[EventType.FRIEND_REQUEST]) -> Callable[
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
- await asyncio.gather(*(callback(account, state) for callback in self.lifecycle_callbacks))
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
- return URL(url)
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[MessageReceipt]:
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[MessageReceipt]:
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[MessageReceipt]:
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(self, channel_id: str, content: str) -> list[MessageReceipt]:
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[MessageReceipt]:
103
- """发送消息。返回一个 `MessageReceipt` 对象构成的数组。
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[MessageReceipt]: `MessageReceipt` 对象构成的数组
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[MessageReceipt]:
122
- """发送消息。返回一个 `MessageReceipt` 对象构成的数组。
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[MessageReceipt]: `MessageReceipt` 对象构成的数组
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[MessageReceipt]:
138
- """发送私聊消息。返回一个 `MessageReceipt` 对象构成的数组。
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[MessageReceipt]: `MessageReceipt` 对象构成的数组
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(channel_id=channel.id, content="".join(str(i) for i in message))
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(self, channel_id: str, content: str) -> list[MessageReceipt]:
173
- """发送消息。返回一个 `MessageReceipt` 对象构成的数组。
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[MessageReceipt]: `MessageReceipt` 对象构成的数组
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 [MessageReceipt.parse(i) for i in res]
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
- return (await self.meta_get()).logins
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"
@@ -213,7 +213,7 @@ class Resource(Element):
213
213
  timeout: int | None = None,
214
214
  **kwargs,
215
215
  ):
216
- data: dict[str, Any] = {"extra": extra or kwargs}
216
+ data: dict[str, Any] = {"extra": extra, **kwargs}
217
217
  if url is not None:
218
218
  data |= {"src": url}
219
219
  elif path:
@@ -27,6 +27,10 @@ class GuildEvent(Event):
27
27
  guild: Guild
28
28
 
29
29
 
30
+ class ChannelEvent(GuildEvent):
31
+ channel: Channel
32
+
33
+
30
34
  class GuildMemberEvent(GuildEvent):
31
35
  user: User
32
36
  member: Member
@@ -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
- return {"id": self.id}
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
- return res
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=self.uvicorn_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
- guild: str
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
- guild: str
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
- guild: str
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