satori-python 0.16.2__tar.gz → 0.16.3__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 (29) hide show
  1. {satori_python-0.16.2 → satori_python-0.16.3}/PKG-INFO +1 -1
  2. {satori_python-0.16.2 → satori_python-0.16.3}/pyproject.toml +1 -1
  3. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/__init__.py +1 -1
  4. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/client/__init__.py +52 -37
  5. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/client/account.py +1 -2
  6. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/client/account.pyi +14 -15
  7. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/client/network/base.py +2 -2
  8. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/client/network/webhook.py +31 -14
  9. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/client/network/websocket.py +11 -4
  10. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/client/protocol.py +72 -46
  11. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/event.py +2 -2
  12. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/model.py +39 -11
  13. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/server/adapter.py +7 -3
  14. {satori_python-0.16.2 → satori_python-0.16.3}/LICENSE +0 -0
  15. {satori_python-0.16.2 → satori_python-0.16.3}/README.md +0 -0
  16. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/client/config.py +0 -0
  17. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/client/network/__init__.py +0 -0
  18. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/client/network/util.py +0 -0
  19. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/const.py +0 -0
  20. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/element.py +0 -0
  21. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/exception.py +0 -0
  22. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/parser.py +0 -0
  23. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/server/__init__.py +0 -0
  24. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/server/conection.py +0 -0
  25. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/server/formdata.py +0 -0
  26. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/server/model.py +0 -0
  27. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/server/route.py +0 -0
  28. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/server/utils.py +0 -0
  29. {satori_python-0.16.2 → satori_python-0.16.3}/src/satori/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: satori-python
3
- Version: 0.16.2
3
+ Version: 0.16.3
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>
@@ -29,7 +29,7 @@ classifiers = [
29
29
  "Programming Language :: Python :: 3.12",
30
30
  "Operating System :: OS Independent",
31
31
  ]
32
- version = "0.16.2"
32
+ version = "0.16.3"
33
33
 
34
34
  [project.license]
35
35
  text = "MIT"
@@ -41,4 +41,4 @@ from .model import Role as Role
41
41
  from .model import Upload as Upload
42
42
  from .model import User as User
43
43
 
44
- __version__ = "0.16.2"
44
+ __version__ = "0.16.3"
@@ -189,32 +189,37 @@ class App(Service):
189
189
  async def post(self, event: Event, conn: BaseNetwork):
190
190
  if not self.event_callbacks:
191
191
  return
