satori-python 0.15.2__tar.gz → 0.16.1__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.15.2 → satori_python-0.16.1}/PKG-INFO +3 -3
  2. {satori_python-0.15.2 → satori_python-0.16.1}/README.md +2 -2
  3. {satori_python-0.15.2 → satori_python-0.16.1}/pyproject.toml +1 -1
  4. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/__init__.py +1 -3
  5. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/client/__init__.py +17 -18
  6. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/client/account.py +20 -17
  7. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/client/account.pyi +33 -11
  8. satori_python-0.16.1/src/satori/client/network/base.py +35 -0
  9. satori_python-0.16.1/src/satori/client/network/webhook.py +111 -0
  10. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/client/network/websocket.py +44 -35
  11. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/client/protocol.py +41 -13
  12. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/model.py +100 -69
  13. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/server/__init__.py +148 -75
  14. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/server/adapter.py +8 -6
  15. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/server/model.py +14 -5
  16. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/server/route.py +2 -2
  17. satori_python-0.15.2/src/satori/client/network/base.py +0 -56
  18. satori_python-0.15.2/src/satori/client/network/webhook.py +0 -131
  19. {satori_python-0.15.2 → satori_python-0.16.1}/LICENSE +0 -0
  20. {satori_python-0.15.2/src/satori → satori_python-0.16.1/src/satori/client}/config.py +0 -0
  21. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/client/network/__init__.py +0 -0
  22. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/client/network/util.py +0 -0
  23. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/const.py +0 -0
  24. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/element.py +0 -0
  25. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/event.py +0 -0
  26. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/exception.py +0 -0
  27. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/parser.py +0 -0
  28. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/server/conection.py +0 -0
  29. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/server/formdata.py +0 -0
  30. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/server/utils.py +0 -0
  31. {satori_python-0.15.2 → satori_python-0.16.1}/src/satori/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: satori-python
3
- Version: 0.15.2
3
+ Version: 0.16.1
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>
@@ -80,9 +80,9 @@ pip install satori-python-server
80
80
  客户端:
81
81
 
82
82
  ```python
83
- from satori import EventType, WebsocketsInfo
83
+ from satori import EventType
84
84
  from satori.event import MessageEvent
85
- from satori.client import Account, App
85
+ from satori.client import Account, App, WebsocketsInfo
86
86
 
87
87
  app = App(WebsocketsInfo(port=5140))
88
88
 
@@ -50,9 +50,9 @@ pip install satori-python-server
50
50
  客户端:
51
51
 
52
52
  ```python
53
- from satori import EventType, WebsocketsInfo
53
+ from satori import EventType
54
54
  from satori.event import MessageEvent
55
- from satori.client import Account, App
55
+ from satori.client import Account, App, WebsocketsInfo
56
56
 
57
57
  app = App(WebsocketsInfo(port=5140))
58
58
 
@@ -29,7 +29,7 @@ classifiers = [
29
29
  "Programming Language :: Python :: 3.12",
30
30
  "Operating System :: OS Independent",
31
31
  ]
32
- version = "0.15.2"
32
+ version = "0.16.1"
33
33
 
34
34
  [project.license]
35
35
  text = "MIT"
@@ -1,5 +1,3 @@
1
- from .config import WebhookInfo as WebhookInfo
2
- from .config import WebsocketsInfo as WebsocketsInfo
3
1
  from .const import Api as Api
4
2
  from .const import EventType as EventType
5
3
  from .element import At as At
@@ -43,4 +41,4 @@ from .model import Role as Role
43
41
  from .model import Upload as Upload
44
42
  from .model import User as User
45
43
 
46
- __version__ = "0.15.2"
44
+ __version__ = "0.16.1"
@@ -14,12 +14,14 @@ from launart import Launart, Service, any_completed
14
14
  from loguru import logger
15
15
 
16
16
  from satori import event as events
17
- from satori.config import Config, WebhookInfo, WebsocketsInfo
18
17
  from satori.const import EventType
19
18
  from satori.model import Event, LoginStatus
20
19
 
21
20
  from .account import Account as Account
22
21
  from .account import ApiInfo as ApiInfo
22
+ from .config import Config
23
+ from .config import WebhookInfo as WebhookInfo
24
+ from .config import WebsocketsInfo as WebsocketsInfo
23
25
  from .network.base import BaseNetwork as BaseNetwork
24
26
  from .network.webhook import WebhookNetwork
25
27
  from .network.websocket import WsNetwork
