satori-python-adapter-milky 0.4.2__tar.gz → 0.5.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 (18) hide show
  1. {satori_python_adapter_milky-0.4.2 → satori_python_adapter_milky-0.5.1}/.mina/adapter_milky.toml +2 -2
  2. {satori_python_adapter_milky-0.4.2 → satori_python_adapter_milky-0.5.1}/PKG-INFO +2 -2
  3. {satori_python_adapter_milky-0.4.2 → satori_python_adapter_milky-0.5.1}/pyproject.toml +2 -2
  4. {satori_python_adapter_milky-0.4.2 → satori_python_adapter_milky-0.5.1}/src/satori/adapters/milky/__init__.py +2 -2
  5. {satori_python_adapter_milky-0.4.2 → satori_python_adapter_milky-0.5.1}/src/satori/adapters/milky/base.py +37 -15
  6. {satori_python_adapter_milky-0.4.2 → satori_python_adapter_milky-0.5.1}/src/satori/adapters/milky/events/base.py +1 -1
  7. {satori_python_adapter_milky-0.4.2 → satori_python_adapter_milky-0.5.1}/src/satori/adapters/milky/main.py +5 -10
  8. {satori_python_adapter_milky-0.4.2 → satori_python_adapter_milky-0.5.1}/src/satori/adapters/milky/message.py +46 -14
  9. {satori_python_adapter_milky-0.4.2 → satori_python_adapter_milky-0.5.1}/src/satori/adapters/milky/sse.py +5 -10
  10. {satori_python_adapter_milky-0.4.2 → satori_python_adapter_milky-0.5.1}/src/satori/adapters/milky/utils.py +2 -0
  11. {satori_python_adapter_milky-0.4.2 → satori_python_adapter_milky-0.5.1}/src/satori/adapters/milky/webhook.py +17 -13
  12. {satori_python_adapter_milky-0.4.2 → satori_python_adapter_milky-0.5.1}/LICENSE +0 -0
  13. {satori_python_adapter_milky-0.4.2 → satori_python_adapter_milky-0.5.1}/README.md +0 -0
  14. {satori_python_adapter_milky-0.4.2 → satori_python_adapter_milky-0.5.1}/src/satori/adapters/milky/api.py +0 -0
  15. {satori_python_adapter_milky-0.4.2 → satori_python_adapter_milky-0.5.1}/src/satori/adapters/milky/events/__init__.py +0 -0
  16. {satori_python_adapter_milky-0.4.2 → satori_python_adapter_milky-0.5.1}/src/satori/adapters/milky/events/group.py +0 -0
  17. {satori_python_adapter_milky-0.4.2 → satori_python_adapter_milky-0.5.1}/src/satori/adapters/milky/events/message.py +0 -0
  18. {satori_python_adapter_milky-0.4.2 → satori_python_adapter_milky-0.5.1}/src/satori/adapters/milky/events/request.py +0 -0
@@ -1,9 +1,9 @@
1
1
  includes = ["src/satori/adapters/milky"]
2
- raw-dependencies = ["satori-python-server<1.4.0,>= 1.3.3"]
2
+ raw-dependencies = ["satori-python-server<1.4.0,>= 1.3.7"]
3
3
 
4
4
  [project]
5
5
  name = "satori-python-adapter-milky"
