Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

适配qq适配器 #105

Merged
merged 10 commits into from
Dec 8, 2023
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,15 @@ await MessageFactory("早上好").send_to(target)
从消息事件中提取发送目标:

```python
from nonebot_plugin_saa import extract_target, get_target
from nonebot_plugin_saa import extract_target, get_target, SaaTarget

@matcher.handle()
async def handle(event: MessageEvent):
target = extract_target(event)
async def handle(event: MessageEvent, bot: Bot):
# 只有混入了 Specifier 的 PlatformTarget(例如 OpenID 版 QQ)需要传入 bot
target = extract_target(event, bot)

@matcher.handle()
async def handle(target: PlatformTarget = Depends(get_target)):
async def handle(target: SaaTarget):
...
```

Expand Down
3 changes: 3 additions & 0 deletions nonebot_plugin_saa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@
from .registries import TargetFeishuGroup as TargetFeishuGroup
from .abstract_factories import MessageFactory as MessageFactory
from .registries import TargetFeishuPrivate as TargetFeishuPrivate
from .registries import TargetQQGroupOpenId as TargetQQGroupOpenId
from .registries import TargetQQGuildDirect as TargetQQGuildDirect
from .registries import TargetTelegramForum as TargetTelegramForum
from .registries import TargetQQGuildChannel as TargetQQGuildChannel
from .registries import TargetTelegramCommon as TargetTelegramCommon
from .registries import TargetKaiheilaChannel as TargetKaiheilaChannel
from .registries import TargetKaiheilaPrivate as TargetKaiheilaPrivate
from .registries import TargetQQPrivateOpenId as TargetQQPrivateOpenId
from .auto_select_bot import enable_auto_select_bot as enable_auto_select_bot
from .abstract_factories import MessageSegmentFactory as MessageSegmentFactory
from .abstract_factories import AggregatedMessageFactory as AggregatedMessageFactory
Expand All @@ -43,5 +45,6 @@
"~telegram",
"~feishu",
"~red",
"~qq",
},
)
4 changes: 2 additions & 2 deletions nonebot_plugin_saa/abstract_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ async def send(self, *, at_sender=False, reply=False) -> "Receipt":
except LookupError as e:
raise RuntimeError("send() 仅能在事件响应器中使用,主动发送消息请使用 send_to") from e

target = extract_target(event)
target = extract_target(event, bot)
return await self._do_send(bot, target, event, at_sender, reply)

async def send_to(
Expand Down Expand Up @@ -426,7 +426,7 @@ async def send(self):
except LookupError as e:
raise RuntimeError("send() 仅能在事件响应器中使用,主动发送消息请使用 send_to") from e

target = extract_target(event)
target = extract_target(event, bot)
await self._do_send(bot, target, event)

async def send_to(self, target: PlatformTarget, bot: Optional[Bot] = None):
Expand Down
1 change: 1 addition & 0 deletions nonebot_plugin_saa/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from . import qq as qq
from . import red as red
from . import dodo as dodo
from . import feishu as feishu
Expand Down
233 changes: 233 additions & 0 deletions nonebot_plugin_saa/adapters/qq.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
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,
PlatformTarget,
QQGuildDMSManager,
TargetQQGroupOpenId,
TargetQQGuildDirect,
TargetQQGuildChannel,
TargetQQPrivateOpenId,
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)

Check warning on line 77 in nonebot_plugin_saa/adapters/qq.py

View check run for this annotation

Codecov / codecov/patch

nonebot_plugin_saa/adapters/qq.py#L76-L77

Added lines #L76 - L77 were not covered by tests

@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")

Check warning on line 92 in nonebot_plugin_saa/adapters/qq.py

View check run for this annotation

Codecov / codecov/patch

nonebot_plugin_saa/adapters/qq.py#L92

Added line #L92 was not covered by tests

@register_target_extractor(C2CMessageCreateEvent)
def extract_c2c_message_event(event: Event, bot: BaseBot) -> PlatformTarget:
assert isinstance(event, C2CMessageCreateEvent)
return TargetQQPrivateOpenId(bot_id=bot.self_id, user_openid=event.author.id)

@register_target_extractor(GroupAtMessageCreateEvent)
def extract_group_at_message_event(event: Event, bot: BaseBot) -> PlatformTarget:
assert isinstance(event, GroupAtMessageCreateEvent)
return TargetQQGroupOpenId(bot_id=bot.self_id, group_openid=event.group_openid)

@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

Check warning on line 139 in nonebot_plugin_saa/adapters/qq.py

View check run for this annotation

Codecov / codecov/patch

nonebot_plugin_saa/adapters/qq.py#L139

Added line #L139 was not covered by tests

@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,
TargetQQGroupOpenId,
TargetQQPrivateOpenId,
),
)

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, TargetQQPrivateOpenId):
msg_return = await bot.send_to_c2c(
openid=target.user_openid,
message=message,
)
elif isinstance(target, TargetQQGroupOpenId):
msg_return = await bot.send_to_group(
group_openid=target.group_openid,
message=message,
)
else:
raise ValueError(f"{type(event)} not supported")