192
- login_sn = f"{event.login.sn}@{id(conn)}"
193
- if login_sn not in self.accounts:
194
- if event.type == EventType.LOGIN_ADDED:
195
- if TYPE_CHECKING:
196
- assert isinstance(event, events.LoginEvent)
197
- account = Account(
198
- event.login,
199
- conn.config,
200
- conn.proxy_urls,
201
- self.default_api_cls,
202
- )
203
- logger.info(f"account added: {account}")
204
- (
205
- account.connected.set()
206
- if event.login.status == LoginStatus.ONLINE
207
- else account.connected.clear()
208
- )
209
- self.accounts[login_sn] = account
210
- conn.accounts[login_sn] = account
211
- await self.account_update(account, event.login.status)
212
- elif event.type == EventType.LOGIN_UPDATED:
213
- if TYPE_CHECKING:
214
- assert isinstance(event, events.LoginEvent)
215
- if event.login.status == LoginStatus.ONLINE:
192
+ if event.type == EventType.LOGIN_ADDED:
193
+ if TYPE_CHECKING:
194
+ assert isinstance(event, events.LoginEvent)
195
+ login = event.login
196
+ if not login.user:
197
+ logger.warning(f"Received login-added event without user info: {login}")
198
+ return
199
+ login_sn = f"{login.user.id}@{id(conn)}"
200
+ account = Account(
201
+ login,
202
+ conn.config,
203
+ conn.proxy_urls,
204
+ self.default_api_cls,
205
+ )
206
+ logger.info(f"account added: {account}")
207
+ (account.connected.set() if login.status == LoginStatus.ONLINE else account.connected.clear())
208
+ self.accounts[login_sn] = account
209
+ conn.accounts[login_sn] = account
210
+ await self.account_update(account, login.status)
211
+ elif event.type == EventType.LOGIN_UPDATED:
212
+ if TYPE_CHECKING:
213
+ assert isinstance(event, events.LoginEvent)
214
+ login = event.login
215
+ if not login.user:
216
+ logger.warning(f"Received login-updated event without user info: {login}")
217
+ return
218
+ login_sn = f"{login.user.id}@{id(conn)}"
219
+ if login_sn not in self.accounts:
220
+ if login.status == LoginStatus.ONLINE:
216
221
  account = Account(
217
- event.login,
222
+ login,
218
223
  conn.config,
219
224
  conn.proxy_urls,
220
225
  self.default_api_cls,
@@ -228,27 +233,37 @@ class App(Service):
228
233
  logger.warning(f"Received event for unknown account: {event}")
229
234
  return
230
235
  else:
231
- logger.warning(f"Received event for unknown account: {event}")
232
- return
233
- else:
234
- account = self.accounts[login_sn]
235
- account.self_info = event.login
236
- if event.type == EventType.LOGIN_UPDATED:
237
- if TYPE_CHECKING:
238
- assert isinstance(event, events.LoginEvent)
236
+ account = self.accounts[login_sn]
237
+ account.self_info = login
239
238
  logger.info(f"account updated: {account}")
240
239
  (
241
240
  account.connected.set()
242
- if event.login.status in (LoginStatus.ONLINE, LoginStatus.CONNECT)
241
+ if login.status in (LoginStatus.ONLINE, LoginStatus.CONNECT)
243
242
  else account.connected.clear()
244
243
  )
245
- await self.account_update(account, event.login.status)
244
+ await self.account_update(account, login.status)
245
+ elif event.type == EventType.LOGIN_REMOVED:
246
+ if TYPE_CHECKING:
247
+ assert isinstance(event, events.LoginEvent)
248
+ login = event.login
249
+ if not login.user:
250
+ logger.warning(f"Received login-removed event without user info: {login}")
251
+ return
252
+ login_sn = f"{login.user.id}@{id(conn)}"
253
+ if login_sn not in self.accounts:
254
+ logger.warning(f"Received event for unknown account: {event}")
255
+ return
256
+ account = self.accounts[login_sn]
257
+ else:
258
+ login_sn = f"{event.login.user.id}@{id(conn)}"
259
+ if login_sn not in self.accounts:
260
+ logger.warning(f"Received event for unknown account: {event}")
261
+ return
262
+ account = self.accounts[login_sn]
246
263
 
247
264
  await asyncio.gather(*(callback(account, event) for callback in self.event_callbacks))
248
265
 
249
266
  if event.type == EventType.LOGIN_REMOVED:
250
- if TYPE_CHECKING:
251
- assert isinstance(event, events.LoginEvent)
252
267
  logger.info(f"account removed: {account}")
253
268
  account.connected.clear()
254
269
  await self.account_update(account, LoginStatus.OFFLINE)
@@ -38,7 +38,6 @@ class Account(Generic[TP]):
38
38
  proxy_urls: list[str],
39
39
  protocol_cls: type[TP] = ApiProtocol,
40
40
  ):
41
- self.sn = login.sn
42
41
  self.adapter = login.adapter
43
42
  self.self_info = login
44
43
  self.config = config
@@ -52,7 +51,7 @@ class Account(Generic[TP]):
52
51
 
53
52
  @property
54
53
  def self_id(self):
55
- return self.self_info.id
54
+ return self.self_info.user.id
56
55
 