6
- version = "0.4.2"
6
+ version = "0.5.1"
7
7
  authors = [
8
8
  {name = "RF-Tar-Railt", email = "rf_tar_railt@qq.com"}
9
9
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: satori-python-adapter-milky
3
- Version: 0.4.2
3
+ Version: 0.5.1
4
4
  Summary: Satori Protocol SDK for python, adapter for Milky
5
5
  Home-page: https://github.com/RF-Tar-Railt/satori-python
6
6
  Author-Email: RF-Tar-Railt <rf_tar_railt@qq.com>
@@ -17,7 +17,7 @@ Classifier: Operating System :: OS Independent
17
17
  Project-URL: Homepage, https://github.com/RF-Tar-Railt/satori-python
18
18
  Project-URL: Repository, https://github.com/RF-Tar-Railt/satori-python
19
19
  Requires-Python: <4.0,>=3.10
20
- Requires-Dist: satori-python-server<1.4.0,>=1.3.3
20
+ Requires-Dist: satori-python-server<1.4.0,>=1.3.7
21
21
  Description-Content-Type: text/markdown
22
22
 
23
23
  # satori-python
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "satori-python-adapter-milky"
3
- version = "0.4.2"
3
+ version = "0.5.1"
4
4
  authors = [
5
5
  { name = "RF-Tar-Railt", email = "rf_tar_railt@qq.com" },
6
6
  ]
7
7
  dependencies = [
8
- "satori-python-server<1.4.0,>= 1.3.3",
8
+ "satori-python-server<1.4.0,>= 1.3.7",
9
9
  ]
10
10
  description = "Satori Protocol SDK for python, adapter for Milky"
11
11
  readme = "README.md"
@@ -1,5 +1,5 @@
1
+ from .base import MilkyBaseConfig as MilkyBaseConfig
1
2
  from .main import MilkyWebsocketAdapter as MilkyWebsocketAdapter
2
3
  from .sse import MilkySSEAdapter as MilkySSEAdapter
3
4
  from .webhook import MilkyWebhookAdapter as MilkyWebhookAdapter
4
-
5
- __all__ = ["MilkyWebsocketAdapter", "MilkySSEAdapter", "MilkyWebhookAdapter"]
5
+ from .webhook import MilkyWebhookConfig as MilkyWebhookConfig
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from abc import ABC
4
+ from dataclasses import dataclass, field
4
5
  from datetime import datetime
6
+ from typing_extensions import TypeVar
5
7
 
6
8
  import aiohttp
7
9
  from launart.status import Phase
@@ -23,26 +25,42 @@ from .utils import decode_login_user
23
25
  DEFAULT_FEATURES = ["guild.plain", "reaction"]
24
26
 
25
27
 
26
- class MilkyBaseAdapter(BaseAdapter, ABC):
28
+ @dataclass
29
+ class MilkyBaseConfig:
30
+ endpoint: str
31
+ """连接 Milky 协议端的路径"""
32
+ token: str | None = None
33
+ """Milky 协议的访问令牌, 默认为空"""
34
+ token_in_query: bool = False
35
+ """是否将 token 放在查询参数中, 默认为 False"""
36
+ headers: dict[str, str] = field(default_factory=dict)
37
+ """连接时使用的自定义请求头, 默认为空字典"""
38
+ fetch_forward_message: bool = False
39
+ """是否在事件中获取转发消息, 默认为 False"""
40
+
41
+
42
+ TMilkyBaseConfig = TypeVar("TMilkyBaseConfig", bound=MilkyBaseConfig, default=MilkyBaseConfig)
43
+
44
+
45
+ class MilkyBaseAdapter(BaseAdapter[TMilkyBaseConfig], ABC):
27
46
  """Base adapter for Milky protocol with common functionality."""
28
47
 
29
48
  session: aiohttp.ClientSession | None
30
49
 
31
50
  def __init__(
32
51
  self,
33
- endpoint: str | URL,
34
- *,
35
- token: str | None = None,
36
- headers: dict[str, str] | None = None,
52
+ config: TMilkyBaseConfig,
37
53
  ):
38
54
  super().__init__()
39
- self.base_url = URL(str(endpoint))
55
+ self.config = config
56
+ self.base_url = URL(self.config.endpoint)
40
57
  base_path = self.base_url.path.rstrip("/")
41
58
  self.api_base = self.base_url.with_path(f"{base_path}/api")
42
- self.token = token
43
- self.headers = headers.copy() if headers else {}
59
+ self.token = config.token
60
+ self.headers = config.headers.copy()
44
61
  self.session = None
45
62
  self.logins: dict[str, Login] = {}
63
+ self.fetch_forward_message = config.fetch_forward_message
46
64
  self.features: list[str] = list(DEFAULT_FEATURES)
47
65
  apply(self, lambda _: self, self._get_login)
48
66
 
@@ -110,18 +128,22 @@ class MilkyBaseAdapter(BaseAdapter, ABC):
110
128
  return
111
129
  login = self.logins[self_id]
112
130
  handler = event_handlers.get(event_type)
131
+ event = None
113
132
  if handler:
114
- event = await handler(login, self, payload)
115
- else:
133
+ try:
134
+ event = await handler(login, self, payload)
135
+ except Exception as e:
136
+ logger.exception(f"failed to handle event {event_type}: {e!r}")
137
+ event = None
138
+ if not event:
116
139
  event = Event(
117
140
  EventType.INTERNAL,
118
141
  datetime.fromtimestamp(payload.get("time", datetime.now().timestamp())),
119
142
  login,
120
143
  )
121
- if event:
122
- event._type = event_type
123
- event._data = payload.get("data", {})
124
- await self.server.post(event)
144
+ event._type = event_type
145
+ event._data = payload.get("data", {})
146
+ await self.server.post(event)
125
147
 
126
148
  async def refresh_login(self):
127
149
  try:
@@ -151,4 +173,4 @@ class MilkyBaseAdapter(BaseAdapter, ABC):
151
173
  return self.logins[self_id]
152
174
 
153
175
 
154
- __all__ = ["MilkyBaseAdapter", "DEFAULT_FEATURES"]
176
+ __all__ = ["MilkyBaseAdapter", "DEFAULT_FEATURES", "MilkyBaseConfig"]
@@ -34,5 +34,5 @@ async def bot_offline(login, net, raw):
34
34
  user=login.user,
35
35
  features=login.features.copy(),
36
36
  ),