Check warning on line 206 in nonebot_plugin_saa/adapters/qq.py

View check run for this annotation

Codecov / codecov/patch

nonebot_plugin_saa/adapters/qq.py#L206

Added line #L206 was not covered by tests

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)
for channel in channels:
targets.append(
TargetQQGuildChannel(
channel_id=int(channel.id),
)
)

return targets

except ImportError:
pass
except Exception as e:
raise e
2 changes: 2 additions & 0 deletions nonebot_plugin_saa/registries/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from .platform_send_target import TargetDoDoPrivate as TargetDoDoPrivate
from .platform_send_target import TargetFeishuGroup as TargetFeishuGroup
from .platform_send_target import TargetFeishuPrivate as TargetFeishuPrivate
from .platform_send_target import TargetQQGroupOpenId as TargetQQGroupOpenId
from .platform_send_target import TargetQQGuildDirect as TargetQQGuildDirect
from .platform_send_target import TargetTelegramForum as TargetTelegramForum
from .platform_send_target import TargetQQGuildChannel as TargetQQGuildChannel
Expand All @@ -25,6 +26,7 @@
from .message_id import register_message_id_getter as register_message_id_getter
from .platform_send_target import TargetKaiheilaChannel as TargetKaiheilaChannel
from .platform_send_target import TargetKaiheilaPrivate as TargetKaiheilaPrivate
from .platform_send_target import TargetQQPrivateOpenId as TargetQQPrivateOpenId
from .platform_send_target import register_convert_to_arg as register_convert_to_arg
from .platform_send_target import register_target_extractor as register_target_extractor
from .platform_send_target import (
Expand Down
26 changes: 19 additions & 7 deletions nonebot_plugin_saa/registries/platform_send_target.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import inspect
from typing_extensions import Annotated
from typing import (
TYPE_CHECKING,
Expand Down Expand Up @@ -256,6 +257,8 @@
AllSupportedPlatformTarget = Union[
TargetQQGroup,
TargetQQPrivate,
TargetQQGroupOpenId,
TargetQQPrivateOpenId,
TargetQQGuildChannel,
TargetQQGuildDirect,
TargetKaiheilaPrivate,
Expand All @@ -281,31 +284,40 @@


Extractor = Callable[[Event], PlatformTarget]
extractor_map: Dict[Type[Event], Extractor] = {}
ExtractorWithBotSpecifier = Callable[[Event, Bot], PlatformTarget]
extractor_map: Dict[Type[Event], Union[Extractor, ExtractorWithBotSpecifier]] = {}


def register_target_extractor(event: Type[Event]):
def wrapper(func: Extractor):
def wrapper(func: Union[Extractor, ExtractorWithBotSpecifier]):
extractor_map[event] = func
return func

return wrapper


def extract_target(event: Event) -> PlatformTarget:
def extract_target(event: Event, bot: Optional[Bot] = None) -> PlatformTarget:
"从事件中提取出发送目标,如果不能提取就抛出错误"
for event_type in event.__class__.mro():
if event_type in extractor_map:
if not issubclass(event_type, Event):
break
return extractor_map[event_type](event)
if len(inspect.signature(extractor_map[event_type]).parameters.keys()) == 2:
# extractor params: event, bot
if bot is None:
raise RuntimeError(

Check warning on line 308 in nonebot_plugin_saa/registries/platform_send_target.py

View check run for this annotation

Codecov / codecov/patch

nonebot_plugin_saa/registries/platform_send_target.py#L308

Added line #L308 was not covered by tests
f"event {event.__class__} need bot parameter to extract target",
)
return extractor_map[event_type](event, bot) # type: ignore
else:
return extractor_map[event_type](event) # type: ignore
raise RuntimeError(f"event {event.__class__} not supported")


def get_target(event: Event) -> Optional[PlatformTarget]:
def get_target(event: Event, bot: Optional[Bot] = None) -> Optional[PlatformTarget]:
"从事件中提取出发送目标,如果不能提取就返回 None"
try:
return extract_target(event)
return extract_target(event, bot)
except RuntimeError:
pass

Expand Down Expand Up @@ -359,6 +371,6 @@
raise RuntimeError(
f"qqguild dms method for {adapter} not registered",
) # pragma: no cover
guild_id = await qqguild_dms(target, bot)
guild_id = await qqguild_dms(target, bot) # type: ignore
cls._cache[target] = guild_id
return guild_id
1 change: 1 addition & 0 deletions nonebot_plugin_saa/utils/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class SupportedAdapters(StrEnum):
feishu = "Feishu"
red = "RedProtocol"
dodo = "DoDo"
qq = "QQ"

fake = "fake" # for nonebug

Expand Down
Loading