57
56
  def custom(
58
57
  self, config: ApiInfo | None = None, protocol_cls: type[TP1] = ApiProtocol, **kwargs
@@ -11,6 +11,7 @@ from satori.model import (
11
11
  Direction,
12
12
  Event,
13
13
  Guild,
14
+ IterablePageResult,
14
15
  Login,
15
16
  Member,
16
17
  MessageObject,
@@ -18,7 +19,6 @@ from satori.model import (
18
19
  Meta,
19
20
  Order,
20
21
  PageDequeResult,
21
- PageResult,
22
22
  Role,
23
23
  Upload,
24
24
  User,
@@ -41,7 +41,6 @@ class ApiInfo(Api):
41
41
  ): ...
42
42
 
43
43
  class Account(Generic[TP]):
44
- sn: str
45
44
  adapter: str
46
45
  self_info: Login
47
46
  proxy_urls: list[str]
@@ -211,7 +210,7 @@ class Account(Generic[TP]):
211
210
  Channel: `Channel` 对象
212
211
  """
213
212
 
214
- async def channel_list(self, guild_id: str, next_token: str | None = None) -> PageResult[Channel]:
213
+ def channel_list(self, guild_id: str, next_token: str | None = None) -> IterablePageResult[Channel]:
215
214
  """获取群组中的全部频道。返回一个 Channel 的分页列表。
216
215
 
217
216
  Args:
@@ -219,7 +218,7 @@ class Account(Generic[TP]):
219
218
  next_token (str | None, optional): 分页令牌,默认为空
220
219
 
221
220
  Returns:
222
- PageResult[Channel]: `Channel` 的分页列表
221
+ IterablePageResult[Channel]: `Channel` 的分页列表
223
222
  """
224
223
 
225
224
  async def channel_create(self, guild_id: str, data: Channel) -> Channel:
@@ -291,14 +290,14 @@ class Account(Generic[TP]):
291
290
  Guild: `Guild` 对象
292
291
  """
293
292
 
294
- async def guild_list(self, next_token: str | None = None) -> PageResult[Guild]:
293
+ def guild_list(self, next_token: str | None = None) -> IterablePageResult[Guild]:
295
294
  """获取当前用户加入的全部群组。返回一个 Guild 的分页列表。
296
295
 
297
296
  Args:
298
297
  next_token (str | None, optional): 分页令牌,默认为空
299
298
 
300
299
  Returns:
301
- PageResult[Guild]: `Guild` 的分页列表
300
+ IterablePageResult[Guild]: `Guild` 的分页列表
302
301
  """
303
302
 
304
303
  async def guild_approve(self, request_id: str, approve: bool, comment: str) -> None:
@@ -313,7 +312,7 @@ class Account(Generic[TP]):
313
312
  None: 该方法无返回值
314
313
  """
315
314
 
316
- async def guild_member_list(self, guild_id: str, next_token: str | None = None) -> PageResult[Member]:
315
+ def guild_member_list(self, guild_id: str, next_token: str | None = None) -> IterablePageResult[Member]:
317
316
  """获取群组成员列表。返回一个 Member 的分页列表。
318
317
 
319
318
  Args:
@@ -321,7 +320,7 @@ class Account(Generic[TP]):
321
320
  next_token (str | None, optional): 分页令牌,默认为空
322
321
 
323
322
  Returns:
324
- PageResult[Member]: `Member` 的分页列表
323
+ IterablePageResult[Member]: `Member` 的分页列表
325
324
  """
326
325
 
327
326
  async def guild_member_get(self, guild_id: str, user_id: str) -> Member:
@@ -397,7 +396,7 @@ class Account(Generic[TP]):
397
396
  None: 该方法无返回值
398
397
  """
399
398
 
400
- async def guild_role_list(self, guild_id: str, next_token: str | None = None) -> PageResult[Role]:
399
+ def guild_role_list(self, guild_id: str, next_token: str | None = None) -> IterablePageResult[Role]:
401
400
  """获取群组角色列表。返回一个 Role 的分页列表。
402
401
 
403
402
  Args:
@@ -405,7 +404,7 @@ class Account(Generic[TP]):
405
404
  next_token (str | None, optional): 分页令牌,默认为空
406
405
 
407
406
  Returns:
408
- PageResult[Role]: `Role` 的分页列表
407
+ IterablePageResult[Role]: `Role` 的分页列表
409
408
  """
410
409
 
411
410
  async def guild_role_create(self, guild_id: str, role: Role) -> Role:
@@ -485,9 +484,9 @@ class Account(Generic[TP]):
485
484
  None: 该方法无返回值
486
485
  """
487
486
 
488
- async def reaction_list(
487
+ def reaction_list(
489
488
  self, channel_id: str, message_id: str, emoji: str, next_token: str | None = None
490
- ) -> PageResult[User]:
489
+ ) -> IterablePageResult[User]:
491
490
  """获取添加特定消息的特定表态的用户列表。返回一个 User 的分页列表。
492
491
 
493
492
  Args:
@@ -497,7 +496,7 @@ class Account(Generic[TP]):
497
496
  next_token (str | None, optional): 分页令牌,默认为空
498
497
 
499
498
  Returns:
500
- PageResult[User]: `User` 的分页列表
499
+ IterablePageResult[User]: `User` 的分页列表
501
500
  """
502
501
 
503
502
  async def login_get(self) -> Login:
@@ -517,14 +516,14 @@ class Account(Generic[TP]):
517
516
  User: `User` 对象
518
517
  """
519
518
 
520
- async def friend_list(self, next_token: str | None = None) -> PageResult[User]:
519
+ def friend_list(self, next_token: str | None = None) -> IterablePageResult[User]:
521
520
  """获取好友列表。返回一个 User 的分页列表。
522
521
 
523
522
  Args:
524
523
  next_token (str | None, optional): 分页令牌,默认为空
525
524
 
526
525
  Returns:
527
- PageResult[User]: `User` 的分页列表
526
+ IterablePageResult[User]: `User` 的分页列表
528
527
  """
529
528
 
530
529
  async def friend_approve(self, request_id: str, approve: bool, comment: str) -> None:
@@ -8,7 +8,7 @@ from launart import Service
8
8
  from ..config import Config as Config
9
9
 
10
10
  if TYPE_CHECKING:
11
- from .. import App
11
+ from .. import Account, App
12
12
 
13
13
  TConfig = TypeVar("TConfig", bound=Config)
14
14
 
@@ -21,7 +21,7 @@ class BaseNetwork(Generic[TConfig], Service):
21
21
  super().__init__()
22
22
  self.app = app
23
23
  self.config = config
24
- self.accounts = {}
24
+ self.accounts: dict[str, Account] = {}
25
25
  self.close_signal = asyncio.Event()
26
26
  self.sequence = -1
27
27
  self.proxy_urls = []
@@ -3,14 +3,16 @@ from __future__ import annotations
3
3
  import asyncio
4
4
 
5
5
  from aiohttp import web
6
+ from graia.amnesia.builtins.aiohttp import AiohttpClientService
6
7
  from launart.manager import Launart
7
8
  from loguru import logger
8
9
 
9
- from satori.model import Event, LoginStatus, MetaPayload, Opcode
10
+ from satori.model import Event, LoginStatus, Meta, MetaPayload, Opcode
10
11
 
11
12
  from ..account import Account
12
13
  from ..config import WebhookInfo as WebhookInfo
13
14
  from .base import BaseNetwork
15
+ from .util import validate_response
14
16
 
15
17
 
16
18
  class WebhookNetwork(BaseNetwork[WebhookInfo]):
@@ -35,6 +37,8 @@ class WebhookNetwork(BaseNetwork[WebhookInfo]):
35
37
  if op_code == Opcode.META:
36
38
  payload = MetaPayload.parse(body)
37
39
  self.proxy_urls = payload.proxy_urls
40
+ for account in self.accounts.values():
41
+ account.proxy_urls = payload.proxy_urls
38
42
  return web.Response()
39
43
  if op_code != Opcode.EVENT:
40
44
  return web.Response(status=202)
@@ -61,18 +65,6 @@ class WebhookNetwork(BaseNetwork[WebhookInfo]):
61
65
  else:
62
66
  logger.trace(f"Received event: {event}")
63
67
  self.sequence = event.sn
64
- login_sn = f"{event.login.sn}@{id(self)}"
65
- if login_sn in self.app.accounts:
66
- account = self.app.accounts[login_sn]
67
- account.connected.set()
68
- account.config = self.config
69
- else:
70
- account = Account(event.login, self.config, self.proxy_urls)
71
- logger.info(f"account registered: {account}")
72
- account.connected.set()
73
- self.app.accounts[login_sn] = account
74
- self.accounts[login_sn] = account
75
- await self.app.account_update(account, LoginStatus.ONLINE)
76
68
  asyncio.create_task(self.app.post(event, self))
77
69
  return web.Response()
78
70
 
@@ -94,12 +86,37 @@ class WebhookNetwork(BaseNetwork[WebhookInfo]):
94
86
  site = web.TCPSite(runner, self.config.host, self.config.port)
95
87
 
96
88
  async with self.stage("blocking"):
89
+ endpoint = self.config.api_base / "meta"
90
+ headers = {
91
+ "Content-Type": "application/json",
92
+ }
93
+ aio = Launart.current().get_component(AiohttpClientService)
94
+
95
+ async with aio.session.request(
96
+ "POST",
97
+ endpoint,
98
+ json={},
99
+ headers=headers,
100
+ ) as resp:
101
+ data = await validate_response(resp)
102
+ meta = Meta.parse(data)
103
+ self.proxy_urls = meta.proxy_urls
104
+ for login in meta.logins:
105
+ if not login.user:
106
+ continue
107
+ login_sn = f"{login.user.id}@{id(self)}"
108
+ account = Account(login, self.config, meta.proxy_urls, self.app.default_api_cls)
109
+ logger.info(f"account registered: {account}")
110
+ (account.connected.set() if login.status == LoginStatus.ONLINE else account.connected.clear())
111
+ self.app.accounts[login_sn] = account
112
+ self.accounts[login_sn] = account
113
+ await self.app.account_update(account, LoginStatus.ONLINE)
97
114
  await site.start()
98
115
  await manager.status.wait_for_sigexit()
99
116
  logger.info(f"{self.id} Webhook server exiting...")
100
117
  self.close_signal.set()
101
118
  for v in list(self.app.accounts.values()):
102
- if (identity := f"{v.sn}@{id(self)}") in self.accounts:
119
+ if (identity := f"{v.self_id}@{id(self)}") in self.accounts:
103
120
  v.connected.clear()
104
121
  await self.app.account_update(v, LoginStatus.OFFLINE)
105
122
  del self.app.accounts[identity]
@@ -10,7 +10,7 @@ from launart.manager import Launart
10
10
  from launart.utilles import any_completed
11
11
  from loguru import logger
12
12
 
13
- from satori.model import Event, Identify, LoginStatus, Opcode, Ready
13
+ from satori.model import Event, Identify, LoginStatus, MetaPayload, Opcode, Ready
14
14
 
15
15
  from ..account import Account
16
16
  from ..config import WebsocketsInfo as WebsocketsInfo
@@ -60,6 +60,11 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
60
60
  logger.trace(f"Received payload: {data}")
61
61
  if data["op"] == Opcode.EVENT:
62
62
  self.post_event(data["body"])
63
+ elif data["op"] == Opcode.META:
64
+ payload = MetaPayload.parse(data["body"])
65
+ self.proxy_urls = payload.proxy_urls
66
+ for account in self.accounts.values():
67
+ account.proxy_urls = payload.proxy_urls.copy()
63
68
  elif data["op"] > 5:
64
69
  logger.warning(f"Received unknown event: {data}")
65
70
  continue
@@ -103,7 +108,9 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
103
108
  ready = Ready.parse(data["body"])
104
109
  self.proxy_urls = ready.proxy_urls
105
110
  for login in ready.logins:
106
- login_sn = f"{login.sn}@{id(self)}"
111
+ if not login.user:
112
+ continue
113
+ login_sn = f"{login.user.id}@{id(self)}"
107
114
  if login_sn in self.app.accounts:
108
115
  account = self.app.accounts[login_sn]
109
116
  self.accounts[login_sn] = account
@@ -121,7 +128,7 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
121
128
  await self.app.account_update(account, LoginStatus.ONLINE)
122
129
  if not self.accounts:
123
130
  logger.warning(f"No account available for {self.config}")
124
- return False
131
+ # return False
125
132
  return True
126
133
 
127
134
  async def _heartbeat(self):
@@ -158,7 +165,7 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
158
165
  self.close_signal.set()
159
166
  self.connection = None
160
167
  for v in list(self.app.accounts.values()):
161
- if (identity := f"{v.sn}@{id(self)}") in self.accounts:
168
+ if (identity := f"{v.self_id}@{id(self)}") in self.accounts:
162
169
  v.connected.clear()
163
170
  await self.app.account_update(v, LoginStatus.OFFLINE)
164
171
  del self.app.accounts[identity]
@@ -15,7 +15,9 @@ from satori.model import (
15
15
  Direction,
16
16
  Event,
17
17
  Guild,
18
+ IterablePageResult,
18
19
  Login,
20
+ LoginPartial,
19
21
  Member,
20
22
  MessageObject,
21
23
  MessageReceipt,
@@ -278,7 +280,7 @@ class ApiProtocol:
278
280
  )
279
281
  return Channel.parse(res)
280
282
 
281
- async def channel_list(self, guild_id: str, next_token: str | None = None) -> PageResult[Channel]:
283
+ def channel_list(self, guild_id: str, next_token: str | None = None) -> IterablePageResult[Channel]:
282
284
  """获取群组中的全部频道。返回一个 Channel 的分页列表。
283
285
 
284
286
  Args:
@@ -286,13 +288,17 @@ class ApiProtocol:
286
288
  next_token (str | None, optional): 分页令牌,默认为空
287
289
 
288
290
  Returns:
289
- PageResult[Channel]: `Channel` 的分页列表
291
+ IterablePageResult[Channel]: `Channel` 的分页列表
290
292
  """
291
- res = await self.call_api(
292
- Api.CHANNEL_LIST,
293
- {"guild_id": guild_id, "next": next_token},
294
- )
295
- return PageResult.parse(res, Channel.parse)
293
+
294
+ async def _(token: str | None):
295
+ res = await self.call_api(
296
+ Api.CHANNEL_LIST,
297
+ {"guild_id": guild_id, "next": token},
298
+ )
299
+ return PageResult.parse(res, Channel.parse)
300
+
301
+ return IterablePageResult(_, next_token)
296
302
 
297
303
  async def channel_create(self, guild_id: str, data: Channel) -> Channel:
298
304
  """创建群组频道。返回一个 Channel 对象。
@@ -393,20 +399,24 @@ class ApiProtocol:
393
399
  )
394
400
  return Guild.parse(res)
395
401
 
396
- async def guild_list(self, next_token: str | None = None) -> PageResult[Guild]:
402
+ def guild_list(self, next_token: str | None = None) -> IterablePageResult[Guild]:
397
403
  """获取当前用户加入的全部群组。返回一个 Guild 的分页列表。
398
404
 
399
405
  Args:
400
406
  next_token (str | None, optional): 分页令牌,默认为空
401
407
 
402
408
  Returns:
403
- PageResult[Guild]: `Guild` 的分页列表
409
+ IterablePageResult[Guild]: `Guild` 的分页列表
404
410
  """
405
- res = await self.call_api(
406
- Api.GUILD_LIST,
407
- {"next": next_token},
408
- )
409
- return PageResult.parse(res, Guild.parse)
411
+
412
+ async def _(token: str | None):
413
+ res = await self.call_api(
414
+ Api.GUILD_LIST,
415
+ {"next": token},
416
+ )
417
+ return PageResult.parse(res, Guild.parse)
418
+
419
+ return IterablePageResult(_, next_token)
410
420
 
411
421
  async def guild_approve(self, request_id: str, approve: bool, comment: str) -> None:
412
422
  """处理来自群组的邀请。
@@ -424,7 +434,7 @@ class ApiProtocol:
424
434
  {"message_id": request_id, "approve": approve, "comment": comment},
425
435
  )
426
436
 
427
- async def guild_member_list(self, guild_id: str, next_token: str | None = None) -> PageResult[Member]:
437
+ def guild_member_list(self, guild_id: str, next_token: str | None = None) -> IterablePageResult[Member]:
428
438
  """获取群组成员列表。返回一个 Member 的分页列表。
429
439
 
430
440
  Args:
@@ -432,13 +442,17 @@ class ApiProtocol:
432
442
  next_token (str | None, optional): 分页令牌,默认为空
433
443
 
434
444
  Returns:
435
- PageResult[Member]: `Member` 的分页列表
445
+ IterablePageResult[Member]: `Member` 的分页列表
436
446
  """
437
- res = await self.call_api(
438
- Api.GUILD_MEMBER_LIST,
439
- {"guild_id": guild_id, "next": next_token},
440
- )
441
- return PageResult.parse(res, Member.parse)
447
+
448
+ async def _(token: str | None):
449
+ res = await self.call_api(
450
+ Api.GUILD_MEMBER_LIST,
451
+ {"guild_id": guild_id, "next": token},
452
+ )
453
+ return PageResult.parse(res, Member.parse)
454
+
455
+ return IterablePageResult(_, next_token)
442
456
 
443
457
  async def guild_member_get(self, guild_id: str, user_id: str) -> Member:
444
458
  """获取群成员信息。返回一个 `Member` 对象。
@@ -538,7 +552,7 @@ class ApiProtocol:
538
552
  {"guild_id": guild_id, "user_id": user_id, "role_id": role_id},
539
553
  )
540
554
 
541
- async def guild_role_list(self, guild_id: str, next_token: str | None = None) -> PageResult[Role]:
555
+ def guild_role_list(self, guild_id: str, next_token: str | None = None) -> IterablePageResult[Role]:
542
556
  """获取群组角色列表。返回一个 Role 的分页列表。
543
557
 
544
558
  Args:
@@ -546,13 +560,17 @@ class ApiProtocol:
546
560
  next_token (str | None, optional): 分页令牌,默认为空
547
561
 
548
562
  Returns:
549
- PageResult[Role]: `Role` 的分页列表
563
+ IterablePageResult[Role]: `Role` 的分页列表
550
564
  """
551
- res = await self.call_api(
552
- Api.GUILD_ROLE_LIST,
553
- {"guild_id": guild_id, "next": next_token},
554
- )
555
- return PageResult.parse(res, Role.parse)
565
+
566
+ async def _(token: str | None):
567
+ res = await self.call_api(
568
+ Api.GUILD_ROLE_LIST,
569
+ {"guild_id": guild_id, "next": token},
570
+ )
571
+ return PageResult.parse(res, Role.parse)
572
+
573
+ return IterablePageResult(_, next_token)
556
574
 
557
575
  async def guild_role_create(self, guild_id: str, role: Role) -> Role:
558
576
  """创建群组角色。返回一个 Role 对象。
@@ -662,9 +680,9 @@ class ApiProtocol:
662
680
  data,
663
681
  )