37
- message=MessageObject("", raw["data"]["reason"])
37
+ message=MessageObject("", raw["data"]["reason"]),
38
38
  )
@@ -5,11 +5,10 @@ import asyncio
5
5
  import aiohttp
6
6
  from launart import Launart, any_completed
7
7
  from loguru import logger
8
- from yarl import URL
9
8
 
10
9
  from satori.utils import decode
11
10
 
12
- from .base import MilkyBaseAdapter
11
+ from .base import MilkyBaseAdapter, MilkyBaseConfig
13
12
 
14
13
 
15
14
  class MilkyWebsocketAdapter(MilkyBaseAdapter):
@@ -18,18 +17,14 @@ class MilkyWebsocketAdapter(MilkyBaseAdapter):
18
17
 
19
18
  def __init__(
20
19
  self,
21
- endpoint: str | URL,
22
- *,
23
- token: str | None = None,
24
- token_in_query: bool = False,
25
- headers: dict[str, str] | None = None,
20
+ config: MilkyBaseConfig,
26
21
  ):
27
- super().__init__(endpoint, token=token, headers=headers)
22
+ super().__init__(config)
28
23
  base_path = self.base_url.path.rstrip("/")
29
24
  ws_scheme = "wss" if self.base_url.scheme == "https" else "ws"
30
25
  self.event_url = self.base_url.with_scheme(ws_scheme).with_path(f"{base_path}/event")
31
- if token_in_query and token:
32
- self.event_url = self.event_url.update_query(access_token=token)
26
+ if config.token_in_query and self.token:
27
+ self.event_url = self.event_url.update_query(access_token=self.token)
33
28
  self.connection = None
34
29
  self.close_signal = asyncio.Event()
35
30
 
@@ -6,6 +6,8 @@ from dataclasses import dataclass, field
6
6
  from datetime import datetime
7
7
  from typing import Any, Literal
8
8
 
9
+ from loguru import logger
10
+
9
11
  from satori.element import Custom, E, Element
10
12
  from satori.model import Channel, ChannelType, Login, MessageObject, User
11
13
  from satori.parser import Element as RawElement
@@ -138,7 +140,10 @@ class MilkyMessageEncoder:
138
140
  if match := _BASE64_RE.match(uri):
139
141
  uri = f"base64://{uri[len(match.group(0)) :]}"
140
142
  self.segments.append(
141
- {"type": "image", "data": {"uri": uri, "sub_type": attrs.get("sub_type", "normal")}}
143
+ {
144
+ "type": "image",
145
+ "data": {"uri": uri, "sub_type": attrs.get("sub_type", attrs.get("subTyoe", "normal"))},
146
+ }
142
147
  )
143
148
  case "audio":
144
149
  uri = attrs.get("src") or attrs.get("url")
@@ -169,7 +174,9 @@ class MilkyMessageEncoder:
169
174
  await self._send_file(attrs)
170
175
  case "milky:light_app":
171
176
  await self.flush()
172
- self.segments.append({"type": "light_app", "data": {"json_payload": attrs["json_payload"]}})
177
+ self.segments.append(
178
+ {"type": "light_app", "data": {"json_payload": attrs.get("json_payload", attrs["jsonPayload"])}}
179
+ )
173
180
  case "author":
174
181
  self.stack[0].author.update(attrs)
175
182
  case "quote":
@@ -196,7 +203,7 @@ class MilkyMessageEncoder:
196
203
  if "id" in attrs:
197
204
  payload["id"] = int(attrs["id"])
198
205
  if "user_id" in attrs:
