mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-02 22:17:08 +08:00
- 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.
181 lines
6.6 KiB
Python
181 lines
6.6 KiB
Python
"""
|
|
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,
|
|
)
|