@@ -187,16 +189,15 @@ class App(Service):
187
189
  async def post(self, event: Event, conn: BaseNetwork):
188
190
  if not self.event_callbacks:
189
191
  return
190
- identity = f"{event.platform_}/{event.self_id_}"
191
- if identity not in self.accounts:
192
+ login_sn = f"{event.login.sn}@{id(conn)}"
193
+ if login_sn not in self.accounts:
192
194
  if event.type == EventType.LOGIN_ADDED:
193
195
  if TYPE_CHECKING:
194
196
  assert isinstance(event, events.LoginEvent)
195
197
  account = Account(
196
- event.platform_,
197
- event.self_id_,
198
198
  event.login,
199
199
  conn.config,
200
+ conn.proxy_urls,
200
201
  self.default_api_cls,
201
202
  )
202
203
  logger.info(f"account added: {account}")
@@ -205,26 +206,23 @@ class App(Service):
205
206
  if event.login.status == LoginStatus.ONLINE
206
207
  else account.connected.clear()
207
208
  )
208
- self.accounts[identity] = account
209
- conn.accounts[identity] = account
210
- await self.account_update(account, LoginStatus.CONNECT)
211
- await self.account_update(account, LoginStatus.ONLINE)
209
+ self.accounts[login_sn] = account
210
+ conn.accounts[login_sn] = account
211
+ await self.account_update(account, event.login.status)
212
212
  elif event.type == EventType.LOGIN_UPDATED:
213
213
  if TYPE_CHECKING:
214
214
  assert isinstance(event, events.LoginEvent)
215
215
  if event.login.status == LoginStatus.ONLINE:
216
216
  account = Account(
217
- event.platform_,
218
- event.self_id_,
219
217
  event.login,
220
218
  conn.config,
219
+ conn.proxy_urls,
221
220
  self.default_api_cls,
222
221
  )
223
222
  logger.info(f"account added: {account}")
224
223
  account.connected.set()
225
- self.accounts[identity] = account
226
- conn.accounts[identity] = account
227
- await self.account_update(account, LoginStatus.CONNECT)
224
+ self.accounts[login_sn] = account
225
+ conn.accounts[login_sn] = account
228
226
  await self.account_update(account, LoginStatus.ONLINE)
229
227
  else:
230
228
  logger.warning(f"Received event for unknown account: {event}")
@@ -233,7 +231,7 @@ class App(Service):
233
231
  logger.warning(f"Received event for unknown account: {event}")
234
232
  return
235
233
  else:
236
- account = self.accounts[identity]
234
+ account = self.accounts[login_sn]
237
235
  if event.type == EventType.LOGIN_UPDATED:
238
236
  if TYPE_CHECKING:
239
237
  assert isinstance(event, events.LoginEvent)
@@ -243,6 +241,7 @@ class App(Service):
243
241
  if event.login.status in (LoginStatus.ONLINE, LoginStatus.CONNECT)
244
242
  else account.connected.clear()
245
243
  )
244
+ await self.account_update(account, event.login.status)
246
245
 
247
246
  await asyncio.gather(*(callback(account, event) for callback in self.event_callbacks))
248
247
 
@@ -251,9 +250,9 @@ class App(Service):
251
250
  assert isinstance(event, events.LoginEvent)
252
251
  logger.info(f"account removed: {account}")
253
252
  account.connected.clear()
254
- await self.account_update(account, LoginStatus.DISCONNECT)
255
- del self.accounts[identity]
256
- del conn.accounts[identity]
253
+ await self.account_update(account, LoginStatus.OFFLINE)
254
+ del self.accounts[login_sn]
255
+ del conn.accounts[login_sn]
257
256
 
258
257
  async def launch(self, manager: Launart):
259
258
  for conn in self.connections:
@@ -6,7 +6,7 @@ from typing import Generic, TypeVar
6
6
 
7
7
  from yarl import URL
8
8
 
9
- from satori.model import LoginType
9
+ from satori.model import Login
10
10
 
11
11
  from .protocol import ApiProtocol
12
12
 
@@ -33,44 +33,47 @@ class ApiInfo:
33
33
  class Account(Generic[TP]):
34
34
  def __init__(
35
35
  self,
36
- platform: str,
37
- self_id: str,
38
- self_info: LoginType,
36
+ login: Login,
39
37
  config: ApiInfo,
38
+ proxy_urls: list[str],
40
39
  protocol_cls: type[TP] = ApiProtocol,
41
40
  ):
