satori-python-adapter-milky 0.1.0__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {satori_python_adapter_milky-0.1.0 → satori_python_adapter_milky-0.1.2}/.mina/adapter_milky.toml +2 -2
- {satori_python_adapter_milky-0.1.0 → satori_python_adapter_milky-0.1.2}/PKG-INFO +2 -2
- {satori_python_adapter_milky-0.1.0 → satori_python_adapter_milky-0.1.2}/pyproject.toml +4 -3
- {satori_python_adapter_milky-0.1.0 → satori_python_adapter_milky-0.1.2}/src/satori/adapters/milky/api.py +6 -1
- {satori_python_adapter_milky-0.1.0 → satori_python_adapter_milky-0.1.2}/src/satori/adapters/milky/main.py +6 -5
- satori_python_adapter_milky-0.1.2/src/satori/adapters/milky/message.py +403 -0
- {satori_python_adapter_milky-0.1.0 → satori_python_adapter_milky-0.1.2}/src/satori/adapters/milky/utils.py +1 -1
- {satori_python_adapter_milky-0.1.0 → satori_python_adapter_milky-0.1.2}/src/satori/adapters/milky/webhook.py +6 -4
- satori_python_adapter_milky-0.1.0/src/satori/adapters/milky/message.py +0 -233
- {satori_python_adapter_milky-0.1.0 → satori_python_adapter_milky-0.1.2}/LICENSE +0 -0
- {satori_python_adapter_milky-0.1.0 → satori_python_adapter_milky-0.1.2}/README.md +0 -0
- {satori_python_adapter_milky-0.1.0 → satori_python_adapter_milky-0.1.2}/src/satori/adapters/milky/__init__.py +0 -0
- {satori_python_adapter_milky-0.1.0 → satori_python_adapter_milky-0.1.2}/src/satori/adapters/milky/events/__init__.py +0 -0
- {satori_python_adapter_milky-0.1.0 → satori_python_adapter_milky-0.1.2}/src/satori/adapters/milky/events/base.py +0 -0
- {satori_python_adapter_milky-0.1.0 → satori_python_adapter_milky-0.1.2}/src/satori/adapters/milky/events/group.py +0 -0
- {satori_python_adapter_milky-0.1.0 → satori_python_adapter_milky-0.1.2}/src/satori/adapters/milky/events/message.py +0 -0
- {satori_python_adapter_milky-0.1.0 → satori_python_adapter_milky-0.1.2}/src/satori/adapters/milky/events/request.py +0 -0
{satori_python_adapter_milky-0.1.0 → satori_python_adapter_milky-0.1.2}/.mina/adapter_milky.toml
RENAMED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
includes = ["src/satori/adapters/milky"]
|
|
2
|
-
raw-dependencies = ["satori-python-server >= 0.17.
|
|
2
|
+
raw-dependencies = ["satori-python-server >= 0.17.6"]
|
|
3
3
|
|
|
4
4
|
[project]
|
|
5
5
|
name = "satori-python-adapter-milky"
|
|
6
|
-
version = "0.1.
|
|
6
|
+
version = "0.1.2"
|
|
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.1.
|
|
3
|
+
Version: 0.1.2
|
|
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>=0.17.
|
|
20
|
+
Requires-Dist: satori-python-server>=0.17.6
|
|
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.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name = "RF-Tar-Railt", email = "rf_tar_railt@qq.com" },
|
|
6
6
|
]
|
|
7
7
|
dependencies = [
|
|
8
|
-
"satori-python-server >= 0.17.
|
|
8
|
+
"satori-python-server >= 0.17.6",
|
|
9
9
|
]
|
|
10
10
|
description = "Satori Protocol SDK for python, adapter for Milky"
|
|
11
11
|
readme = "README.md"
|
|
@@ -46,7 +46,7 @@ dev = [
|
|
|
46
46
|
"mina-build<0.6,>=0.5.1",
|
|
47
47
|
"pdm-mina>=0.3.2",
|
|
48
48
|
"nonechat<0.7.0,>=0.6.0",
|
|
49
|
-
"uvicorn[standard]>=0.
|
|
49
|
+
"uvicorn[standard]>=0.37.0",
|
|
50
50
|
]
|
|
51
51
|
|
|
52
52
|
[tool.pdm.build]
|
|
@@ -89,6 +89,7 @@ exclude = [
|
|
|
89
89
|
"exam_qps.py",
|
|
90
90
|
"exam1.py",
|
|
91
91
|
"exam2.py",
|
|
92
|
+
"src/satori/_vendor/*",
|
|
92
93
|
]
|
|
93
94
|
|
|
94
95
|
[tool.ruff.lint]
|
|
@@ -214,7 +214,7 @@ def apply(
|
|
|
214
214
|
{
|
|
215
215
|
"group_id": int(request.params["guild_id"]),
|
|
216
216
|
"user_id": int(request.params["user_id"]),
|
|
217
|
-
"duration": int(request.params["duration"]),
|
|
217
|
+
"duration": int(request.params["duration"] / 1000),
|
|
218
218
|
},
|
|
219
219
|
)
|
|
220
220
|
return
|
|
@@ -309,3 +309,8 @@ def apply(
|
|
|
309
309
|
payload["reason"] = request.params.get("comment")
|
|
310
310
|
await net.call_api("reject_friend_request", payload)
|
|
311
311
|
return
|
|
312
|
+
|
|
313
|
+
@adapter.route("*")
|
|
314
|
+
async def internal_api(request: Request[dict]):
|
|
315
|
+
net = net_getter(request.self_id)
|
|
316
|
+
return await net.call_api(request.action.removeprefix("internal/"), request.params)
|
|
@@ -15,7 +15,7 @@ from satori.exception import ActionFailed
|
|
|
15
15
|
from satori.model import Event, Login, LoginStatus
|
|
16
16
|
from satori.server.adapter import Adapter as BaseAdapter
|
|
17
17
|
from satori.server.model import Request
|
|
18
|
-
from satori.utils import decode
|
|
18
|
+
from satori.utils import decode, encode
|
|
19
19
|
|
|
20
20
|
from .api import apply
|
|
21
21
|
from .events import event_handlers
|
|
@@ -116,17 +116,18 @@ class MilkyAdapter(BaseAdapter):
|
|
|
116
116
|
content = await resp.read()
|
|
117
117
|
return Response(content=content, media_type=resp.headers.get("Content-Type"))
|
|
118
118
|
|
|
119
|
-
async def call_api(self, action: str, params: dict | None = None) -> dict
|
|
119
|
+
async def call_api(self, action: str, params: dict | None = None) -> dict:
|
|
120
120
|
if not self.session:
|
|
121
121
|
raise RuntimeError("HTTP session not initialized")
|
|
122
122
|
url = self.api_base.with_path(f"{self.api_base.path.rstrip('/')}/{action}")
|
|
123
123
|
headers = self.headers.copy()
|
|
124
|
+
headers["Content-Type"] = "application/json"
|
|
124
125
|
if self.token:
|
|
125
126
|
headers.setdefault("Authorization", f"Bearer {self.token}")
|
|
126
|
-
async with self.session.post(url,
|
|
127
|
+
async with self.session.post(url, data=encode(params or {}), headers=headers) as resp:
|
|
127
128
|
resp.raise_for_status()
|
|
128
|
-
data = await resp.
|
|
129
|
-
if data.get("status") == "failed":
|
|
129
|
+
data = decode(await resp.text())
|
|
130
|
+
if data.get("status") == "failed" or data.get("retcode", 0) != 0:
|
|
130
131
|
raise ActionFailed(f"{data.get('retcode')}: {data.get('message')}", data)
|
|
131
132
|
return data.get("data")
|
|
132
133
|
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
from satori.element import Custom, E, Element
|
|
10
|
+
from satori.model import Channel, ChannelType, Login, MessageObject, User
|
|
11
|
+
from satori.parser import Element as RawElement
|
|
12
|
+
from satori.parser import parse
|
|
13
|
+
|
|
14
|
+
from .utils import MilkyNetwork, decode_guild, decode_guild_channel_id, decode_member, get_scene_and_peer, user_avatar
|
|
15
|
+
|
|
16
|
+
_BASE64_RE = re.compile(r"^data:([\w/.+-]+);base64,")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class State:
|
|
21
|
+
type: Literal["message", "reply", "forward"]
|
|
22
|
+
children: list[dict[str, Any]] = field(default_factory=list)
|
|
23
|
+
author: dict[str, Any] = field(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MilkyMessageEncoder:
|
|
27
|
+
def __init__(self, login: Login, net: MilkyNetwork, channel_id: str):
|
|
28
|
+
self.login = login
|
|
29
|
+
self.net = net
|
|
30
|
+
self.channel_id = channel_id
|
|
31
|
+
self.segments: list[dict[str, Any]] = []
|
|
32
|
+
self.stack = [State("message")]
|
|
33
|
+
self.results: list[MessageObject] = []
|
|
34
|
+
|
|
35
|
+
async def send_forward(self):
|
|
36
|
+
if not self.stack[0].children:
|
|
37
|
+
return
|
|
38
|
+
scene, peer_id = get_scene_and_peer(self.channel_id)
|
|
39
|
+
seg = {"type": "forward", "data": {"messages": self.stack[0].children}}
|
|
40
|
+
if scene == "group":
|
|
41
|
+
resp = await self.net.call_api("send_group_message", {"group_id": peer_id, "message": [seg]})
|
|
42
|
+
else:
|
|
43
|
+
resp = await self.net.call_api("send_private_message", {"user_id": peer_id, "message": [seg]})
|
|
44
|
+
if resp:
|
|
45
|
+
channel_type = ChannelType.TEXT if scene == "group" else ChannelType.DIRECT
|
|
46
|
+
channel_id = str(peer_id) if scene == "group" else self.channel_id
|
|
47
|
+
channel = Channel(channel_id, channel_type)
|
|
48
|
+
created_at = datetime.fromtimestamp(resp.get("time", datetime.now().timestamp()))
|
|
49
|
+
message = MessageObject(str(resp.get("message_seq", "")), "", channel=channel, created_at=created_at)
|
|
50
|
+
self.results.append(message)
|
|
51
|
+
|
|
52
|
+
async def _send_file(self, attrs: dict[str, Any]):
|
|
53
|
+
uri = attrs.get("src") or attrs.get("url")
|
|
54
|
+
if not uri:
|
|
55
|
+
return
|
|
56
|
+
name = attrs.get("title") or uri.split("/")[-1]
|
|
57
|
+
if match := _BASE64_RE.match(uri):
|
|
58
|
+
uri = f"base64://{uri[len(match.group(0)) :]}"
|
|
59
|
+
scene, peer_id = get_scene_and_peer(self.channel_id)
|
|
60
|
+
if scene == "group":
|
|
61
|
+
await self.net.call_api(
|
|
62
|
+
"upload_group_file",
|
|
63
|
+
{
|
|
64
|
+
"group_id": peer_id,
|
|
65
|
+
"file_uri": uri,
|
|
66
|
+
"file_name": name,
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
await self.net.call_api(
|
|
71
|
+
"upload_private_file",
|
|
72
|
+
{
|
|
73
|
+
"user_id": peer_id,
|
|
74
|
+
"file_uri": uri,
|
|
75
|
+
"file_name": name,
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
self.results.append(MessageObject("", ""))
|
|
79
|
+
|
|
80
|
+
async def send(self, content: str) -> list[MessageObject]:
|
|
81
|
+
raw_elements = parse(content)
|
|
82
|
+
await self.render(raw_elements)
|
|
83
|
+
await self.flush()
|
|
84
|
+
return self.results
|
|
85
|
+
|
|
86
|
+
async def render(self, elements: Sequence[RawElement]):
|
|
87
|
+
for element in elements:
|
|
88
|
+
await self.visit(element)
|
|
89
|
+
|
|
90
|
+
async def visit(self, element: RawElement):
|
|
91
|
+
type_ = element.type
|
|
92
|
+
attrs = element.attrs
|
|
93
|
+
children = element.children
|
|
94
|
+
if type_ == "text":
|
|
95
|
+
text = attrs.get("text", "")
|
|
96
|
+
if not self.segments or self.segments[-1]["type"] != "text":
|
|
97
|
+
self.segments.append({"type": "text", "data": {"text": text}})
|
|
98
|
+
else:
|
|
99
|
+
self.segments[-1]["data"]["text"] += text
|
|
100
|
+
elif type_ == "br":
|
|
101
|
+
if not self.segments or self.segments[-1]["type"] != "text":
|
|
102
|
+
self.segments.append({"type": "text", "data": {"text": "\n"}})
|
|
103
|
+
else:
|
|
104
|
+
self.segments[-1]["data"]["text"] += "\n"
|
|
105
|
+
elif type_ == "p":
|
|
106
|
+
prev = self.segments[-1] if self.segments else None
|
|
107
|
+
if prev and prev["type"] == "text":
|
|
108
|
+
if not prev["data"]["text"].endswith("\n"):
|
|
109
|
+
prev["data"]["text"] += "\n"
|
|
110
|
+
else:
|
|
111
|
+
self.segments.append({"type": "text", "data": {"text": "\n"}})
|
|
112
|
+
await self.render(children)
|
|
113
|
+
if self.segments and self.segments[-1]["type"] == "text":
|
|
114
|
+
if not self.segments[-1]["data"]["text"].endswith("\n"):
|
|
115
|
+
self.segments[-1]["data"]["text"] += "\n"
|
|
116
|
+
else:
|
|
117
|
+
self.segments.append({"type": "text", "data": {"text": "\n"}})
|
|
118
|
+
elif type_ == "at":
|
|
119
|
+
if attrs.get("type") == "all":
|
|
120
|
+
self.segments.append({"type": "mention_all", "data": {}})
|
|
121
|
+
elif "id" in attrs:
|
|
122
|
+
target = attrs["id"]
|
|
123
|
+
self.segments.append({"type": "mention", "data": {"user_id": int(target)}})
|
|
124
|
+
elif type_ == "sharp":
|
|
125
|
+
self.segments.append({"type": "text", "data": {"text": attrs["id"]}})
|
|
126
|
+
elif type_ == "a":
|
|
127
|
+
await self.render(children)
|
|
128
|
+
if "href" in attrs:
|
|
129
|
+
if not self.segments or self.segments[-1]["type"] != "text":
|
|
130
|
+
self.segments.append({"type": "text", "data": {"text": f" ({attrs['href']})"}})
|
|
131
|
+
else:
|
|
132
|
+
self.segments[-1]["data"]["text"] += f" ({attrs['href']})"
|
|
133
|
+
elif type_ in {"img", "image"}:
|
|
134
|
+
uri = attrs.get("src") or attrs.get("url")
|
|
135
|
+
if not uri:
|
|
136
|
+
return
|
|
137
|
+
if match := _BASE64_RE.match(uri):
|
|
138
|
+
uri = f"base64://{uri[len(match.group(0)) :]}"
|
|
139
|
+
self.segments.append({"type": "image", "data": {"uri": uri, "sub_type": attrs.get("sub_type", "normal")}})
|
|
140
|
+
elif type_ == "audio":
|
|
141
|
+
uri = attrs.get("src") or attrs.get("url")
|
|
142
|
+
if not uri:
|
|
143
|
+
return
|
|
144
|
+
if match := _BASE64_RE.match(uri):
|
|
145
|
+
uri = f"base64://{uri[len(match.group(0)) :]}"
|
|
146
|
+
self.segments.append({"type": "record", "data": {"uri": uri}})
|
|
147
|
+
elif type_ == "video":
|
|
148
|
+
uri = attrs.get("src") or attrs.get("url")
|
|
149
|
+
if not uri:
|
|
150
|
+
return
|
|
151
|
+
if match := _BASE64_RE.match(uri):
|
|
152
|
+
uri = f"base64://{uri[len(match.group(0)) :]}"
|
|
153
|
+
payload = {"uri": uri}
|
|
154
|
+
if poster := attrs.get("poster"):
|
|
155
|
+
payload["thumb_uri"] = poster
|
|
156
|
+
self.segments.append({"type": "video", "data": payload})
|
|
157
|
+
elif type_ == "milky:face":
|
|
158
|
+
self.segments.append({"type": "face", "data": {"face_id": attrs["id"]}})
|
|
159
|
+
elif type_ == "file":
|
|
160
|
+
await self.flush()
|
|
161
|
+
await self._send_file(attrs)
|
|
162
|
+
elif type_ == "author":
|
|
163
|
+
self.stack[0].author.update(attrs)
|
|
164
|
+
elif type_ == "quote":
|
|
165
|
+
await self.flush()
|
|
166
|
+
self.segments.append({"type": "reply", "data": {"message_seq": int(attrs["id"])}})
|
|
167
|
+
elif type_ == "message":
|
|
168
|
+
await self.flush()
|
|
169
|
+
if "forward" in attrs:
|
|
170
|
+
self.stack.insert(0, State("forward"))
|
|
171
|
+
await self.render(children)
|
|
172
|
+
await self.flush()
|
|
173
|
+
self.stack.pop(0)
|
|
174
|
+
await self.send_forward()
|
|
175
|
+
elif "id" in attrs:
|
|
176
|
+
self.stack[0].author["seq"] = int(attrs["id"])
|
|
177
|
+
else:
|
|
178
|
+
payload = {}
|
|
179
|
+
if "name" in attrs:
|
|
180
|
+
payload["name"] = attrs["name"]
|
|
181
|
+
if "nickname" in attrs:
|
|
182
|
+
payload["name"] = attrs["nickname"]
|
|
183
|
+
if "username" in attrs:
|
|
184
|
+
payload["name"] = attrs["username"]
|
|
185
|
+
if "id" in attrs:
|
|
186
|
+
payload["id"] = int(attrs["id"])
|
|
187
|
+
if "user_id" in attrs:
|
|
188
|
+
payload["id"] = int(attrs["user_id"])
|
|
189
|
+
if "time" in attrs:
|
|
190
|
+
payload["time"] = int(attrs["time"])
|
|
191
|
+
self.stack[0].author.update(payload)
|
|
192
|
+
await self.render(children)
|
|
193
|
+
await self.flush()
|
|
194
|
+
else:
|
|
195
|
+
await self.render(children)
|
|
196
|
+
|
|
197
|
+
async def flush(self):
|
|
198
|
+
if not self.segments:
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
while True:
|
|
202
|
+
first = self.segments[0]
|
|
203
|
+
if first["type"] != "text":
|
|
204
|
+
break
|
|
205
|
+
first["data"]["text"] = first["data"]["text"].lstrip()
|
|
206
|
+
if first["data"]["text"]:
|
|
207
|
+
break
|
|
208
|
+
self.segments.pop(0)
|
|
209
|
+
|
|
210
|
+
while True:
|
|
211
|
+
last = self.segments[-1]
|
|
212
|
+
if last["type"] != "text":
|
|
213
|
+
break
|
|
214
|
+
last["data"]["text"] = last["data"]["text"].rstrip()
|
|
215
|
+
if last["data"]["text"]:
|
|
216
|
+
break
|
|
217
|
+
self.segments.pop()
|
|
218
|
+
|
|
219
|
+
scene, peer_id = get_scene_and_peer(self.channel_id)
|
|
220
|
+
slot = self.stack[0]
|
|
221
|
+
type_, author = slot.type, slot.author
|
|
222
|
+
if not self.segments and "seq" not in author:
|
|
223
|
+
return
|
|
224
|
+
if type_ == "forward":
|
|
225
|
+
if "seq" in author:
|
|
226
|
+
origin = await self.net.call_api(
|
|
227
|
+
"get_message", {"message_scene": scene, "peer_id": peer_id, "message_seq": author["seq"]}
|
|
228
|
+
)
|
|
229
|
+
segments = origin["message"]["segments"]
|
|
230
|
+
nickname = (
|
|
231
|
+
origin["message"]["friend"]["nickname"]
|
|
232
|
+
if scene == "friend"
|
|
233
|
+
else origin["message"]["group_member"]["nickname"]
|
|
234
|
+
)
|
|
235
|
+
self.stack[1].children.append(
|
|
236
|
+
{
|
|
237
|
+
"user_id": origin["message"]["sender_id"],
|
|
238
|
+
"sender_name": nickname,
|
|
239
|
+
"segments": await self.sendable(segments),
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
else:
|
|
243
|
+
self.stack[1].children.append(
|
|
244
|
+
{
|
|
245
|
+
"user_id": int(author.get("id", self.login.id)),
|
|
246
|
+
"sender_name": author.get("name", self.login.user.name or self.login.id),
|
|
247
|
+
"segments": self.segments,
|
|
248
|
+
}
|
|
249
|
+
)
|
|
250
|
+
self.segments = []
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
if scene == "group":
|
|
254
|
+
resp = await self.net.call_api("send_group_message", {"group_id": peer_id, "message": self.segments})
|
|
255
|
+
else:
|
|
256
|
+
resp = await self.net.call_api("send_private_message", {"user_id": peer_id, "message": self.segments})
|
|
257
|
+
if resp:
|
|
258
|
+
channel_type = ChannelType.TEXT if scene == "group" else ChannelType.DIRECT
|
|
259
|
+
channel_id = str(peer_id) if scene == "group" else self.channel_id
|
|
260
|
+
channel = Channel(channel_id, channel_type)
|
|
261
|
+
created_at = datetime.fromtimestamp(resp.get("time", datetime.now().timestamp()))
|
|
262
|
+
message = MessageObject(str(resp.get("message_seq", "")), "", channel=channel, created_at=created_at)
|
|
263
|
+
self.results.append(message)
|
|
264
|
+
self.segments = []
|
|
265
|
+
|
|
266
|
+
async def sendable(self, segments: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
267
|
+
new = []
|
|
268
|
+
for seg in segments:
|
|
269
|
+
if (
|
|
270
|
+
seg["type"] in ("image", "record", "video")
|
|
271
|
+
and "resource_id" in seg["data"]
|
|
272
|
+
and "uri" not in seg["data"]
|
|
273
|
+
):
|
|
274
|
+
data = seg["data"]
|
|
275
|
+
if "temp_url" not in data:
|
|
276
|
+
data["uri"] = (
|
|
277
|
+
await self.net.call_api("get_resource_temp_url", {"resource_id": data["resource_id"]})
|
|
278
|
+
)["url"]
|
|
279
|
+
else:
|
|
280
|
+
data["uri"] = data["temp_url"]
|
|
281
|
+
new.append({"type": seg["type"], "data": data})
|
|
282
|
+
elif seg["type"] == "forward" and "forward_id" in seg["data"]:
|
|
283
|
+
forward_id = seg["data"]["forward_id"]
|
|
284
|
+
messages = (await self.net.call_api("get_forwarded_messages", {"forward_id": forward_id}))["messages"]
|
|
285
|
+
new.append(
|
|
286
|
+
{
|
|
287
|
+
"type": "forward",
|
|
288
|
+
"data": {
|
|
289
|
+
"messages": [
|
|
290
|
+
{
|
|
291
|
+
"user_id": int(self.login.id),
|
|
292
|
+
"sender_name": msg["sender_name"],
|
|
293
|
+
"segments": await self.sendable(msg["segments"]),
|
|
294
|
+
}
|
|
295
|
+
for msg in messages
|
|
296
|
+
]
|
|
297
|
+
},
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
elif seg["type"] in ("market_face", "light_app", "xml"):
|
|
301
|
+
continue
|
|
302
|
+
return new
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
async def decode_message(net: MilkyNetwork, payload: dict) -> MessageObject:
|
|
306
|
+
elements = await _decode_segments(net, payload, payload.get("segments") or [])
|
|
307
|
+
guild_id, channel_id = decode_guild_channel_id(payload)
|
|
308
|
+
channel_type = ChannelType.TEXT if guild_id else ChannelType.DIRECT
|
|
309
|
+
channel_name = None
|
|
310
|
+
guild = None
|
|
311
|
+
member = None
|
|
312
|
+
if payload["message_scene"] == "group":
|
|
313
|
+
group_info = payload.get("group")
|
|
314
|
+
if group_info:
|
|
315
|
+
guild = decode_guild(group_info)
|
|
316
|
+
channel_name = group_info.get("group_name")
|
|
317
|
+
member_info = payload.get("group_member")
|
|
318
|
+
if member_info:
|
|
319
|
+
member = decode_member(member_info)
|
|
320
|
+
elif payload["message_scene"] == "friend":
|
|
321
|
+
friend_info = payload.get("friend")
|
|
322
|
+
if friend_info:
|
|
323
|
+
channel_name = friend_info.get("nickname")
|
|
324
|
+
channel = Channel(channel_id, channel_type, channel_name)
|
|
325
|
+
|
|
326
|
+
user_name = None
|
|
327
|
+
if payload["message_scene"] == "group":
|
|
328
|
+
member_info = payload.get("group_member")
|
|
329
|
+
if member_info:
|
|
330
|
+
user_name = member_info.get("nickname")
|
|
331
|
+
elif payload["message_scene"] == "friend":
|
|
332
|
+
friend_info = payload.get("friend")
|
|
333
|
+
if friend_info:
|
|
334
|
+
user_name = friend_info.get("nickname")
|
|
335
|
+
user = User(str(payload["sender_id"]), user_name, avatar=user_avatar(payload["sender_id"]))
|
|
336
|
+
|
|
337
|
+
message = MessageObject(
|
|
338
|
+
str(payload["message_seq"]),
|
|
339
|
+
"".join(str(elem) for elem in elements),
|
|
340
|
+
channel=channel,
|
|
341
|
+
guild=guild,
|
|
342
|
+
member=member,
|
|
343
|
+
user=user,
|
|
344
|
+
created_at=datetime.fromtimestamp(payload["time"]),
|
|
345
|
+
)
|
|
346
|
+
message.message = elements
|
|
347
|
+
return message
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
async def _decode_segments(net: MilkyNetwork, payload: dict, segments: Sequence[dict]) -> list[Element]:
|
|
351
|
+
result: list[Element] = []
|
|
352
|
+
for segment in segments:
|
|
353
|
+
seg_type = segment.get("type")
|
|
354
|
+
data = segment.get("data", {})
|
|
355
|
+
if seg_type == "text":
|
|
356
|
+
result.append(E.text(data.get("text", "")))
|
|
357
|
+
elif seg_type == "mention":
|
|
358
|
+
result.append(E.at(str(data.get("user_id"))))
|
|
359
|
+
elif seg_type == "mention_all":
|
|
360
|
+
result.append(E.at_all())
|
|
361
|
+
elif seg_type == "image":
|
|
362
|
+
result.append(E.image(_resource_url(data)))
|
|
363
|
+
elif seg_type == "record":
|
|
364
|
+
result.append(E.audio(_resource_url(data)))
|
|
365
|
+
elif seg_type == "video":
|
|
366
|
+
result.append(E.video(_resource_url(data)))
|
|
367
|
+
elif seg_type == "file":
|
|
368
|
+
result.append(E.file(_resource_url(data)))
|
|
369
|
+
elif seg_type == "reply":
|
|
370
|
+
seq = data.get("message_seq")
|
|
371
|
+
if seq is not None:
|
|
372
|
+
quote = await _decode_reply(net, payload, int(seq))
|
|
373
|
+
if quote:
|
|
374
|
+
result.append(quote)
|
|
375
|
+
else:
|
|
376
|
+
result.append(Custom(f"milky:{seg_type}", data))
|
|
377
|
+
return result
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
async def _decode_reply(net: MilkyNetwork, payload: dict, message_seq: int) -> Element | None:
|
|
381
|
+
try:
|
|
382
|
+
response = await net.call_api(
|
|
383
|
+
"get_message",
|
|
384
|
+
{
|
|
385
|
+
"message_scene": payload["message_scene"],
|
|
386
|
+
"peer_id": payload["peer_id"],
|
|
387
|
+
"message_seq": message_seq,
|
|
388
|
+
},
|
|
389
|
+
)
|
|
390
|
+
except Exception:
|
|
391
|
+
return None
|
|
392
|
+
if not response or "message" not in response:
|
|
393
|
+
return None
|
|
394
|
+
quoted = await decode_message(net, response["message"])
|
|
395
|
+
content = []
|
|
396
|
+
if quoted.user:
|
|
397
|
+
content.append(E.author(quoted.user.id, quoted.user.name, quoted.user.avatar))
|
|
398
|
+
content.extend(quoted.message)
|
|
399
|
+
return E.quote(str(message_seq), content=content)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _resource_url(data: dict) -> str:
|
|
403
|
+
return data.get("temp_url") or data.get("url") or data.get("uri") or ""
|
|
@@ -10,7 +10,7 @@ GROUP_AVATAR_URL = "https://p.qlogo.cn/gh/{group}/{group}/640"
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class MilkyNetwork(Protocol):
|
|
13
|
-
async def call_api(self, action: str, params: dict | None = None) -> dict
|
|
13
|
+
async def call_api(self, action: str, params: dict | None = None) -> dict: ...
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def user_avatar(uin: int | str) -> str:
|
|
@@ -16,6 +16,7 @@ from satori.exception import ActionFailed
|
|
|
16
16
|
from satori.model import Event, Login, LoginStatus
|
|
17
17
|
from satori.server.adapter import Adapter as BaseAdapter
|
|
18
18
|
from satori.server.model import Request
|
|
19
|
+
from satori.utils import decode, encode
|
|
19
20
|
|
|
20
21
|
from .api import apply
|
|
21
22
|
from .events import event_handlers
|
|
@@ -109,17 +110,18 @@ class MilkyWebhookAdapter(BaseAdapter):
|
|
|
109
110
|
content = await resp.read()
|
|
110
111
|
return Response(content=content, media_type=resp.headers.get("Content-Type"))
|
|
111
112
|
|
|
112
|
-
async def call_api(self, action: str, params: dict | None = None) -> dict
|
|
113
|
+
async def call_api(self, action: str, params: dict | None = None) -> dict:
|
|
113
114
|
if not self.session:
|
|
114
115
|
raise RuntimeError("HTTP session not initialized")
|
|
115
116
|
url = self.api_base.with_path(f"{self.api_base.path.rstrip('/')}/{action}")
|
|
116
117
|
headers = self.headers.copy()
|
|
118
|
+
headers["Content-Type"] = "application/json"
|
|
117
119
|
if self.token:
|
|
118
120
|
headers.setdefault("Authorization", f"Bearer {self.token}")
|
|
119
|
-
async with self.session.post(url,
|
|
121
|
+
async with self.session.post(url, data=encode(params or {}), headers=headers) as resp:
|
|
120
122
|
resp.raise_for_status()
|
|
121
|
-
data = await resp.
|
|
122
|
-
if data.get("status") == "failed":
|
|
123
|
+
data = decode(await resp.text())
|
|
124
|
+
if data.get("status") == "failed" or data.get("retcode", 0) != 0:
|
|
123
125
|
raise ActionFailed(f"{data.get('retcode')}: {data.get('message')}", data)
|
|
124
126
|
return data.get("data")
|
|
125
127
|
|
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
from collections.abc import Sequence
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
from satori.element import Custom, E, Element
|
|
9
|
-
from satori.model import Channel, ChannelType, Login, MessageObject, User
|
|
10
|
-
from satori.parser import Element as RawElement
|
|
11
|
-
from satori.parser import parse
|
|
12
|
-
|
|
13
|
-
from .utils import MilkyNetwork, decode_guild, decode_guild_channel_id, decode_member, get_scene_and_peer, user_avatar
|
|
14
|
-
|
|
15
|
-
_BASE64_RE = re.compile(r"^data:([\w/.+-]+);base64,")
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class MilkyMessageEncoder:
|
|
19
|
-
def __init__(self, login: Login, net: MilkyNetwork, channel_id: str):
|
|
20
|
-
self.login = login
|
|
21
|
-
self.net = net
|
|
22
|
-
self.channel_id = channel_id
|
|
23
|
-
self.segments: list[dict[str, Any]] = []
|
|
24
|
-
self.elements: list[Element] = []
|
|
25
|
-
self.results: list[MessageObject] = []
|
|
26
|
-
|
|
27
|
-
async def send(self, content: str) -> list[MessageObject]:
|
|
28
|
-
raw_elements = parse(content)
|
|
29
|
-
await self.render(raw_elements)
|
|
30
|
-
await self.flush()
|
|
31
|
-
return self.results
|
|
32
|
-
|
|
33
|
-
async def render(self, elements: Sequence[RawElement]):
|
|
34
|
-
for element in elements:
|
|
35
|
-
await self.visit(element)
|
|
36
|
-
|
|
37
|
-
async def visit(self, element: RawElement):
|
|
38
|
-
type_ = element.type
|
|
39
|
-
attrs = element.attrs
|
|
40
|
-
children = element.children
|
|
41
|
-
if type_ == "text":
|
|
42
|
-
text = attrs.get("text", "")
|
|
43
|
-
if not self.segments or self.segments[-1]["type"] != "text":
|
|
44
|
-
self.segments.append({"type": "text", "data": {"text": text}})
|
|
45
|
-
else:
|
|
46
|
-
self.segments[-1]["data"]["text"] += text
|
|
47
|
-
self.elements.append(E.text(text))
|
|
48
|
-
elif type_ == "br":
|
|
49
|
-
if not self.segments or self.segments[-1]["type"] != "text":
|
|
50
|
-
self.segments.append({"type": "text", "data": {"text": "\n"}})
|
|
51
|
-
else:
|
|
52
|
-
self.segments[-1]["data"]["text"] += "\n"
|
|
53
|
-
self.elements.append(E.text("\n"))
|
|
54
|
-
elif type_ == "p":
|
|
55
|
-
await self.render(children)
|
|
56
|
-
if self.segments:
|
|
57
|
-
if self.segments[-1]["type"] == "text":
|
|
58
|
-
self.segments[-1]["data"]["text"] += "\n"
|
|
59
|
-
else:
|
|
60
|
-
self.segments.append({"type": "text", "data": {"text": "\n"}})
|
|
61
|
-
self.elements.append(E.text("\n"))
|
|
62
|
-
elif type_ == "at":
|
|
63
|
-
if attrs.get("type") == "all":
|
|
64
|
-
self.segments.append({"type": "mention_all", "data": {}})
|
|
65
|
-
self.elements.append(E.at_all())
|
|
66
|
-
elif "id" in attrs:
|
|
67
|
-
target = attrs["id"]
|
|
68
|
-
self.segments.append({"type": "mention", "data": {"user_id": int(target)}})
|
|
69
|
-
self.elements.append(E.at(str(target)))
|
|
70
|
-
elif type_ == "quote":
|
|
71
|
-
quote_id = attrs.get("id")
|
|
72
|
-
if quote_id is not None:
|
|
73
|
-
try:
|
|
74
|
-
seq = int(quote_id)
|
|
75
|
-
except ValueError:
|
|
76
|
-
seq = None
|
|
77
|
-
if seq is not None:
|
|
78
|
-
self.segments.append({"type": "reply", "data": {"message_seq": seq}})
|
|
79
|
-
self.elements.append(E.quote(str(seq), content=[E.text("")]))
|
|
80
|
-
await self.render(children)
|
|
81
|
-
elif type_ in {"img", "image"}:
|
|
82
|
-
uri = attrs.get("src") or attrs.get("url")
|
|
83
|
-
if not uri:
|
|
84
|
-
return
|
|
85
|
-
if match := _BASE64_RE.match(uri):
|
|
86
|
-
uri = f"base64://{uri[len(match.group(0)) :]}"
|
|
87
|
-
self.segments.append({"type": "image", "data": {"uri": uri, "sub_type": attrs.get("sub_type", "normal")}})
|
|
88
|
-
self.elements.append(E.image(uri))
|
|
89
|
-
elif type_ == "audio":
|
|
90
|
-
uri = attrs.get("src") or attrs.get("url")
|
|
91
|
-
if not uri:
|
|
92
|
-
return
|
|
93
|
-
if match := _BASE64_RE.match(uri):
|
|
94
|
-
uri = f"base64://{uri[len(match.group(0)) :]}"
|
|
95
|
-
self.segments.append({"type": "record", "data": {"uri": uri}})
|
|
96
|
-
self.elements.append(E.audio(uri))
|
|
97
|
-
elif type_ == "video":
|
|
98
|
-
uri = attrs.get("src") or attrs.get("url")
|
|
99
|
-
if not uri:
|
|
100
|
-
return
|
|
101
|
-
if match := _BASE64_RE.match(uri):
|
|
102
|
-
uri = f"base64://{uri[len(match.group(0)) :]}"
|
|
103
|
-
payload = {"uri": uri}
|
|
104
|
-
if poster := attrs.get("poster"):
|
|
105
|
-
payload["thumb_uri"] = poster
|
|
106
|
-
self.segments.append({"type": "video", "data": payload})
|
|
107
|
-
self.elements.append(E.video(uri))
|
|
108
|
-
else:
|
|
109
|
-
await self.render(children)
|
|
110
|
-
|
|
111
|
-
async def flush(self):
|
|
112
|
-
if not self.segments:
|
|
113
|
-
return
|
|
114
|
-
scene, peer_id = get_scene_and_peer(self.channel_id)
|
|
115
|
-
payload: dict = {"message": self.segments}
|
|
116
|
-
if scene == "group":
|
|
117
|
-
payload["group_id"] = peer_id
|
|
118
|
-
resp = await self.net.call_api("send_group_message", payload)
|
|
119
|
-
else:
|
|
120
|
-
payload["user_id"] = peer_id
|
|
121
|
-
resp = await self.net.call_api("send_private_message", payload)
|
|
122
|
-
if resp:
|
|
123
|
-
channel_type = ChannelType.TEXT if scene == "group" else ChannelType.DIRECT
|
|
124
|
-
channel_id = str(peer_id) if scene == "group" else self.channel_id
|
|
125
|
-
channel = Channel(channel_id, channel_type)
|
|
126
|
-
created_at = datetime.fromtimestamp(resp.get("time", datetime.now().timestamp()))
|
|
127
|
-
content = "".join(str(elem) for elem in self.elements)
|
|
128
|
-
message = MessageObject(str(resp.get("message_seq", "")), content, channel=channel, created_at=created_at)
|
|
129
|
-
message.message = list(self.elements)
|
|
130
|
-
self.results.append(message)
|
|
131
|
-
self.segments = []
|
|
132
|
-
self.elements = []
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
async def decode_message(net: MilkyNetwork, payload: dict) -> MessageObject:
|
|
136
|
-
elements = await _decode_segments(net, payload, payload.get("segments") or [])
|
|
137
|
-
guild_id, channel_id = decode_guild_channel_id(payload)
|
|
138
|
-
channel_type = ChannelType.TEXT if guild_id else ChannelType.DIRECT
|
|
139
|
-
channel_name = None
|
|
140
|
-
guild = None
|
|
141
|
-
member = None
|
|
142
|
-
if payload["message_scene"] == "group":
|
|
143
|
-
group_info = payload.get("group")
|
|
144
|
-
if group_info:
|
|
145
|
-
guild = decode_guild(group_info)
|
|
146
|
-
channel_name = group_info.get("group_name")
|
|
147
|
-
member_info = payload.get("group_member")
|
|
148
|
-
if member_info:
|
|
149
|
-
member = decode_member(member_info)
|
|
150
|
-
elif payload["message_scene"] == "friend":
|
|
151
|
-
friend_info = payload.get("friend")
|
|
152
|
-
if friend_info:
|
|
153
|
-
channel_name = friend_info.get("nickname")
|
|
154
|
-
channel = Channel(channel_id, channel_type, channel_name)
|
|
155
|
-
|
|
156
|
-
user_name = None
|
|
157
|
-
if payload["message_scene"] == "group":
|
|
158
|
-
member_info = payload.get("group_member")
|
|
159
|
-
if member_info:
|
|
160
|
-
user_name = member_info.get("nickname")
|
|
161
|
-
elif payload["message_scene"] == "friend":
|
|
162
|
-
friend_info = payload.get("friend")
|
|
163
|
-
if friend_info:
|
|
164
|
-
user_name = friend_info.get("nickname")
|
|
165
|
-
user = User(str(payload["sender_id"]), user_name, avatar=user_avatar(payload["sender_id"]))
|
|
166
|
-
|
|
167
|
-
message = MessageObject(
|
|
168
|
-
str(payload["message_seq"]),
|
|
169
|
-
"".join(str(elem) for elem in elements),
|
|
170
|
-
channel=channel,
|
|
171
|
-
guild=guild,
|
|
172
|
-
member=member,
|
|
173
|
-
user=user,
|
|
174
|
-
created_at=datetime.fromtimestamp(payload["time"]),
|
|
175
|
-
)
|
|
176
|
-
message.message = elements
|
|
177
|
-
return message
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
async def _decode_segments(net: MilkyNetwork, payload: dict, segments: Sequence[dict]) -> list[Element]:
|
|
181
|
-
result: list[Element] = []
|
|
182
|
-
for segment in segments:
|
|
183
|
-
seg_type = segment.get("type")
|
|
184
|
-
data = segment.get("data", {})
|
|
185
|
-
if seg_type == "text":
|
|
186
|
-
result.append(E.text(data.get("text", "")))
|
|
187
|
-
elif seg_type == "mention":
|
|
188
|
-
result.append(E.at(str(data.get("user_id"))))
|
|
189
|
-
elif seg_type == "mention_all":
|
|
190
|
-
result.append(E.at_all())
|
|
191
|
-
elif seg_type == "image":
|
|
192
|
-
result.append(E.image(_resource_url(data)))
|
|
193
|
-
elif seg_type == "record":
|
|
194
|
-
result.append(E.audio(_resource_url(data)))
|
|
195
|
-
elif seg_type == "video":
|
|
196
|
-
result.append(E.video(_resource_url(data)))
|
|
197
|
-
elif seg_type == "file":
|
|
198
|
-
result.append(E.file(_resource_url(data)))
|
|
199
|
-
elif seg_type == "reply":
|
|
200
|
-
seq = data.get("message_seq")
|
|
201
|
-
if seq is not None:
|
|
202
|
-
quote = await _decode_reply(net, payload, int(seq))
|
|
203
|
-
if quote:
|
|
204
|
-
result.append(quote)
|
|
205
|
-
else:
|
|
206
|
-
result.append(Custom(f"milky:{seg_type}", data))
|
|
207
|
-
return result
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
async def _decode_reply(net: MilkyNetwork, payload: dict, message_seq: int) -> Element | None:
|
|
211
|
-
try:
|
|
212
|
-
response = await net.call_api(
|
|
213
|
-
"get_message",
|
|
214
|
-
{
|
|
215
|
-
"message_scene": payload["message_scene"],
|
|
216
|
-
"peer_id": payload["peer_id"],
|
|
217
|
-
"message_seq": message_seq,
|
|
218
|
-
},
|
|
219
|
-
)
|
|
220
|
-
except Exception:
|
|
221
|
-
return None
|
|
222
|
-
if not response or "message" not in response:
|
|
223
|
-
return None
|
|
224
|
-
quoted = await decode_message(net, response["message"])
|
|
225
|
-
content = []
|
|
226
|
-
if quoted.user:
|
|
227
|
-
content.append(E.author(quoted.user.id, quoted.user.name, quoted.user.avatar))
|
|
228
|
-
content.extend(quoted.message)
|
|
229
|
-
return E.quote(str(message_seq), content=content)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def _resource_url(data: dict) -> str:
|
|
233
|
-
return data.get("temp_url") or data.get("url") or data.get("uri") or ""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|