Skip to content

Commit

Permalink
✨ 适配qq适配器 (#105)
Browse files Browse the repository at this point in the history
* ✨ 适配QQ适配器的频道部分

* 🚨 auto fix by pre-commit hooks

* 📝 元数据添加QQ适配器支持

* 🐛 Mention user_id为str而非int

* ✨ 支持发送群聊和私聊消息

* ✅ 修复 qq auto select bot 测试

* ➖ 移除多余依赖

* 🔥 删除多余日志

* ✨ QQ 适配器部分实现 (#121)

* ✨ 适配QQ适配器的频道部分

* 🚨 auto fix by pre-commit hooks

* 📝 元数据添加QQ适配器支持

* 🐛 Mention user_id为str而非int

* ✨ 支持发送群聊和私聊消息

* ✅ 修复 qq auto select bot 测试

* ➖ 移除多余依赖

* 🔥 删除多余日志

* ⬇️ update poetry lock

* 🐛 取消版本限制

* ✨ Use openid target in qq adapter

* ✨ Format openid models

* 🚨 auto fix by pre-commit hooks

* 🚨 auto fix by pre-commit hooks

* ✅ Add auto select bot test for openid targets

* 🔥 Remove unused platform enum

* 🎨 Rename extractor typevar

* 🎨 Replace name with length in extrator param detection

* ✅ Add tests of qqgroup and target dependency injection

* ✅ Update qq dependency injection test

---------

Co-authored-by: AzideCupric <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: uy_sun <[email protected]>

* 📝 update doc for qq

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: uy_sun <[email protected]>
Co-authored-by: mobyw <[email protected]>
Co-authored-by: felinae98 <[email protected]>
  • Loading branch information
5 people authored Dec 8, 2023
1 parent bd94927 commit 6985677
Show file tree
Hide file tree
Showing 14 changed files with 1,249 additions and 241 deletions.
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
1 change: 1 addition & 0 deletions nonebot_plugin_saa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,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)

@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, 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

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

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
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 @@ class TargetDoDoPrivate(PlatformTarget):
AllSupportedPlatformTarget = Union[
TargetQQGroup,
TargetQQPrivate,
TargetQQGroupOpenId,
TargetQQPrivateOpenId,
TargetQQGuildChannel,
TargetQQGuildDirect,
TargetKaiheilaPrivate,
Expand All @@ -281,31 +284,40 @@ def wrapper(func: ConvertToArg):


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(
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 @@ async def aget_guild_id(cls, target: TargetQQGuildDirect, bot: Bot) -> int:
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

0 comments on commit 6985677

Please sign in to comment.