42
- self.platform = platform
43
- self.self_id = self_id
44
- self.self_info = self_info
41
+ self.sn = login.sn
42
+ self.adapter = login.adapter
43
+ self.self_info = login
45
44
  self.config = config
45
+ self.proxy_urls = proxy_urls
46
46
  self.protocol = protocol_cls(self) # type: ignore
47
47
  self.connected = asyncio.Event()
48
48
 
49
+ @property
50
+ def platform(self):
51
+ return self.self_info.platform or "satori"
52
+
53
+ @property
54
+ def self_id(self):
55
+ return self.self_info.id
56
+
49
57
  def custom(
50
58
  self, config: ApiInfo | None = None, protocol_cls: type[TP1] = ApiProtocol, **kwargs
51
59
  ) -> "Account[TP1]":
52
60
  return Account(
53
- self.platform,
54
- self.self_id,
55
61
  self.self_info,
56
62
  config or (ApiInfo(**kwargs) if kwargs else self.config),
63
+ self.proxy_urls,
57
64
  protocol_cls, # type: ignore
58
65
  )
59
66
 
60
- @property
61
- def identity(self):
62
- return f"{self.platform}/{self.self_id}"
63
-
64
67
  def ensure_url(self, url: str) -> URL:
65
68
  """确定链接形式。
66
69
 
67
70
  若链接符合以下条件之一,则返回链接的代理形式 ({host}/{path}/{version}/proxy/{url}):
68
- - 链接以 "upload://" 开头
69
- - 链接开头出现在 self_info.proxy_urls 中的某一项
71
+ - 链接以 "internal:" 开头
72
+ - 链接开头出现在 proxy_urls 中的某一项
70
73
  """
71
- if url.startswith("upload"):
74
+ if url.startswith("internal:"):
72
75
  return self.config.api_base / "proxy" / url.lstrip("/")
73
- for proxy_url in self.self_info.proxy_urls:
76
+ for proxy_url in self.proxy_urls:
74
77
  if url.startswith(proxy_url):
75
78
  return self.config.api_base / "proxy" / url.lstrip("/")
76
79
  return URL(url)
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  from collections.abc import Iterable
3
3
  from typing import Any, Generic, Protocol, TypeVar, overload
4
+ from typing_extensions import deprecated
4
5
 
5
6
  from yarl import URL
6
7
 
@@ -10,10 +11,11 @@ from satori.model import (
10
11
  Direction,
11
12
  Event,
12
13
  Guild,
13
- LoginType,
14
+ Login,
14
15
  Member,
15
16
  MessageObject,
16
17
  MessageReceipt,
18
+ Meta,
17
19
  Order,
18
20
  PageDequeResult,
19
21
  PageResult,
@@ -39,23 +41,25 @@ class ApiInfo(Api):
39
41
  ): ...
40
42
 
41
43
  class Account(Generic[TP]):
42
- platform: str
43
- self_id: str
44
- self_info: LoginType
44
+ sn: str
45
+ adapter: str
46
+ self_info: Login
47
+ proxy_urls: list[str]
45
48
  config: Api
46
49
  protocol: TP
47
50
  connected: asyncio.Event
48
51
 
49
52
  def __init__(
50
53
  self,
51
- platform: str,
52
- self_id: str,
53
- self_info: LoginType,
54
+ login: Login,
54
55
  config: Api,
56
+ proxy_urls: list[str],
55
57
  protocol_cls: type[TP] = ApiProtocol,
56
58
  ): ...
57
59
  @property
58
- def identity(self) -> str: ...
60
+ def platform(self) -> str: ...
61
+ @property
62
+ def self_id(self) -> str: ...
59
63
  @overload
60
64
  def custom(self, config: Api, protocol_cls: type[TP1] = ApiProtocol) -> Account[TP1]: ...
61
65
  @overload
@@ -496,7 +500,7 @@ class Account(Generic[TP]):
496
500
  PageResult[User]: `User` 的分页列表
497
501
  """
498
502
 
499
- async def login_get(self) -> LoginType:
503
+ async def login_get(self) -> Login:
500
504
  """获取当前登录信息。返回一个 `Login` 对象。
501
505
 
502
506
  Returns:
@@ -535,21 +539,36 @@ class Account(Generic[TP]):
535
539
  None: 该方法无返回值
536
540
  """
537
541
 
538
- async def internal(self, action: str, **kwargs) -> Any:
542
+ async def internal(self, action: str, method: str = "POST", **kwargs) -> Any:
539
543
  """内部接口调用。
