feat: added support for file, voice, and video messages for QQ Official Bot (including WebSocket mode). (#6063)
This commit is contained in:
@@ -18,7 +18,7 @@ from botpy.types.message import MarkdownPayload, Media
|
|||||||
|
|
||||||
from astrbot.api import logger
|
from astrbot.api import logger
|
||||||
from astrbot.api.event import AstrMessageEvent, MessageChain
|
from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||||
from astrbot.api.message_components import Image, Plain, Record
|
from astrbot.api.message_components import File, Image, Plain, Record, Video
|
||||||
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
|
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
|
||||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||||
from astrbot.core.utils.io import download_image_by_url, file_to_base64
|
from astrbot.core.utils.io import download_image_by_url, file_to_base64
|
||||||
@@ -47,6 +47,10 @@ _patch_qq_botpy_formdata()
|
|||||||
|
|
||||||
class QQOfficialMessageEvent(AstrMessageEvent):
|
class QQOfficialMessageEvent(AstrMessageEvent):
|
||||||
MARKDOWN_NOT_ALLOWED_ERROR = "不允许发送原生 markdown"
|
MARKDOWN_NOT_ALLOWED_ERROR = "不允许发送原生 markdown"
|
||||||
|
IMAGE_FILE_TYPE = 1
|
||||||
|
VIDEO_FILE_TYPE = 2
|
||||||
|
VOICE_FILE_TYPE = 3
|
||||||
|
FILE_FILE_TYPE = 4
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -126,6 +130,9 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|||||||
image_base64,
|
image_base64,
|
||||||
image_path,
|
image_path,
|
||||||
record_file_path,
|
record_file_path,
|
||||||
|
video_file_source,
|
||||||
|
file_source,
|
||||||
|
file_name,
|
||||||
) = await QQOfficialMessageEvent._parse_to_qqofficial(self.send_buffer)
|
) = await QQOfficialMessageEvent._parse_to_qqofficial(self.send_buffer)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -133,6 +140,8 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|||||||
and not image_base64
|
and not image_base64
|
||||||
and not image_path
|
and not image_path
|
||||||
and not record_file_path
|
and not record_file_path
|
||||||
|
and not video_file_source
|
||||||
|
and not file_source
|
||||||
):
|
):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -157,7 +166,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|||||||
if image_base64:
|
if image_base64:
|
||||||
media = await self.upload_group_and_c2c_image(
|
media = await self.upload_group_and_c2c_image(
|
||||||
image_base64,
|
image_base64,
|
||||||
1,
|
self.IMAGE_FILE_TYPE,
|
||||||
group_openid=source.group_openid,
|
group_openid=source.group_openid,
|
||||||
)
|
)
|
||||||
payload["media"] = media
|
payload["media"] = media
|
||||||
@@ -165,15 +174,39 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|||||||
payload.pop("markdown", None)
|
payload.pop("markdown", None)
|
||||||
payload["content"] = plain_text or None
|
payload["content"] = plain_text or None
|
||||||
if record_file_path: # group record msg
|
if record_file_path: # group record msg
|
||||||
media = await self.upload_group_and_c2c_record(
|
media = await self.upload_group_and_c2c_media(
|
||||||
record_file_path,
|
record_file_path,
|
||||||
3,
|
self.VOICE_FILE_TYPE,
|
||||||
group_openid=source.group_openid,
|
group_openid=source.group_openid,
|
||||||
)
|
)
|
||||||
payload["media"] = media
|
if media:
|
||||||
payload["msg_type"] = 7
|
payload["media"] = media
|
||||||
payload.pop("markdown", None)
|
payload["msg_type"] = 7
|
||||||
payload["content"] = plain_text or None
|
payload.pop("markdown", None)
|
||||||
|
payload["content"] = plain_text or None
|
||||||
|
if video_file_source:
|
||||||
|
media = await self.upload_group_and_c2c_media(
|
||||||
|
video_file_source,
|
||||||
|
self.VIDEO_FILE_TYPE,
|
||||||
|
group_openid=source.group_openid,
|
||||||
|
)
|
||||||
|
if media:
|
||||||
|
payload["media"] = media
|
||||||
|
payload["msg_type"] = 7
|
||||||
|
payload.pop("markdown", None)
|
||||||
|
payload["content"] = plain_text or None
|
||||||
|
if file_source:
|
||||||
|
media = await self.upload_group_and_c2c_media(
|
||||||
|
file_source,
|
||||||
|
self.FILE_FILE_TYPE,
|
||||||
|
file_name=file_name,
|
||||||
|
group_openid=source.group_openid,
|
||||||
|
)
|
||||||
|
if media:
|
||||||
|
payload["media"] = media
|
||||||
|
payload["msg_type"] = 7
|
||||||
|
payload.pop("markdown", None)
|
||||||
|
payload["content"] = plain_text or None
|
||||||
ret = await self._send_with_markdown_fallback(
|
ret = await self._send_with_markdown_fallback(
|
||||||
send_func=lambda retry_payload: self.bot.api.post_group_message(
|
send_func=lambda retry_payload: self.bot.api.post_group_message(
|
||||||
group_openid=source.group_openid, # type: ignore
|
group_openid=source.group_openid, # type: ignore
|
||||||
@@ -187,7 +220,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|||||||
if image_base64:
|
if image_base64:
|
||||||
media = await self.upload_group_and_c2c_image(
|
media = await self.upload_group_and_c2c_image(
|
||||||
image_base64,
|
image_base64,
|
||||||
1,
|
self.IMAGE_FILE_TYPE,
|
||||||
openid=source.author.user_openid,
|
openid=source.author.user_openid,
|
||||||
)
|
)
|
||||||
payload["media"] = media
|
payload["media"] = media
|
||||||
@@ -195,15 +228,39 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|||||||
payload.pop("markdown", None)
|
payload.pop("markdown", None)
|
||||||
payload["content"] = plain_text or None
|
payload["content"] = plain_text or None
|
||||||
if record_file_path: # c2c record
|
if record_file_path: # c2c record
|
||||||
media = await self.upload_group_and_c2c_record(
|
media = await self.upload_group_and_c2c_media(
|
||||||
record_file_path,
|
record_file_path,
|
||||||
3,
|
self.VOICE_FILE_TYPE,
|
||||||
openid=source.author.user_openid,
|
openid=source.author.user_openid,
|
||||||
)
|
)
|
||||||
payload["media"] = media
|
if media:
|
||||||
payload["msg_type"] = 7
|
payload["media"] = media
|
||||||
payload.pop("markdown", None)
|
payload["msg_type"] = 7
|
||||||
payload["content"] = plain_text or None
|
payload.pop("markdown", None)
|
||||||
|
payload["content"] = plain_text or None
|
||||||
|
if video_file_source:
|
||||||
|
media = await self.upload_group_and_c2c_media(
|
||||||
|
video_file_source,
|
||||||
|
self.VIDEO_FILE_TYPE,
|
||||||
|
openid=source.author.user_openid,
|
||||||
|
)
|
||||||
|
if media:
|
||||||
|
payload["media"] = media
|
||||||
|
payload["msg_type"] = 7
|
||||||
|
payload.pop("markdown", None)
|
||||||
|
payload["content"] = plain_text or None
|
||||||
|
if file_source:
|
||||||
|
media = await self.upload_group_and_c2c_media(
|
||||||
|
file_source,
|
||||||
|
self.FILE_FILE_TYPE,
|
||||||
|
file_name=file_name,
|
||||||
|
openid=source.author.user_openid,
|
||||||
|
)
|
||||||
|
if media:
|
||||||
|
payload["media"] = media
|
||||||
|
payload["msg_type"] = 7
|
||||||
|
payload.pop("markdown", None)
|
||||||
|
payload["content"] = plain_text or None
|
||||||
if stream:
|
if stream:
|
||||||
ret = await self._send_with_markdown_fallback(
|
ret = await self._send_with_markdown_fallback(
|
||||||
send_func=lambda retry_payload: self.post_c2c_message(
|
send_func=lambda retry_payload: self.post_c2c_message(
|
||||||
@@ -327,16 +384,19 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|||||||
ttl=result.get("ttl", 0),
|
ttl=result.get("ttl", 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def upload_group_and_c2c_record(
|
async def upload_group_and_c2c_media(
|
||||||
self,
|
self,
|
||||||
file_source: str,
|
file_source: str,
|
||||||
file_type: int,
|
file_type: int,
|
||||||
srv_send_msg: bool = False,
|
srv_send_msg: bool = False,
|
||||||
|
file_name: str | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Media | None:
|
) -> Media | None:
|
||||||
"""上传媒体文件"""
|
"""上传媒体文件"""
|
||||||
# 构建基础payload
|
# 构建基础payload
|
||||||
payload = {"file_type": file_type, "srv_send_msg": srv_send_msg}
|
payload = {"file_type": file_type, "srv_send_msg": srv_send_msg}
|
||||||
|
if file_name:
|
||||||
|
payload["file_name"] = file_name
|
||||||
|
|
||||||
# 处理文件数据
|
# 处理文件数据
|
||||||
if os.path.exists(file_source):
|
if os.path.exists(file_source):
|
||||||
@@ -416,6 +476,9 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|||||||
image_base64 = None # only one img supported
|
image_base64 = None # only one img supported
|
||||||
image_file_path = None
|
image_file_path = None
|
||||||
record_file_path = None
|
record_file_path = None
|
||||||
|
video_file_source = None
|
||||||
|
file_source = None
|
||||||
|
file_name = None
|
||||||
for i in message.chain:
|
for i in message.chain:
|
||||||
if isinstance(i, Plain):
|
if isinstance(i, Plain):
|
||||||
plain_text += i.text
|
plain_text += i.text
|
||||||
@@ -454,6 +517,30 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"处理语音时出错: {e}")
|
logger.error(f"处理语音时出错: {e}")
|
||||||
record_file_path = None
|
record_file_path = None
|
||||||
|
elif isinstance(i, Video) and not video_file_source:
|
||||||
|
if i.file.startswith("file:///"):
|
||||||
|
video_file_source = i.file[8:]
|
||||||
|
else:
|
||||||
|
video_file_source = i.file
|
||||||
|
elif isinstance(i, File) and not file_source:
|
||||||
|
file_name = i.name
|
||||||
|
if i.file_:
|
||||||
|
file_path = i.file_
|
||||||
|
if file_path.startswith("file:///"):
|
||||||
|
file_path = file_path[8:]
|
||||||
|
elif file_path.startswith("file://"):
|
||||||
|
file_path = file_path[7:]
|
||||||
|
file_source = file_path
|
||||||
|
elif i.url:
|
||||||
|
file_source = i.url
|
||||||
else:
|
else:
|
||||||
logger.debug(f"qq_official 忽略 {i.type}")
|
logger.debug(f"qq_official 忽略 {i.type}")
|
||||||
return plain_text, image_base64, image_file_path, record_file_path
|
return (
|
||||||
|
plain_text,
|
||||||
|
image_base64,
|
||||||
|
image_file_path,
|
||||||
|
record_file_path,
|
||||||
|
video_file_source,
|
||||||
|
file_source,
|
||||||
|
file_name,
|
||||||
|
)
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
import time
|
import time
|
||||||
from typing import cast
|
from types import SimpleNamespace
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
import botpy
|
import botpy
|
||||||
import botpy.message
|
import botpy.message
|
||||||
@@ -12,7 +14,7 @@ from botpy import Client
|
|||||||
|
|
||||||
from astrbot import logger
|
from astrbot import logger
|
||||||
from astrbot.api.event import MessageChain
|
from astrbot.api.event import MessageChain
|
||||||
from astrbot.api.message_components import At, File, Image, Plain
|
from astrbot.api.message_components import At, File, Image, Plain, Record, Video
|
||||||
from astrbot.api.platform import (
|
from astrbot.api.platform import (
|
||||||
AstrBotMessage,
|
AstrBotMessage,
|
||||||
MessageMember,
|
MessageMember,
|
||||||
@@ -46,6 +48,7 @@ class botClient(Client):
|
|||||||
)
|
)
|
||||||
abm.group_id = cast(str, message.group_openid)
|
abm.group_id = cast(str, message.group_openid)
|
||||||
abm.session_id = abm.group_id
|
abm.session_id = abm.group_id
|
||||||
|
self.platform.remember_session_scene(abm.session_id, "group")
|
||||||
self._commit(abm)
|
self._commit(abm)
|
||||||
|
|
||||||
# 收到频道消息
|
# 收到频道消息
|
||||||
@@ -56,6 +59,7 @@ class botClient(Client):
|
|||||||
)
|
)
|
||||||
abm.group_id = message.channel_id
|
abm.group_id = message.channel_id
|
||||||
abm.session_id = abm.group_id
|
abm.session_id = abm.group_id
|
||||||
|
self.platform.remember_session_scene(abm.session_id, "channel")
|
||||||
self._commit(abm)
|
self._commit(abm)
|
||||||
|
|
||||||
# 收到私聊消息
|
# 收到私聊消息
|
||||||
@@ -67,6 +71,7 @@ class botClient(Client):
|
|||||||
MessageType.FRIEND_MESSAGE,
|
MessageType.FRIEND_MESSAGE,
|
||||||
)
|
)
|
||||||
abm.session_id = abm.sender.user_id
|
abm.session_id = abm.sender.user_id
|
||||||
|
self.platform.remember_session_scene(abm.session_id, "friend")
|
||||||
self._commit(abm)
|
self._commit(abm)
|
||||||
|
|
||||||
# 收到 C2C 消息
|
# 收到 C2C 消息
|
||||||
@@ -76,9 +81,11 @@ class botClient(Client):
|
|||||||
MessageType.FRIEND_MESSAGE,
|
MessageType.FRIEND_MESSAGE,
|
||||||
)
|
)
|
||||||
abm.session_id = abm.sender.user_id
|
abm.session_id = abm.sender.user_id
|
||||||
|
self.platform.remember_session_scene(abm.session_id, "friend")
|
||||||
self._commit(abm)
|
self._commit(abm)
|
||||||
|
|
||||||
def _commit(self, abm: AstrBotMessage) -> None:
|
def _commit(self, abm: AstrBotMessage) -> None:
|
||||||
|
self.platform.remember_session_message_id(abm.session_id, abm.message_id)
|
||||||
self.platform.commit_event(
|
self.platform.commit_event(
|
||||||
QQOfficialMessageEvent(
|
QQOfficialMessageEvent(
|
||||||
abm.message_str,
|
abm.message_str,
|
||||||
@@ -124,6 +131,9 @@ class QQOfficialPlatformAdapter(Platform):
|
|||||||
|
|
||||||
self.client.set_platform(self)
|
self.client.set_platform(self)
|
||||||
|
|
||||||
|
self._session_last_message_id: dict[str, str] = {}
|
||||||
|
self._session_scene: dict[str, str] = {}
|
||||||
|
|
||||||
self.test_mode = os.environ.get("TEST_MODE", "off") == "on"
|
self.test_mode = os.environ.get("TEST_MODE", "off") == "on"
|
||||||
|
|
||||||
async def send_by_session(
|
async def send_by_session(
|
||||||
@@ -131,14 +141,185 @@ class QQOfficialPlatformAdapter(Platform):
|
|||||||
session: MessageSesion,
|
session: MessageSesion,
|
||||||
message_chain: MessageChain,
|
message_chain: MessageChain,
|
||||||
) -> None:
|
) -> None:
|
||||||
raise NotImplementedError("QQ 机器人官方 API 适配器不支持 send_by_session")
|
await self._send_by_session_common(session, message_chain)
|
||||||
|
|
||||||
|
async def _send_by_session_common(
|
||||||
|
self,
|
||||||
|
session: MessageSesion,
|
||||||
|
message_chain: MessageChain,
|
||||||
|
) -> None:
|
||||||
|
(
|
||||||
|
plain_text,
|
||||||
|
image_base64,
|
||||||
|
image_path,
|
||||||
|
record_file_path,
|
||||||
|
video_file_source,
|
||||||
|
file_source,
|
||||||
|
file_name,
|
||||||
|
) = await QQOfficialMessageEvent._parse_to_qqofficial(message_chain)
|
||||||
|
if (
|
||||||
|
not plain_text
|
||||||
|
and not image_path
|
||||||
|
and not image_base64
|
||||||
|
and not record_file_path
|
||||||
|
and not video_file_source
|
||||||
|
and not file_source
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
msg_id = self._session_last_message_id.get(session.session_id)
|
||||||
|
if not msg_id:
|
||||||
|
logger.warning(
|
||||||
|
"[QQOfficial] No cached msg_id for session: %s, skip send_by_session",
|
||||||
|
session.session_id,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
payload: dict[str, Any] = {"content": plain_text, "msg_id": msg_id}
|
||||||
|
ret: Any = None
|
||||||
|
send_helper = SimpleNamespace(bot=self.client)
|
||||||
|
|
||||||
|
if session.message_type == MessageType.GROUP_MESSAGE:
|
||||||
|
scene = self._session_scene.get(session.session_id)
|
||||||
|
if scene == "group":
|
||||||
|
payload["msg_seq"] = random.randint(1, 10000)
|
||||||
|
if image_base64:
|
||||||
|
media = await QQOfficialMessageEvent.upload_group_and_c2c_image(
|
||||||
|
send_helper, # type: ignore
|
||||||
|
image_base64,
|
||||||
|
QQOfficialMessageEvent.IMAGE_FILE_TYPE,
|
||||||
|
group_openid=session.session_id,
|
||||||
|
)
|
||||||
|
payload["media"] = media
|
||||||
|
payload["msg_type"] = 7
|
||||||
|
if record_file_path:
|
||||||
|
media = await QQOfficialMessageEvent.upload_group_and_c2c_media(
|
||||||
|
send_helper, # type: ignore
|
||||||
|
record_file_path,
|
||||||
|
QQOfficialMessageEvent.VOICE_FILE_TYPE,
|
||||||
|
group_openid=session.session_id,
|
||||||
|
)
|
||||||
|
if media:
|
||||||
|
payload["media"] = media
|
||||||
|
payload["msg_type"] = 7
|
||||||
|
if video_file_source:
|
||||||
|
media = await QQOfficialMessageEvent.upload_group_and_c2c_media(
|
||||||
|
send_helper, # type: ignore
|
||||||
|
video_file_source,
|
||||||
|
QQOfficialMessageEvent.VIDEO_FILE_TYPE,
|
||||||
|
group_openid=session.session_id,
|
||||||
|
)
|
||||||
|
if media:
|
||||||
|
payload["media"] = media
|
||||||
|
payload["msg_type"] = 7
|
||||||
|
if file_source:
|
||||||
|
media = await QQOfficialMessageEvent.upload_group_and_c2c_media(
|
||||||
|
send_helper, # type: ignore
|
||||||
|
file_source,
|
||||||
|
QQOfficialMessageEvent.FILE_FILE_TYPE,
|
||||||
|
file_name=file_name,
|
||||||
|
group_openid=session.session_id,
|
||||||
|
)
|
||||||
|
if media:
|
||||||
|
payload["media"] = media
|
||||||
|
payload["msg_type"] = 7
|
||||||
|
ret = await self.client.api.post_group_message(
|
||||||
|
group_openid=session.session_id,
|
||||||
|
**payload,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if image_path:
|
||||||
|
payload["file_image"] = image_path
|
||||||
|
ret = await self.client.api.post_message(
|
||||||
|
channel_id=session.session_id,
|
||||||
|
**payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
elif session.message_type == MessageType.FRIEND_MESSAGE:
|
||||||
|
payload["msg_seq"] = random.randint(1, 10000)
|
||||||
|
if image_base64:
|
||||||
|
media = await QQOfficialMessageEvent.upload_group_and_c2c_image(
|
||||||
|
send_helper, # type: ignore
|
||||||
|
image_base64,
|
||||||
|
QQOfficialMessageEvent.IMAGE_FILE_TYPE,
|
||||||
|
openid=session.session_id,
|
||||||
|
)
|
||||||
|
payload["media"] = media
|
||||||
|
payload["msg_type"] = 7
|
||||||
|
if record_file_path:
|
||||||
|
media = await QQOfficialMessageEvent.upload_group_and_c2c_media(
|
||||||
|
send_helper, # type: ignore
|
||||||
|
record_file_path,
|
||||||
|
QQOfficialMessageEvent.VOICE_FILE_TYPE,
|
||||||
|
openid=session.session_id,
|
||||||
|
)
|
||||||
|
if media:
|
||||||
|
payload["media"] = media
|
||||||
|
payload["msg_type"] = 7
|
||||||
|
if video_file_source:
|
||||||
|
media = await QQOfficialMessageEvent.upload_group_and_c2c_media(
|
||||||
|
send_helper, # type: ignore
|
||||||
|
video_file_source,
|
||||||
|
QQOfficialMessageEvent.VIDEO_FILE_TYPE,
|
||||||
|
openid=session.session_id,
|
||||||
|
)
|
||||||
|
if media:
|
||||||
|
payload["media"] = media
|
||||||
|
payload["msg_type"] = 7
|
||||||
|
if file_source:
|
||||||
|
media = await QQOfficialMessageEvent.upload_group_and_c2c_media(
|
||||||
|
send_helper, # type: ignore
|
||||||
|
file_source,
|
||||||
|
QQOfficialMessageEvent.FILE_FILE_TYPE,
|
||||||
|
file_name=file_name,
|
||||||
|
openid=session.session_id,
|
||||||
|
)
|
||||||
|
if media:
|
||||||
|
payload["media"] = media
|
||||||
|
payload["msg_type"] = 7
|
||||||
|
|
||||||
|
ret = await QQOfficialMessageEvent.post_c2c_message(
|
||||||
|
send_helper, # type: ignore
|
||||||
|
openid=session.session_id,
|
||||||
|
**payload,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"[QQOfficial] Unsupported message type for send_by_session: %s",
|
||||||
|
session.message_type,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
sent_message_id = self._extract_message_id(ret)
|
||||||
|
if sent_message_id:
|
||||||
|
self.remember_session_message_id(session.session_id, sent_message_id)
|
||||||
|
await super().send_by_session(session, message_chain)
|
||||||
|
|
||||||
|
def remember_session_message_id(self, session_id: str, message_id: str) -> None:
|
||||||
|
if not session_id or not message_id:
|
||||||
|
return
|
||||||
|
self._session_last_message_id[session_id] = message_id
|
||||||
|
|
||||||
|
def remember_session_scene(self, session_id: str, scene: str) -> None:
|
||||||
|
if not session_id or not scene:
|
||||||
|
return
|
||||||
|
self._session_scene[session_id] = scene
|
||||||
|
|
||||||
|
def _extract_message_id(self, ret: Any) -> str | None:
|
||||||
|
if isinstance(ret, dict):
|
||||||
|
message_id = ret.get("id")
|
||||||
|
return str(message_id) if message_id else None
|
||||||
|
message_id = getattr(ret, "id", None)
|
||||||
|
if message_id:
|
||||||
|
return str(message_id)
|
||||||
|
return None
|
||||||
|
|
||||||
def meta(self) -> PlatformMetadata:
|
def meta(self) -> PlatformMetadata:
|
||||||
return PlatformMetadata(
|
return PlatformMetadata(
|
||||||
name="qq_official",
|
name="qq_official",
|
||||||
description="QQ 机器人官方 API 适配器",
|
description="QQ 机器人官方 API 适配器",
|
||||||
id=cast(str, self.config.get("id")),
|
id=cast(str, self.config.get("id")),
|
||||||
support_proactive_message=False,
|
support_proactive_message=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -158,7 +339,10 @@ class QQOfficialPlatformAdapter(Platform):
|
|||||||
return
|
return
|
||||||
|
|
||||||
for attachment in attachments:
|
for attachment in attachments:
|
||||||
content_type = cast(str, getattr(attachment, "content_type", "") or "")
|
content_type = cast(
|
||||||
|
str,
|
||||||
|
getattr(attachment, "content_type", "") or "",
|
||||||
|
).lower()
|
||||||
url = QQOfficialPlatformAdapter._normalize_attachment_url(
|
url = QQOfficialPlatformAdapter._normalize_attachment_url(
|
||||||
cast(str | None, getattr(attachment, "url", None))
|
cast(str | None, getattr(attachment, "url", None))
|
||||||
)
|
)
|
||||||
@@ -174,7 +358,32 @@ class QQOfficialPlatformAdapter(Platform):
|
|||||||
or getattr(attachment, "name", None)
|
or getattr(attachment, "name", None)
|
||||||
or "attachment",
|
or "attachment",
|
||||||
)
|
)
|
||||||
msg.append(File(name=filename, file=url, url=url))
|
ext = os.path.splitext(filename)[1].lower()
|
||||||
|
image_exts = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"}
|
||||||
|
audio_exts = {
|
||||||
|
".mp3",
|
||||||
|
".wav",
|
||||||
|
".ogg",
|
||||||
|
".m4a",
|
||||||
|
".amr",
|
||||||
|
".silk",
|
||||||
|
}
|
||||||
|
video_exts = {
|
||||||
|
".mp4",
|
||||||
|
".mov",
|
||||||
|
".avi",
|
||||||
|
".mkv",
|
||||||
|
".webm",
|
||||||
|
}
|
||||||
|
|
||||||
|
if content_type.startswith("audio") or ext in audio_exts:
|
||||||
|
msg.append(Record.fromURL(url))
|
||||||
|
elif content_type.startswith("video") or ext in video_exts:
|
||||||
|
msg.append(Video.fromURL(url))
|
||||||
|
elif content_type.startswith("image") or ext in image_exts:
|
||||||
|
msg.append(Image.fromURL(url))
|
||||||
|
else:
|
||||||
|
msg.append(File(name=filename, file=url, url=url))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_from_qqofficial(
|
def _parse_from_qqofficial(
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import random
|
|
||||||
from types import SimpleNamespace
|
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
import botpy
|
import botpy
|
||||||
@@ -15,7 +13,6 @@ from astrbot.core.platform.astr_message_event import MessageSesion
|
|||||||
from astrbot.core.utils.webhook_utils import log_webhook_info
|
from astrbot.core.utils.webhook_utils import log_webhook_info
|
||||||
|
|
||||||
from ...register import register_platform_adapter
|
from ...register import register_platform_adapter
|
||||||
from ..qqofficial.qqofficial_message_event import QQOfficialMessageEvent
|
|
||||||
from ..qqofficial.qqofficial_platform_adapter import QQOfficialPlatformAdapter
|
from ..qqofficial.qqofficial_platform_adapter import QQOfficialPlatformAdapter
|
||||||
from .qo_webhook_event import QQOfficialWebhookMessageEvent
|
from .qo_webhook_event import QQOfficialWebhookMessageEvent
|
||||||
from .qo_webhook_server import QQOfficialWebhook
|
from .qo_webhook_server import QQOfficialWebhook
|
||||||
@@ -123,95 +120,11 @@ class QQOfficialWebhookPlatformAdapter(Platform):
|
|||||||
session: MessageSesion,
|
session: MessageSesion,
|
||||||
message_chain: MessageChain,
|
message_chain: MessageChain,
|
||||||
) -> None:
|
) -> None:
|
||||||
(
|
await QQOfficialPlatformAdapter._send_by_session_common(
|
||||||
plain_text,
|
cast(Any, self),
|
||||||
image_base64,
|
session,
|
||||||
image_path,
|
message_chain,
|
||||||
record_file_path,
|
)
|
||||||
) = await QQOfficialMessageEvent._parse_to_qqofficial(message_chain)
|
|
||||||
if not plain_text and not image_path:
|
|
||||||
return
|
|
||||||
|
|
||||||
msg_id = self._session_last_message_id.get(session.session_id)
|
|
||||||
if not msg_id:
|
|
||||||
logger.warning(
|
|
||||||
"[QQOfficialWebhook] No cached msg_id for session: %s, skip send_by_session",
|
|
||||||
session.session_id,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
payload: dict[str, Any] = {"content": plain_text, "msg_id": msg_id}
|
|
||||||
ret: Any = None
|
|
||||||
send_helper = SimpleNamespace(bot=self.client)
|
|
||||||
if session.message_type == MessageType.GROUP_MESSAGE:
|
|
||||||
scene = self._session_scene.get(session.session_id)
|
|
||||||
if scene == "group":
|
|
||||||
payload["msg_seq"] = random.randint(1, 10000)
|
|
||||||
if image_base64:
|
|
||||||
media = await QQOfficialMessageEvent.upload_group_and_c2c_image(
|
|
||||||
send_helper, # type: ignore
|
|
||||||
image_base64,
|
|
||||||
1,
|
|
||||||
group_openid=session.session_id,
|
|
||||||
)
|
|
||||||
payload["media"] = media
|
|
||||||
payload["msg_type"] = 7
|
|
||||||
if record_file_path:
|
|
||||||
media = await QQOfficialMessageEvent.upload_group_and_c2c_record(
|
|
||||||
send_helper, # type: ignore
|
|
||||||
record_file_path,
|
|
||||||
3,
|
|
||||||
group_openid=session.session_id,
|
|
||||||
)
|
|
||||||
payload["media"] = media
|
|
||||||
payload["msg_type"] = 7
|
|
||||||
ret = await self.client.api.post_group_message(
|
|
||||||
group_openid=session.session_id,
|
|
||||||
**payload,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if image_path:
|
|
||||||
payload["file_image"] = image_path
|
|
||||||
ret = await self.client.api.post_message(
|
|
||||||
channel_id=session.session_id,
|
|
||||||
**payload,
|
|
||||||
)
|
|
||||||
elif session.message_type == MessageType.FRIEND_MESSAGE:
|
|
||||||
payload["msg_seq"] = random.randint(1, 10000)
|
|
||||||
if image_base64:
|
|
||||||
media = await QQOfficialMessageEvent.upload_group_and_c2c_image(
|
|
||||||
send_helper, # type: ignore
|
|
||||||
image_base64,
|
|
||||||
1,
|
|
||||||
openid=session.session_id,
|
|
||||||
)
|
|
||||||
payload["media"] = media
|
|
||||||
payload["msg_type"] = 7
|
|
||||||
if record_file_path:
|
|
||||||
media = await QQOfficialMessageEvent.upload_group_and_c2c_record(
|
|
||||||
send_helper, # type: ignore
|
|
||||||
record_file_path,
|
|
||||||
3,
|
|
||||||
openid=session.session_id,
|
|
||||||
)
|
|
||||||
payload["media"] = media
|
|
||||||
payload["msg_type"] = 7
|
|
||||||
ret = await QQOfficialMessageEvent.post_c2c_message(
|
|
||||||
send_helper, # type: ignore
|
|
||||||
openid=session.session_id,
|
|
||||||
**payload,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
"[QQOfficialWebhook] Unsupported message type for send_by_session: %s",
|
|
||||||
session.message_type,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
sent_message_id = self._extract_message_id(ret)
|
|
||||||
if sent_message_id:
|
|
||||||
self.remember_session_message_id(session.session_id, sent_message_id)
|
|
||||||
await super().send_by_session(session, message_chain)
|
|
||||||
|
|
||||||
def remember_session_message_id(self, session_id: str, message_id: str) -> None:
|
def remember_session_message_id(self, session_id: str, message_id: str) -> None:
|
||||||
if not session_id or not message_id:
|
if not session_id or not message_id:
|
||||||
|
|||||||
@@ -7,17 +7,17 @@
|
|||||||
|
|
||||||
## Supported Basic Message Types
|
## Supported Basic Message Types
|
||||||
|
|
||||||
> Version v4.15.0.
|
> Version v4.19.6.
|
||||||
|
|
||||||
| Message Type | Receive | Send | Notes |
|
| Message Type | Receive | Send | Notes |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Text | Yes | Yes | |
|
| Text | Yes | Yes | |
|
||||||
| Image | Yes | Yes | |
|
| Image | Yes | Yes | |
|
||||||
| Voice | No | No | |
|
| Voice | Yes | Yes | |
|
||||||
| Video | No | No | |
|
| Video | Yes | Yes | |
|
||||||
| File | No | No | |
|
| File | Yes | Yes | |
|
||||||
|
|
||||||
Proactive message push: Not supported.
|
Proactive message push: Supported.
|
||||||
|
|
||||||
## Apply for a Bot
|
## Apply for a Bot
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
|
|
||||||
## Supported Basic Message Types
|
## Supported Basic Message Types
|
||||||
|
|
||||||
> Version v4.15.0.
|
> Version v4.19.6.
|
||||||
|
|
||||||
| Message Type | Receive | Send | Notes |
|
| Message Type | Receive | Send | Notes |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Text | Yes | Yes | |
|
| Text | Yes | Yes | |
|
||||||
| Image | Yes | Yes | |
|
| Image | Yes | Yes | |
|
||||||
| Voice | No | No | |
|
| Voice | Yes | Yes | |
|
||||||
| Video | No | No | |
|
| Video | Yes | Yes | |
|
||||||
| File | No | No | |
|
| File | Yes | Yes | |
|
||||||
|
|
||||||
Proactive message push: Not supported.
|
Proactive message push: Supported.
|
||||||
|
|
||||||
## Quick Deployment Steps
|
## Quick Deployment Steps
|
||||||
|
|
||||||
|
|||||||
@@ -10,17 +10,17 @@
|
|||||||
|
|
||||||
## 支持的基本消息类型
|
## 支持的基本消息类型
|
||||||
|
|
||||||
> 版本 v4.15.0。
|
> 版本 v4.19.6。
|
||||||
|
|
||||||
| 消息类型 | 是否支持接收 | 是否支持发送 | 备注 |
|
| 消息类型 | 是否支持接收 | 是否支持发送 | 备注 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 文本 | 是 | 是 | |
|
| 文本 | 是 | 是 | |
|
||||||
| 图片 | 是 | 是 | |
|
| 图片 | 是 | 是 | |
|
||||||
| 语音 | 否 | 否 | |
|
| 语音 | 是 | 是 | |
|
||||||
| 视频 | 否 | 否 | |
|
| 视频 | 是 | 是 | |
|
||||||
| 文件 | 否 | 否 | |
|
| 文件 | 是 | 是 | |
|
||||||
|
|
||||||
主动消息推送:不支持。
|
主动消息推送:支持。
|
||||||
|
|
||||||
## 申请一个机器人
|
## 申请一个机器人
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,17 @@
|
|||||||
|
|
||||||
## 支持的基本消息类型
|
## 支持的基本消息类型
|
||||||
|
|
||||||
> 版本 v4.15.0。
|
> 版本 v4.19.6。
|
||||||
|
|
||||||
| 消息类型 | 是否支持接收 | 是否支持发送 | 备注 |
|
| 消息类型 | 是否支持接收 | 是否支持发送 | 备注 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 文本 | 是 | 是 | |
|
| 文本 | 是 | 是 | |
|
||||||
| 图片 | 是 | 是 | |
|
| 图片 | 是 | 是 | |
|
||||||
| 语音 | 否 | 否 | |
|
| 语音 | 是 | 是 | |
|
||||||
| 视频 | 否 | 否 | |
|
| 视频 | 是 | 是 | |
|
||||||
| 文件 | 否 | 否 | |
|
| 文件 | 是 | 是 | |
|
||||||
|
|
||||||
主动消息推送:不支持。
|
主动消息推送:支持。
|
||||||
|
|
||||||
## 快速部署通道
|
## 快速部署通道
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user