664
682
 
665
- async def reaction_list(
683
+ def reaction_list(
666
684
  self, channel_id: str, message_id: str, emoji: str, next_token: str | None = None
667
- ) -> PageResult[User]:
685
+ ) -> IterablePageResult[User]:
668
686
  """获取添加特定消息的特定表态的用户列表。返回一个 User 的分页列表。
669
687
 
670
688
  Args:
@@ -674,18 +692,22 @@ class ApiProtocol:
674
692
  next_token (str | None, optional): 分页令牌,默认为空
675
693
 
676
694
  Returns:
677
- PageResult[User]: `User` 的分页列表
695
+ IterablePageResult[User]: `User` 的分页列表
678
696
  """
679
- res = await self.call_api(
680
- Api.REACTION_LIST,
681
- {
682
- "channel_id": channel_id,
683
- "message_id": message_id,
684
- "emoji": emoji,
685
- "next": next_token,
686
- },
687
- )
688
- return PageResult.parse(res, User.parse)
697
+
698
+ async def _(token: str | None):
699
+ res = await self.call_api(
700
+ Api.REACTION_LIST,
701
+ {
702
+ "channel_id": channel_id,
703
+ "message_id": message_id,
704
+ "emoji": emoji,
705
+ "next": token,
706
+ },
707
+ )
708
+ return PageResult.parse(res, User.parse)
709
+
710
+ return IterablePageResult(_, next_token)
689
711
 