540
544
 
541
545
  Args:
542
546
  action (str): 内部接口名称
547
+ method (str, optional): 请求方法,默认为 POST
543
548
  **kwargs: 参数
544
549
  """
545
550
 
546
- async def admin_login_list(self) -> list[LoginType]:
551
+ async def meta_get(self) -> Meta:
552
+ """获取元信息。返回一个 `Meta` 对象。
553
+
554
+ Returns:
555
+ Meta: `Meta` 对象
556
+ """
557
+
558
+ @deprecated("Use `meta_get` instead")
559
+ async def admin_login_list(self) -> list[Login]:
547
560
  """获取登录信息列表。返回一个 `Login` 对象构成的数组。
548
561
 
549
562
  Returns:
550
563
  list[Login]: `Login` 对象构成的数组
551
564
  """
552
565
 
566
+ async def webhook_create(self, url: str, token: str | None = None):
567
+ """创建 Webhook。"""
568
+
569
+ async def webhook_delete(self, url: str):
570
+ """删除 Webhook。"""
571
+
553
572
  @overload
554
573
  async def upload_create(self, *uploads: Upload) -> list[str]: ...
555
574
  @overload
@@ -564,3 +583,6 @@ class Account(Generic[TP]):
564
583
 
565
584
  async def download(self, url: str):
566
585
  """访问内部链接。"""
586
+
587
+ async def request_internal(self, url: str, method: str = "GET", **kwargs) -> dict:
588
+ """访问内部链接。"""
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import TYPE_CHECKING, Generic, TypeVar
5
+
6
+ from launart import Service
7
+
8
+ from ..config import Config as Config
9
+
10
+ if TYPE_CHECKING:
11
+ from .. import App
12
+
13
+ TConfig = TypeVar("TConfig", bound=Config)
14
+
15
+
16
+ class BaseNetwork(Generic[TConfig], Service):
17
+ close_signal: asyncio.Event
18
+ sequence: int
19
+
20
+ def __init__(self, app: App, config: TConfig):
21
+ super().__init__()
22
+ self.app = app
23
+ self.config = config
24
+ self.accounts = {}
25
+ self.close_signal = asyncio.Event()
26
+ self.sequence = -1
27
+ self.proxy_urls = []
28
+
29
+ async def wait_for_available(self): ...
30
+
31
+ @property
32
+ def alive(self) -> bool: ...
33
+
34
+ async def connection_closed(self):
35
+ self.close_signal.set()
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ from aiohttp import web
6
+ from launart.manager import Launart
7
+ from loguru import logger
8
+
9
+ from satori.model import Event, LoginStatus, MetaPayload, Opcode
10
+
11
+ from ..account import Account
12
+ from ..config import WebhookInfo as WebhookInfo
13
+ from .base import BaseNetwork
14
+
15
+
16
+ class WebhookNetwork(BaseNetwork[WebhookInfo]):
17
+ required: set[str] = set()
18
+ stages: set[str] = {"preparing", "blocking", "cleanup"}
19
+ wsgi: web.Application | None = None
20
+
21
+ @property
22
+ def id(self):
23
+ return f"satori/network/webhook/{self.config.identity}#{id(self)}"
24
+
25
+ async def handle_request(self, req: web.Request):
26
+ header = req.headers
27
+ auth = header["Authorization"]
28
+ if not auth.startswith("Bearer"):
29
+ return web.Response(status=401)
30
+ token = auth.split(" ", 1)[1]
31
+ if self.config.token and self.config.token != token:
32
+ return web.Response(status=401)
33
+ op_code = int(header.get("Satori-OpCode", "0"))
34
+ body = await req.json()
35
+ if op_code == Opcode.META:
36
+ payload = MetaPayload.parse(body)
37
+ self.proxy_urls = payload.proxy_urls
38
+ return web.Response()
39
+ if op_code != Opcode.EVENT:
40
+ return web.Response(status=202)
41
+ # if "X-Platform" in header and "X-Self-ID" in header:
42
+ # platform = header["X-Platform"]
43
+ # self_id = header["X-Self-ID"]
44
+ # elif "Satori-Platform" in header and "Satori-User-ID" in header:
45
+ # platform = header["Satori-Platform"]
46
+ # self_id = header["Satori-User-ID"]
47
+ # else:
48
+ # return web.Response(status=400)
49
+ try:
50
+ event = Event.parse(body)
51
+ except Exception as e:
52
+ if (
53
+ "self_id" in body
54
+ or ("login" in body and "self_id" in body["login"])
55
+ or ("login" in body and "user" in body["login"] and "self_id" in body["login"]["user"])
56
+ ):
57
+ logger.warning(f"Failed to parse event: {body}\nCaused by {e!r}")
58
+ else:
59
+ logger.trace(f"Failed to parse event: {body}\nCaused by {e!r}")
60
+ return web.Response(status=500, reason=f"Failed to parse event caused by {e!r}")
61
+ else:
62
+ logger.trace(f"Received event: {event}")
63
+ self.sequence = event.sn
64
+ login_sn = f"{event.login.sn}@{id(self)}"
65
+ if login_sn in self.app.accounts:
66
+ account = self.app.accounts[login_sn]
67
+ account.connected.set()
68
+ account.config = self.config
69
+ else:
70
+ account = Account(event.login, self.config, self.proxy_urls)
71
+ logger.info(f"account registered: {account}")
72
+ account.connected.set()
73
+ self.app.accounts[login_sn] = account
74
+ self.accounts[login_sn] = account
75
+ await self.app.account_update(account, LoginStatus.ONLINE)
76
+ asyncio.create_task(self.app.post(event, self))
77
+ return web.Response()
78
+
79
+ @property
80
+ def alive(self):
81
+ return self.wsgi is not None
82
+
83
+ async def wait_for_available(self):
84
+ await self.status.wait_for_available()
85
+
86
+ async def launch(self, manager: Launart):
87
+ async with self.stage("preparing"):
88
+ logger.info(f"starting server on {self.config.identity}")
89
+ self.wsgi = web.Application(logger=logger) # type: ignore
90
+ self.wsgi.router.freeze = lambda: None # monkey patch
91
+ self.wsgi.router.add_post(self.config.path, self.handle_request)
92
+ runner = web.AppRunner(self.wsgi)
93
+ await runner.setup()
94
+ site = web.TCPSite(runner, self.config.host, self.config.port)
95
+
96
+ async with self.stage("blocking"):
97
+ await site.start()
98
+ await manager.status.wait_for_sigexit()
99
+ logger.info(f"{self.id} Webhook server exiting...")
100
+ self.close_signal.set()
101
+ for v in list(self.app.accounts.values()):
102
+ if (identity := f"{v.sn}@{id(self)}") in self.accounts:
103
+ v.connected.clear()
104
+ await self.app.account_update(v, LoginStatus.OFFLINE)
105
+ del self.app.accounts[identity]
106
+ del self.accounts[identity]
107
+
108
+ async with self.stage("cleanup"):
109
+ await site.stop()
110
+ await self.wsgi.shutdown()
111
+ await self.wsgi.cleanup()
@@ -10,10 +10,10 @@ from launart.manager import Launart
10
10
  from launart.utilles import any_completed
11
11
  from loguru import logger
12
12
 
13
- from satori.config import WebsocketsInfo as WebsocketsInfo
14
- from satori.model import Login, LoginPreview, LoginStatus, Opcode
13
+ from satori.model import Event, Identify, LoginStatus, Opcode, Ready
15
14
 
16
15
  from ..account import Account
16
+ from ..config import WebsocketsInfo as WebsocketsInfo
17
17
  from .base import BaseNetwork
18
18
 
19
19
 
@@ -27,6 +27,26 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
27
27
 
28
28
  connection: aiohttp.ClientWebSocketResponse | None = None
29
29
 
30
+ def post_event(self, body: dict):
31
+ async def event_parse_task(raw: dict):
32
+ try:
33
+ event = Event.parse(raw)
34
+ except Exception as e:
35
+ if (
36
+ "self_id" in raw
37
+ or ("login" in raw and "self_id" in raw["login"])
38
+ or ("login" in raw and "user" in raw["login"] and "self_id" in raw["login"]["user"])
39
+ ):
40
+ logger.warning(f"Failed to parse event: {raw}\nCaused by {e!r}")
41
+ else:
42
+ logger.trace(f"Failed to parse event: {raw}\nCaused by {e!r}")
43
+ else:
44
+ logger.trace(f"Received event: {event}")
45
+ self.sequence = event.sn
46
+ await self.app.post(event, self)
47
+
48
+ return asyncio.create_task(event_parse_task(body))
49
+
30
50
  async def message_receive(self):
31
51
  if self.connection is None:
32
52
  raise RuntimeError("connection is not established")
@@ -40,7 +60,7 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
40
60
  logger.trace(f"Received payload: {data}")
41
61
  if data["op"] == Opcode.EVENT:
42
62
  self.post_event(data["body"])
43
- elif data["op"] > 4:
63
+ elif data["op"] > 5:
44
64
  logger.warning(f"Received unknown event: {data}")
45
65
  continue
46
66
  else:
@@ -63,18 +83,13 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
63
83
  """鉴权连接"""
64
84
  if not self.connection:
65
85
  raise RuntimeError("connection is not established")
66
- payload = {
67
- "op": Opcode.IDENTIFY,
68
- "body": {
69
- "token": self.config.token,
70
- },
71
- }
86
+ payload = Identify(self.config.token)
72
87
  if self.sequence > -1:
73
- payload["body"]["sequence"] = self.sequence
88
+ payload.sn = self.sequence
74
89
  try:
75
- await self.send(payload)
90
+ await self.send({"op": Opcode.IDENTIFY.value, "body": payload.dump()})
76
91
  except Exception as e:
77
- logger.error(f"Error while sending IDENTIFY event: {e}")
92
+ logger.error(f"Error while sending IDENTIFY event: {e!r}")
78
93
  return False
79
94
 
80
95
  resp = await self.connection.receive()
@@ -85,32 +100,24 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
85
100
  if data["op"] != Opcode.READY:
86
101
  logger.error(f"Received unexpected payload: {data}")
87
102
  return False
88
- for login in data["body"]["logins"]:
89
- obj = LoginPreview.parse(login) if "user" in login else Login.parse(login)
90
- if obj.id is None:
91
- continue
92
- platform = obj.platform or "satori"
93
- self_id = obj.id
94
- identity = f"{platform}/{self_id}"
95
- if identity in self.app.accounts:
96
- account = self.app.accounts[identity]
97
- self.accounts[identity] = account
98
- if not obj.status or obj.status == LoginStatus.ONLINE:
103
+ ready = Ready.parse(data["body"])
104
+ self.proxy_urls = ready.proxy_urls
105
+ for login in ready.logins:
106
+ login_sn = f"{login.sn}@{id(self)}"
107
+ if login_sn in self.app.accounts:
108
+ account = self.app.accounts[login_sn]
109
+ self.accounts[login_sn] = account
110
+ if login.status == LoginStatus.ONLINE:
99
111
  account.connected.set()
100
112
  else:
101
113
  account.connected.clear()
102
114
  account.config = self.config
103
115
  else:
104
- account = Account(platform, self_id, obj, self.config, self.app.default_api_cls)
116
+ account = Account(login, self.config, ready.proxy_urls, self.app.default_api_cls)
105
117
  logger.info(f"account registered: {account}")
106
- (
107
- account.connected.set()
108
- if not obj.status or obj.status == LoginStatus.ONLINE
109
- else account.connected.clear()
110
- )
111
- self.app.accounts[identity] = account
112
- self.accounts[identity] = account
113
- await self.app.account_update(account, LoginStatus.CONNECT)
118
+ (account.connected.set() if login.status == LoginStatus.ONLINE else account.connected.clear())
119
+ self.app.accounts[login_sn] = account
120
+ self.accounts[login_sn] = account
114
121
  await self.app.account_update(account, LoginStatus.ONLINE)
115
122
  if not self.accounts:
116
123
  logger.warning(f"No account available for {self.config}")
@@ -151,9 +158,11 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
151
158
  self.close_signal.set()
152
159
  self.connection = None
153
160
  for v in list(self.app.accounts.values()):
154
- if v.identity in self.accounts:
161
+ if (identity := f"{v.sn}@{id(self)}") in self.accounts:
155
162
  v.connected.clear()
156
- del self.accounts[v.identity]
163
+ await self.app.account_update(v, LoginStatus.OFFLINE)
164
+ del self.app.accounts[identity]
165
+ del self.accounts[identity]
157
166
  return
158
167
  if close_task in done:
159
168
  receiver_task.cancel()
@@ -162,7 +171,7 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
162
171
  logger.debug(f"Unregistering satori account {k}...")
163
172
  account = self.app.accounts[k]
164
173
  account.connected.clear()
165
- await self.app.account_update(account, LoginStatus.DISCONNECT)
174
+ await self.app.account_update(account, LoginStatus.RECONNECT)
166
175
  self.accounts.clear()
167
176
  await asyncio.sleep(5)
168
177
  logger.info(f"{self} Reconnecting...")