satori-python-adapter-milky 0.1.2__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {satori_python_adapter_milky-0.1.2 → satori_python_adapter_milky-0.3.0}/.mina/adapter_milky.toml +2 -2
- {satori_python_adapter_milky-0.1.2 → satori_python_adapter_milky-0.3.0}/PKG-INFO +12 -10
- {satori_python_adapter_milky-0.1.2 → satori_python_adapter_milky-0.3.0}/README.md +10 -8
- {satori_python_adapter_milky-0.1.2 → satori_python_adapter_milky-0.3.0}/pyproject.toml +2 -2
- satori_python_adapter_milky-0.3.0/src/satori/adapters/milky/__init__.py +5 -0
- {satori_python_adapter_milky-0.1.2 → satori_python_adapter_milky-0.3.0}/src/satori/adapters/milky/api.py +45 -6
- satori_python_adapter_milky-0.1.2/src/satori/adapters/milky/webhook.py → satori_python_adapter_milky-0.3.0/src/satori/adapters/milky/base.py +10 -90
- {satori_python_adapter_milky-0.1.2 → satori_python_adapter_milky-0.3.0}/src/satori/adapters/milky/events/message.py +4 -3
- satori_python_adapter_milky-0.3.0/src/satori/adapters/milky/main.py +105 -0
- {satori_python_adapter_milky-0.1.2 → satori_python_adapter_milky-0.3.0}/src/satori/adapters/milky/message.py +123 -119
- satori_python_adapter_milky-0.3.0/src/satori/adapters/milky/sse.py +142 -0
- {satori_python_adapter_milky-0.1.2 → satori_python_adapter_milky-0.3.0}/src/satori/adapters/milky/utils.py +11 -3
- satori_python_adapter_milky-0.3.0/src/satori/adapters/milky/webhook.py +82 -0
- satori_python_adapter_milky-0.1.2/src/satori/adapters/milky/__init__.py +0 -4
- satori_python_adapter_milky-0.1.2/src/satori/adapters/milky/main.py +0 -248
- {satori_python_adapter_milky-0.1.2 → satori_python_adapter_milky-0.3.0}/LICENSE +0 -0
- {satori_python_adapter_milky-0.1.2 → satori_python_adapter_milky-0.3.0}/src/satori/adapters/milky/events/__init__.py +0 -0
- {satori_python_adapter_milky-0.1.2 → satori_python_adapter_milky-0.3.0}/src/satori/adapters/milky/events/base.py +0 -0
- {satori_python_adapter_milky-0.1.2 → satori_python_adapter_milky-0.3.0}/src/satori/adapters/milky/events/group.py +0 -0
- {satori_python_adapter_milky-0.1.2 → satori_python_adapter_milky-0.3.0}/src/satori/adapters/milky/events/request.py +0 -0
{satori_python_adapter_milky-0.1.2 → satori_python_adapter_milky-0.3.0}/.mina/adapter_milky.toml
RENAMED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
includes = ["src/satori/adapters/milky"]
|
|
2
|
-
raw-dependencies = ["satori-python-server
|
|
2
|
+
raw-dependencies = ["satori-python-server<1.4.0,>= 1.3.0"]
|
|
3
3
|
|
|
4
4
|
[project]
|
|
5
5
|
name = "satori-python-adapter-milky"
|
|
6
|
-
version = "0.
|
|
6
|
+
version = "0.3.0"
|
|
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.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
20
|
+
Requires-Dist: satori-python-server<1.4.0,>=1.3.0
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
|
|
23
23
|
# satori-python
|
|
@@ -27,17 +27,18 @@ Description-Content-Type: text/markdown
|
|
|
27
27
|
[](https://pypi.org/project/satori-python)
|
|
28
28
|
[](https://www.python.org/)
|
|
29
29
|
|
|
30
|
-
基于 [Satori](https://satori.
|
|
30
|
+
基于 [Satori](https://satori.chat/zh-CN/) 协议的 Python 开发工具包
|
|
31
31
|
|
|
32
32
|
## 协议介绍
|
|
33
33
|
|
|
34
|
-
[Satori Protocol](https://satori.
|
|
34
|
+
[Satori Protocol](https://satori.chat/zh-CN/)
|
|
35
35
|
|
|
36
36
|
### 协议端
|
|
37
37
|
|
|
38
38
|
目前提供了 `satori` 协议实现的有:
|
|
39
39
|
|
|
40
40
|
- [Chronocat](https://chronocat.vercel.app)
|
|
41
|
+
- [LLBot](https://www.llonebot.com/guide/introduction)
|
|
41
42
|
- [nekobox](https://github.com/wyapx/nekobox)
|
|
42
43
|
- Koishi (搭配 `@koishijs/plugin-server`)
|
|
43
44
|
|
|
@@ -69,12 +70,13 @@ pip install satori-python-server
|
|
|
69
70
|
|
|
70
71
|
### 官方适配器
|
|
71
72
|
|
|
72
|
-
| 适配器 | 安装 | 路径
|
|
73
|
-
|
|
74
|
-
| Satori | `pip install satori-python-adapter-satori` | satori.adapters.satori
|
|
75
|
-
| OneBot V11 | `pip install satori-python-adapter-onebot11` | satori.adapters.onebot11.forward, satori.adapters.onebot11.reverse
|
|
76
|
-
| Console | `pip install satori-python-adapter-console` | satori.adapters.console
|
|
77
|
-
| Milky | `pip install satori-python-adapter-milky` | satori.adapters.milky.main, satori.adapters.milky.webhook
|
|
73
|
+
| 适配器 | 安装 | 路径 |
|
|
74
|
+
|------------|----------------------------------------------|--------------------------------------------------------------------------------------|
|
|
75
|
+
| Satori | `pip install satori-python-adapter-satori` | satori.adapters.satori |
|
|
76
|
+
| OneBot V11 | `pip install satori-python-adapter-onebot11` | satori.adapters.onebot11.forward, satori.adapters.onebot11.reverse |
|
|
77
|
+
| Console | `pip install satori-python-adapter-console` | satori.adapters.console |
|
|
78
|
+
| Milky | `pip install satori-python-adapter-milky` | satori.adapters.milky.main, satori.adapters.milky.webhook, satori.adapters.milky.sse |
|
|
79
|
+
| QQ | `pip install satori-python-adapter-qq` | satori.adapters.qq.main, satori.adapters.qq.websocket |
|
|
78
80
|
|
|
79
81
|
### 社区适配器
|
|
80
82
|
|
|
@@ -5,17 +5,18 @@
|
|
|
5
5
|
[](https://pypi.org/project/satori-python)
|
|
6
6
|
[](https://www.python.org/)
|
|
7
7
|
|
|
8
|
-
基于 [Satori](https://satori.
|
|
8
|
+
基于 [Satori](https://satori.chat/zh-CN/) 协议的 Python 开发工具包
|
|
9
9
|
|
|
10
10
|
## 协议介绍
|
|
11
11
|
|
|
12
|
-
[Satori Protocol](https://satori.
|
|
12
|
+
[Satori Protocol](https://satori.chat/zh-CN/)
|
|
13
13
|
|
|
14
14
|
### 协议端
|
|
15
15
|
|
|
16
16
|
目前提供了 `satori` 协议实现的有:
|
|
17
17
|
|
|
18
18
|
- [Chronocat](https://chronocat.vercel.app)
|
|
19
|
+
- [LLBot](https://www.llonebot.com/guide/introduction)
|
|
19
20
|
- [nekobox](https://github.com/wyapx/nekobox)
|
|
20
21
|
- Koishi (搭配 `@koishijs/plugin-server`)
|
|
21
22
|
|
|
@@ -47,12 +48,13 @@ pip install satori-python-server
|
|
|
47
48
|
|
|
48
49
|
### 官方适配器
|
|
49
50
|
|
|
50
|
-
| 适配器 | 安装 | 路径
|
|
51
|
-
|
|
52
|
-
| Satori | `pip install satori-python-adapter-satori` | satori.adapters.satori
|
|
53
|
-
| OneBot V11 | `pip install satori-python-adapter-onebot11` | satori.adapters.onebot11.forward, satori.adapters.onebot11.reverse
|
|
54
|
-
| Console | `pip install satori-python-adapter-console` | satori.adapters.console
|
|
55
|
-
| Milky | `pip install satori-python-adapter-milky` | satori.adapters.milky.main, satori.adapters.milky.webhook
|
|
51
|
+
| 适配器 | 安装 | 路径 |
|
|
52
|
+
|------------|----------------------------------------------|--------------------------------------------------------------------------------------|
|
|
53
|
+
| Satori | `pip install satori-python-adapter-satori` | satori.adapters.satori |
|
|
54
|
+
| OneBot V11 | `pip install satori-python-adapter-onebot11` | satori.adapters.onebot11.forward, satori.adapters.onebot11.reverse |
|
|
55
|
+
| Console | `pip install satori-python-adapter-console` | satori.adapters.console |
|
|
56
|
+
| Milky | `pip install satori-python-adapter-milky` | satori.adapters.milky.main, satori.adapters.milky.webhook, satori.adapters.milky.sse |
|
|
57
|
+
| QQ | `pip install satori-python-adapter-qq` | satori.adapters.qq.main, satori.adapters.qq.websocket |
|
|
56
58
|
|
|
57
59
|
### 社区适配器
|
|
58
60
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "satori-python-adapter-milky"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name = "RF-Tar-Railt", email = "rf_tar_railt@qq.com" },
|
|
6
6
|
]
|
|
7
7
|
dependencies = [
|
|
8
|
-
"satori-python-server
|
|
8
|
+
"satori-python-server<1.4.0,>= 1.3.0",
|
|
9
9
|
]
|
|
10
10
|
description = "Satori Protocol SDK for python, adapter for Milky"
|
|
11
11
|
readme = "README.md"
|
|
@@ -17,6 +17,7 @@ from satori.server.route import (
|
|
|
17
17
|
GuildMemberGetParam,
|
|
18
18
|
GuildMemberKickParam,
|
|
19
19
|
GuildMemberMuteParam,
|
|
20
|
+
GuildMemberRoleParam,
|
|
20
21
|
GuildXXXListParam,
|
|
21
22
|
MessageListParam,
|
|
22
23
|
MessageOpParam,
|
|
@@ -24,11 +25,12 @@ from satori.server.route import (
|
|
|
24
25
|
ReactionCreateParam,
|
|
25
26
|
ReactionDeleteParam,
|
|
26
27
|
UserChannelCreateParam,
|
|
27
|
-
|
|
28
|
+
UserOpParam,
|
|
28
29
|
)
|
|
29
30
|
|
|
30
31
|
from .message import MilkyMessageEncoder, decode_message
|
|
31
32
|
from .utils import (
|
|
33
|
+
ROLE_MAPPING,
|
|
32
34
|
MilkyNetwork,
|
|
33
35
|
decode_friend,
|
|
34
36
|
decode_group_channel,
|
|
@@ -219,12 +221,42 @@ def apply(
|
|
|
219
221
|
)
|
|
220
222
|
return
|
|
221
223
|
|
|
224
|
+
@adapter.route(Api.GUILD_MEMBER_ROLE_SET)
|
|
225
|
+
async def guild_member_role_set(request: Request[GuildMemberRoleParam]):
|
|
226
|
+
net = net_getter(request.self_id)
|
|
227
|
+
await net.call_api(
|
|
228
|
+
"set_group_member_admin ",
|
|
229
|
+
{
|
|
230
|
+
"group_id": int(request.params["guild_id"]),
|
|
231
|
+
"user_id": int(request.params["user_id"]),
|
|
232
|
+
"is_set": request.params["role_id"] == "admin",
|
|
233
|
+
},
|
|
234
|
+
)
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
@adapter.route(Api.GUILD_MEMBER_ROLE_UNSET)
|
|
238
|
+
async def guild_member_role_unset(request: Request[GuildMemberRoleParam]):
|
|
239
|
+
net = net_getter(request.self_id)
|
|
240
|
+
await net.call_api(
|
|
241
|
+
"set_group_member_admin ",
|
|
242
|
+
{
|
|
243
|
+
"group_id": int(request.params["guild_id"]),
|
|
244
|
+
"user_id": int(request.params["user_id"]),
|
|
245
|
+
"is_set": request.params["role_id"] != "admin",
|
|
246
|
+
},
|
|
247
|
+
)
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
@adapter.route(Api.GUILD_ROLE_LIST)
|
|
251
|
+
async def guild_role_list(request: Request[GuildXXXListParam]):
|
|
252
|
+
return PageResult(list(ROLE_MAPPING.values()))
|
|
253
|
+
|
|
222
254
|
@adapter.route(Api.GUILD_MEMBER_APPROVE)
|
|
223
255
|
async def guild_member_approve(request: Request[ApproveParam]):
|
|
224
256
|
net = net_getter(request.self_id)
|
|
225
257
|
message_id = request.params["message_id"]
|
|
226
258
|
notification_seq, notification_type, group_id, is_filtered = message_id.split("|")
|
|
227
|
-
params = {
|
|
259
|
+
params: dict = {
|
|
228
260
|
"notification_seq": int(notification_seq),
|
|
229
261
|
"notification_type": notification_type,
|
|
230
262
|
"group_id": int(group_id),
|
|
@@ -259,7 +291,7 @@ def apply(
|
|
|
259
291
|
{
|
|
260
292
|
"group_id": peer_id,
|
|
261
293
|
"message_seq": int(request.params["message_id"]),
|
|
262
|
-
"reaction": request.params["
|
|
294
|
+
"reaction": request.params["emoji_id"],
|
|
263
295
|
"is_add": True,
|
|
264
296
|
},
|
|
265
297
|
)
|
|
@@ -276,14 +308,14 @@ def apply(
|
|
|
276
308
|
{
|
|
277
309
|
"group_id": peer_id,
|
|
278
310
|
"message_seq": int(request.params["message_id"]),
|
|
279
|
-
"reaction": request.params["
|
|
311
|
+
"reaction": request.params["emoji_id"],
|
|
280
312
|
"is_add": False,
|
|
281
313
|
},
|
|
282
314
|
)
|
|
283
315
|
return
|
|
284
316
|
|
|
285
317
|
@adapter.route(Api.USER_GET)
|
|
286
|
-
async def user_get(request: Request[
|
|
318
|
+
async def user_get(request: Request[UserOpParam]):
|
|
287
319
|
net = net_getter(request.self_id)
|
|
288
320
|
user_id = request.params["user_id"]
|
|
289
321
|
profile = await net.call_api("get_user_profile", {"user_id": int(user_id)})
|
|
@@ -291,6 +323,13 @@ def apply(
|
|
|
291
323
|
raise RuntimeError("Failed to get user profile")
|
|
292
324
|
return decode_user_profile(profile, user_id)
|
|
293
325
|
|
|
326
|
+
@adapter.route(Api.FRIEND_DELETE)
|
|
327
|
+
async def friend_delete(request: Request[UserOpParam]):
|
|
328
|
+
net = net_getter(request.self_id)
|
|
329
|
+
user_id = request.params["user_id"]
|
|
330
|
+
await net.call_api("delete_friend", {"user_id": int(user_id)})
|
|
331
|
+
return
|
|
332
|
+
|
|
294
333
|
@adapter.route(Api.FRIEND_LIST)
|
|
295
334
|
async def friend_list(request: Request[FriendListParam]):
|
|
296
335
|
net = net_getter(request.self_id)
|
|
@@ -302,7 +341,7 @@ def apply(
|
|
|
302
341
|
async def friend_approve(request: Request[ApproveParam]):
|
|
303
342
|
net = net_getter(request.self_id)
|
|
304
343
|
initiator_uid, is_filtered = request.params["message_id"].split("|")
|
|
305
|
-
payload = {"initiator_uid": initiator_uid, "is_filtered": bool(int(is_filtered))}
|
|
344
|
+
payload: dict = {"initiator_uid": initiator_uid, "is_filtered": bool(int(is_filtered))}
|
|
306
345
|
if request.params["approve"]:
|
|
307
346
|
await net.call_api("accept_friend_request", payload)
|
|
308
347
|
else:
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from abc import ABC
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
|
|
5
6
|
import aiohttp
|
|
6
|
-
from launart import Launart
|
|
7
7
|
from launart.status import Phase
|
|
8
8
|
from loguru import logger
|
|
9
|
-
from starlette.requests import Request as StarletteRequest
|
|
10
9
|
from starlette.responses import JSONResponse, Response
|
|
11
|
-
from starlette.routing import Route
|
|
12
10
|
from yarl import URL
|
|
13
11
|
|
|
14
12
|
from satori import EventType
|
|
@@ -20,20 +18,13 @@ from satori.utils import decode, encode
|
|
|
20
18
|
|
|
21
19
|
from .api import apply
|
|
22
20
|
from .events import event_handlers
|
|
23
|
-
from .utils import
|
|
21
|
+
from .utils import decode_login_user
|
|
24
22
|
|
|
25
23
|
DEFAULT_FEATURES = ["guild.plain", "reaction"]
|
|
26
24
|
|
|
27
25
|
|
|
28
|
-
class
|
|
29
|
-
|
|
30
|
-
self.adapter = adapter
|
|
31
|
-
|
|
32
|
-
async def call_api(self, action: str, params: dict | None = None):
|
|
33
|
-
return await self.adapter.call_api(action, params or {})
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class MilkyWebhookAdapter(BaseAdapter):
|
|
26
|
+
class MilkyBaseAdapter(BaseAdapter, ABC):
|
|
27
|
+
"""Base adapter for Milky protocol with common functionality."""
|
|
37
28
|
|
|
38
29
|
session: aiohttp.ClientSession | None
|
|
39
30
|
|
|
@@ -43,8 +34,6 @@ class MilkyWebhookAdapter(BaseAdapter):
|
|
|
43
34
|
*,
|
|
44
35
|
token: str | None = None,
|
|
45
36
|
headers: dict[str, str] | None = None,
|
|
46
|
-
path: str = "/milky",
|
|
47
|
-
self_token: str | None = None,
|
|
48
37
|
):
|
|
49
38
|
super().__init__()
|
|
50
39
|
self.base_url = URL(str(endpoint))
|
|
@@ -54,11 +43,8 @@ class MilkyWebhookAdapter(BaseAdapter):
|
|
|
54
43
|
self.headers = headers.copy() if headers else {}
|
|
55
44
|
self.session = None
|
|
56
45
|
self.logins: dict[str, Login] = {}
|
|
57
|
-
self.
|
|
58
|
-
self
|
|
59
|
-
self.webhook_token = self_token if self_token is not None else token
|
|
60
|
-
self.webhook_paths = self._normalize_webhook_paths(path)
|
|
61
|
-
apply(self, self._get_network, self._get_login)
|
|
46
|
+
self.features: list[str] = list(DEFAULT_FEATURES)
|
|
47
|
+
apply(self, lambda _: self, self._get_login)
|
|
62
48
|
|
|
63
49
|
def get_platform(self) -> str:
|
|
64
50
|
return "milky"
|
|
@@ -80,19 +66,6 @@ class MilkyWebhookAdapter(BaseAdapter):
|
|
|
80
66
|
def stages(self) -> set[Phase]:
|
|
81
67
|
return {"preparing", "blocking", "cleanup"}
|
|
82
68
|
|
|
83
|
-
async def launch(self, manager: Launart):
|
|
84
|
-
async with self.stage("preparing"):
|
|
85
|
-
self.session = aiohttp.ClientSession()
|
|
86
|
-
|
|
87
|
-
async with self.stage("blocking"):
|
|
88
|
-
await manager.status.wait_for_sigexit()
|
|
89
|
-
|
|
90
|
-
async with self.stage("cleanup"):
|
|
91
|
-
if self.session:
|
|
92
|
-
await self.session.close()
|
|
93
|
-
self.session = None
|
|
94
|
-
await self._handle_disconnect()
|
|
95
|
-
|
|
96
69
|
def proxy_urls(self) -> list[str]:
|
|
97
70
|
return []
|
|
98
71
|
|
|
@@ -125,9 +98,6 @@ class MilkyWebhookAdapter(BaseAdapter):
|
|
|
125
98
|
raise ActionFailed(f"{data.get('retcode')}: {data.get('message')}", data)
|
|
126
99
|
return data.get("data")
|
|
127
100
|
|
|
128
|
-
def get_routes(self) -> list[Route]:
|
|
129
|
-
return [Route(path, self.webhook_endpoint, methods=["POST"]) for path in self.webhook_paths]
|
|
130
|
-
|
|
131
101
|
async def handle_event(self, payload: dict):
|
|
132
102
|
event_type = payload.get("event_type")
|
|
133
103
|
self_id = str(payload.get("self_id"))
|
|
@@ -140,22 +110,17 @@ class MilkyWebhookAdapter(BaseAdapter):
|
|
|
140
110
|
return
|
|
141
111
|
login = self.logins[self_id]
|
|
142
112
|
handler = event_handlers.get(event_type)
|
|
143
|
-
network = self.networks.get(self_id)
|
|
144
|
-
if not network:
|
|
145
|
-
network = _MilkyNetwork(self)
|
|
146
|
-
self.networks[self_id] = network
|
|
147
113
|
if handler:
|
|
148
|
-
event = await handler(login,
|
|
114
|
+
event = await handler(login, self, payload)
|
|
149
115
|
else:
|
|
150
|
-
body = payload.get("data", {})
|
|
151
116
|
event = Event(
|
|
152
117
|
EventType.INTERNAL,
|
|
153
118
|
datetime.fromtimestamp(payload.get("time", datetime.now().timestamp())),
|
|
154
119
|
login,
|
|
155
|
-
_type=event_type,
|
|
156
|
-
_data=body,
|
|
157
120
|
)
|
|
158
121
|
if event:
|
|
122
|
+
event._type = event_type
|
|
123
|
+
event._data = payload.get("data", {})
|
|
159
124
|
await self.server.post(event)
|
|
160
125
|
|
|
161
126
|
async def refresh_login(self):
|
|
@@ -171,7 +136,6 @@ class MilkyWebhookAdapter(BaseAdapter):
|
|
|
171
136
|
self_id = login.id
|
|
172
137
|
previous = self.logins.get(self_id)
|
|
173
138
|
self.logins[self_id] = login
|
|
174
|
-
self.networks[self_id] = _MilkyNetwork(self)
|
|
175
139
|
event_type = EventType.LOGIN_ADDED if previous is None else EventType.LOGIN_UPDATED
|
|
176
140
|
await self.server.post(Event(event_type, datetime.now(), login))
|
|
177
141
|
|
|
@@ -180,53 +144,9 @@ class MilkyWebhookAdapter(BaseAdapter):
|
|
|
180
144
|
login.status = LoginStatus.OFFLINE
|
|
181
145
|
await self.server.post(Event(EventType.LOGIN_REMOVED, datetime.now(), login))
|
|
182
146
|
self.logins.pop(self_id, None)
|
|
183
|
-
self.networks.pop(self_id, None)
|
|
184
|
-
|
|
185
|
-
def _normalize_webhook_paths(self, webhook_path: str) -> tuple[str, ...]:
|
|
186
|
-
path = webhook_path or "/"
|
|
187
|
-
normalized = path if path.startswith("/") else f"/{path}"
|
|
188
|
-
paths: set[str] = {normalized}
|
|
189
|
-
stripped = normalized.rstrip("/")
|
|
190
|
-
if stripped and stripped != normalized:
|
|
191
|
-
paths.add(stripped)
|
|
192
|
-
return tuple(sorted(paths))
|
|
193
|
-
|
|
194
|
-
def _get_network(self, self_id: str) -> MilkyNetwork:
|
|
195
|
-
network = self.networks.get(self_id)
|
|
196
|
-
if not network:
|
|
197
|
-
network = _MilkyNetwork(self)
|
|
198
|
-
self.networks[self_id] = network
|
|
199
|
-
return network
|
|
200
147
|
|
|
201
148
|
def _get_login(self, self_id: str) -> Login:
|
|
202
149
|
return self.logins[self_id]
|
|
203
150
|
|
|
204
|
-
async def webhook_endpoint(self, request: StarletteRequest) -> Response:
|
|
205
|
-
if self.webhook_token:
|
|
206
|
-
auth_header = request.headers.get("Authorization")
|
|
207
|
-
provided = None
|
|
208
|
-
if auth_header:
|
|
209
|
-
if auth_header.lower().startswith("bearer "):
|
|
210
|
-
provided = auth_header[7:]
|
|
211
|
-
else:
|
|
212
|
-
provided = auth_header
|
|
213
|
-
if provided is None:
|
|
214
|
-
provided = request.query_params.get("access_token")
|
|
215
|
-
if provided != self.webhook_token:
|
|
216
|
-
return JSONResponse({"error": "unauthorized"}, status_code=401)
|
|
217
|
-
try:
|
|
218
|
-
payload = await request.json()
|
|
219
|
-
except Exception as e: # pragma: no cover - defensive
|
|
220
|
-
logger.error(f"Failed to parse milky webhook payload: {e}")
|
|
221
|
-
return JSONResponse({"error": "invalid json"}, status_code=400)
|
|
222
|
-
if not isinstance(payload, dict):
|
|
223
|
-
return JSONResponse({"error": "invalid payload"}, status_code=400)
|
|
224
|
-
try:
|
|
225
|
-
await self.handle_event(payload)
|
|
226
|
-
except Exception as e: # pragma: no cover - defensive
|
|
227
|
-
logger.exception("Error while processing milky webhook event", exc_info=e)
|
|
228
|
-
return JSONResponse({"error": "internal error"}, status_code=500)
|
|
229
|
-
return Response(status_code=204)
|
|
230
|
-
|
|
231
151
|
|
|
232
|
-
__all__ = ["
|
|
152
|
+
__all__ = ["MilkyBaseAdapter", "DEFAULT_FEATURES"]
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
|
|
5
5
|
from satori import EventType
|
|
6
|
-
from satori.model import Channel, ChannelType, Event, Guild, MessageObject, User
|
|
6
|
+
from satori.model import Channel, ChannelType, EmojiObject, Event, Guild, MessageObject, User
|
|
7
7
|
|
|
8
8
|
from ..message import decode_message
|
|
9
9
|
from ..utils import group_avatar, user_avatar
|
|
@@ -61,9 +61,9 @@ async def group_message_reaction(login, net, raw):
|
|
|
61
61
|
guild = Guild(guild_id, avatar=group_avatar(guild_id))
|
|
62
62
|
channel = Channel(guild_id, ChannelType.TEXT)
|
|
63
63
|
user = User(str(data["user_id"]), avatar=user_avatar(data["user_id"]))
|
|
64
|
-
|
|
64
|
+
emoji_id = data["face_id"]
|
|
65
65
|
message = MessageObject(
|
|
66
|
-
str(data["message_seq"]), f"<
|
|
66
|
+
str(data["message_seq"]), f"<emoji id='{emoji_id}'>", channel=channel, guild=guild, user=user
|
|
67
67
|
)
|
|
68
68
|
if data["is_add"]:
|
|
69
69
|
event_type = EventType.REACTION_ADDED
|
|
@@ -77,4 +77,5 @@ async def group_message_reaction(login, net, raw):
|
|
|
77
77
|
guild=guild,
|
|
78
78
|
user=user,
|
|
79
79
|
message=message,
|
|
80
|
+
emoji=EmojiObject(emoji_id),
|
|
80
81
|
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
from launart import Launart, any_completed
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from yarl import URL
|
|
9
|
+
|
|
10
|
+
from satori.utils import decode
|
|
11
|
+
|
|
12
|
+
from .base import MilkyBaseAdapter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MilkyWebsocketAdapter(MilkyBaseAdapter):
|
|
16
|
+
|
|
17
|
+
connection: aiohttp.ClientWebSocketResponse | None
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
endpoint: str | URL,
|
|
22
|
+
*,
|
|
23
|
+
token: str | None = None,
|
|
24
|
+
token_in_query: bool = False,
|
|
25
|
+
headers: dict[str, str] | None = None,
|
|
26
|
+
):
|
|
27
|
+
super().__init__(endpoint, token=token, headers=headers)
|
|
28
|
+
base_path = self.base_url.path.rstrip("/")
|
|
29
|
+
ws_scheme = "wss" if self.base_url.scheme == "https" else "ws"
|
|
30
|
+
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)
|
|
33
|
+
self.connection = None
|
|
34
|
+
self.close_signal = asyncio.Event()
|
|
35
|
+
|
|
36
|
+
async def launch(self, manager: Launart):
|
|
37
|
+
async with self.stage("preparing"):
|
|
38
|
+
self.session = aiohttp.ClientSession()
|
|
39
|
+
|
|
40
|
+
async with self.stage("blocking"):
|
|
41
|
+
await self.connection_daemon(manager, self.session)
|
|
42
|
+
|
|
43
|
+
async with self.stage("cleanup"):
|
|
44
|
+
if self.connection and not self.connection.closed:
|
|
45
|
+
await self.connection.close()
|
|
46
|
+
if self.session:
|
|
47
|
+
await self.session.close()
|
|
48
|
+
self.connection = None
|
|
49
|
+
self.session = None
|
|
50
|
+
await self._handle_disconnect()
|
|
51
|
+
|
|
52
|
+
async def connection_daemon(self, manager: Launart, session: aiohttp.ClientSession):
|
|
53
|
+
while not manager.status.exiting:
|
|
54
|
+
headers = self.headers.copy()
|
|
55
|
+
if self.token:
|
|
56
|
+
headers.setdefault("Authorization", f"Bearer {self.token}")
|
|
57
|
+
try:
|
|
58
|
+
self.connection = await session.ws_connect(self.event_url, headers=headers)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.error(f"Milky adapter websocket connect failed: {e}")
|
|
61
|
+
await asyncio.sleep(5)
|
|
62
|
+
continue
|
|
63
|
+
logger.info("Milky adapter websocket connected")
|
|
64
|
+
self.close_signal.clear()
|
|
65
|
+
await self.refresh_login()
|
|
66
|
+
receiver_task = asyncio.create_task(self.message_handle())
|
|
67
|
+
close_task = asyncio.create_task(self.close_signal.wait())
|
|
68
|
+
sigexit_task = asyncio.create_task(manager.status.wait_for_sigexit())
|
|
69
|
+
|
|
70
|
+
done, pending = await any_completed(receiver_task, close_task, sigexit_task)
|
|
71
|
+
for task in pending:
|
|
72
|
+
task.cancel()
|
|
73
|
+
await asyncio.gather(*pending, return_exceptions=True)
|
|
74
|
+
if sigexit_task in done:
|
|
75
|
+
break
|
|
76
|
+
logger.warning("Milky adapter websocket closed, retrying in 5 seconds")
|
|
77
|
+
await self._handle_disconnect()
|
|
78
|
+
await asyncio.sleep(5)
|
|
79
|
+
await self._handle_disconnect()
|
|
80
|
+
|
|
81
|
+
async def message_handle(self):
|
|
82
|
+
assert self.connection is not None
|
|
83
|
+
async for msg in self.connection:
|
|
84
|
+
if msg.type in (aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.ERROR, aiohttp.WSMsgType.CLOSED):
|
|
85
|
+
self.close_signal.set()
|
|
86
|
+
break
|
|
87
|
+
if msg.type != aiohttp.WSMsgType.TEXT:
|
|
88
|
+
continue
|
|
89
|
+
try:
|
|
90
|
+
data = decode(msg.data)
|
|
91
|
+
except Exception as e: # pragma: no cover - defensive
|
|
92
|
+
logger.error(f"Failed to decode milky event: {e}")
|
|
93
|
+
continue
|
|
94
|
+
if not isinstance(data, dict):
|
|
95
|
+
continue
|
|
96
|
+
await self.handle_event(data)
|
|
97
|
+
|
|
98
|
+
async def _handle_disconnect(self):
|
|
99
|
+
await super()._handle_disconnect()
|
|
100
|
+
if self.connection and not self.connection.closed:
|
|
101
|
+
await self.connection.close()
|
|
102
|
+
self.close_signal.set()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = ["MilkyWebsocketAdapter"]
|