diff --git a/nonebot_plugin_saa/__init__.py b/nonebot_plugin_saa/__init__.py index cb8d2dad..6327e7a9 100644 --- a/nonebot_plugin_saa/__init__.py +++ b/nonebot_plugin_saa/__init__.py @@ -41,5 +41,6 @@ "~telegram", "~feishu", "~red", + "~qq", }, ) diff --git a/nonebot_plugin_saa/adapters/__init__.py b/nonebot_plugin_saa/adapters/__init__.py index abea08b8..5d66e6a5 100644 --- a/nonebot_plugin_saa/adapters/__init__.py +++ b/nonebot_plugin_saa/adapters/__init__.py @@ -1,3 +1,4 @@ +from . import qq as qq from . import red as red from . import feishu as feishu from . import qqguild as qqguild diff --git a/nonebot_plugin_saa/adapters/qq.py b/nonebot_plugin_saa/adapters/qq.py new file mode 100644 index 00000000..1d35d578 --- /dev/null +++ b/nonebot_plugin_saa/adapters/qq.py @@ -0,0 +1,227 @@ +from functools import partial +from typing import List, Union, Literal, Optional + +from nonebot.adapters import Event +from nonebot.adapters import Bot as BaseBot + +from ..utils import SupportedAdapters +from ..types import Text, Image, Reply, Mention +from ..auto_select_bot import register_list_targets +from ..abstract_factories import ( + MessageFactory, + MessageSegmentFactory, + register_ms_adapter, + assamble_message_factory, +) +from ..registries import ( + Receipt, + MessageId, + TargetQQGroup, + PlatformTarget, + TargetQQPrivate, + QQGuildDMSManager, + TargetQQGuildDirect, + TargetQQGuildChannel, + register_sender, + register_qqguild_dms, + register_target_extractor, +) + +try: + from nonebot.adapters.qq.event import GuildMessageEvent + from nonebot.adapters.qq.models import Message as ApiMessage + from nonebot.adapters.qq.models import ( + PostC2CFilesReturn, + PostGroupFilesReturn, + PostC2CMessagesReturn, + PostGroupMessagesReturn, + ) + from nonebot.adapters.qq import ( + Bot, + Message, + MessageSegment, + MessageCreateEvent, + AtMessageCreateEvent, + C2CMessageCreateEvent, + DirectMessageCreateEvent, + GroupAtMessageCreateEvent, + ) + + adapter = SupportedAdapters.qq + register_qq = partial(register_ms_adapter, adapter) + + MessageFactory.register_adapter_message(adapter, Message) + + class QQMessageId(MessageId): + adapter_name: Literal[adapter] = adapter + message_id: str + + @register_qq(Text) + def _text(t: Text) -> MessageSegment: + return MessageSegment.text(t.data["text"]) + + @register_qq(Image) + def _image(i: Image) -> MessageSegment: + if isinstance(i.data["image"], str): + return MessageSegment.image(i.data["image"]) + else: + return MessageSegment.file_image(i.data["image"]) + + @register_qq(Mention) + def _mention(m: Mention) -> MessageSegment: + return MessageSegment.mention_user(m.data["user_id"]) + + @register_qq(Reply) + def _reply(r: Reply) -> MessageSegment: + assert isinstance(r.data, QQMessageId) + return MessageSegment.reference(r.data.message_id) + + @register_target_extractor(GuildMessageEvent) + def extract_message_event(event: Event) -> PlatformTarget: + if isinstance(event, DirectMessageCreateEvent): + assert event.guild_id + assert event.author and event.author.id + return TargetQQGuildDirect( + source_guild_id=int(event.guild_id), recipient_id=int(event.author.id) + ) + elif isinstance(event, (MessageCreateEvent, AtMessageCreateEvent)): + assert event.channel_id + return TargetQQGuildChannel(channel_id=int(event.channel_id)) + else: + raise ValueError(f"{type(event)} not supported") + + @register_target_extractor(C2CMessageCreateEvent) + def extract_c2c_message_event(event: Event) -> PlatformTarget: + assert isinstance(event, C2CMessageCreateEvent) + return TargetQQPrivate(user_id=int(event.author.id)) + + @register_target_extractor(GroupAtMessageCreateEvent) + def extract_group_at_message_event(event: Event) -> PlatformTarget: + assert isinstance(event, GroupAtMessageCreateEvent) + return TargetQQGroup(group_id=int(event.group_id)) + + @register_qqguild_dms(adapter) + async def get_dms(target: TargetQQGuildDirect, bot: BaseBot) -> int: + assert isinstance(bot, Bot) + + dms = await bot.post_dms( + recipient_id=str(target.recipient_id), + source_guild_id=str(target.source_guild_id), + ) + assert dms.guild_id + return int(dms.guild_id) + + class QQReceipt(Receipt): + msg_return: Union[ + ApiMessage, + PostC2CMessagesReturn, + PostGroupMessagesReturn, + PostC2CFilesReturn, + PostGroupFilesReturn, + ] + adapter_name: Literal[adapter] = adapter + + async def revoke(self, hidetip=False): + if not isinstance(self.msg_return, ApiMessage): + raise NotImplementedError("only guild message can be revoked") + + assert self.msg_return.channel_id + assert self.msg_return.id + return await self._get_bot().delete_message( + channel_id=self.msg_return.channel_id, + message_id=self.msg_return.id, + hidetip=hidetip, + ) + + @property + def raw(self): + return self.msg_return + + @register_sender(SupportedAdapters.qq) + async def send( + bot, + msg: MessageFactory[MessageSegmentFactory], + target: PlatformTarget, + event: Optional[Event], + at_sender: bool, + reply: bool, + ) -> QQReceipt: + assert isinstance(bot, Bot) + assert isinstance( + target, + (TargetQQGuildChannel, TargetQQGuildDirect, TargetQQGroup, TargetQQPrivate), + ) + + full_msg = msg + if event: + assert isinstance( + event, + (GuildMessageEvent, C2CMessageCreateEvent, GroupAtMessageCreateEvent), + ) + assert event.author + assert event.id + full_msg = assamble_message_factory( + msg, + Mention(event.author.id), + Reply(QQMessageId(message_id=event.id)), + at_sender, + reply, + ) + + # parse Message + message = await full_msg._build(bot) + assert isinstance(message, Message) + + if event: # reply to user + msg_return = await bot.send(event, message) + else: + if isinstance(target, TargetQQGuildDirect): + guild_id = await QQGuildDMSManager.aget_guild_id(target, bot) + msg_return = await bot.send_to_dms( + guild_id=str(guild_id), + message=message, + ) + elif isinstance(target, TargetQQGuildChannel): + msg_return = await bot.send_to_channel( + channel_id=str(target.channel_id), + message=message, + ) + elif isinstance(target, TargetQQPrivate): + msg_return = await bot.send_to_c2c( + user_id=str(target.user_id), + message=message, + ) + elif isinstance(target, TargetQQGroup): + msg_return = await bot.send_to_group( + group_id=str(target.group_id), + message=message, + ) + else: + raise ValueError(f"{type(event)} not supported") + + return QQReceipt(bot_id=bot.self_id, msg_return=msg_return) + + @register_list_targets(SupportedAdapters.qq) + async def list_targets(bot: BaseBot) -> List[PlatformTarget]: + assert isinstance(bot, Bot) + + targets = [] + + # TODO: 私聊 + + guilds = await bot.guilds() + for guild in guilds: + channels = await bot.get_channels(guild_id=guild.id) # type: ignore + for channel in channels: + targets.append( + TargetQQGuildChannel( + channel_id=channel.id, # type: ignore + ) + ) + + return targets + +except ImportError: + pass +except Exception as e: + raise e diff --git a/nonebot_plugin_saa/adapters/qqguild.py b/nonebot_plugin_saa/adapters/qqguild.py index 5c46d394..aa5b5530 100644 --- a/nonebot_plugin_saa/adapters/qqguild.py +++ b/nonebot_plugin_saa/adapters/qqguild.py @@ -27,6 +27,10 @@ try: from nonebot.adapters.qqguild.api import Message as ApiMessage + from nonebot.adapters.qqguild.exception import ( + AuditException, + QQGuildAdapterException, + ) from nonebot.adapters.qqguild import ( Bot, Message, @@ -42,6 +46,9 @@ MessageFactory.register_adapter_message(adapter, Message) + class QQGuildAuditRejectException(QQGuildAdapterException): + ... + class QQGuildMessageId(MessageId): adapter_name: Literal[adapter] = adapter message_id: str @@ -151,56 +158,66 @@ async def send( if reference := (message["reference"] or None): reference = reference[-1].data["reference"] - if event: # reply to user - if isinstance(event, DirectMessageCreateEvent): - sent_msg = await bot.post_dms_messages( - guild_id=event.guild_id, # type: ignore - msg_id=event.id, - content=content, - embed=embed, # type: ignore - ark=ark, # type: ignore - image=image, # type: ignore - file_image=file_image, # type: ignore - markdown=markdown, # type: ignore - message_reference=reference, # type: ignore - ) - else: - sent_msg = await bot.post_messages( - channel_id=event.channel_id, # type: ignore - msg_id=event.id, - content=content, - embed=embed, # type: ignore - ark=ark, # type: ignore - image=image, # type: ignore - file_image=file_image, # type: ignore - markdown=markdown, # type: ignore - message_reference=reference, # type: ignore - ) - else: - if isinstance(target, TargetQQGuildChannel): - assert target.channel_id - sent_msg = await bot.post_messages( - channel_id=target.channel_id, - content=content, - embed=embed, # type: ignore - ark=ark, # type: ignore - image=image, # type: ignore - file_image=file_image, # type: ignore - markdown=markdown, # type: ignore - message_reference=reference, # type: ignore - ) + try: + if event: # reply to user + if isinstance(event, DirectMessageCreateEvent): + sent_msg = await bot.post_dms_messages( + guild_id=event.guild_id, # type: ignore + msg_id=event.id, + content=content, + embed=embed, # type: ignore + ark=ark, # type: ignore + image=image, # type: ignore + file_image=file_image, # type: ignore + markdown=markdown, # type: ignore + message_reference=reference, # type: ignore + ) + else: + sent_msg = await bot.post_messages( + channel_id=event.channel_id, # type: ignore + msg_id=event.id, + content=content, + embed=embed, # type: ignore + ark=ark, # type: ignore + image=image, # type: ignore + file_image=file_image, # type: ignore + markdown=markdown, # type: ignore + message_reference=reference, # type: ignore + ) else: - guild_id = await QQGuildDMSManager.aget_guild_id(target, bot) - sent_msg = await bot.post_dms_messages( - guild_id=guild_id, # type: ignore - content=content, - embed=embed, # type: ignore - ark=ark, # type: ignore - image=image, # type: ignore - file_image=file_image, # type: ignore - markdown=markdown, # type: ignore - message_reference=reference, # type: ignore - ) + if isinstance(target, TargetQQGuildChannel): + assert target.channel_id + sent_msg = await bot.post_messages( + channel_id=target.channel_id, + content=content, + embed=embed, # type: ignore + ark=ark, # type: ignore + image=image, # type: ignore + file_image=file_image, # type: ignore + markdown=markdown, # type: ignore + message_reference=reference, # type: ignore + ) + else: + guild_id = await QQGuildDMSManager.aget_guild_id(target, bot) + sent_msg = await bot.post_dms_messages( + guild_id=guild_id, # type: ignore + content=content, + embed=embed, # type: ignore + ark=ark, # type: ignore + image=image, # type: ignore + file_image=file_image, # type: ignore + markdown=markdown, # type: ignore + message_reference=reference, # type: ignore + ) + except AuditException as e: + audit = await e.get_audit_result() + if type(audit) == "MESSAGE_AUDIT_REJECT": + raise QQGuildAuditRejectException() + sent_msg = ApiMessage( + id=audit.message_id, + channel_id=audit.channel_id, + guild_id=audit.guild_id, + ) return QQGuildReceipt(bot_id=bot.self_id, sent_msg=sent_msg) diff --git a/nonebot_plugin_saa/utils/const.py b/nonebot_plugin_saa/utils/const.py index 629826bc..284f1e93 100644 --- a/nonebot_plugin_saa/utils/const.py +++ b/nonebot_plugin_saa/utils/const.py @@ -9,6 +9,7 @@ class SupportedAdapters(StrEnum): telegram = "Telegram" feishu = "Feishu" red = "RedProtocol" + qq = "QQ" fake = "fake" # for nonebug diff --git a/poetry.lock b/poetry.lock index abe8bfff..4344193f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "anyio" version = "3.7.1" description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -26,7 +25,6 @@ trio = ["trio (<0.22)"] name = "appnope" version = "0.1.3" description = "Disable App Nap on macOS >= 10.9" -category = "dev" optional = false python-versions = "*" files = [ @@ -38,7 +36,6 @@ files = [ name = "asgiref" version = "3.7.2" description = "ASGI specs, helper code, and adapters" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -54,27 +51,26 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] [[package]] name = "asttokens" -version = "2.4.0" +version = "2.4.1" description = "Annotate AST trees with source code positions" -category = "dev" optional = false python-versions = "*" files = [ - {file = "asttokens-2.4.0-py2.py3-none-any.whl", hash = "sha256:cf8fc9e61a86461aa9fb161a14a0841a03c405fa829ac6b202670b3495d2ce69"}, - {file = "asttokens-2.4.0.tar.gz", hash = "sha256:2e0171b991b2c959acc6c49318049236844a5da1d65ba2672c4880c1c894834e"}, + {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, + {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, ] [package.dependencies] six = ">=1.12.0" [package.extras] -test = ["astroid", "pytest"] +astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] +test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] [[package]] name = "async-asgi-testclient" version = "1.4.11" description = "Async client for testing ASGI web applications" -category = "dev" optional = false python-versions = "*" files = [ @@ -89,7 +85,6 @@ requests = ">=2.21,<3.0" name = "backcall" version = "0.2.0" description = "Specifications for callback functions passed in to an API" -category = "dev" optional = false python-versions = "*" files = [ @@ -101,7 +96,6 @@ files = [ name = "cashews" version = "6.3.0" description = "cache tools with async power" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -120,7 +114,6 @@ tests = ["hypothesis", "pytest", "pytest-asyncio"] name = "certifi" version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -132,7 +125,6 @@ files = [ name = "charset-normalizer" version = "3.3.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -232,7 +224,6 @@ files = [ name = "click" version = "8.1.7" description = "Composable command line interface toolkit" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -247,7 +238,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -259,7 +249,6 @@ files = [ name = "coverage" version = "7.3.2" description = "Code coverage measurement for Python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -327,7 +316,6 @@ toml = ["tomli"] name = "decorator" version = "5.1.1" description = "Decorators for Humans" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -339,7 +327,6 @@ files = [ name = "exceptiongroup" version = "1.1.3" description = "Backport of PEP 654 (exception groups)" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -354,7 +341,6 @@ test = ["pytest (>=6)"] name = "executing" version = "2.0.0" description = "Get the currently executing AST node of a frame, and other information" -category = "dev" optional = false python-versions = "*" files = [ @@ -369,7 +355,6 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth name = "fastapi" version = "0.104.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -390,7 +375,6 @@ all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)" name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -402,7 +386,6 @@ files = [ name = "h2" version = "4.1.0" description = "HTTP/2 State-Machine based protocol implementation" -category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -418,7 +401,6 @@ hyperframe = ">=6.0,<7" name = "hpack" version = "4.0.0" description = "Pure-Python HPACK header compression" -category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -430,7 +412,6 @@ files = [ name = "httpcore" version = "0.18.0" description = "A minimal low-level HTTP client." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -442,17 +423,16 @@ files = [ anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" -sniffio = ">=1.0.0,<2.0.0" +sniffio = "==1.*" [package.extras] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] [[package]] name = "httptools" version = "0.6.1" description = "A collection of framework independent HTTP protocol utils." -category = "dev" optional = false python-versions = ">=3.8.0" files = [ @@ -501,7 +481,6 @@ test = ["Cython (>=0.29.24,<0.30.0)"] name = "httpx" version = "0.25.0" description = "The next generation HTTP client." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -518,15 +497,14 @@ sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] [[package]] name = "hyperframe" version = "6.0.1" description = "HTTP/2 framing layer for Python" -category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -538,7 +516,6 @@ files = [ name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -550,7 +527,6 @@ files = [ name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -562,7 +538,6 @@ files = [ name = "ipdb" version = "0.13.13" description = "IPython-enabled pdb" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -579,7 +554,6 @@ tomli = {version = "*", markers = "python_version > \"3.6\" and python_version < name = "ipython" version = "8.12.3" description = "IPython: Productive Interactive Computing" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -619,7 +593,6 @@ test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pa name = "jedi" version = "0.19.1" description = "An autocompletion tool for Python that can be used for text editors." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -639,7 +612,6 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] name = "loguru" version = "0.7.2" description = "Python logging made (stupidly) simple" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -658,7 +630,6 @@ dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptio name = "matplotlib-inline" version = "0.1.6" description = "Inline Matplotlib backend for Jupyter" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -673,7 +644,6 @@ traitlets = "*" name = "msgpack" version = "1.0.7" description = "MessagePack serializer" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -739,7 +709,6 @@ files = [ name = "multidict" version = "6.0.4" description = "multidict implementation" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -823,7 +792,6 @@ files = [ name = "nonebot-adapter-feishu" version = "2.2.1" description = "feishu(larksuite) adapter for nonebot2" -category = "dev" optional = false python-versions = ">=3.8,<4.0" files = [ @@ -841,7 +809,6 @@ pycryptodome = ">=3.18.0,<4.0.0" name = "nonebot-adapter-kaiheila" version = "0.2.12" description = "kaiheila adapter for nonebot2" -category = "dev" optional = false python-versions = ">=3.8,<4.0" files = [ @@ -856,7 +823,6 @@ nonebot2 = ">=2.0.0,<3.0.0" name = "nonebot-adapter-onebot" version = "2.3.1" description = "OneBot(CQHTTP) adapter for nonebot2" -category = "dev" optional = false python-versions = ">=3.8,<4.0" files = [ @@ -869,11 +835,27 @@ msgpack = ">=1.0.3,<2.0.0" nonebot2 = ">=2.1.0,<3.0.0" typing-extensions = ">=4.0.0,<5.0.0" +[[package]] +name = "nonebot-adapter-qq" +version = "1.0.1" +description = "QQ adapter for nonebot2" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "nonebot_adapter_qq-1.0.1-py3-none-any.whl", hash = "sha256:09b4df2f8bf665e254a57da24662352d25ce096a8c165c8d55c51e645bac889e"}, + {file = "nonebot_adapter_qq-1.0.1.tar.gz", hash = "sha256:2fc98a37fc34f501496faa5aa37265e15a9762b78ccfb91ae41154d495f406bb"}, +] + +[package.dependencies] +nonebot2 = ">=2.1.0,<3.0.0" +pydantic = ">=1.9.0,<2.0.0" +typing-extensions = ">=4.4.0,<5.0.0" +yarl = ">=1.9.0,<2.0.0" + [[package]] name = "nonebot-adapter-qqguild" version = "0.4.0" description = "QQ Guild adapter for nonebot2" -category = "dev" optional = false python-versions = ">=3.8,<4.0" files = [ @@ -890,7 +872,6 @@ typing-extensions = ">=4.4.0,<5.0.0" name = "nonebot-adapter-red" version = "0.6.1" description = "Red Protocol Adapter for Nonebot2" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -909,7 +890,6 @@ auto-detect = ["PyYAML"] name = "nonebot-adapter-telegram" version = "0.1.0b14" description = "Telegram Adapter for NoneBot2" -category = "dev" optional = false python-versions = ">=3.8,<4.0" files = [ @@ -925,7 +905,6 @@ nonebot2 = ">=2.0.0b1,<3.0.0" name = "nonebot2" version = "2.1.1" description = "An asynchronous python bot framework." -category = "main" optional = false python-versions = ">=3.8,<4.0" files = [ @@ -957,7 +936,6 @@ websockets = ["websockets (>=10.0)"] name = "nonebug" version = "0.3.5" description = "nonebot2 test framework" -category = "dev" optional = false python-versions = ">=3.8,<4.0" files = [ @@ -976,7 +954,6 @@ typing-extensions = ">=4.0.0,<5.0.0" name = "packaging" version = "23.2" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -988,7 +965,6 @@ files = [ name = "parso" version = "0.8.3" description = "A Python Parser" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1004,7 +980,6 @@ testing = ["docopt", "pytest (<6.0.0)"] name = "pexpect" version = "4.8.0" description = "Pexpect allows easy control of interactive console applications." -category = "dev" optional = false python-versions = "*" files = [ @@ -1019,7 +994,6 @@ ptyprocess = ">=0.5" name = "pickleshare" version = "0.7.5" description = "Tiny 'shelve'-like database with concurrency support" -category = "dev" optional = false python-versions = "*" files = [ @@ -1031,7 +1005,6 @@ files = [ name = "pip" version = "23.3.1" description = "The PyPA recommended tool for installing Python packages." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1043,7 +1016,6 @@ files = [ name = "pluggy" version = "1.3.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1059,7 +1031,6 @@ testing = ["pytest", "pytest-benchmark"] name = "prompt-toolkit" version = "3.0.39" description = "Library for building powerful interactive command lines in Python" -category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -1074,7 +1045,6 @@ wcwidth = "*" name = "ptyprocess" version = "0.7.0" description = "Run a subprocess in a pseudo terminal" -category = "dev" optional = false python-versions = "*" files = [ @@ -1086,7 +1056,6 @@ files = [ name = "pure-eval" version = "0.2.2" description = "Safely evaluate AST nodes without side effects" -category = "dev" optional = false python-versions = "*" files = [ @@ -1101,7 +1070,6 @@ tests = ["pytest"] name = "pycryptodome" version = "3.19.0" description = "Cryptographic library for Python" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1143,7 +1111,6 @@ files = [ name = "pydantic" version = "1.10.13" description = "Data validation and settings management using python type hints" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1197,7 +1164,6 @@ email = ["email-validator (>=1.0.3)"] name = "pygments" version = "2.16.1" description = "Pygments is a syntax highlighting package written in Python." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1212,7 +1178,6 @@ plugins = ["importlib-metadata"] name = "pygtrie" version = "2.5.0" description = "A pure Python trie data structure implementation." -category = "main" optional = false python-versions = "*" files = [ @@ -1224,7 +1189,6 @@ files = [ name = "pytest" version = "7.4.3" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1247,7 +1211,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-asyncio" version = "0.20.3" description = "Pytest support for asyncio" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1266,7 +1229,6 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy name = "pytest-cov" version = "4.1.0" description = "Pytest plugin for measuring coverage." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1285,7 +1247,6 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale name = "pytest-mock" version = "3.12.0" description = "Thin-wrapper around the mock package for easier use with pytest" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1303,7 +1264,6 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] name = "python-dotenv" version = "1.0.0" description = "Read key-value pairs from a .env file and set them as environment variables" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1318,7 +1278,6 @@ cli = ["click (>=5.0)"] name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1368,7 +1327,6 @@ files = [ name = "requests" version = "2.31.0" description = "Python HTTP for Humans." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1390,7 +1348,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "respx" version = "0.20.2" description = "A utility for mocking out the Python HTTPX and HTTP Core libraries." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1405,7 +1362,6 @@ httpx = ">=0.21.0" name = "ruff" version = "0.0.253" description = "An extremely fast Python linter, written in Rust." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1432,7 +1388,6 @@ files = [ name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1444,7 +1399,6 @@ files = [ name = "sniffio" version = "1.3.0" description = "Sniff out which async library your code is running under" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1456,7 +1410,6 @@ files = [ name = "stack-data" version = "0.6.3" description = "Extract data from python stack frames and tracebacks for informative displays" -category = "dev" optional = false python-versions = "*" files = [ @@ -1476,7 +1429,6 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] name = "starlette" version = "0.27.0" description = "The little ASGI library that shines." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1495,7 +1447,6 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyam name = "strenum" version = "0.4.15" description = "An Enum that inherits from str." -category = "main" optional = false python-versions = "*" files = [ @@ -1512,7 +1463,6 @@ test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1524,7 +1474,6 @@ files = [ name = "traitlets" version = "5.12.0" description = "Traitlets Python configuration system" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1540,7 +1489,6 @@ test = ["argcomplete (>=3.0.3)", "mypy (>=1.6.0)", "pre-commit", "pytest (>=7.0, name = "typing-extensions" version = "4.8.0" description = "Backported and Experimental Type Hints for Python 3.8+" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1552,7 +1500,6 @@ files = [ name = "urllib3" version = "2.0.7" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1570,7 +1517,6 @@ zstd = ["zstandard (>=0.18.0)"] name = "uvicorn" version = "0.23.2" description = "The lightning-fast ASGI server." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1586,7 +1532,7 @@ httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standar python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} -uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} @@ -1597,7 +1543,6 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", name = "uvloop" version = "0.19.0" description = "Fast implementation of asyncio event loop on top of libuv" -category = "dev" optional = false python-versions = ">=3.8.0" files = [ @@ -1642,7 +1587,6 @@ test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)" name = "watchfiles" version = "0.21.0" description = "Simple, modern and high performance file watching and code reload in python." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1730,7 +1674,6 @@ anyio = ">=3.0.0" name = "wcwidth" version = "0.2.8" description = "Measures the displayed width of unicode strings in a terminal" -category = "dev" optional = false python-versions = "*" files = [ @@ -1742,7 +1685,6 @@ files = [ name = "websockets" version = "12.0" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1824,7 +1766,6 @@ files = [ name = "win32-setctime" version = "1.1.0" description = "A small Python utility to set file creation time on Windows" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1839,7 +1780,6 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] name = "yarl" version = "1.9.2" description = "Yet another URL library" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1926,4 +1866,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "f2aa48fbcae2b9dc6be41ccf70e28eb1761093e3d184bd4209582817e483fb84" +content-hash = "8fd95246345e0674c8934a9b3438f6d9bfdfd7eeb2d21d1778b199b846e6fea7" diff --git a/pyproject.toml b/pyproject.toml index f68b31f4..dbde43b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ nonebot-adapter-kaiheila = "^0.2.12" nonebot-adapter-telegram = "^0.1.0b12" nonebot-adapter-feishu = "^2.1.1" nonebot-adapter-red = ">=0.4.1" +nonebot-adapter-qq = "^1.0.1" [tool.black] line-length = 88 diff --git a/tests/conftest.py b/tests/conftest.py index 9b1033a4..b3b9d80c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ import pytest import nonebot from nonebug import NONEBOT_INIT_KWARGS, App +from nonebot.adapters.qq import Adapter as QQAdapter from nonebot.adapters.red import Adapter as RedAdapter from nonebot.adapters.feishu import Adapter as FeishuAdapter from nonebot.adapters.qqguild import Adapter as QQGuildAdapter @@ -32,3 +33,4 @@ def load_adapters(nonebug_init: None): driver.register_adapter(TelegramAdapter) driver.register_adapter(FeishuAdapter) driver.register_adapter(RedAdapter) + driver.register_adapter(QQAdapter) diff --git a/tests/test_auto_select_qq_bot.py b/tests/test_auto_select_qq_bot.py new file mode 100644 index 00000000..1af9c9da --- /dev/null +++ b/tests/test_auto_select_qq_bot.py @@ -0,0 +1,189 @@ +import asyncio +from datetime import datetime +from functools import partial + +import pytest +from nonebug import App +from pytest_mock import MockerFixture +from nonebot import get_driver, get_adapter +from nonebot.adapters.qq import Bot, Adapter +from nonebot.adapters.qq.config import BotInfo +from nonebot.adapters.qq.models import User, Guild, Channel, Message + +MockGuild = partial( + Guild, + id="1", + name="test1", + icon="", + owner_id="1", + owner=True, + member_count=1, + max_members=1, + description="", + joined_at=datetime(2023, 10, 20), +) +MockChannel = partial( + Channel, + id="2233", + guild_id="0", + name="test1", + type=0, + sub_type=0, + position=0, + private_type=0, + speak_permission=0, +) +MockMessage = partial( + Message, id="1", channel_id="2233", guild_id="1", author=User(id="1") +) + + +async def test_disable(app: App): + from nonebot_plugin_saa import TargetQQGuildChannel + from nonebot_plugin_saa.auto_select_bot import get_bot + + async with app.test_api() as ctx: + adapter = get_adapter(Adapter) + ctx.create_bot( + base=Bot, + adapter=adapter, + bot_info=BotInfo(id="3344", token="", secret=""), + ) + + await asyncio.sleep(0.1) + + target = TargetQQGuildChannel(channel_id=2233) + + with pytest.raises(RuntimeError): + get_bot(target) + + +async def test_enable(app: App, mocker: MockerFixture): + from nonebot_plugin_saa.auto_select_bot import get_bot + from nonebot_plugin_saa import TargetQQGuildChannel, enable_auto_select_bot + + # 结束后会自动恢复到原来的状态 + mocker.patch("nonebot_plugin_saa.auto_select_bot.inited", False) + + enable_auto_select_bot() + + async with app.test_api() as ctx: + adapter = get_adapter(Adapter) + bot = ctx.create_bot( + base=Bot, + adapter=adapter, + bot_info=BotInfo(id="3344", token="", secret=""), + ) + + ctx.should_call_api("guilds", {}, [MockGuild(id="1", name="test1")]) + ctx.should_call_api( + "get_channels", {"guild_id": "1"}, [MockChannel(id="2233", name="test1")] + ) + await asyncio.sleep(0.1) + + target = TargetQQGuildChannel(channel_id=2233) + assert bot is get_bot(target) + + # 清理 + driver = get_driver() + driver._bot_connection_hook.clear() + driver._bot_disconnection_hook.clear() + + +async def test_send_auto_select(app: App, mocker: MockerFixture): + from nonebot import get_driver + + from nonebot_plugin_saa.auto_select_bot import refresh_bots + from nonebot_plugin_saa import ( + Text, + MessageFactory, + SupportedAdapters, + TargetQQGuildChannel, + AggregatedMessageFactory, + ) + + mocker.patch("nonebot_plugin_saa.auto_select_bot.inited", True) + + async with app.test_api() as ctx: + adapter_qqguild = get_driver()._adapters[str(SupportedAdapters.qq)] + ctx.create_bot( + base=Bot, + adapter=adapter_qqguild, + bot_info=BotInfo(id="3344", token="", secret=""), + ) + + ctx.should_call_api("guilds", {}, [MockGuild(id="1", name="test1")]) + ctx.should_call_api( + "get_channels", {"guild_id": "1"}, [MockChannel(id="2233", name="test1")] + ) + await refresh_bots() + + ctx.should_call_api( + "post_messages", + data={ + "channel_id": "2233", + "msg_id": None, + "event_id": None, + "content": "123", + }, + result=MockMessage(id="1255124", channel_id="2233"), + ) + target = TargetQQGuildChannel(channel_id=2233) + await MessageFactory("123").send_to(target) + + target = TargetQQGuildChannel(channel_id=2) + with pytest.raises(RuntimeError): + await MessageFactory("123").send_to(target) + + async with app.test_api() as ctx: + adapter_qqguild = get_driver()._adapters[str(SupportedAdapters.qq)] + bot = ctx.create_bot( + base=Bot, + adapter=adapter_qqguild, + bot_info=BotInfo(id="3344", token="", secret=""), + ) + + ctx.should_call_api("guilds", {}, [MockGuild(id="1", name="test1")]) + ctx.should_call_api( + "get_channels", {"guild_id": "1"}, [MockChannel(id="2233", name="test1")] + ) + await refresh_bots() + + ctx.should_call_api( + "post_messages", + data={ + "channel_id": "2233", + "msg_id": None, + "event_id": None, + "content": "123", + }, + result=MockMessage(id="1255124", channel_id="2233"), + ) + ctx.should_call_api( + "post_messages", + data={ + "channel_id": "2233", + "msg_id": None, + "event_id": None, + "content": "456", + }, + result=MockMessage(id="1255124", channel_id="2233"), + ) + + target = TargetQQGuildChannel(channel_id=2233) + await AggregatedMessageFactory([Text("123"), Text("456")]).send_to(target) + + target = TargetQQGuildChannel(channel_id=2) + with pytest.raises(RuntimeError): + await MessageFactory("123").send_to(target) + + adapter_qqguild.bot_disconnect(bot) + + await refresh_bots() + + target = TargetQQGuildChannel(channel_id=2233) + with pytest.raises(RuntimeError): + await AggregatedMessageFactory([Text("123"), Text("456")]).send_to(target) + + # should connect back + adapter_qqguild.bot_connect(bot) diff --git a/tests/test_auto_select_bot.py b/tests/test_auto_select_qqguild_bot.py similarity index 100% rename from tests/test_auto_select_bot.py rename to tests/test_auto_select_qqguild_bot.py diff --git a/tests/test_qq.py b/tests/test_qq.py new file mode 100644 index 00000000..2a9a0281 --- /dev/null +++ b/tests/test_qq.py @@ -0,0 +1,308 @@ +from pathlib import Path +from datetime import datetime +from functools import partial + +from nonebug import App +from nonebot import get_adapter +from pytest_mock import MockerFixture +from nonebot.adapters.qq import Bot, Adapter +from nonebot.adapters.qq.config import BotInfo +from nonebot.adapters.qq.models import DMS, User, Guild, Channel, Message + +from nonebot_plugin_saa.utils import SupportedAdapters + +from .utils import assert_ms, mock_qq_guild_message_event + +MockGuild = partial( + Guild, + id="1", + name="test1", + icon="", + owner_id="1", + owner=True, + member_count=1, + max_members=1, + description="", + joined_at=datetime(2023, 10, 20), +) +MockChannel = partial( + Channel, + id="2233", + guild_id="0", + name="test1", + type=0, + sub_type=0, + position=0, + private_type=0, + speak_permission=0, +) +MockMessage = partial( + Message, id="1", channel_id="2233", guild_id="1", author=User(id="1") +) + +assert_qqguild = partial( + assert_ms, + Bot, + SupportedAdapters.qq, + self_id="314159", + bot_info=BotInfo(id="314159", token="token", secret="secret"), +) + + +async def test_text(app: App): + from nonebot.adapters.qq import MessageSegment + + from nonebot_plugin_saa import Text + + await assert_qqguild(app, Text("text"), MessageSegment.text("text")) + + +async def test_image(app: App, tmp_path: Path): + from nonebot.adapters.qq import MessageSegment + + from nonebot_plugin_saa import Image + + await assert_qqguild( + app, + Image("https://picsum.photos/200"), + MessageSegment.image("https://picsum.photos/200"), + ) + + data = b"\x89PNG\r" + await assert_qqguild(app, Image(data), MessageSegment.file_image(data)) + + image_path = tmp_path / "image.png" + with open(image_path, "wb") as f: + f.write(data) + await assert_qqguild(app, Image(image_path), MessageSegment.file_image(image_path)) + + +async def test_mention_user(app: App): + from nonebot.adapters.qq import MessageSegment + + from nonebot_plugin_saa import Mention + + await assert_qqguild(app, Mention("314159"), MessageSegment.mention_user("314159")) + + +async def test_send(app: App): + from nonebot import get_driver, on_message + from nonebot.adapters.qq import Bot, Message + + from nonebot_plugin_saa import Text, MessageFactory, SupportedAdapters + + matcher = on_message() + + @matcher.handle() + async def handle(): + await MessageFactory(Text("123")).send() + + async with app.test_matcher(matcher) as ctx: + qqguild_adapter = get_driver()._adapters[SupportedAdapters.qq] + bot = ctx.create_bot( + base=Bot, + adapter=qqguild_adapter, + bot_info=BotInfo(id="3344", token="", secret=""), + ) + event = mock_qq_guild_message_event(Message("321")) + ctx.receive_event(bot, event) + ctx.should_call_send( + event, + Message("123"), + result=MockMessage(id="1234871", channel_id=event.channel_id), + ) + + event = mock_qq_guild_message_event(Message("322"), direct=True) + ctx.receive_event(bot, event) + ctx.should_call_send( + event, + Message("123"), + result=MockMessage(id="1234871", channel_id=event.channel_id), + ) + + +async def test_send_revoke(app: App): + from nonebot import get_driver, on_message + from nonebot.adapters.qq import Bot, Message + + from nonebot_plugin_saa import Text, MessageFactory, SupportedAdapters + + matcher = on_message() + + @matcher.handle() + async def handle(): + receipt = await MessageFactory(Text("123")).send() + await receipt.revoke() + + async with app.test_matcher(matcher) as ctx: + qqguild_adapter = get_driver()._adapters[SupportedAdapters.qq] + bot = ctx.create_bot( + base=Bot, + adapter=qqguild_adapter, + bot_info=BotInfo(id="3344", token="", secret=""), + ) + event = mock_qq_guild_message_event(Message("321")) + ctx.receive_event(bot, event) + ctx.should_call_send( + event, + Message("123"), + result=MockMessage(id="1234871", channel_id=event.channel_id), + ) + ctx.should_call_api( + "delete_message", + data={ + "channel_id": event.channel_id, + "message_id": "1234871", + "hidetip": False, + }, + ) + + +async def test_send_active(app: App): + from nonebot import get_driver + + from nonebot_plugin_saa import ( + MessageFactory, + TargetQQGuildDirect, + TargetQQGuildChannel, + ) + + async with app.test_api() as ctx: + adapter_qqguild = get_driver()._adapters[str(SupportedAdapters.qq)] + bot = ctx.create_bot( + base=Bot, + adapter=adapter_qqguild, + bot_info=BotInfo(id="3344", token="", secret=""), + ) + + ctx.should_call_api( + "post_messages", + data={ + "channel_id": "2233", + "msg_id": None, + "event_id": None, + "content": "123", + }, + result=MockMessage(id="1234871", channel_id="2233"), + ) + target = TargetQQGuildChannel(channel_id=2233) + await MessageFactory("123").send_to(target, bot) + + target = TargetQQGuildDirect(recipient_id=1111, source_guild_id=2222) + ctx.should_call_api( + "post_dms", + data={ + "recipient_id": "1111", + "source_guild_id": "2222", + }, + result=DMS(guild_id="3333"), + ) + ctx.should_call_api( + "post_dms_messages", + data={ + "guild_id": "3333", + "msg_id": None, + "event_id": None, + "content": "123", + }, + result=MockMessage(id="1234871", channel_id="12479234"), + ) + await MessageFactory("123").send_to(target, bot) + + # 再次发送,这次直接从缓存中获取 guild_id + ctx.should_call_api( + "post_dms_messages", + data={ + "guild_id": "3333", + "msg_id": None, + "event_id": None, + "content": "1234", + }, + result=MockMessage(id="1234871", channel_id="12355131"), + ) + await MessageFactory("1234").send_to(target, bot) + + +async def test_list_targets(app: App, mocker: MockerFixture): + from nonebot_plugin_saa import TargetQQGuildChannel + from nonebot_plugin_saa.auto_select_bot import get_bot, refresh_bots + + mocker.patch("nonebot_plugin_saa.auto_select_bot.inited", True) + + async with app.test_api() as ctx: + adapter = get_adapter(Adapter) + bot = ctx.create_bot( + base=Bot, + adapter=adapter, + bot_info=BotInfo(id="3344", token="", secret=""), + ) + + ctx.should_call_api("guilds", {}, [MockGuild(id="1", name="test1")]) + ctx.should_call_api( + "get_channels", {"guild_id": "1"}, [MockChannel(id="2233", name="test1")] + ) + await refresh_bots() + + target = TargetQQGuildChannel(channel_id=2233) + assert bot is get_bot(target) + + +def test_extract_target(app: App): + from nonebot.adapters.qq.models import Author + from nonebot.adapters.qq import ( + EventType, + MessageCreateEvent, + C2CMessageCreateEvent, + DirectMessageCreateEvent, + GroupAtMessageCreateEvent, + ) + + from nonebot_plugin_saa import ( + TargetQQGroup, + TargetQQPrivate, + TargetQQGuildDirect, + TargetQQGuildChannel, + extract_target, + ) + + guild_message_event = MessageCreateEvent( + __type__=EventType.CHANNEL_CREATE, + id="1", + channel_id="6677", + guild_id="5566", + author=User(id="1"), + ) + assert extract_target(guild_message_event) == TargetQQGuildChannel(channel_id=6677) + + direct_message_event = DirectMessageCreateEvent( + __type__=EventType.DIRECT_MESSAGE_CREATE, + id="1", + channel_id="6677", + guild_id="5566", + author=User(id="1"), + ) + + assert extract_target(direct_message_event) == TargetQQGuildDirect( + recipient_id=1, source_guild_id=5566 + ) + + c2c_message_event = C2CMessageCreateEvent( + __type__=EventType.C2C_MESSAGE_CREATE, + id="1", + author=Author(id="3344"), + content="test", + timestamp="12345678", + ) + + assert extract_target(c2c_message_event) == TargetQQPrivate(user_id=3344) + + group_at_message_event = GroupAtMessageCreateEvent( + __type__=EventType.GROUP_AT_MESSAGE_CREATE, + id="1", + author=Author(id="3344"), + group_id="1122", + content="test", + timestamp="12345678", + ) + + assert extract_target(group_at_message_event) == TargetQQGroup(group_id=1122) diff --git a/tests/utils.py b/tests/utils.py index 5e75448e..9dd41fb8 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -7,6 +7,7 @@ if TYPE_CHECKING: from nonebug import App from nonebot.internal.adapter.bot import Bot + from nonebot.adapters.qq import Message as QQMessage from nonebot.adapters.telegram import Message as TGMessage from nonebot.internal.adapter.message import MessageSegment from nonebot.adapters.onebot.v11 import Message as OB11Message @@ -199,6 +200,55 @@ def mock_qqguild_message_event(message: "QQGuildMessage", direct=False): ) +def mock_qq_guild_message_event(message: "QQMessage", direct=False): + from nonebot.adapters.qq.models import User + from nonebot.adapters.qq.event import EventType + from nonebot.adapters.qq import MessageCreateEvent, DirectMessageCreateEvent + + if not direct: + return MessageCreateEvent( + __type__=EventType.MESSAGE_CREATE, + id=str(random.randrange(0, 10000)), + guild_id="1122", + channel_id="2233", + content=message.extract_content(), + author=User(id="3344"), + ) + else: + return DirectMessageCreateEvent( + __type__=EventType.DIRECT_MESSAGE_CREATE, + id=str(random.randrange(0, 10000)), + guild_id="1122", + channel_id="2233", + content=message.extract_content(), + author=User(id="3344"), + ) + + +def mock_qq_message_event(message: "QQMessage", direct=False): + from nonebot.adapters.qq.models import Author + from nonebot.adapters.qq.event import EventType + from nonebot.adapters.qq import C2CMessageCreateEvent, GroupAtMessageCreateEvent + + if not direct: + return GroupAtMessageCreateEvent( + __type__=EventType.GROUP_AT_MESSAGE_CREATE, + id=str(random.randrange(0, 10000)), + author=Author(id="3344"), + group_id="1122", + content=message.extract_content(), + timestamp="12345678", + ) + else: + return C2CMessageCreateEvent( + __type__=EventType.C2C_MESSAGE_CREATE, + id=str(random.randrange(0, 10000)), + author=Author(id="3344"), + content=message.extract_content(), + timestamp="12345678", + ) + + def ob12_kwargs(platform="qq", impl="walle") -> Dict[str, Any]: return {"platform": platform, "impl": impl}