Skip to content

Commit 5c3643c

Browse files
authored
feat: added support for file, voice, and video messages for QQ Official Bot (including WebSocket mode). (AstrBotDevs#6063)
1 parent 589cce1 commit 5c3643c

7 files changed

Lines changed: 344 additions & 135 deletions

File tree

astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py

Lines changed: 104 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
from astrbot.api import logger
2020
from astrbot.api.event import AstrMessageEvent, MessageChain
21-
from astrbot.api.message_components import Image, Plain, Record
21+
from astrbot.api.message_components import File, Image, Plain, Record, Video
2222
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
2323
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
2424
from astrbot.core.utils.io import download_image_by_url, file_to_base64
@@ -47,6 +47,10 @@ def _patch_qq_botpy_formdata() -> None:
4747

4848
class QQOfficialMessageEvent(AstrMessageEvent):
4949
MARKDOWN_NOT_ALLOWED_ERROR = "不允许发送原生 markdown"
50+
IMAGE_FILE_TYPE = 1
51+
VIDEO_FILE_TYPE = 2
52+
VOICE_FILE_TYPE = 3
53+
FILE_FILE_TYPE = 4
5054

5155
def __init__(
5256
self,
@@ -126,13 +130,18 @@ async def _post_send(self, stream: dict | None = None):
126130
image_base64,
127131
image_path,
128132
record_file_path,
133+
video_file_source,
134+
file_source,
135+
file_name,
129136
) = await QQOfficialMessageEvent._parse_to_qqofficial(self.send_buffer)
130137

131138
if (
132139
not plain_text
133140
and not image_base64
134141
and not image_path
135142
and not record_file_path
143+
and not video_file_source
144+
and not file_source
136145
):
137146
return None
138147

@@ -157,23 +166,47 @@ async def _post_send(self, stream: dict | None = None):
157166
if image_base64:
158167
media = await self.upload_group_and_c2c_image(
159168
image_base64,
160-
1,
169+
self.IMAGE_FILE_TYPE,
161170
group_openid=source.group_openid,
162171
)
163172
payload["media"] = media
164173
payload["msg_type"] = 7
165174
payload.pop("markdown", None)
166175
payload["content"] = plain_text or None
167176
if record_file_path: # group record msg
168-
media = await self.upload_group_and_c2c_record(
177+
media = await self.upload_group_and_c2c_media(
169178
record_file_path,
170-
3,
179+
self.VOICE_FILE_TYPE,
171180
group_openid=source.group_openid,
172181
)
173-
payload["media"] = media
174-
payload["msg_type"] = 7
175-
payload.pop("markdown", None)
176-
payload["content"] = plain_text or None
182+
if media:
183+
payload["media"] = media
184+
payload["msg_type"] = 7
185+
payload.pop("markdown", None)
186+
payload["content"] = plain_text or None
187+
if video_file_source:
188+
media = await self.upload_group_and_c2c_media(
189+
video_file_source,
190+
self.VIDEO_FILE_TYPE,
191+
group_openid=source.group_openid,
192+
)
193+
if media:
194+
payload["media"] = media
195+
payload["msg_type"] = 7
196+
payload.pop("markdown", None)
197+
payload["content"] = plain_text or None
198+
if file_source:
199+
media = await self.upload_group_and_c2c_media(
200+
file_source,
201+
self.FILE_FILE_TYPE,
202+
file_name=file_name,
203+
group_openid=source.group_openid,
204+
)
205+
if media:
206+
payload["media"] = media
207+
payload["msg_type"] = 7
208+
payload.pop("markdown", None)
209+
payload["content"] = plain_text or None
177210
ret = await self._send_with_markdown_fallback(
178211
send_func=lambda retry_payload: self.bot.api.post_group_message(
179212
group_openid=source.group_openid, # type: ignore
@@ -187,23 +220,47 @@ async def _post_send(self, stream: dict | None = None):
187220
if image_base64:
188221
media = await self.upload_group_and_c2c_image(
189222
image_base64,
190-
1,
223+
self.IMAGE_FILE_TYPE,
191224
openid=source.author.user_openid,
192225
)
193226
payload["media"] = media
194227
payload["msg_type"] = 7
195228
payload.pop("markdown", None)
196229
payload["content"] = plain_text or None
197230
if record_file_path: # c2c record
198-
media = await self.upload_group_and_c2c_record(
231+
media = await self.upload_group_and_c2c_media(
199232
record_file_path,
200-
3,
233+
self.VOICE_FILE_TYPE,
201234
openid=source.author.user_openid,
202235
)
203-
payload["media"] = media
204-
payload["msg_type"] = 7
205-
payload.pop("markdown", None)
206-
payload["content"] = plain_text or None
236+
if media:
237+
payload["media"] = media
238+
payload["msg_type"] = 7
239+
payload.pop("markdown", None)
240+
payload["content"] = plain_text or None
241+
if video_file_source:
242+
media = await self.upload_group_and_c2c_media(
243+
video_file_source,
244+
self.VIDEO_FILE_TYPE,
245+
openid=source.author.user_openid,
246+
)
247+
if media:
248+
payload["media"] = media
249+
payload["msg_type"] = 7
250+
payload.pop("markdown", None)
251+
payload["content"] = plain_text or None
252+
if file_source:
253+
media = await self.upload_group_and_c2c_media(
254+
file_source,
255+
self.FILE_FILE_TYPE,
256+
file_name=file_name,
257+
openid=source.author.user_openid,
258+
)
259+
if media:
260+
payload["media"] = media
261+
payload["msg_type"] = 7
262+
payload.pop("markdown", None)
263+
payload["content"] = plain_text or None
207264
if stream:
208265
ret = await self._send_with_markdown_fallback(
209266
send_func=lambda retry_payload: self.post_c2c_message(
@@ -327,16 +384,19 @@ async def upload_group_and_c2c_image(
327384
ttl=result.get("ttl", 0),
328385
)
329386

330-
async def upload_group_and_c2c_record(
387+
async def upload_group_and_c2c_media(
331388
self,
332389
file_source: str,
333390
file_type: int,
334391
srv_send_msg: bool = False,
392+
file_name: str | None = None,
335393
**kwargs,
336394
) -> Media | None:
337395
"""上传媒体文件"""
338396
# 构建基础payload
339397
payload = {"file_type": file_type, "srv_send_msg": srv_send_msg}
398+
if file_name:
399+
payload["file_name"] = file_name
340400

341401
# 处理文件数据
342402
if os.path.exists(file_source):
@@ -416,6 +476,9 @@ async def _parse_to_qqofficial(message: MessageChain):
416476
image_base64 = None # only one img supported
417477
image_file_path = None
418478
record_file_path = None
479+
video_file_source = None
480+
file_source = None
481+
file_name = None
419482
for i in message.chain:
420483
if isinstance(i, Plain):
421484
plain_text += i.text
@@ -454,6 +517,30 @@ async def _parse_to_qqofficial(message: MessageChain):
454517
except Exception as e:
455518
logger.error(f"处理语音时出错: {e}")
456519
record_file_path = None
520+
elif isinstance(i, Video) and not video_file_source:
521+
if i.file.startswith("file:///"):
522+
video_file_source = i.file[8:]
523+
else:
524+
video_file_source = i.file
525+
elif isinstance(i, File) and not file_source:
526+
file_name = i.name
527+
if i.file_:
528+
file_path = i.file_
529+
if file_path.startswith("file:///"):
530+
file_path = file_path[8:]
531+
elif file_path.startswith("file://"):
532+
file_path = file_path[7:]
533+
file_source = file_path
534+
elif i.url:
535+
file_source = i.url
457536
else:
458537
logger.debug(f"qq_official 忽略 {i.type}")
459-
return plain_text, image_base64, image_file_path, record_file_path
538+
return (
539+
plain_text,
540+
image_base64,
541+
image_file_path,
542+
record_file_path,
543+
video_file_source,
544+
file_source,
545+
file_name,
546+
)

0 commit comments

Comments
 (0)