From d839e729983828e45e0c135b8bd7adf169c1d4c0 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 11 Feb 2025 01:18:25 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=20Webhook=20?= =?UTF-8?q?=E6=96=B9=E5=BC=8F=E6=8E=A5=E5=85=A5=20QQ=20=E5=AE=98=E6=96=B9?= =?UTF-8?q?=E6=9C=BA=E5=99=A8=E4=BA=BA=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../qqofficial/qqofficial_platform_adapter.py | 13 +-- .../qqofficial_webhook/qo_webhook_adapter.py | 81 +++++++++++++------ .../qqofficial_webhook/qo_webhook_event.py | 18 +++++ .../qqofficial_webhook/qo_webhook_server.py | 62 +++++++++----- requirements.txt | 3 +- 6 files changed, 128 insertions(+), 50 deletions(-) create mode 100644 astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py diff --git a/.gitignore b/.gitignore index c1311a9ff..7745d18ba 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ package-lock.json package.json venv/* packages/python_interpreter/workplace +.venv/* diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py b/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py index de94ab6a4..4548bbf3c 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import botpy import logging import time @@ -28,25 +30,25 @@ class botClient(Client): # 收到群消息 async def on_group_at_message_create(self, message: botpy.message.GroupMessage): - abm = self.platform._parse_from_qqofficial(message, MessageType.GROUP_MESSAGE) + abm = QQOfficialPlatformAdapter._parse_from_qqofficial(message, MessageType.GROUP_MESSAGE) abm.session_id = abm.sender.user_id if self.platform.unique_session else message.group_openid self._commit(abm) # 收到频道消息 async def on_at_message_create(self, message: botpy.message.Message): - abm = self.platform._parse_from_qqofficial(message, MessageType.GROUP_MESSAGE) + abm = QQOfficialPlatformAdapter._parse_from_qqofficial(message, MessageType.GROUP_MESSAGE) abm.session_id = abm.sender.user_id if self.platform.unique_session else message.channel_id self._commit(abm) # 收到私聊消息 async def on_direct_message_create(self, message: botpy.message.DirectMessage): - abm = self.platform._parse_from_qqofficial(message, MessageType.FRIEND_MESSAGE) + abm = QQOfficialPlatformAdapter._parse_from_qqofficial(message, MessageType.FRIEND_MESSAGE) abm.session_id = abm.sender.user_id self._commit(abm) # 收到 C2C 消息 async def on_c2c_message_create(self, message: botpy.message.C2CMessage): - abm = self.platform._parse_from_qqofficial(message, MessageType.FRIEND_MESSAGE) + abm = QQOfficialPlatformAdapter._parse_from_qqofficial(message, MessageType.FRIEND_MESSAGE) abm.session_id = abm.sender.user_id self._commit(abm) @@ -102,7 +104,8 @@ class QQOfficialPlatformAdapter(Platform): "QQ 机器人官方 API 适配器", ) - def _parse_from_qqofficial(self, message: Union[botpy.message.Message, botpy.message.GroupMessage], + @staticmethod + def _parse_from_qqofficial(message: Union[botpy.message.Message, botpy.message.GroupMessage], message_type: MessageType): abm = AstrBotMessage() abm.type = message_type diff --git a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py index 2a5b22d2f..ad443df80 100644 --- a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +++ b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py @@ -1,23 +1,60 @@ import botpy import logging -import time import asyncio -import os +import botpy.message +import botpy.types +import botpy.types.message -from botpy import BotAPI, BotHttp -from astrbot.api.platform import Platform, AstrBotMessage, MessageMember, MessageType, PlatformMetadata +from botpy import Client +from astrbot.api.platform import Platform, AstrBotMessage, MessageType, PlatformMetadata from astrbot.api.event import MessageChain -from typing import Union, List -from astrbot.api.message_components import Image, Plain, At from astrbot.core.platform.astr_message_event import MessageSesion -# from .qqofficial_message_event import QQOfficialMessageEvent +from .qo_webhook_event import QQOfficialWebhookMessageEvent from ...register import register_platform_adapter -from astrbot.core.message.components import BaseMessageComponent from .qo_webhook_server import QQOfficialWebhook +from ..qqofficial.qqofficial_platform_adapter import QQOfficialPlatformAdapter # remove logger handler for handler in logging.root.handlers[:]: logging.root.removeHandler(handler) + +# QQ 机器人官方框架 +class botClient(Client): + def set_platform(self, platform: 'QQOfficialWebhookPlatformAdapter'): + self.platform = platform + + # 收到群消息 + async def on_group_at_message_create(self, message: botpy.message.GroupMessage): + abm = QQOfficialPlatformAdapter._parse_from_qqofficial(message, MessageType.GROUP_MESSAGE) + abm.session_id = abm.sender.user_id if self.platform.unique_session else message.group_openid + self._commit(abm) + + # 收到频道消息 + async def on_at_message_create(self, message: botpy.message.Message): + abm = QQOfficialPlatformAdapter._parse_from_qqofficial(message, MessageType.GROUP_MESSAGE) + abm.session_id = abm.sender.user_id if self.platform.unique_session else message.channel_id + self._commit(abm) + + # 收到私聊消息 + async def on_direct_message_create(self, message: botpy.message.DirectMessage): + abm = QQOfficialPlatformAdapter._parse_from_qqofficial(message, MessageType.FRIEND_MESSAGE) + abm.session_id = abm.sender.user_id + self._commit(abm) + + # 收到 C2C 消息 + async def on_c2c_message_create(self, message: botpy.message.C2CMessage): + abm = self.platform._parse_from_qqofficial(message, MessageType.FRIEND_MESSAGE) + abm.session_id = abm.sender.user_id + self._commit(abm) + + def _commit(self, abm: AstrBotMessage): + self.platform.commit_event(QQOfficialWebhookMessageEvent( + abm.message_str, + abm, + self.platform.meta(), + abm.session_id, + self + )) @register_platform_adapter("qq_official_webhook", "QQ 机器人官方 API 适配器(Webhook)") class QQOfficialWebhookPlatformAdapter(Platform): @@ -30,21 +67,18 @@ class QQOfficialWebhookPlatformAdapter(Platform): self.appid = platform_config['appid'] self.secret = platform_config['secret'] self.unique_session = platform_settings['unique_session'] - qq_group = platform_config['enable_group_c2c'] - guild_dm = platform_config['enable_guild_direct_message'] - - if qq_group: - self.intents = botpy.Intents( - public_messages=True, - public_guild_messages=True, - direct_message=guild_dm - ) - else: - self.intents = botpy.Intents( - public_guild_messages=True, - direct_message=guild_dm - ) + intents = botpy.Intents( + public_messages=True, + public_guild_messages=True, + direct_message=True + ) + self.client = botClient( + intents=intents, # 已经无用 + bot_log=False, + timeout=20, + ) + self.client.set_platform(self) async def send_by_session(self, session: MessageSesion, message_chain: MessageChain): raise NotImplementedError("QQ 机器人官方 API 适配器不支持 send_by_session") @@ -58,7 +92,8 @@ class QQOfficialWebhookPlatformAdapter(Platform): async def run(self): self.webhook_helper = QQOfficialWebhook( self.config, - self._event_queue + self._event_queue, + self.client ) await self.webhook_helper.initialize() await self.webhook_helper.start_polling() \ No newline at end of file diff --git a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py new file mode 100644 index 000000000..2056ab56e --- /dev/null +++ b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py @@ -0,0 +1,18 @@ +import botpy +import botpy.message +import botpy.types +import botpy.types.message +from astrbot.core.utils.io import file_to_base64, download_image_by_url +from astrbot.api.event import AstrMessageEvent, MessageChain +from astrbot.api.platform import AstrBotMessage, PlatformMetadata +from astrbot.api.message_components import Plain, Image, Reply +from botpy import Client +from botpy.http import Route +from astrbot.api import logger +from ..qqofficial.qqofficial_message_event import QQOfficialMessageEvent + + +class QQOfficialWebhookMessageEvent(QQOfficialMessageEvent): + def __init__(self, message_str: str, message_obj: AstrBotMessage, platform_meta: PlatformMetadata, session_id: str, bot: Client): + super().__init__(message_str, message_obj, platform_meta, session_id, bot) + \ No newline at end of file diff --git a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py index f61cc76ff..f265e7fc0 100644 --- a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +++ b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py @@ -1,13 +1,20 @@ import aiohttp import quart import json +import logging import asyncio import typing from botpy import BotAPI, BotHttp, Client, Token, BotWebSocket, ConnectionSession from astrbot.api import logger +import traceback +from cryptography.hazmat.primitives.asymmetric import ed25519 + +# remove logger handler +for handler in logging.root.handlers[:]: + logging.root.removeHandler(handler) class QQOfficialWebhook(): - def __init__(self, config: dict, event_queue: asyncio.Queue): + def __init__(self, config: dict, event_queue: asyncio.Queue, botpy_client: Client): self.appid = config['appid'] self.secret = config['secret'] self.port = config.get("port", 6194) @@ -17,17 +24,20 @@ class QQOfficialWebhook(): self.http: BotHttp = BotHttp(timeout=300) self.api: BotAPI = BotAPI(http=self.http) - self.token = Token(self.appid, self.secret) self.server = quart.Quart(__name__) self.server.add_url_rule('/astrbot-qo-webhook/callback', view_func=self.callback, methods=['POST']) - + self.client = botpy_client self.event_queue = event_queue async def initialize(self): + logger.info(f"正在登录到 QQ 官方机器人...") self.user = await self.http.login(self.token) logger.info(f"已登录 QQ 官方机器人账号: {self.user}") + # 直接注入到 botpy 的 Client,移花接木! + self.client.api = self.api + self.client.http = self.http async def bot_connect(): pass @@ -35,34 +45,43 @@ class QQOfficialWebhook(): self._connection = ConnectionSession( max_async=1, connect=bot_connect, - dispatch=self.dispatch, + dispatch=self.client.ws_dispatch, loop=asyncio.get_event_loop(), api=self.api, ) - - async def dispatch(self, event: str, *args: typing.Any, **kwargs: typing.Any): - print("dispatch:", locals()) + + async def repeat_seed(self, bot_secret: str, target_size: int = 32) -> bytes: + seed = bot_secret + while len(seed) < target_size: + seed *= 2 + return seed[:target_size].encode('utf-8') + + + async def webhook_validation(self, validation_payload: dict): + seed = await self.repeat_seed(self.secret) + private_key = ed25519.Ed25519PrivateKey.from_private_bytes(seed) + msg = validation_payload.get("event_ts", "") + validation_payload.get("plain_token", "") + # sign + signature = private_key.sign(msg.encode()).hex() + response = { + "plain_token": validation_payload.get("plain_token"), + "signature": signature + } + return response async def callback(self): msg: dict = await quart.request.json logger.debug(f"收到 qq_official_webhook 回调: {msg}") - # if await self._is_system_event(msg, ws): - # return - event = msg.get("t") opcode = msg.get("op") - event_seq = msg["s"] - # if event_seq > 0: - # self._session["last_seq"] = event_seq - - if event == "READY": - # 心跳检查 - pass - - if event == "RESUMED": - # 心跳检查 - pass + data = msg.get("d") + + if opcode == 13: + # validation + signed = await self.webhook_validation(data) + print(signed) + return signed if event and opcode == BotWebSocket.WS_DISPATCH_EVENT: event = msg["t"].lower() @@ -73,6 +92,7 @@ class QQOfficialWebhook(): else: func(msg) + return {"opcode": 12} async def start_polling(self): await self.server.run_task( diff --git a/requirements.txt b/requirements.txt index 58c531504..c077496e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,4 +19,5 @@ aiodocker silk-python lark-oapi -ormsgpack \ No newline at end of file +ormsgpack +cryptography \ No newline at end of file