690
712
  async def login_get(self) -> Login:
691
713
  """获取当前登录信息。返回一个 `Login` 对象。
@@ -708,17 +730,21 @@ class ApiProtocol:
708
730
  res = await self.call_api(Api.USER_GET, {"user_id": user_id})
709
731
  return User.parse(res)
710
732
 
711
- async def friend_list(self, next_token: str | None = None) -> PageResult[User]:
733
+ def friend_list(self, next_token: str | None = None) -> IterablePageResult[User]:
712
734
  """获取好友列表。返回一个 User 的分页列表。
713
735
 
714
736
  Args:
715
737
  next_token (str | None, optional): 分页令牌,默认为空
716
738
 
717
739
  Returns:
718
- PageResult[User]: `User` 的分页列表
740
+ IterablePageResult[User]: `User` 的分页列表
719
741
  """
720
- res = await self.call_api(Api.FRIEND_LIST, {"next": next_token})
721
- return PageResult.parse(res, User.parse)
742
+
743
+ async def _(token: str | None):
744
+ res = await self.call_api(Api.FRIEND_LIST, {"next": token})
745
+ return PageResult.parse(res, User.parse)
746
+
747
+ return IterablePageResult(_, next_token)
722
748
 
723
749
  async def friend_approve(self, request_id: str, approve: bool, comment: str) -> None:
724
750
  """处理好友申请。
