diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py index 8b192b37..d5034cd4 100644 --- a/nonebot/adapters/mirai/bot.py +++ b/nonebot/adapters/mirai/bot.py @@ -2,9 +2,13 @@ from pprint import pprint from typing import Optional from nonebot.adapters import Bot as BaseBot +from nonebot.adapters import Event as BaseEvent from nonebot.drivers import Driver, WebSocket +from nonebot.message import handle_event from nonebot.typing import overrides +from .event import Event + class MiraiBot(BaseBot): @@ -28,12 +32,13 @@ class MiraiBot(BaseBot): @overrides(BaseBot) async def handle_message(self, message: dict): - pprint(message) + event = Event.new(message) + await handle_event(self, event) @overrides(BaseBot) async def call_api(self, api: str, **data): return super().call_api(api, **data) @overrides(BaseBot) - async def send(self, event: "Event", message: str, **kwargs): + async def send(self, event: "BaseEvent", message: str, **kwargs): return super().send(event, message, **kwargs) diff --git a/nonebot/adapters/mirai/event/__init__.py b/nonebot/adapters/mirai/event/__init__.py index e69de29b..903f4eb8 100644 --- a/nonebot/adapters/mirai/event/__init__.py +++ b/nonebot/adapters/mirai/event/__init__.py @@ -0,0 +1,4 @@ +from .base import Event, SenderInfo, PrivateSenderInfo, SenderGroup +from .message import * +from .notice import * +from .request import * \ No newline at end of file diff --git a/nonebot/adapters/mirai/event/base.py b/nonebot/adapters/mirai/event/base.py index 7b18c39a..451a858e 100644 --- a/nonebot/adapters/mirai/event/base.py +++ b/nonebot/adapters/mirai/event/base.py @@ -1,13 +1,12 @@ from enum import Enum - -from pydantic import BaseModel, Field +from typing import Dict, Any, Optional, Type +from pydantic import BaseModel, Field, ValidationError from typing_extensions import Literal from nonebot.adapters import Event as BaseEvent from nonebot.adapters import Message as BaseMessage from nonebot.typing import overrides - -from .constants import EVENT_TYPES +from nonebot.log import logger class SenderPermission(str, Enum): @@ -29,12 +28,54 @@ class SenderInfo(BaseModel): group: SenderGroup +class PrivateSenderInfo(BaseModel): + id: int + nickname: str + remark: str + + class Event(BaseEvent): type: str + @classmethod + def new(cls, data: Dict[str, Any]) -> "Event": + type = data['type'] + + def all_subclasses(cls: Type[Event]): + return set(cls.__subclasses__()).union( + [s for c in cls.__subclasses__() for s in all_subclasses(c)]) + + event_class: Optional[Type[Event]] = None + for subclass in all_subclasses(cls): + if subclass.__name__ != type: + continue + event_class = subclass + + if event_class is None: + return Event.parse_obj(data) + + while issubclass(event_class, Event): + try: + return event_class.parse_obj(data) + except ValidationError as e: + logger.info( + f'Failed to parse {data} to class {event_class.__name__}: {e}. ' + 'Fallback to parent class.') + event_class = event_class.__base__ + + raise ValueError(f'Failed to serialize {data}.') + @overrides(BaseEvent) def get_type(self) -> Literal["message", "notice", "request", "meta_event"]: - return EVENT_TYPES.get(self.type, 'meta_event') + from . import message, notice, request + if isinstance(self, message.MessageEvent): + return 'message' + elif isinstance(self, notice.NoticeEvent): + return 'notice' + elif isinstance(self, request.RequestEvent): + return 'request' + else: + return 'meta_event' @overrides(BaseEvent) def get_event_name(self) -> str: diff --git a/nonebot/adapters/mirai/event/constants.py b/nonebot/adapters/mirai/event/constants.py deleted file mode 100644 index 1ede39a8..00000000 --- a/nonebot/adapters/mirai/event/constants.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import List, Dict -from typing_extensions import Literal - -EventType = Literal["message", "notice", "request", "meta_event"] - -_EVENT_CLASSIFY: Dict[EventType, List[str]] = { - # XXX Reference: https://github.com/project-mirai/mirai-api-http/blob/v1.9.7/docs/EventType.md - 'meta_event': [ - 'BotOnlineEvent', 'BotOfflineEventActive', 'BotOfflineEventForce', - 'BotOfflineEventDropped', 'BotReloginEvent' - ], - 'notice': [ - 'BotGroupPermissionChangeEvent', 'BotMuteEvent', 'BotUnmuteEvent', - 'BotJoinGroupEvent', 'BotLeaveEventActive', 'BotLeaveEventKick', - 'GroupRecallEvent', 'FriendRecallEvent', 'GroupNameChangeEvent', - 'GroupEntranceAnnouncementChangeEvent', 'GroupMuteAllEvent', - 'GroupAllowAnonymousChatEvent', 'GroupAllowConfessTalkEvent', - 'GroupAllowMemberInviteEvent', 'MemberJoinEvent', - 'MemberLeaveEventKick', 'MemberLeaveEventQuit', 'MemberCardChangeEvent', - 'MemberSpecialTitleChangeEvent', 'MemberPermissionChangeEvent', - 'MemberMuteEvent', 'MemberUnmuteEvent' - ], - 'request': [ - 'NewFriendRequestEvent', 'MemberJoinRequestEvent', - 'BotInvitedJoinGroupRequestEvent' - ], - 'message': ['GroupMessage', 'FriendMessage', 'TempMessage'] -} -EVENT_TYPES: Dict[str, EventType] = {} -for event_type, events in _EVENT_CLASSIFY.items(): - _EVENT_TYPES.update({k: event_type for k in events}) # type: ignore diff --git a/nonebot/adapters/mirai/event/message.py b/nonebot/adapters/mirai/event/message.py new file mode 100644 index 00000000..f680b0a2 --- /dev/null +++ b/nonebot/adapters/mirai/event/message.py @@ -0,0 +1,40 @@ +from typing import TYPE_CHECKING + +from pydantic import Field +from .base import Event, SenderInfo, PrivateSenderInfo + +from ..message import MessageChain +from nonebot.typing import overrides + + +class MessageEvent(Event): + message_chain: MessageChain = Field(alias='messageChain') + sender: SenderInfo + + @overrides(Event) + def get_message(self) -> MessageChain: + return self.message_chain + + @overrides(Event) + def get_plaintext(self) -> str: + return self.message_chain.__str__() + + @overrides(Event) + def get_user_id(self) -> str: + return str(self.sender.id) + + @overrides(Event) + def get_session_id(self) -> str: + return self.get_user_id() + + +class GroupMessage(MessageEvent): + pass + + +class FriendMessage(MessageEvent): + sender: PrivateSenderInfo + + +class TempMessage(MessageEvent): + pass \ No newline at end of file diff --git a/nonebot/adapters/mirai/event/notice.py b/nonebot/adapters/mirai/event/notice.py index 536bc12b..ae144b91 100644 --- a/nonebot/adapters/mirai/event/notice.py +++ b/nonebot/adapters/mirai/event/notice.py @@ -5,34 +5,34 @@ from pydantic import Field from .base import Event, SenderGroup, SenderInfo, SenderPermission -class BaseNoticeEvent(Event): +class NoticeEvent(Event): pass -class BaseMuteEvent(BaseNoticeEvent): +class MuteEvent(NoticeEvent): operator: SenderInfo -class BotMuteEvent(BaseMuteEvent): +class BotMuteEvent(MuteEvent): pass -class BotUnmuteEvent(BaseMuteEvent): +class BotUnmuteEvent(MuteEvent): pass -class MemberMuteEvent(BaseMuteEvent): +class MemberMuteEvent(MuteEvent): duration_seconds: int = Field(alias='durationSeconds') member: SenderInfo operator: Optional[SenderInfo] = None -class MemberUnmuteEvent(BaseMuteEvent): +class MemberUnmuteEvent(MuteEvent): member: SenderInfo operator: Optional[SenderInfo] = None -class BotJoinGroupEvent(BaseNoticeEvent): +class BotJoinGroupEvent(NoticeEvent): group: SenderGroup @@ -44,7 +44,7 @@ class BotLeaveEventKick(BotJoinGroupEvent): pass -class MemberJoinEvent(BaseNoticeEvent): +class MemberJoinEvent(NoticeEvent): member: SenderInfo @@ -56,7 +56,7 @@ class MemberLeaveEventKick(MemberJoinEvent): operator: Optional[SenderInfo] = None -class FriendRecallEvent(BaseNoticeEvent): +class FriendRecallEvent(NoticeEvent): author_id: int = Field(alias='authorId') message_id: int = Field(alias='messageId') time: int @@ -68,7 +68,7 @@ class GroupRecallEvent(FriendRecallEvent): operator: Optional[SenderInfo] = None -class GroupStateChangeEvent(BaseNoticeEvent): +class GroupStateChangeEvent(NoticeEvent): origin: Any current: Any group: SenderGroup @@ -105,7 +105,7 @@ class GroupAllowMemberInviteEvent(GroupStateChangeEvent): current: bool -class MemberStateChangeEvent(BaseNoticeEvent): +class MemberStateChangeEvent(NoticeEvent): member: SenderInfo operator: Optional[SenderInfo] = None diff --git a/nonebot/adapters/mirai/event/request.py b/nonebot/adapters/mirai/event/request.py index 7e813f29..44a70c17 100644 --- a/nonebot/adapters/mirai/event/request.py +++ b/nonebot/adapters/mirai/event/request.py @@ -1 +1,26 @@ +from pydantic import Field + from .base import Event + + +class RequestEvent(Event): + event_id: int = Field(alias='eventId') + message: str + nick: str + + +class NewFriendRequestEvent(RequestEvent): + from_id: int = Field(alias='fromId') + group_id: int = Field(0, alias='groupId') + + +class MemberJoinRequestEvent(RequestEvent): + from_id: int = Field(alias='fromId') + group_id: int = Field(alias='groupId') + group_name: str = Field(alias='groupName') + + +class BotInvitedJoinGroupRequestEvent(RequestEvent): + from_id: int = Field(alias='fromId') + group_id: int = Field(alias='groupId') + group_name: str = Field(alias='groupName') diff --git a/nonebot/adapters/mirai/message.py b/nonebot/adapters/mirai/message.py index edf1b6d0..06fc0d28 100644 --- a/nonebot/adapters/mirai/message.py +++ b/nonebot/adapters/mirai/message.py @@ -1 +1,100 @@ -from nonebot.adapters import Message \ No newline at end of file +from enum import Enum +from typing import Any, Dict, List, Union, Iterable + +from pydantic import validate_arguments + +from nonebot.adapters import Message as BaseMessage +from nonebot.adapters import MessageSegment as BaseMessageSegment +from nonebot.typing import overrides + + +class MessageType(str, Enum): + SOURCE = 'Source' + QUOTE = 'Quote' + AT = 'At' + AT_ALL = 'AtAll' + FACE = 'Face' + PLAIN = 'Plain' + IMAGE = 'Image' + FLASH_IMAGE = 'FlashImage' + VOICE = 'Voice' + XML = 'Xml' + JSON = 'Json' + APP = 'App' + POKE = 'Poke' + + +class MessageSegment(BaseMessageSegment): + type: MessageType + data: Dict[str, Any] + + @overrides(BaseMessageSegment) + @validate_arguments + def __init__(self, type: MessageType, **data): + super().__init__(type=type, data=data) + + @overrides(BaseMessageSegment) + def __str__(self) -> str: + if self.is_text(): + return self.data.get('text', '') + return '[mirai:%s]' % ','.join([ + self.type.value, + *map( + lambda s: '%s=%r' % s, + self.data.items(), + ), + ]) + + @overrides(BaseMessageSegment) + def __add__(self, other) -> "MessageChain": + return MessageChain(self) + other + + @overrides(BaseMessageSegment) + def __radd__(self, other) -> "MessageChain": + return MessageChain(other) + self + + @overrides(BaseMessageSegment) + def is_text(self) -> bool: + return self.type == MessageType.PLAIN + + def as_dict(self) -> Dict[str, Any]: + return {'type': self.type.value, **self.data} + + +class MessageChain(BaseMessage): + + @overrides(BaseMessage) + def __init__(self, message: Union[List[Dict[str, Any]], MessageSegment], + **kwargs): + super().__init__(**kwargs) + if isinstance(message, MessageSegment): + self.append(message) + elif isinstance(message, Iterable): + self.extend(self._construct(message)) + else: + raise ValueError( + f'Type {type(message).__name__} is not supported in mirai adapter.' + ) + + @overrides(BaseMessage) + def _construct( + self, message: Iterable[Union[Dict[str, Any], MessageSegment]] + ) -> List[MessageSegment]: + if isinstance(message, str): + raise ValueError( + "String operation is not supported in mirai adapter") + return [ + *map( + lambda segment: segment if isinstance(segment, MessageSegment) + else MessageSegment(**segment), message) + ] + + def export(self) -> List[Dict[str, Any]]: + chain: List[Dict[str, Any]] = [] + for segment in self.copy(): + segment: MessageSegment + chain.append({'type': segment.type.value, **segment.data}) + return chain + + def __repr__(self) -> str: + return f'<{self.__class__.__name__} {[*self.copy()]}>'