diff --git a/nonebot/adapters/qq/bot.py b/nonebot/adapters/qq/bot.py index ba89502..785413f 100644 --- a/nonebot/adapters/qq/bot.py +++ b/nonebot/adapters/qq/bot.py @@ -29,6 +29,7 @@ Event, FriendAddEvent, GroupAddRobotEvent, + GroupAtMessageCreateEvent, GroupMessageCreateEvent, GuildMessageEvent, InteractionCreateEvent, @@ -115,23 +116,45 @@ async def _check_reply( bot: Bot 对象 event: MessageEvent 对象 """ - if not isinstance(event, GuildMessageEvent) or event.message_reference is None: - return - try: - event.reply = await bot.get_message_of_id( - channel_id=event.channel_id, - message_id=event.message_reference.message_id, - ) - if event.reply.author.id == bot.self_info.id: + if isinstance(event, GuildMessageEvent): + if event.message_reference is None: + return + try: + event.reply = await bot.get_message_of_id( + channel_id=event.channel_id, + message_id=event.message_reference.message_id, + ) + if event.reply.author.id == bot.self_info.id: + event.to_me = True + except Exception as e: + log("WARNING", f"Error when getting message reply info: {e!r}", e) + else: + if not event.msg_elements: + return + event.reply = event.msg_elements[0] + if ( + event.reply.author + and event.reply.author.bot + and event.reply.author.username == bot.self_info.username + ): event.to_me = True - except Exception as e: - log("WARNING", f"Error when getting message reply info: {e!r}", e) def _check_at_me( bot: "Bot", event: GuildMessageEvent | QQMessageEvent, ): + message = event.get_message() + if not message: + message.append(MessageSegment.text("")) + if isinstance(event, GroupAtMessageCreateEvent): + event.original_message = message.copy() + event.original_message.insert(0, MessageSegment.mention_user(bot.self_info.id)) + if message and message[0].type == "text": + message[0].data["text"] = message[0].data["text"].lstrip("\xa0").lstrip() + if not message[0].data["text"]: + del message[0] + return if ( isinstance(event, GuildMessageEvent) and event.mentions is not None @@ -139,12 +162,12 @@ def _check_at_me( ): event.to_me = True - if ( - isinstance(event, QQMessageEvent) - and event.mentions is not None - and any(user.is_you for user in event.mentions) - ): - event.to_me = True + if isinstance(event, GroupMessageCreateEvent): + for seg in message: + if seg.type == "mention_user" and seg.data.get("is_bot", False): + seg.data["user_id"] = bot.self_info.id + + event.original_message = message.copy() def _is_at_me_seg(segment: MessageSegment) -> bool: if segment.type == "mention_user": @@ -319,7 +342,9 @@ def _prepare_message(message: str | Message | MessageSegment) -> Message: @staticmethod def _extract_send_message( - message: Message, escape_text: bool = True + message: Message, + msg_ref_id: str | None = None, + escape_text: bool = True, ) -> dict[str, Any]: kwargs = {} content = message.extract_content(escape_text) or None @@ -332,6 +357,10 @@ def _extract_send_message( kwargs["markdown"] = markdown[-1].data["markdown"] if reference := (message["reference"] or None): kwargs["message_reference"] = reference[-1].data["reference"] + if msg_ref_id and not reference[-1].data["reference"].message_id.startswith( + "REFIDX" + ): + kwargs["message_reference"] = MessageReference(message_id=msg_ref_id) if keyboard := (message["keyboard"] or None): kwargs["keyboard"] = keyboard[-1].data["keyboard"] if stream := (message["stream"] or None): @@ -429,9 +458,12 @@ async def send_to_c2c( msg_id: str | None = None, msg_seq: int | None = None, event_id: str | None = None, + msg_ref_id: str | None = None, ) -> PostC2CMessagesReturn | PostC2CFilesReturn: message = self._prepare_message(message) - kwargs = self._extract_send_message(message=message, escape_text=False) + kwargs = self._extract_send_message( + message=message, msg_ref_id=msg_ref_id, escape_text=False + ) if kwargs.get("embed"): msg_type = 4 elif kwargs.get("ark"): @@ -489,9 +521,12 @@ async def send_to_group( msg_id: str | None = None, msg_seq: int | None = None, event_id: str | None = None, + msg_ref_id: str | None = None, ) -> PostGroupMessagesReturn | PostGroupFilesReturn: message = self._prepare_message(message) - kwargs = self._extract_send_message(message=message, escape_text=False) + kwargs = self._extract_send_message( + message=message, msg_ref_id=msg_ref_id, escape_text=False + ) if kwargs.get("embed"): msg_type = 4 elif kwargs.get("ark"): @@ -560,19 +595,41 @@ async def send( ) elif isinstance(event, C2CMessageCreateEvent): event._reply_seq += 1 + ref_idx = None + if event.message_scene: + ref_idx = next( + ( + ext.partition("=")[-1] + for ext in event.message_scene.ext + if ext.startswith("msg_idx=") + ), + "", + ) return await self.send_to_c2c( openid=event.author.id, message=message, msg_id=event.id, msg_seq=event._reply_seq, + msg_ref_id=ref_idx, ) elif isinstance(event, GroupMessageCreateEvent): event._reply_seq += 1 + ref_idx = None + if event.message_scene: + ref_idx = next( + ( + ext.partition("=")[-1] + for ext in event.message_scene.ext + if ext.startswith("msg_idx=") + ), + "", + ) return await self.send_to_group( group_openid=event.group_openid, message=message, msg_id=event.id, msg_seq=event._reply_seq, + msg_ref_id=ref_idx, ) elif isinstance(event, InteractionCreateEvent): if gid := event.group_openid: @@ -1726,7 +1783,7 @@ async def post_c2c_messages( ark: MessageArk | None = None, embed: MessageEmbed | None = None, image: None = None, - message_reference: None = None, + message_reference: MessageReference | None = None, stream: MessageStream | None = None, prompt_keyboard: MessagePromptKeyboard | None = None, action_button: MessageActionButton | None = None, @@ -1964,7 +2021,7 @@ async def post_group_messages( ark: MessageArk | None = None, embed: MessageEmbed | None = None, image: None = None, - message_reference: None = None, + message_reference: MessageReference | None = None, event_id: str | None = None, msg_id: str | None = None, msg_seq: int | None = None, diff --git a/nonebot/adapters/qq/event.py b/nonebot/adapters/qq/event.py index a54d63d..fce2b92 100644 --- a/nonebot/adapters/qq/event.py +++ b/nonebot/adapters/qq/event.py @@ -1,6 +1,6 @@ from datetime import datetime from enum import Enum -from typing import TypeVar, cast +from typing import TYPE_CHECKING, TypeVar, cast from typing_extensions import override from nonebot.utils import escape_tag @@ -23,6 +23,7 @@ MessageReaction, Post, QQMessage, + QQReplyMessage, Reply, RichText, Thread, @@ -270,6 +271,10 @@ class GuildMemberRemoveEvent(GuildMemberEvent): class MessageEvent(Event): to_me: bool = False + if TYPE_CHECKING: + message: Message + original_message: Message + @override def get_type(self) -> str: return "message" @@ -306,9 +311,9 @@ def get_event_description(self) -> str: @override def get_message(self) -> Message: - if not hasattr(self, "_message"): - setattr(self, "_message", Message.from_guild_message(self)) - return getattr(self, "_message") + if not hasattr(self, "message"): + self.message = Message.from_guild_message(self) + return self.message @register_event_class @@ -360,13 +365,18 @@ class DirectMessageDeleteEvent(MessageDeleteEvent): class QQMessageEvent(MessageEvent, QQMessage): + reply: QQReplyMessage | None = None _reply_seq: int = 0 @override def get_message(self) -> Message: - if not hasattr(self, "_message"): - setattr(self, "_message", Message.from_qq_message(self)) - return getattr(self, "_message") + if not hasattr(self, "message"): + self.message = Message.from_qq_message(self) + return self.message + + def get_reply_message(self) -> Message | None: + if self.reply: + return Message.from_qq_message(self.reply) @register_event_class @@ -396,8 +406,8 @@ class GroupMessageCreateEvent(QQMessageEvent): __type__ = EventType.GROUP_MESSAGE_CREATE author: GroupMemberAuthor - group_openid: str group_id: str + group_openid: str @override def get_message(self) -> Message: @@ -405,9 +415,9 @@ def get_message(self) -> Message: msg = Message.from_qq_message(self) if msg and msg[0].type == "text": msg[0].data["text"] = msg[0].data["text"].lstrip() - if not hasattr(self, "_message"): - setattr(self, "_message", msg) - return getattr(self, "_message") + if not hasattr(self, "message"): + self.message = msg + return self.message @override def get_user_id(self) -> str: @@ -731,6 +741,7 @@ class GroupMsgReceiveEvent(GroupRobotEvent): "GroupAddRobotEvent", "GroupAtMessageCreateEvent", "GroupDelRobotEvent", + "GroupMessageCreateEvent", "GroupMsgReceiveEvent", "GroupMsgRejectEvent", "GroupRobotEvent", diff --git a/nonebot/adapters/qq/message.py b/nonebot/adapters/qq/message.py index 2f6ccb8..3357461 100644 --- a/nonebot/adapters/qq/message.py +++ b/nonebot/adapters/qq/message.py @@ -4,7 +4,7 @@ from pathlib import Path import re from typing import TYPE_CHECKING, Literal, TypedDict, Union, overload -from typing_extensions import Self, override +from typing_extensions import NotRequired, Self, override from nonebot.compat import type_validate_python @@ -12,9 +12,8 @@ from nonebot.adapters import MessageSegment as BaseMessageSegment from .models import Attachment as QQAttachment -from .models import GroupMentionUser as QQGroupMentionUser -from .models import Message as GuildMessage from .models import ( + GroupMentionUser, MessageActionButton, MessageArk, MessageEmbed, @@ -24,7 +23,9 @@ MessageReference, MessageStream, QQMessage, + QQReplyMessage, ) +from .models import Message as GuildMessage from .utils import escape, unescape @@ -43,8 +44,11 @@ def emoji(id: str) -> "Emoji": return Emoji("emoji", data={"id": id}) @staticmethod - def mention_user(user_id: str) -> "MentionUser": - return MentionUser("mention_user", {"user_id": str(user_id)}) + def mention_user(user_id: str, username: str | None = None) -> "MentionUser": + data: "_MentionUserData" = {"user_id": str(user_id)} + if username: + data["username"] = username + return MentionUser("mention_user", data) @staticmethod def mention_channel(channel_id: str) -> "MentionChannel": @@ -283,42 +287,10 @@ def __str__(self) -> str: return f"" -class _GroupMentionUserData(TypedDict): - bot: bool - id: str - is_you: bool - member_openid: str - scope: str - username: str - - -@dataclass -class GroupMentionUser(MessageSegment): - if TYPE_CHECKING: - data: _GroupMentionUserData - - @override - def __str__(self) -> str: - return f"<@{self.data['id']}>" - - @classmethod - @override - def _validate(cls, value) -> Self: - instance = super()._validate(value) - mention_user = type_validate_python(QQGroupMentionUser, instance.data) - instance.data = { - "bot": mention_user.bot, - "id": mention_user.id, - "is_you": mention_user.is_you, - "member_openid": mention_user.member_openid, - "scope": mention_user.scope, - "username": mention_user.username, - } - return instance - - class _MentionUserData(TypedDict): user_id: str + username: NotRequired[str] + is_bot: NotRequired[bool] @dataclass @@ -524,7 +496,6 @@ def _validate(cls, value) -> Self: "mention_user": MentionUser, "mention_channel": MentionChannel, "mention_everyone": MentionEveryone, - "group_mention_user": GroupMentionUser, "image": Attachment, "file_image": LocalAttachment, "audio": Attachment, @@ -605,8 +576,10 @@ def __radd__(self, other: str | MessageSegment | Iterable[MessageSegment]) -> Se @override def _construct(msg: str) -> Iterable[MessageSegment]: text_begin = 0 + msg = msg.replace("@everyone", "") + msg = re.sub(r"\", "", msg) for embed in re.finditer( - r"\<(?P(?:@|#|emoji:))!?(?P\w+?)\>", + r"\<(?P(?:@|#|emoji:))!?(?P\w+?)\>|\<(?Pqqbot-at-user) id=\"(?P\w+)\"\s/\>|\\d+),faceId=\"(?P\d+)\",ext=\"[\w\=]+\"\>", # noqa: E501 msg, ): content = msg[text_begin : embed.pos + embed.start()] @@ -614,13 +587,20 @@ def _construct(msg: str) -> Iterable[MessageSegment]: yield Text("text", {"text": unescape(content)}) text_begin = embed.pos + embed.end() if embed.group("type") == "@": - yield MentionUser("mention_user", {"user_id": embed.group("id")}) + if embed.group("id") == "all": + yield MessageSegment.mention_everyone() + else: + yield MentionUser("mention_user", {"user_id": embed.group("id")}) elif embed.group("type") == "#": yield MentionChannel( "mention_channel", {"channel_id": embed.group("id")} ) - else: + elif embed.group("type") == "emoji": yield Emoji("emoji", {"id": embed.group("id")}) + elif embed.group("type1") == "qqbot-at-user": + yield MentionUser("mention_user", {"user_id": embed.group("id1")}) + elif embed.group("faceType") and embed.group("faceId") != "0": + yield Emoji("emoji", {"id": embed["faceId"]}) content = msg[text_begin:] if content: yield Text("text", {"text": unescape(msg[text_begin:])}) @@ -643,8 +623,15 @@ def from_guild_message(cls, message: GuildMessage) -> Self: return msg @classmethod - def from_qq_message(cls, message: QQMessage) -> Self: + def from_qq_message(cls, message: QQMessage | QQReplyMessage) -> Self: msg = cls() + # if isinstance(message, QQMessage) and message.msg_elements: + # msg.append(Reference("reference", { + # "reference": MessageReference( + # message_id=message.msg_elements[0].msg_idx + # ), + # "message": message.msg_elements[0] + # })) if message.content: msg.extend(Message(message.content)) if message.attachments: @@ -663,21 +650,42 @@ def content_type(seg: QQAttachment): for seg in message.attachments if seg.url ) - if message.mentions: - msg.extend( - GroupMentionUser( - "group_mention_user", - data={ - "bot": mention.bot, - "id": mention.id, - "is_you": mention.is_you, - "member_openid": mention.member_openid, - "scope": mention.scope, - "username": mention.username, - }, - ) - for mention in message.mentions - ) + mentions = { + m.id: m + for m in getattr(message, "mentions", []) + if isinstance(m, GroupMentionUser) + } + ats = msg["mention_user"] + if not ats: + for mention in mentions.values(): + if mention.is_you: + msg.insert( + 0, + MentionUser( + "mention_user", + { + "user_id": mention.id, + "username": mention.username, + "is_bot": True, + }, + ), + ) + else: + msg.append( + MentionUser( + "mention_user", + { + "user_id": mention.id, + "username": mention.username, + "is_bot": False, + }, + ) + ) + else: + for at in ats: + if mention := mentions.get(at.data["user_id"]): + at.data["username"] = mention.username + at.data["is_bot"] = mention.is_you return msg def extract_content(self, escape_text: bool = True) -> str: diff --git a/nonebot/adapters/qq/models/qq.py b/nonebot/adapters/qq/models/qq.py index 735e53a..2890e43 100644 --- a/nonebot/adapters/qq/models/qq.py +++ b/nonebot/adapters/qq/models/qq.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Literal +from typing import Literal, TypeAlias from urllib.parse import urlparse from nonebot.compat import field_validator @@ -15,20 +15,30 @@ class FriendAuthor(BaseModel): class GroupMemberAuthor(BaseModel): id: str + bot: bool member_openid: str union_openid: str | None = None username: str | None = None class GroupMentionUser(BaseModel): + scope: Literal["single"] bot: bool id: str is_you: bool member_openid: str - scope: str username: str +class GroupMentionEveryone(BaseModel): + scope: Literal["all"] + is_you: Literal[True] + username: str + + +GroupMention: TypeAlias = GroupMentionUser | GroupMentionEveryone + + class Attachment(BaseModel): content_type: str filename: str | None = None @@ -48,12 +58,42 @@ class Media(BaseModel): file_info: str +class _QQMessageScene(BaseModel): + ext: list[str] + source: str + + +class _ReplyAuthor(BaseModel): + bot: bool + username: str + + +class QQReplyMessage(BaseModel): + content: str + attachments: list[Attachment] | None = None + message_type: int + msg_idx: str + author: _ReplyAuthor | None = None + + class QQMessage(BaseModel): id: str content: str timestamp: str - mentions: list[GroupMentionUser] | None = None + mentions: list[GroupMention] | None = None attachments: list[Attachment] | None = None + message_scene: _QQMessageScene | None = None + message_type: int | None = None + msg_idx: str | None = None + msg_elements: list[QQReplyMessage] | None = None + + +class UserQQMessage(QQMessage): + author: FriendAuthor + + +class GroupQQMessage(QQMessage): + author: GroupMemberAuthor class PostC2CMessagesReturn(BaseModel): @@ -168,7 +208,10 @@ class MessageStream(BaseModel): "FriendAuthor", "GroupMember", "GroupMemberAuthor", + "GroupMention", + "GroupMentionEveryone", "GroupMentionUser", + "GroupQQMessage", "Media", "MessageActionButton", "MessagePromptKeyboard", @@ -187,4 +230,6 @@ class MessageStream(BaseModel): "PromptRenderData", "PromptRow", "QQMessage", + "QQReplyMessage", + "UserQQMessage", ]