ErisPulse-KookAdapter 0.1.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.
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: ErisPulse-KookAdapter
3
+ Version: 0.1.0
4
+ Summary: ErisPulse Kook Adapter - Kook平台适配器
5
+ Author-email: ShanFish <shanfishdev@qq.com>
6
+ License: Apache-2.0
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: ErisPulse
11
+ Requires-Dist: websockets>=10.0
12
+ Requires-Dist: aiohttp>=3.8.0
13
+ Dynamic: license-file
14
+
15
+ # ErisPulse-KookAdapter
16
+ 基于开源机器人框架[ErisPulse](https://github.com/ErisPulse/ErisPulse)的Kook(开黑啦)机器人适配器。
17
+
18
+ 项目目前已托管至PyPi,使用以下命令以完成安装:
19
+ ```bash
20
+ pip install ErisPulse-KookAdapter
21
+ ```
22
+ 此时执行`ep list`,应该可以看到`ErisPulse-KookAdapter`在已安装的适配器列表中。
23
+
24
+ ## 使用
25
+ ```python
26
+ from ErisPulse import sdk
27
+ adapter = sdk.adapter.get("Kook")
28
+ ```
29
+ 然后就可以对adapter变量进行适配器方法操作了。
30
+
31
+ 适配器支持链式调用:
32
+ ```python
33
+ # @用户
34
+ await kook.Send.To("group", "频道ID").At("用户ID").Text("Hello")
35
+
36
+ # @多个用户
37
+ await kook.Send.To("group", "频道ID").At("用户1").At("用户2").Text("Hello")
38
+
39
+ # @全体
40
+ await kook.Send.To("group", "频道ID").AtAll().Text("公告")
41
+
42
+ # 回复消息
43
+ await kook.Send.To("group", "频道ID").Reply("消息ID").Text("回复内容")
44
+
45
+ # 组合使用
46
+ await kook.Send.To("group", "频道ID").At("用户ID").Reply("消息ID").Text("Hello")
47
+ ```
48
+
49
+ 另外,支持不同消息`SendDSL`调用:
50
+ ```python
51
+ # 文本消息
52
+ await kook.Send.To("group", "频道ID").Text("Hello World")
53
+
54
+ # 图片消息
55
+ await kook.Send.To("group", "频道ID").Image("http://example.com/image.jpg")
56
+
57
+ # 视频消息
58
+ await kook.Send.To("group", "频道ID").Video("http://example.com/video.mp4")
59
+
60
+ # 文件消息
61
+ await kook.Send.To("group", "频道ID").File("http://example.com/file.pdf")
62
+
63
+ # 语音消息
64
+ await kook.Send.To("group", "频道ID").Voice("http://example.com/voice.mp3")
65
+
66
+ # KMarkdown 消息
67
+ await kook.Send.To("group", "频道ID").Markdown("**粗体** *斜体*")
68
+
69
+ # 卡片消息
70
+ await kook.Send.To("group", "频道ID").Card({
71
+ "type": "card",
72
+ "theme": "primary",
73
+ "size": "lg",
74
+ "modules": [
75
+ {"type": "header", "text": {"type": "plain-text", "content": "标题"}},
76
+ {"type": "section", "text": {"type": "kmarkdown", "content": "内容"}}
77
+ ]
78
+ })
79
+ ```
80
+
81
+ ### 私信消息
82
+ ```python
83
+ # 发送私信给指定用户
84
+ await kook.Send.To("user", "用户ID").Text("Hello")
85
+ await kook.Send.To("user", "用户ID").Markdown("**私信内容**")
86
+ await kook.Send.To("user", "用户ID").Card({...})
87
+ ```
88
+
89
+ ### 消息编辑与撤回
90
+ ```python
91
+ # 发送消息并获取消息ID
92
+ result = await kook.Send.To("group", "频道ID").Markdown("**原始内容**")
93
+ msg_id = result["data"]["msg_id"]
94
+
95
+ # 编辑消息(仅支持 KMarkdown 和 CardMessage)
96
+ await kook.Send.To("group", "频道ID").Edit(msg_id, "**更新后的内容**")
97
+
98
+ # 撤回消息
99
+ await kook.Send.To("group", "频道ID").Recall(msg_id)
100
+
101
+ # 上传本地文件 并获取文件URL
102
+ result = await kook.Send.Upload("C:/path/to/image.jpg")
103
+ file_url = result["data"]["url"]
104
+ ```
105
+
106
+
107
+ ## 参考文档
108
+ - [Kook API 文档](https://developer.kookapp.cn/doc)
109
+ - [ErisPulse 文档](https://erisdev.com)
110
+ - [ErisPulse 主库](https://github.com/ErisPulse/ErisPulse)
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ ErisPulse_KookAdapter.egg-info/PKG-INFO
5
+ ErisPulse_KookAdapter.egg-info/SOURCES.txt
6
+ ErisPulse_KookAdapter.egg-info/dependency_links.txt
7
+ ErisPulse_KookAdapter.egg-info/entry_points.txt
8
+ ErisPulse_KookAdapter.egg-info/requires.txt
9
+ ErisPulse_KookAdapter.egg-info/top_level.txt
10
+ Kook/CallApi.py
11
+ Kook/Converter.py
12
+ Kook/Core.py
13
+ Kook/__init__.py
@@ -0,0 +1,2 @@
1
+ [erispulse.adapter]
2
+ Kook = Kook:KookAdapter
@@ -0,0 +1,3 @@
1
+ ErisPulse
2
+ websockets>=10.0
3
+ aiohttp>=3.8.0
@@ -0,0 +1,291 @@
1
+ import aiohttp
2
+ import uuid
3
+
4
+ class CallApi:
5
+ def __init__(self, token: str):
6
+ self.token = token
7
+ self.session = aiohttp.ClientSession()
8
+
9
+ async def close(self):
10
+ if self.session.closed:
11
+ return
12
+ await self.session.close()
13
+
14
+ async def start(self):
15
+ if not self.session.closed:
16
+ return
17
+ await self.session.start()
18
+
19
+ async def send_message(
20
+ self,
21
+ target_id: str,
22
+ type: int,
23
+ content: str,
24
+ quote_msg_id: str = None,
25
+ template_id: str = None,
26
+ **kwargs
27
+ ) -> dict:
28
+ """发送频道消息"""
29
+ if not self.token:
30
+ return {
31
+ "code": -1,
32
+ "msg": "token未刷新, 请刷新后重试",
33
+ "data": {}
34
+ }
35
+ nonce = str(uuid.uuid4())
36
+ payload = {
37
+ "nonce": nonce,
38
+ "target_id": target_id,
39
+ "type": type,
40
+ "content": content,
41
+ }
42
+ if quote_msg_id:
43
+ payload["quote"] = quote_msg_id
44
+ if template_id:
45
+ payload["template_id"] = template_id
46
+
47
+ # 添加其他可选参数(如 mention, mention_all 等)
48
+ for key, value in kwargs.items():
49
+ if value is not None:
50
+ payload[key] = value
51
+
52
+ async with self.session.post(
53
+ "https://www.kookapp.cn/api/v3/message/create",
54
+ json=payload,
55
+ headers={
56
+ "Authorization": f"Bot {self.token}",
57
+ "Content-Type": "application/json"
58
+ }
59
+ ) as resp:
60
+ data = await resp.json()
61
+ return data
62
+
63
+ async def send_direct_message(
64
+ self,
65
+ target_id: str,
66
+ type: int,
67
+ content: str,
68
+ quote_msg_id: str = None,
69
+ template_id: str = None,
70
+ **kwargs
71
+ ) -> dict:
72
+ """发送私信消息"""
73
+ if not self.token:
74
+ return {
75
+ "code": -1,
76
+ "msg": "token未刷新, 请刷新后重试",
77
+ "data": {}
78
+ }
79
+ nonce = str(uuid.uuid4())
80
+ payload = {
81
+ "nonce": nonce,
82
+ "target_id": target_id,
83
+ "type": type,
84
+ "content": content,
85
+ }
86
+ if quote_msg_id:
87
+ payload["quote"] = quote_msg_id
88
+ if template_id:
89
+ payload["template_id"] = template_id
90
+
91
+ # 添加其他可选参数(如 reply_msg_id 等)
92
+ for key, value in kwargs.items():
93
+ if value is not None:
94
+ payload[key] = value
95
+
96
+ async with self.session.post(
97
+ "https://www.kookapp.cn/api/v3/direct-message/create",
98
+ json=payload,
99
+ headers={
100
+ "Authorization": f"Bot {self.token}",
101
+ "Content-Type": "application/json"
102
+ }
103
+ ) as resp:
104
+ data = await resp.json()
105
+ return data
106
+
107
+ async def update_direct_message(
108
+ self,
109
+ msg_id: str,
110
+ content: str,
111
+ quote: str = None,
112
+ template_id: str = None,
113
+ **kwargs
114
+ ) -> dict:
115
+ """更新私信消息(仅支持 KMarkdown type=9 和 CardMessage type=10)"""
116
+ if not self.token:
117
+ return {
118
+ "code": -1,
119
+ "msg": "token未刷新, 请刷新后重试",
120
+ "data": {}
121
+ }
122
+ payload = {
123
+ "msg_id": msg_id,
124
+ "content": content,
125
+ }
126
+ if quote is not None:
127
+ payload["quote"] = quote
128
+ if template_id:
129
+ payload["template_id"] = template_id
130
+
131
+ # 添加其他可选参数
132
+ for key, value in kwargs.items():
133
+ if value is not None:
134
+ payload[key] = value
135
+
136
+ async with self.session.post(
137
+ "https://www.kookapp.cn/api/v3/direct-message/update",
138
+ json=payload,
139
+ headers={
140
+ "Authorization": f"Bot {self.token}",
141
+ "Content-Type": "application/json"
142
+ }
143
+ ) as resp:
144
+ data = await resp.json()
145
+ return data
146
+
147
+ async def delete_direct_message(
148
+ self,
149
+ msg_id: str,
150
+ **kwargs
151
+ ) -> dict:
152
+ """删除私信消息"""
153
+ if not self.token:
154
+ return {
155
+ "code": -1,
156
+ "msg": "token未刷新, 请刷新后重试",
157
+ "data": {}
158
+ }
159
+ payload = {
160
+ "msg_id": msg_id,
161
+ }
162
+
163
+ # 添加其他可选参数
164
+ for key, value in kwargs.items():
165
+ if value is not None:
166
+ payload[key] = value
167
+
168
+ async with self.session.post(
169
+ "https://www.kookapp.cn/api/v3/direct-message/delete",
170
+ json=payload,
171
+ headers={
172
+ "Authorization": f"Bot {self.token}",
173
+ "Content-Type": "application/json"
174
+ }
175
+ ) as resp:
176
+ data = await resp.json()
177
+ return data
178
+
179
+ async def update_channel_message(
180
+ self,
181
+ msg_id: str,
182
+ content: str,
183
+ quote: str = None,
184
+ temp_target_id: str = None,
185
+ template_id: str = None,
186
+ **kwargs
187
+ ) -> dict:
188
+ """更新频道消息(仅支持 KMarkdown type=9 和 CardMessage type=10)"""
189
+ if not self.token:
190
+ return {
191
+ "code": -1,
192
+ "msg": "token未刷新, 请刷新后重试",
193
+ "data": {}
194
+ }
195
+ payload = {
196
+ "msg_id": msg_id,
197
+ "content": content,
198
+ }
199
+ if quote is not None:
200
+ payload["quote"] = quote
201
+ if temp_target_id:
202
+ payload["temp_target_id"] = temp_target_id
203
+ if template_id:
204
+ payload["template_id"] = template_id
205
+
206
+ # 添加其他可选参数
207
+ for key, value in kwargs.items():
208
+ if value is not None:
209
+ payload[key] = value
210
+
211
+ async with self.session.post(
212
+ "https://www.kookapp.cn/api/v3/message/update",
213
+ json=payload,
214
+ headers={
215
+ "Authorization": f"Bot {self.token}",
216
+ "Content-Type": "application/json"
217
+ }
218
+ ) as resp:
219
+ data = await resp.json()
220
+ return data
221
+
222
+ async def delete_channel_message(
223
+ self,
224
+ msg_id: str,
225
+ **kwargs
226
+ ) -> dict:
227
+ """删除频道消息"""
228
+ if not self.token:
229
+ return {
230
+ "code": -1,
231
+ "msg": "token未刷新, 请刷新后重试",
232
+ "data": {}
233
+ }
234
+ payload = {
235
+ "msg_id": msg_id,
236
+ }
237
+
238
+ # 添加其他可选参数
239
+ for key, value in kwargs.items():
240
+ if value is not None:
241
+ payload[key] = value
242
+
243
+ async with self.session.post(
244
+ "https://www.kookapp.cn/api/v3/message/delete",
245
+ json=payload,
246
+ headers={
247
+ "Authorization": f"Bot {self.token}",
248
+ "Content-Type": "application/json"
249
+ }
250
+ ) as resp:
251
+ data = await resp.json()
252
+ return data
253
+
254
+ async def upload_file(self, file_path: str) -> dict:
255
+ if not self.token:
256
+ return {
257
+ "code": -1,
258
+ "msg": "token未刷新, 请刷新后重试",
259
+ "data": {}
260
+ }
261
+
262
+ async with self.session.post(
263
+ "https://www.kookapp.cn/api/v3/asset/create",
264
+ headers={
265
+ "Authorization": f"Bot {self.token}",
266
+ "Content-Type": "multipart/form-data"
267
+ },
268
+ data={
269
+ "file": open(file_path, "rb")
270
+ }
271
+ ) as resp:
272
+ data = await resp.json()
273
+ return data
274
+
275
+ async def get_ws_gateway(self, need_compress: bool = True) -> str:
276
+ if not self.token:
277
+ return ""
278
+ async with self.session.post(
279
+ "https://www.kookapp.cn/api/v3/gateway/index",
280
+ headers={
281
+ "Authorization": f"Bot {self.token}",
282
+ "Content-Type": "application/json"
283
+ },
284
+ json={
285
+ "need_compress": 1 if need_compress else 0
286
+ }
287
+ ) as resp:
288
+ data = await resp.json()
289
+ if data.get("code") == 0:
290
+ return data.get("data", {}).get("url", "")
291
+ return ""
@@ -0,0 +1,175 @@
1
+ """
2
+ Kook Event -> OneBot V12
3
+ 标准协议转换器
4
+ """
5
+
6
+ import json
7
+ import uuid
8
+ import time
9
+
10
+
11
+ class KookAdapterConverter:
12
+ def convert(self, data):
13
+ d = data.get("d", {})
14
+ extra = d.get("extra", {})
15
+ author = extra.get("author", {})
16
+ kook_type = d.get("type", 0)
17
+ channel_type = d.get("channel_type", "")
18
+
19
+ onebot_data = {
20
+ "id": str(uuid.uuid4()).replace("-", ""),
21
+ "time": time.time(),
22
+ "type": self._get_message_type(data),
23
+ "detail_type": self._get_detail_type(data),
24
+ "platform": "Kook",
25
+ "self": {
26
+ "platform": "Kook",
27
+ "user_id": ""
28
+ },
29
+ "Kook_raw": data,
30
+ "Kook_raw_type": kook_type,
31
+ }
32
+
33
+ if onebot_data["type"] == "message":
34
+ onebot_data.update({
35
+ "message_id": d.get("msg_id", ""),
36
+ "user_id": author.get("id", ""),
37
+ "message": self._convert_message_content(data),
38
+ "alt_message": d.get("content", ""),
39
+ })
40
+
41
+ if channel_type == "PERSON":
42
+ onebot_data["detail_type"] = "private"
43
+ elif channel_type == "GROUP":
44
+ onebot_data["group_id"] = d.get("target_id", "")
45
+
46
+ mentions = extra.get("mention", [])
47
+ if mentions:
48
+ onebot_data["mentions"] = mentions
49
+
50
+ elif onebot_data["type"] == "notice":
51
+ onebot_data["user_id"] = author.get("id", "")
52
+ onebot_data["guild_id"] = extra.get("guild_id", "")
53
+ onebot_data.update(self._convert_notice_data(data))
54
+
55
+ return onebot_data
56
+
57
+ def _get_message_type(self, data):
58
+ kook_type = data.get("d", {}).get("type", 0)
59
+ return {
60
+ 255: "notice"
61
+ }.get(kook_type, "message")
62
+
63
+ def _get_detail_type(self, data):
64
+ d = data.get("d", {})
65
+ kook_type = d.get("type", 0)
66
+ if kook_type == 255:
67
+ return data.get("d", {}).get("extra", {}).get("type", "")
68
+
69
+ return {
70
+ 1: "text",
71
+ 2: "image",
72
+ 3: "video",
73
+ 4: "file",
74
+ 8: "record",
75
+ 9: "kmarkdown",
76
+ 10: "json",
77
+ }.get(kook_type, "unknown")
78
+
79
+ def _convert_message_content(self, data):
80
+ d = data.get("d", {})
81
+ extra = d.get("extra", {})
82
+ kook_type = d.get("type", 0)
83
+ content = d.get("content", "")
84
+
85
+ message_segments = []
86
+
87
+ if kook_type == 1:
88
+ message_segments.append({"type": "text", "data": {"text": content}})
89
+ elif kook_type == 2:
90
+ attachments = extra.get("attachments", {})
91
+ url = attachments.get("url", content)
92
+ message_segments.append({"type": "image", "data": {"file": url, "url": url}})
93
+ elif kook_type == 3:
94
+ attachments = extra.get("attachments", {})
95
+ url = attachments.get("url", content)
96
+ message_segments.append({"type": "video", "data": {"file": url, "url": url}})
97
+ elif kook_type == 4:
98
+ attachments = extra.get("attachments", {})
99
+ url = attachments.get("url", content)
100
+ message_segments.append({"type": "file", "data": {"file": url, "url": url}})
101
+ elif kook_type == 8:
102
+ attachments = extra.get("attachments", {})
103
+ url = attachments.get("url", content)
104
+ message_segments.append({"type": "record", "data": {"file": url, "url": url}})
105
+ elif kook_type == 9:
106
+ kmarkdown = extra.get("kmarkdown", {})
107
+ raw_content = kmarkdown.get("raw_content", content)
108
+ message_segments.append({"type": "text", "data": {"text": raw_content}})
109
+ elif kook_type == 10:
110
+ message_segments.append({"type": "json", "data": {"data": content}})
111
+ else:
112
+ message_segments.append({"type": "text", "data": {"text": content}})
113
+
114
+ mention = extra.get("mention", [])
115
+ if mention:
116
+ for user_id in mention:
117
+ message_segments.insert(0, {"type": "mention", "data": {"user_id": user_id}})
118
+
119
+ if extra.get("mention_all", False):
120
+ message_segments.insert(0, {"type": "mention_all", "data": {}})
121
+
122
+ return message_segments
123
+
124
+ def _convert_notice_data(self, data):
125
+ d = data.get("d", {})
126
+ extra = d.get("extra", {})
127
+ event_type = extra.get("type", "")
128
+ body = extra.get("body", {})
129
+
130
+ notice_data = {}
131
+
132
+ # 映射 Kook 事件类型到标准 notice_type
133
+ if event_type == "added_reaction":
134
+ notice_data["notice_type"] = "reaction_added"
135
+ notice_data["sub_type"] = "added_reaction"
136
+ notice_data["channel_id"] = body.get("channel_id", "")
137
+ notice_data["message_id"] = body.get("msg_id", "")
138
+ notice_data["user_id"] = body.get("user_id", "")
139
+ notice_data["emoji"] = body.get("emoji", {})
140
+ elif event_type == "deleted_reaction":
141
+ notice_data["notice_type"] = "reaction_removed"
142
+ notice_data["sub_type"] = "deleted_reaction"
143
+ notice_data["channel_id"] = body.get("channel_id", "")
144
+ notice_data["message_id"] = body.get("msg_id", "")
145
+ notice_data["user_id"] = body.get("user_id", "")
146
+ notice_data["emoji"] = body.get("emoji", {})
147
+ elif event_type == "private_added_reaction":
148
+ notice_data["notice_type"] = "reaction_added"
149
+ notice_data["sub_type"] = "private_added_reaction"
150
+ notice_data["message_id"] = body.get("msg_id", "")
151
+ notice_data["user_id"] = body.get("user_id", "")
152
+ notice_data["emoji"] = body.get("emoji", {})
153
+ elif event_type == "private_deleted_reaction":
154
+ notice_data["notice_type"] = "reaction_removed"
155
+ notice_data["sub_type"] = "private_deleted_reaction"
156
+ notice_data["message_id"] = body.get("msg_id", "")
157
+ notice_data["user_id"] = body.get("user_id", "")
158
+ notice_data["emoji"] = body.get("emoji", {})
159
+ elif event_type == "updated_private_message":
160
+ notice_data["notice_type"] = "message_updated"
161
+ notice_data["sub_type"] = "updated_private_message"
162
+ notice_data["message_id"] = body.get("msg_id", "")
163
+ notice_data["user_id"] = body.get("author_id", "")
164
+ notice_data["content"] = body.get("content", "")
165
+ elif event_type == "deleted_private_message":
166
+ notice_data["notice_type"] = "message_deleted"
167
+ notice_data["sub_type"] = "deleted_private_message"
168
+ notice_data["message_id"] = body.get("msg_id", "")
169
+ notice_data["user_id"] = body.get("author_id", "")
170
+ else:
171
+ notice_data["notice_type"] = "system"
172
+ notice_data["sub_type"] = event_type if event_type else "system"
173
+ notice_data["raw_event"] = extra
174
+
175
+ return notice_data