@@ -756,7 +782,7 @@ class ApiProtocol:
756
782
  return Meta.parse(res)
757
783
 
758
784
  @deprecated("Use `meta_get` instead")
759
- async def admin_login_list(self) -> list[Login]:
785
+ async def admin_login_list(self) -> list[LoginPartial]:
760
786
  """获取登录信息列表。返回一个 `Login` 对象构成的数组。
761
787
 
762
788
  Returns:
@@ -4,7 +4,7 @@ from satori.model import (
4
4
  Channel,
5
5
  Event,
6
6
  Guild,
7
- Login,
7
+ LoginPartial,
8
8
  Member,
9
9
  MessageObject,
10
10
  Role,
@@ -37,7 +37,7 @@ class GuildRoleEvent(GuildEvent):
37
37
 
38
38
 
39
39
  class LoginEvent(Event):
40
- login: Login
40
+ login: LoginPartial
41
41
 
42
42
 
43
43
  class ReactionEvent(Event):
@@ -1,4 +1,5 @@
1
1
  import mimetypes
2
+ from collections.abc import AsyncIterable, Awaitable
2
3
  from dataclasses import asdict, dataclass, field, fields
3
4
  from datetime import datetime
4
5
  from enum import IntEnum
@@ -145,11 +146,11 @@ class LoginStatus(IntEnum):
145
146
 
146
147
  @dataclass
147
148
  class Login(ModelBase):
148
- sn: str
149
+ sn: int
149
150
  status: LoginStatus
150
151
  adapter: str
151
- platform: Optional[str] = None
152
- user: Optional[User] = None
152
+ platform: str
153
+ user: User
153
154
  features: list[str] = field(default_factory=list)
154
155
 
155
156
  __converter__ = {"user": User.parse, "status": LoginStatus}
@@ -173,7 +174,7 @@ class Login(ModelBase):
173
174
  if "self_id" in raw and "user" not in raw:
174
175
  raw["user"] = {"id": raw["self_id"]}
175
176
  if "sn" not in raw:
176
- raw["sn"] = raw["user"]["id"]
177
+ raw["sn"] = 0
177
178
  if "adapter" not in raw:
178
179
  raw["adapter"] = "satori"
179
180
  if "status" not in raw:
@@ -182,11 +183,15 @@ class Login(ModelBase):
182
183
 
183
184
  @property
184
185
  def id(self) -> str:
185
- if not self.user:
186
- raise ValueError(f"Login {self.sn} has not complete yet")
187
186
  return self.user.id
188
187
 
189
188
 
189
+ @dataclass
190
+ class LoginPartial(Login):
191
+ platform: Optional[str] = None
192
+ user: Optional[User] = None
193
+
194
+
190
195
  @dataclass
191
196
  class ArgvInteraction(ModelBase):
192
197
  name: str
@@ -241,10 +246,10 @@ class Identify(ModelBase):
241
246
 
242
247
  @dataclass
243
248
  class Ready(ModelBase):
244
- logins: list[Login]
249
+ logins: list[LoginPartial]
245
250
  proxy_urls: list[str] = field(default_factory=list)
246
251
 
247
- __converter__ = {"logins": lambda raw: [Login.parse(login) for login in raw]}
252
+ __converter__ = {"logins": lambda raw: [LoginPartial.parse(login) for login in raw]}
248
253
 
249
254
  def dump(self):
250
255
  return asdict(self)
@@ -264,10 +269,10 @@ class MetaPayload(ModelBase):
264
269
  class Meta(ModelBase):
265
270
  """Meta 数据"""
266
271
 
267
- logins: list[Login]
272
+ logins: list[LoginPartial]
268
273
  proxy_urls: list[str] = field(default_factory=list)
269
274
 
270
- __converter__ = {"logins": lambda raw: [Login.parse(login) for login in raw]}
275
+ __converter__ = {"logins": lambda raw: [LoginPartial.parse(login) for login in raw]}
271
276
 
272
277
  def dump(self):
273
278
  return asdict(self)
@@ -414,7 +419,7 @@ class Event(ModelBase):
414
419
  raw["sn"] = raw["id"]
415
420
  if "platform" in raw and "self_id" in raw and "login" not in raw:
416
421
  raw["login"] = {
417
- "sn": raw["self_id"],
422
+ "sn": 0,
418
423
  "platform": raw["platform"],
419
424
  "user": {"id": raw["self_id"]},
420
425
  "status": LoginStatus.ONLINE,
@@ -501,6 +506,29 @@ class PageDequeResult(PageResult[T]):
501
506
  return res
502
507
 
503
508
 
509
+ class IterablePageResult(Generic[T], AsyncIterable[T], Awaitable[PageResult[T]]):
510
+ def __init__(
511
+ self, func: Callable[[Optional[str]], Awaitable[PageResult[T]]], initial_page: Optional[str] = None
512
+ ):
513
+ self.func = func
514
+ self.next_page = initial_page
515
+
516
+ def __await__(self):
517
+ return self.func(self.next_page).__await__()
518
+
519
+ def __aiter__(self):
520
+ async def _gen():
521
+ while True:
522
+ result = await self.func(self.next_page)
523
+ for item in result.data:
524
+ yield item
525
+ self.next_page = result.next
526
+ if not self.next_page:
527
+ break
528
+
529
+ return _gen()
530
+
531
+
504
532
  Direction: TypeAlias = Literal["before", "after", "around"]
505
533
  Order: TypeAlias = Literal["asc", "desc"]
506
534
 
@@ -1,12 +1,13 @@
1
1
  from abc import abstractmethod
2
2
  from collections.abc import AsyncIterator
3
- from typing import TYPE_CHECKING, Optional
3
+ from typing import TYPE_CHECKING, Optional, Union
4
4
 
5
5
  from launart import Service
6
6
  from starlette.responses import Response
7
7
  from starlette.routing import BaseRoute
8
8
 
9
- from ..model import Event, Login
9
+ from satori.model import Event, Login, LoginPartial
10
+
10
11
  from .model import Request
11
12
  from .route import RouterMixin
12
13
  from .utils import ctx
@@ -15,6 +16,9 @@ if TYPE_CHECKING:
15
16
  from . import Server
16
17
 
17
18
 
19
+ LoginType = Union[Login, LoginPartial]
20
+
21
+
18
22
  class Adapter(Service, RouterMixin):
19
23
  server: "Server"
20
24
 
@@ -39,7 +43,7 @@ class Adapter(Service, RouterMixin):
39
43
  return Response(await resp.read())
40
44
 
41
45
  @abstractmethod
42
- async def get_logins(self) -> list[Login]: ...
46
+ async def get_logins(self) -> list[LoginType]: ...
43
47
 
44
48
  def __init__(self):
45
49
  super().__init__()
File without changes
File without changes