python-library-napcat-adapter 0.1.0__tar.gz → 0.1.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.
- {python_library_napcat_adapter-0.1.0 → python_library_napcat_adapter-0.1.1}/PKG-INFO +1 -1
- {python_library_napcat_adapter-0.1.0 → python_library_napcat_adapter-0.1.1}/napcat_adapter/models.py +16 -11
- python_library_napcat_adapter-0.1.1/napcat_adapter/protocol_adapt.py +318 -0
- {python_library_napcat_adapter-0.1.0 → python_library_napcat_adapter-0.1.1}/pyproject.toml +1 -1
- python_library_napcat_adapter-0.1.0/napcat_adapter/protocol_adapt.py +0 -194
- {python_library_napcat_adapter-0.1.0 → python_library_napcat_adapter-0.1.1}/.gitignore +0 -0
- {python_library_napcat_adapter-0.1.0 → python_library_napcat_adapter-0.1.1}/napcat_adapter/__init__.py +0 -0
- {python_library_napcat_adapter-0.1.0 → python_library_napcat_adapter-0.1.1}/napcat_adapter/adapter.py +0 -0
- {python_library_napcat_adapter-0.1.0 → python_library_napcat_adapter-0.1.1}/napcat_adapter/bot.py +0 -0
- {python_library_napcat_adapter-0.1.0 → python_library_napcat_adapter-0.1.1}/napcat_adapter/listener.py +0 -0
{python_library_napcat_adapter-0.1.0 → python_library_napcat_adapter-0.1.1}/napcat_adapter/models.py
RENAMED
|
@@ -98,11 +98,18 @@ class ImageSegment(BaseSegment):
|
|
|
98
98
|
"""图片段。"""
|
|
99
99
|
|
|
100
100
|
type: Literal["image"] = "image"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class RecordSegment(BaseSegment):
|
|
104
|
+
"""语音段(CQ record)。"""
|
|
105
|
+
|
|
106
|
+
type: Literal["record"] = "record"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class FileSegment(BaseSegment):
|
|
110
|
+
"""文件段。"""
|
|
111
|
+
|
|
112
|
+
type: Literal["file"] = "file"
|
|
106
113
|
|
|
107
114
|
|
|
108
115
|
class ForwardSegment(BaseSegment):
|
|
@@ -116,18 +123,14 @@ class VideoSegment(BaseSegment):
|
|
|
116
123
|
"""视频段。"""
|
|
117
124
|
|
|
118
125
|
type: Literal["video"] = "video"
|
|
119
|
-
file: str | None = None
|
|
120
|
-
url: str | None = None
|
|
121
|
-
|
|
122
|
-
def model_post_init(self, ctx) -> None:
|
|
123
|
-
self.file = self.data["file"]
|
|
124
|
-
self.url = self.data["url"]
|
|
125
126
|
|
|
126
127
|
|
|
127
128
|
Segment = Annotated[
|
|
128
129
|
Union[
|
|
129
130
|
TextSegment,
|
|
130
131
|
ImageSegment,
|
|
132
|
+
RecordSegment,
|
|
133
|
+
FileSegment,
|
|
131
134
|
FaceSegment,
|
|
132
135
|
AtSegment,
|
|
133
136
|
ForwardSegment,
|
|
@@ -158,6 +161,8 @@ __all__ = [
|
|
|
158
161
|
"BaseSegment",
|
|
159
162
|
"TextSegment",
|
|
160
163
|
"ImageSegment",
|
|
164
|
+
"RecordSegment",
|
|
165
|
+
"FileSegment",
|
|
161
166
|
"FaceSegment",
|
|
162
167
|
"AtSegment",
|
|
163
168
|
"ForwardSegment",
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
from typing import List, Union, Optional
|
|
2
|
+
|
|
3
|
+
import onebot_protocol
|
|
4
|
+
from onebot_protocol import MessagePayload
|
|
5
|
+
from onebot_protocol.models import (
|
|
6
|
+
FileSegmentData,
|
|
7
|
+
ImageSegmentData,
|
|
8
|
+
LocationSegmentData,
|
|
9
|
+
ReplySegmentData,
|
|
10
|
+
VideoSegmentData,
|
|
11
|
+
VoiceSegmentData,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from napcat_adapter.models import (
|
|
15
|
+
AtSegment,
|
|
16
|
+
BaseSegment,
|
|
17
|
+
BotMessage,
|
|
18
|
+
FileSegment,
|
|
19
|
+
ImageSegment,
|
|
20
|
+
LocationSegment,
|
|
21
|
+
MessageType,
|
|
22
|
+
RecordSegment,
|
|
23
|
+
ReplySegment,
|
|
24
|
+
TextSegment,
|
|
25
|
+
VideoSegment,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
SEGMENT_MAP = {
|
|
29
|
+
"text": TextSegment,
|
|
30
|
+
"image": ImageSegment,
|
|
31
|
+
"record": RecordSegment,
|
|
32
|
+
"file": FileSegment,
|
|
33
|
+
"at": AtSegment,
|
|
34
|
+
"reply": ReplySegment,
|
|
35
|
+
"video": VideoSegment,
|
|
36
|
+
"location": LocationSegment,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
USER_MAP: dict[str, str] = {}
|
|
40
|
+
|
|
41
|
+
MENTION_ALL_NAME = "全体成员"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def data_to_segments(data_list: list[dict], bot_name: str, bot_id: str) -> list[BaseSegment]:
|
|
45
|
+
"""把原始段字典列表转为强类型段,并拆分正文里的 @ 机器人。
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
data_list: 事件中的 CQ 段列表
|
|
49
|
+
bot_name: 机器人昵称,用于匹配 @
|
|
50
|
+
bot_id: 机器人 QQ 号
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
过滤无效项并展开 @ 后的段列表
|
|
54
|
+
"""
|
|
55
|
+
segments = [_cast_segment(x) for x in data_list]
|
|
56
|
+
segments = [x for x in segments if x]
|
|
57
|
+
|
|
58
|
+
segments = [
|
|
59
|
+
(_extract_mention_robot(x, bot_name, bot_id) if isinstance(x, TextSegment) else [x])
|
|
60
|
+
for x in segments
|
|
61
|
+
]
|
|
62
|
+
segments = [x for data in segments for x in data]
|
|
63
|
+
|
|
64
|
+
return segments
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def onebot_to_bot(payload: MessagePayload) -> BotMessage:
|
|
68
|
+
"""把对外统一载荷转成 NapCat 可发送的 CQ 段列表。
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
payload: 会话、消息段与机器人标识
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
含 data_list 的包内机器人消息
|
|
75
|
+
"""
|
|
76
|
+
data_list = []
|
|
77
|
+
for message in payload.messages:
|
|
78
|
+
segment = _onebot_to_cq_segment(message)
|
|
79
|
+
if segment is None:
|
|
80
|
+
continue
|
|
81
|
+
data_list.append(segment.model_dump())
|
|
82
|
+
|
|
83
|
+
msg = BotMessage(
|
|
84
|
+
message_id=payload.message_id,
|
|
85
|
+
data_list=data_list,
|
|
86
|
+
message_type=MessageType(payload.source_type),
|
|
87
|
+
bot_id=payload.bot_id,
|
|
88
|
+
session_id=payload.session_id,
|
|
89
|
+
user_name=payload.user_id or "",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if msg.message_type == MessageType.GROUP:
|
|
93
|
+
if msg.user_name:
|
|
94
|
+
msg.data_list = [
|
|
95
|
+
AtSegment(data={"qq": msg.user_name, "name": USER_MAP.get(msg.user_name, "")}).model_dump(),
|
|
96
|
+
TextSegment(data={"text": " "}).model_dump(),
|
|
97
|
+
] + msg.data_list
|
|
98
|
+
|
|
99
|
+
return msg
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def bot_to_onebot(msg: BotMessage) -> Optional[MessagePayload]:
|
|
103
|
+
"""把 NapCat 入站消息转为对外统一载荷;群聊未 @ 机器人时返回空。
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
msg: 事件解析后的包内消息
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
可上报的统一载荷;无需上报时为 None
|
|
110
|
+
"""
|
|
111
|
+
global USER_MAP
|
|
112
|
+
|
|
113
|
+
segments = data_to_segments(msg.data_list, msg.bot_name, msg.bot_id)
|
|
114
|
+
|
|
115
|
+
if not _should_broadcast(msg, segments):
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
segments = [s for s in segments if not (isinstance(s, AtSegment) and s.qq == msg.bot_id)]
|
|
119
|
+
|
|
120
|
+
messages = []
|
|
121
|
+
for segment in segments:
|
|
122
|
+
if isinstance(segment, AtSegment) and segment.qq:
|
|
123
|
+
USER_MAP[segment.qq] = segment.name or ""
|
|
124
|
+
converted = _segment_to_onebot(segment)
|
|
125
|
+
if converted is not None:
|
|
126
|
+
messages.append(converted)
|
|
127
|
+
|
|
128
|
+
if not messages:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
return MessagePayload(
|
|
132
|
+
message_id=msg.message_id,
|
|
133
|
+
source_type=msg.message_type.value,
|
|
134
|
+
bot_id=msg.bot_id,
|
|
135
|
+
session_id=msg.session_id,
|
|
136
|
+
user_id=msg.user_name,
|
|
137
|
+
messages=messages,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _cq_data_to_file_data(data: dict) -> Optional[FileSegmentData]:
|
|
142
|
+
"""从 CQ data 提取 FileData;无可用内容引用时返回 None。"""
|
|
143
|
+
content = data.get("url") or data.get("file") or data.get("path")
|
|
144
|
+
if not content:
|
|
145
|
+
return None
|
|
146
|
+
name = data.get("name") or data.get("filename")
|
|
147
|
+
mime_type = data.get("mime") or data.get("mime_type")
|
|
148
|
+
raw_size = data.get("file_size", data.get("size"))
|
|
149
|
+
size: int | None = None
|
|
150
|
+
if raw_size is not None:
|
|
151
|
+
try:
|
|
152
|
+
size = int(raw_size)
|
|
153
|
+
except (TypeError, ValueError):
|
|
154
|
+
size = None
|
|
155
|
+
return FileSegmentData(name=name, content=str(content), mime_type=mime_type, size=size)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _file_data_to_cq(data: FileSegmentData) -> dict:
|
|
159
|
+
"""把 FileData 编成 NapCat / OneBot 11 可识别的 CQ data。"""
|
|
160
|
+
out: dict = {}
|
|
161
|
+
content = data.content
|
|
162
|
+
if not content:
|
|
163
|
+
return out
|
|
164
|
+
out["file"] = content
|
|
165
|
+
if content.startswith(("http://", "https://")):
|
|
166
|
+
out["url"] = content
|
|
167
|
+
if data.name:
|
|
168
|
+
out["name"] = data.name
|
|
169
|
+
if data.size is not None:
|
|
170
|
+
out["file_size"] = data.size
|
|
171
|
+
return out
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _onebot_to_cq_segment(
|
|
175
|
+
message: onebot_protocol.MessageSegment,
|
|
176
|
+
) -> BaseSegment | None:
|
|
177
|
+
if isinstance(message, onebot_protocol.TextMessageSegment):
|
|
178
|
+
return TextSegment(data={"text": message.data.text})
|
|
179
|
+
if isinstance(message, onebot_protocol.MentionMessageSegment):
|
|
180
|
+
name = USER_MAP.get(message.data.user_id)
|
|
181
|
+
if not name:
|
|
182
|
+
return None
|
|
183
|
+
return AtSegment(data={"qq": message.data.user_id, "name": name})
|
|
184
|
+
if isinstance(message, onebot_protocol.ImageMessageSegment):
|
|
185
|
+
cq_data = _file_data_to_cq(message.data)
|
|
186
|
+
return ImageSegment(data=cq_data) if cq_data else None
|
|
187
|
+
if isinstance(message, onebot_protocol.VoiceMessageSegment):
|
|
188
|
+
cq_data = _file_data_to_cq(message.data)
|
|
189
|
+
return RecordSegment(data=cq_data) if cq_data else None
|
|
190
|
+
if isinstance(message, onebot_protocol.AudioMessageSegment):
|
|
191
|
+
cq_data = _file_data_to_cq(message.data)
|
|
192
|
+
return FileSegment(data=cq_data) if cq_data else None
|
|
193
|
+
if isinstance(message, onebot_protocol.VideoMessageSegment):
|
|
194
|
+
cq_data = _file_data_to_cq(message.data)
|
|
195
|
+
return VideoSegment(data=cq_data) if cq_data else None
|
|
196
|
+
if isinstance(message, onebot_protocol.FileMessageSegment):
|
|
197
|
+
cq_data = _file_data_to_cq(message.data)
|
|
198
|
+
return FileSegment(data=cq_data) if cq_data else None
|
|
199
|
+
if isinstance(message, onebot_protocol.LocationMessageSegment):
|
|
200
|
+
loc = message.data
|
|
201
|
+
return LocationSegment(
|
|
202
|
+
data={
|
|
203
|
+
"lat": loc.latitude,
|
|
204
|
+
"lon": loc.longitude,
|
|
205
|
+
"title": loc.title,
|
|
206
|
+
"content": loc.content,
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
if isinstance(message, onebot_protocol.ReplyMessageSegment):
|
|
210
|
+
reply_id = message.data.message_id
|
|
211
|
+
if not reply_id:
|
|
212
|
+
return None
|
|
213
|
+
return ReplySegment(data={"id": reply_id})
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _segment_to_onebot(
|
|
218
|
+
segment: BaseSegment,
|
|
219
|
+
) -> onebot_protocol.MessageSegment | None:
|
|
220
|
+
if isinstance(segment, TextSegment):
|
|
221
|
+
text = (segment.text or "").strip()
|
|
222
|
+
if not text:
|
|
223
|
+
return None
|
|
224
|
+
return onebot_protocol.TextMessageSegment(data={"text": text})
|
|
225
|
+
if isinstance(segment, AtSegment):
|
|
226
|
+
if segment.name == MENTION_ALL_NAME:
|
|
227
|
+
return onebot_protocol.MentionAllMessageSegment()
|
|
228
|
+
return onebot_protocol.MentionMessageSegment(data={"user_id": segment.qq})
|
|
229
|
+
if isinstance(segment, ImageSegment):
|
|
230
|
+
file_data = _cq_data_to_file_data(segment.data)
|
|
231
|
+
if file_data is None:
|
|
232
|
+
return None
|
|
233
|
+
return onebot_protocol.ImageMessageSegment(data=ImageSegmentData(**file_data.model_dump()))
|
|
234
|
+
if isinstance(segment, RecordSegment):
|
|
235
|
+
file_data = _cq_data_to_file_data(segment.data)
|
|
236
|
+
if file_data is None:
|
|
237
|
+
return None
|
|
238
|
+
return onebot_protocol.VoiceMessageSegment(data=VoiceSegmentData(**file_data.model_dump()))
|
|
239
|
+
if isinstance(segment, VideoSegment):
|
|
240
|
+
file_data = _cq_data_to_file_data(segment.data)
|
|
241
|
+
if file_data is None:
|
|
242
|
+
return None
|
|
243
|
+
return onebot_protocol.VideoMessageSegment(data=VideoSegmentData(**file_data.model_dump()))
|
|
244
|
+
if isinstance(segment, FileSegment):
|
|
245
|
+
file_data = _cq_data_to_file_data(segment.data)
|
|
246
|
+
if file_data is None:
|
|
247
|
+
return None
|
|
248
|
+
return onebot_protocol.FileMessageSegment(data=FileSegmentData(**file_data.model_dump()))
|
|
249
|
+
if isinstance(segment, LocationSegment):
|
|
250
|
+
raw = segment.data
|
|
251
|
+
try:
|
|
252
|
+
lat = float(raw.get("lat", 0))
|
|
253
|
+
lon = float(raw.get("lon", 0))
|
|
254
|
+
except (TypeError, ValueError):
|
|
255
|
+
return None
|
|
256
|
+
return onebot_protocol.LocationMessageSegment(
|
|
257
|
+
data=LocationSegmentData(
|
|
258
|
+
latitude=lat,
|
|
259
|
+
longitude=lon,
|
|
260
|
+
title=str(raw.get("title") or ""),
|
|
261
|
+
content=str(raw.get("content") or ""),
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
if isinstance(segment, ReplySegment):
|
|
265
|
+
reply_id = segment.data.get("id")
|
|
266
|
+
if not reply_id:
|
|
267
|
+
return None
|
|
268
|
+
return onebot_protocol.ReplyMessageSegment(
|
|
269
|
+
data=ReplySegmentData(message_id=str(reply_id))
|
|
270
|
+
)
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _should_broadcast(msg: BotMessage, segments: list[BaseSegment]) -> bool:
|
|
275
|
+
"""群聊仅在 @ 到机器人时向上层上报。"""
|
|
276
|
+
if msg.message_type == MessageType.GROUP:
|
|
277
|
+
for segment in segments:
|
|
278
|
+
if isinstance(segment, AtSegment) and segment.qq == msg.bot_id:
|
|
279
|
+
return True
|
|
280
|
+
return False
|
|
281
|
+
return True
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _cast_segment(data: dict) -> Optional[BaseSegment]:
|
|
285
|
+
"""按 type 字段实例化对应段模型;未知或校验失败返回 None。"""
|
|
286
|
+
cls = SEGMENT_MAP.get(data["type"])
|
|
287
|
+
if not cls:
|
|
288
|
+
return None
|
|
289
|
+
try:
|
|
290
|
+
return cls(**data)
|
|
291
|
+
except Exception:
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _extract_mention_robot(
|
|
296
|
+
text: TextSegment, bot_name: str, bot_id: str
|
|
297
|
+
) -> List[Union[TextSegment, AtSegment]]:
|
|
298
|
+
"""把正文中「@昵称 」拆成文本段与 @ 段。"""
|
|
299
|
+
|
|
300
|
+
def split(text: TextSegment) -> List[Union[TextSegment, AtSegment]]:
|
|
301
|
+
content = text.text
|
|
302
|
+
|
|
303
|
+
keyword = f"@{bot_name} "
|
|
304
|
+
if keyword in content:
|
|
305
|
+
index = content.find(keyword)
|
|
306
|
+
before_text = content[:index]
|
|
307
|
+
after_text = content[index + len(keyword):]
|
|
308
|
+
return (
|
|
309
|
+
split(TextSegment(data={"text": before_text}))
|
|
310
|
+
+ [AtSegment(data={"name": bot_name, "qq": bot_id})]
|
|
311
|
+
+ split(TextSegment(data={"text": after_text}))
|
|
312
|
+
)
|
|
313
|
+
return [text]
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
return split(text)
|
|
317
|
+
except Exception:
|
|
318
|
+
return [text]
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
from typing import List, Union, Optional
|
|
2
|
-
from napcat_adapter.models import (
|
|
3
|
-
BaseSegment,
|
|
4
|
-
TextSegment,
|
|
5
|
-
ImageSegment,
|
|
6
|
-
FaceSegment,
|
|
7
|
-
AtSegment,
|
|
8
|
-
ForwardSegment,
|
|
9
|
-
ReplySegment,
|
|
10
|
-
JsonSegment,
|
|
11
|
-
VideoSegment,
|
|
12
|
-
MfaceSegment,
|
|
13
|
-
LocationSegment,
|
|
14
|
-
BotMessage,
|
|
15
|
-
MessageType,
|
|
16
|
-
)
|
|
17
|
-
import onebot_protocol
|
|
18
|
-
from onebot_protocol import MessagePayload
|
|
19
|
-
|
|
20
|
-
SEGMENT_MAP = {
|
|
21
|
-
"text": TextSegment,
|
|
22
|
-
"image": ImageSegment,
|
|
23
|
-
"face": FaceSegment,
|
|
24
|
-
"at": AtSegment,
|
|
25
|
-
"forward": ForwardSegment,
|
|
26
|
-
"reply": ReplySegment,
|
|
27
|
-
"json": JsonSegment,
|
|
28
|
-
"video": VideoSegment,
|
|
29
|
-
"mface": MfaceSegment,
|
|
30
|
-
"location": LocationSegment,
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
USER_MAP: dict[str, str] = {}
|
|
34
|
-
|
|
35
|
-
MENTION_ALL_NAME = "全体成员"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def data_to_segments(data_list: list[dict], bot_name: str, bot_id: str) -> list[BaseSegment]:
|
|
39
|
-
"""把原始段字典列表转为强类型段,并拆分正文里的 @ 机器人。
|
|
40
|
-
|
|
41
|
-
Args:
|
|
42
|
-
data_list: 事件中的 CQ 段列表
|
|
43
|
-
bot_name: 机器人昵称,用于匹配 @
|
|
44
|
-
bot_id: 机器人 QQ 号
|
|
45
|
-
|
|
46
|
-
Returns:
|
|
47
|
-
过滤无效项并展开 @ 后的段列表
|
|
48
|
-
"""
|
|
49
|
-
segments = [_cast_segment(x) for x in data_list]
|
|
50
|
-
segments = [x for x in segments if x]
|
|
51
|
-
|
|
52
|
-
segments = [
|
|
53
|
-
(_extract_mention_robot(x, bot_name, bot_id) if isinstance(x, TextSegment) else [x])
|
|
54
|
-
for x in segments
|
|
55
|
-
]
|
|
56
|
-
segments = [x for data in segments for x in data]
|
|
57
|
-
|
|
58
|
-
return segments
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def onebot_to_bot(payload: MessagePayload) -> BotMessage:
|
|
62
|
-
"""把对外统一载荷转成 NapCat 可发送的 CQ 段列表。
|
|
63
|
-
|
|
64
|
-
Args:
|
|
65
|
-
payload: 会话、消息段与机器人标识
|
|
66
|
-
|
|
67
|
-
Returns:
|
|
68
|
-
含 data_list 的包内机器人消息
|
|
69
|
-
"""
|
|
70
|
-
data_list = []
|
|
71
|
-
for message in payload.messages:
|
|
72
|
-
if isinstance(message, onebot_protocol.TextMessageSegment):
|
|
73
|
-
message = TextSegment(data={"text": message.data.text})
|
|
74
|
-
elif isinstance(message, onebot_protocol.MentionMessageSegment):
|
|
75
|
-
name = USER_MAP.get(message.data.user_id)
|
|
76
|
-
if not name:
|
|
77
|
-
continue
|
|
78
|
-
message = AtSegment(data={"qq": message.data.user_id, "name": name})
|
|
79
|
-
data_list.append(message.model_dump())
|
|
80
|
-
|
|
81
|
-
msg = BotMessage(
|
|
82
|
-
message_id=payload.message_id,
|
|
83
|
-
data_list=data_list,
|
|
84
|
-
message_type=MessageType(payload.source_type),
|
|
85
|
-
bot_id=payload.bot_id,
|
|
86
|
-
session_id=payload.session_id,
|
|
87
|
-
user_name=payload.user_id or "",
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
if msg.message_type == MessageType.GROUP:
|
|
91
|
-
if msg.user_name:
|
|
92
|
-
msg.data_list = [
|
|
93
|
-
AtSegment(data={"qq": msg.user_name, "name": USER_MAP.get(msg.user_name, "")}).model_dump(),
|
|
94
|
-
TextSegment(data={"text": " "}).model_dump(),
|
|
95
|
-
] + msg.data_list
|
|
96
|
-
|
|
97
|
-
return msg
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def bot_to_onebot(msg: BotMessage) -> Optional[MessagePayload]:
|
|
101
|
-
"""把 NapCat 入站消息转为对外统一载荷;群聊未 @ 机器人时返回空。
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
msg: 事件解析后的包内消息
|
|
105
|
-
|
|
106
|
-
Returns:
|
|
107
|
-
可上报的统一载荷;无需上报时为 None
|
|
108
|
-
"""
|
|
109
|
-
global USER_MAP
|
|
110
|
-
|
|
111
|
-
segments = data_to_segments(msg.data_list, msg.bot_name, msg.bot_id)
|
|
112
|
-
|
|
113
|
-
if not _should_broadcast(msg, segments):
|
|
114
|
-
return None
|
|
115
|
-
|
|
116
|
-
for segment in segments:
|
|
117
|
-
if isinstance(segment, AtSegment) and segment.qq == msg.bot_id:
|
|
118
|
-
segments.remove(segment)
|
|
119
|
-
|
|
120
|
-
messages = []
|
|
121
|
-
for segment in segments:
|
|
122
|
-
if isinstance(segment, TextSegment):
|
|
123
|
-
text = segment.text.strip()
|
|
124
|
-
if not text:
|
|
125
|
-
continue
|
|
126
|
-
message = onebot_protocol.TextMessageSegment(data={"text": text})
|
|
127
|
-
elif isinstance(segment, AtSegment):
|
|
128
|
-
if segment.name == MENTION_ALL_NAME:
|
|
129
|
-
message = onebot_protocol.MentionAllMessageSegment()
|
|
130
|
-
else:
|
|
131
|
-
message = onebot_protocol.MentionMessageSegment(data={"user_id": segment.qq})
|
|
132
|
-
USER_MAP[segment.qq] = segment.name
|
|
133
|
-
else:
|
|
134
|
-
continue
|
|
135
|
-
messages.append(message)
|
|
136
|
-
|
|
137
|
-
if not messages:
|
|
138
|
-
return None
|
|
139
|
-
|
|
140
|
-
return MessagePayload(
|
|
141
|
-
message_id=msg.message_id,
|
|
142
|
-
source_type=msg.message_type.value,
|
|
143
|
-
bot_id=msg.bot_id,
|
|
144
|
-
session_id=msg.session_id,
|
|
145
|
-
user_id=msg.user_name,
|
|
146
|
-
messages=messages,
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def _should_broadcast(msg: BotMessage, segments: list[BaseSegment]) -> bool:
|
|
151
|
-
"""群聊仅在 @ 到机器人时向上层上报。"""
|
|
152
|
-
if msg.message_type == MessageType.GROUP:
|
|
153
|
-
for segment in segments:
|
|
154
|
-
if isinstance(segment, AtSegment) and segment.qq == msg.bot_id:
|
|
155
|
-
return True
|
|
156
|
-
return False
|
|
157
|
-
return True
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def _cast_segment(data: dict) -> Optional[BaseSegment]:
|
|
161
|
-
"""按 type 字段实例化对应段模型;未知或校验失败返回 None。"""
|
|
162
|
-
cls = SEGMENT_MAP.get(data["type"])
|
|
163
|
-
if not cls:
|
|
164
|
-
return None
|
|
165
|
-
try:
|
|
166
|
-
return cls(**data)
|
|
167
|
-
except Exception:
|
|
168
|
-
return None
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def _extract_mention_robot(
|
|
172
|
-
text: TextSegment, bot_name: str, bot_id: str
|
|
173
|
-
) -> List[Union[TextSegment, AtSegment]]:
|
|
174
|
-
"""把正文中「@昵称 」拆成文本段与 @ 段。"""
|
|
175
|
-
|
|
176
|
-
def split(text: TextSegment) -> List[Union[TextSegment, AtSegment]]:
|
|
177
|
-
content = text.text
|
|
178
|
-
|
|
179
|
-
keyword = f"@{bot_name} "
|
|
180
|
-
if keyword in content:
|
|
181
|
-
index = content.find(keyword)
|
|
182
|
-
before_text = content[:index]
|
|
183
|
-
after_text = content[index + len(keyword):]
|
|
184
|
-
return (
|
|
185
|
-
split(TextSegment(data={"text": before_text}))
|
|
186
|
-
+ [AtSegment(data={"name": bot_name, "qq": bot_id})]
|
|
187
|
-
+ split(TextSegment(data={"text": after_text}))
|
|
188
|
-
)
|
|
189
|
-
return [text]
|
|
190
|
-
|
|
191
|
-
try:
|
|
192
|
-
return split(text)
|
|
193
|
-
except Exception:
|
|
194
|
-
return [text]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_library_napcat_adapter-0.1.0 → python_library_napcat_adapter-0.1.1}/napcat_adapter/bot.py
RENAMED
|
File without changes
|
|
File without changes
|