Files
MoviePilot/app/modules/qqbot/__init__.py
EkkoG 65c18b1d52 feat(qqbot): implement QQ Bot notification module with API and WebSocket support
- Added QQ Bot notification module to facilitate proactive message sending and message reception via Gateway.
- Implemented API functions for sending C2C and group messages.
- Established WebSocket client for real-time message handling.
- Updated requirements to include websocket-client dependency.
- Enhanced schemas to support QQ channel capabilities and notification configurations.
2026-03-07 23:21:07 +08:00

181 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
QQ Bot 通知模块
基于 QQ 开放平台,支持主动消息推送和 Gateway 接收消息
注意:用户/群需曾与机器人交互过才能收到主动消息,且每月有配额限制
"""
import json
from typing import Optional, List, Tuple, Union, Any
from app.core.context import MediaInfo, Context
from app.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.modules.qqbot.qqbot import QQBot
from app.schemas import CommingMessage, MessageChannel, Notification
from app.schemas.types import ModuleType
class QQBotModule(_ModuleBase, _MessageBase[QQBot]):
"""QQ Bot 通知模块"""
def init_module(self) -> None:
super().init_service(service_name=QQBot.__name__.lower(), service_type=QQBot)
self._channel = MessageChannel.QQ
@staticmethod
def get_name() -> str:
return "QQ"
@staticmethod
def get_type() -> ModuleType:
return ModuleType.Notification
@staticmethod
def get_subtype() -> MessageChannel:
return MessageChannel.QQ
@staticmethod
def get_priority() -> int:
return 10
def stop(self) -> None:
for client in self.get_instances().values():
if hasattr(client, "stop"):
client.stop()
def test(self) -> Optional[Tuple[bool, str]]:
if not self.get_instances():
return None
for name, client in self.get_instances().items():
if not client.get_state():
return False, f"QQ Bot {name} 未就绪"
return True, ""
def init_setting(self) -> Tuple[str, Union[str, bool]]:
pass
def message_parser(
self, source: str, body: Any, form: Any, args: Any
) -> Optional[CommingMessage]:
"""
解析 Gateway 转发的 QQ 消息
body 格式: {"type": "C2C_MESSAGE_CREATE"|"GROUP_AT_MESSAGE_CREATE", "content": "...", "author": {...}, "id": "...", ...}
"""
client_config = self.get_config(source)
if not client_config:
return None
try:
if isinstance(body, bytes):
msg_body = json.loads(body)
elif isinstance(body, dict):
msg_body = body
else:
return None
except (json.JSONDecodeError, TypeError) as err:
logger.debug(f"解析 QQ 消息失败: {err}")
return None
msg_type = msg_body.get("type")
content = (msg_body.get("content") or "").strip()
if not content:
return None
if msg_type == "C2C_MESSAGE_CREATE":
author = msg_body.get("author", {})
user_openid = author.get("user_openid", "")
if not user_openid:
return None
logger.info(f"收到 QQ 私聊消息: userid={user_openid}, text={content[:50]}...")
return CommingMessage(
channel=MessageChannel.QQ,
source=client_config.name,
userid=user_openid,
username=user_openid,
text=content,
)
elif msg_type == "GROUP_AT_MESSAGE_CREATE":
author = msg_body.get("author", {})
member_openid = author.get("member_openid", "")
group_openid = msg_body.get("group_openid", "")
# 群聊用 group:group_openid 作为 userid便于回复时识别
userid = f"group:{group_openid}" if group_openid else member_openid
logger.info(f"收到 QQ 群消息: group={group_openid}, userid={member_openid}, text={content[:50]}...")
return CommingMessage(
channel=MessageChannel.QQ,
source=client_config.name,
userid=userid,
username=member_openid or group_openid,
text=content,
)
return None
def post_message(self, message: Notification, **kwargs) -> None:
for conf in self.get_configs().values():
if not self.check_message(message, conf.name):
continue
targets = message.targets
userid = message.userid
if not userid and targets:
userid = targets.get("qq_userid") or targets.get("qq_openid")
if not userid:
userid = targets.get("qq_group_openid") or targets.get("qq_group")
if userid:
userid = f"group:{userid}"
# 无 userid 且无默认配置时,由 client 向曾发过消息的用户/群广播
client: QQBot = self.get_instance(conf.name)
if client:
client.send_msg(
title=message.title,
text=message.text,
image=message.image,
link=message.link,
userid=userid,
targets=targets,
)
def post_medias_message(self, message: Notification, medias: List[MediaInfo]) -> None:
for conf in self.get_configs().values():
if not self.check_message(message, conf.name):
continue
targets = message.targets
userid = message.userid
if not userid and targets:
userid = targets.get("qq_userid") or targets.get("qq_openid")
if not userid:
g = targets.get("qq_group_openid") or targets.get("qq_group")
if g:
userid = f"group:{g}"
client: QQBot = self.get_instance(conf.name)
if client:
client.send_medias_msg(
medias=medias,
userid=userid,
title=message.title,
link=message.link,
targets=targets,
)
def post_torrents_message(
self, message: Notification, torrents: List[Context]
) -> None:
for conf in self.get_configs().values():
if not self.check_message(message, conf.name):
continue
targets = message.targets
userid = message.userid
if not userid and targets:
userid = targets.get("qq_userid") or targets.get("qq_openid")
if not userid:
g = targets.get("qq_group_openid") or targets.get("qq_group")
if g:
userid = f"group:{g}"
client: QQBot = self.get_instance(conf.name)
if client:
client.send_torrents_msg(
torrents=torrents,
userid=userid,
title=message.title,
link=message.link,
targets=targets,
)