199
- payload["id"] = int(attrs["user_id"])
206
+ payload["id"] = int(attrs.get("user_id", attrs["userId"]))
200
207
  if "time" in attrs:
201
208
  payload["time"] = int(attrs["time"])
202
209
  self.stack[0].author.update(payload)
@@ -373,13 +380,13 @@ async def _decode_segments(net: MilkyNetwork, payload: dict, segments: Sequence[
373
380
  case "face":
374
381
  result.append(E.emoji(str(data.get("face_id"))))
375
382
  case "image":
376
- result.append(E.image(_resource_url(data)))
383
+ result.append(E.image(await _resource_url(net, data)))
377
384
  case "record":
378
- result.append(E.audio(_resource_url(data)))
385
+ result.append(E.audio(await _resource_url(net, data)))
379
386
  case "video":
380
- result.append(E.video(_resource_url(data)))
387
+ result.append(E.video(await _resource_url(net, data)))
381
388
  case "file":
382
- result.append(E.file(_resource_url(data)))
389
+ result.append(E.file(await _resource_url(net, data)))
383
390
  case "reply":
384
391
  seq = data.get("message_seq")
385
392
  if seq is not None:
@@ -389,7 +396,18 @@ async def _decode_segments(net: MilkyNetwork, payload: dict, segments: Sequence[
389
396
  case "forward":
390
397
  forward_id = data.get("forward_id")
391
398
  if forward_id is not None:
392
- messages = (await net.call_api("get_forwarded_messages", {"forward_id": forward_id}))["messages"]
399
+ if not net.fetch_forward_message:
400
+ result.append(E.message(forward_id, content=[], forward=True))
401
+ continue
402
+ try:
403
+ messages = (await net.call_api("get_forwarded_messages", {"forward_id": forward_id}))[
404
+ "messages"
405
+ ]
406
+ except Exception as e:
407
+ logger.error(
408
+ f"Failed to fetch forwarded messages for forward_id {forward_id}: {payload}", exc_info=e
409
+ )
410
+ messages = []
393
411
  forward_elements = []
394
412
  for msg in messages:
395
413
  sender_name = msg.get("sender_name")
@@ -405,7 +423,10 @@ async def _decode_segments(net: MilkyNetwork, payload: dict, segments: Sequence[
405
423
  "message_seq": msg["message_seq"],
406
424
  },
407
425
  )
408
- except Exception:
426
+ except Exception as e:
427
+ logger.error(
428
+ f"Failed to fetch message {msg['message_seq']} for forward: {payload}", exc_info=e
429
+ )
409
430
  pass
410
431
  if response:
411
432
  origin = await decode_message(net, response["message"])
@@ -417,14 +438,16 @@ async def _decode_segments(net: MilkyNetwork, payload: dict, segments: Sequence[
417
438
  )
418
439
  )
419
440
  content.extend(origin.message)
420
-
421
441
  else:
422
442
  content = await _decode_segments(net, payload, msg.get("segments", []))
423
443
  content.insert(0, E.author(str(msg.get("sender_id")), sender_name, avatar))
424
444
  forward_elements.append(E.message(str(msg.get("message_seq")), content=content))
425
445
  result.append(E.message(forward_id, content=forward_elements, forward=True))
426
446
  case _:
427
- result.append(Custom(f"milky:{seg_type}", data))
447
+ elem = Custom(f"milky:{seg_type}", data)
448
+ if seg_type == "market_face" and "url" in data:
449
+ elem.children.append(E.image(data["url"]))
450
+ result.append(elem)
428
451
  return result
429
452
 
430
453
 
@@ -446,7 +469,8 @@ async def _decode_reply(net: MilkyNetwork, payload: dict, data: dict, message_se
446
469
  "message_seq": message_seq,
447
470
  },
448
471
  )
449
- except Exception:
472
+ except Exception as e:
473
+ logger.error(f"Failed to fetch message {message_seq} for reply: {payload}", exc_info=e)
450
474
  return None
451
475
  if not response or "message" not in response:
452
476
  return None
@@ -458,5 +482,13 @@ async def _decode_reply(net: MilkyNetwork, payload: dict, data: dict, message_se
458
482
  return E.quote(str(message_seq), content=content)
459
483
 
460
484
 
461
- def _resource_url(data: dict) -> str:
462
- return data.get("temp_url") or data.get("url") or data.get("uri") or ""
485
+ async def _resource_url(net: MilkyNetwork, data: dict) -> str:
486
+ if "uri" in data:
487
+ return data["uri"]
488
+ if "url" in data:
489
+ return data["url"]
490
+ if "temp_url" in data:
491
+ return data["temp_url"]
492
+ if "resource_id" in data:
493
+ return (await net.call_api("get_resource_temp_url", {"resource_id": data["resource_id"]}))["url"]
494
+ return ""
@@ -5,28 +5,23 @@ import asyncio
5
5
  import aiohttp
6
6
  from launart import Launart, any_completed
7
7
  from loguru import logger
8
- from yarl import URL
9
8
 
10
9
  from satori.utils import decode
11
10
 
12
- from .base import MilkyBaseAdapter
11
+ from .base import MilkyBaseAdapter, MilkyBaseConfig
13
12
 
14
13
 
15
14
  class MilkySSEAdapter(MilkyBaseAdapter):
16
15
 
17
16
  def __init__(
18
17
  self,
19
- endpoint: str | URL,
20
- *,
21
- token: str | None = None,
22
- token_in_query: bool = False,
23
- headers: dict[str, str] | None = None,
18
+ config: MilkyBaseConfig,
24
19
  ):
25
- super().__init__(endpoint, token=token, headers=headers)
20
+ super().__init__(config)
26
21
  base_path = self.base_url.path.rstrip("/")
27
22
  self.event_url = self.base_url.with_path(f"{base_path}/event")
28
- if token_in_query and token:
29
- self.event_url = self.event_url.update_query(access_token=token)
23
+ if config.token_in_query and self.token:
24
+ self.event_url = self.event_url.update_query(access_token=self.token)
30
25
  self.close_signal = asyncio.Event()
31
26
 
32
27
  async def launch(self, manager: Launart):
@@ -10,6 +10,8 @@ GROUP_AVATAR_URL = "https://p.qlogo.cn/gh/{group}/{group}/640"
10
10
 
11
11
 
12
12
  class MilkyNetwork(Protocol):
13
+ fetch_forward_message: bool
14
+
13
15
  async def call_api(self, action: str, params: dict | None = None) -> dict: ...
14
16
 
15
17
 
@@ -1,30 +1,34 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from dataclasses import dataclass
4
+
3
5
  import aiohttp
4
6
  from launart import Launart
5
7
  from loguru import logger
6
8
  from starlette.requests import Request as StarletteRequest
7
9
  from starlette.responses import JSONResponse, Response
8
10
  from starlette.routing import Route
9
- from yarl import URL
10
11
 
11
- from .base import MilkyBaseAdapter
12
+ from .base import MilkyBaseAdapter, MilkyBaseConfig
13
+
14
+
15
+ @dataclass
16
+ class MilkyWebhookConfig(MilkyBaseConfig):
17
+ path: str = "/milky"
18
+ """Webhook 适配器于 Server 的路径, 默认为 /milky"""
19
+ self_token: str | None = None
20
+ """Webhook 适配器的自定义访问令牌, 默认为 None, 若未设置则使用 Milky 协议的 token"""
12
21
 
13
22
 
14
- class MilkyWebhookAdapter(MilkyBaseAdapter):
23
+ class MilkyWebhookAdapter(MilkyBaseAdapter[MilkyWebhookConfig]):
15
24
 
16
25
  def __init__(
17
26
  self,
18
- endpoint: str | URL,
19
- *,
20
- token: str | None = None,
21
- headers: dict[str, str] | None = None,
22
- path: str = "/milky",
23
- self_token: str | None = None,
27
+ config: MilkyWebhookConfig,
24
28
  ):
25
- super().__init__(endpoint, token=token, headers=headers)
26
- self.webhook_token = self_token if self_token is not None else token
27
- self.webhook_paths = self._normalize_webhook_paths(path)
29
+ super().__init__(config)
30
+ self.webhook_token = config.self_token if config.self_token is not None else config.token
31
+ self.webhook_paths = self._normalize_webhook_paths(config.path)
28
32
 
29
33
  async def launch(self, manager: Launart):
30
34
  async with self.stage("preparing"):
@@ -79,4 +83,4 @@ class MilkyWebhookAdapter(MilkyBaseAdapter):
79
83
  return Response(status_code=204)
80
84
 
81
85
 
82
- __all__ = ["MilkyWebhookAdapter"]
86
+ __all__ = ["MilkyWebhookAdapter", "MilkyWebhookConfig"]