From 8574b2ec72365ab07bf21b7d9a2ac85cb5378599 Mon Sep 17 00:00:00 2001 From: Mix Date: Fri, 29 Jan 2021 17:37:44 +0800 Subject: [PATCH 01/37] :construction: start working on mirai-api-http adapter --- nonebot/adapters/mirai/__init__.py | 1 + nonebot/adapters/mirai/bot.py | 39 ++++++++++++++++++++++++++++++ nonebot/adapters/mirai/message.py | 1 + 3 files changed, 41 insertions(+) create mode 100644 nonebot/adapters/mirai/__init__.py create mode 100644 nonebot/adapters/mirai/bot.py create mode 100644 nonebot/adapters/mirai/message.py diff --git a/nonebot/adapters/mirai/__init__.py b/nonebot/adapters/mirai/__init__.py new file mode 100644 index 00000000..c832d378 --- /dev/null +++ b/nonebot/adapters/mirai/__init__.py @@ -0,0 +1 @@ +from .bot import MiraiBot \ No newline at end of file diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py new file mode 100644 index 00000000..8b192b37 --- /dev/null +++ b/nonebot/adapters/mirai/bot.py @@ -0,0 +1,39 @@ +from pprint import pprint +from typing import Optional + +from nonebot.adapters import Bot as BaseBot +from nonebot.drivers import Driver, WebSocket +from nonebot.typing import overrides + + +class MiraiBot(BaseBot): + + def __init__(self, + connection_type: str, + self_id: str, + *, + websocket: Optional["WebSocket"] = None): + super().__init__(connection_type, self_id, websocket=websocket) + + @property + @overrides(BaseBot) + def type(self) -> str: + return "mirai" + + @classmethod + @overrides(BaseBot) + async def check_permission(cls, driver: "Driver", connection_type: str, + headers: dict, body: Optional[dict]) -> str: + return '' + + @overrides(BaseBot) + async def handle_message(self, message: dict): + pprint(message) + + @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): + return super().send(event, message, **kwargs) diff --git a/nonebot/adapters/mirai/message.py b/nonebot/adapters/mirai/message.py new file mode 100644 index 00000000..edf1b6d0 --- /dev/null +++ b/nonebot/adapters/mirai/message.py @@ -0,0 +1 @@ +from nonebot.adapters import Message \ No newline at end of file From 5a9798121c7a8e8861d298b39870824d7f5fa9fb Mon Sep 17 00:00:00 2001 From: Mix Date: Fri, 29 Jan 2021 17:38:39 +0800 Subject: [PATCH 02/37] :construction: add some support for mirai basic events --- nonebot/adapters/mirai/event/__init__.py | 0 nonebot/adapters/mirai/event/base.py | 65 +++++++++++ nonebot/adapters/mirai/event/constants.py | 31 ++++++ nonebot/adapters/mirai/event/notice.py | 130 ++++++++++++++++++++++ nonebot/adapters/mirai/event/request.py | 1 + 5 files changed, 227 insertions(+) create mode 100644 nonebot/adapters/mirai/event/__init__.py create mode 100644 nonebot/adapters/mirai/event/base.py create mode 100644 nonebot/adapters/mirai/event/constants.py create mode 100644 nonebot/adapters/mirai/event/notice.py create mode 100644 nonebot/adapters/mirai/event/request.py diff --git a/nonebot/adapters/mirai/event/__init__.py b/nonebot/adapters/mirai/event/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/nonebot/adapters/mirai/event/base.py b/nonebot/adapters/mirai/event/base.py new file mode 100644 index 00000000..7b18c39a --- /dev/null +++ b/nonebot/adapters/mirai/event/base.py @@ -0,0 +1,65 @@ +from enum import Enum + +from pydantic import BaseModel, Field +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 + + +class SenderPermission(str, Enum): + OWNER = 'OWNER' + ADMINISTRATOR = 'ADMINISTRATOR' + MEMBER = 'MEMBER' + + +class SenderGroup(BaseModel): + id: int + name: str + permission: SenderPermission + + +class SenderInfo(BaseModel): + id: int + name: str = Field(alias='memberName') + permission: SenderPermission + group: SenderGroup + + +class Event(BaseEvent): + type: str + + @overrides(BaseEvent) + def get_type(self) -> Literal["message", "notice", "request", "meta_event"]: + return EVENT_TYPES.get(self.type, 'meta_event') + + @overrides(BaseEvent) + def get_event_name(self) -> str: + return self.type + + @overrides(BaseEvent) + def get_event_description(self) -> str: + return str(self.dict()) + + @overrides(BaseEvent) + def get_message(self) -> BaseMessage: + raise ValueError("Event has no message!") + + @overrides(BaseEvent) + def get_plaintext(self) -> str: + raise ValueError("Event has no message!") + + @overrides(BaseEvent) + def get_user_id(self) -> str: + raise ValueError("Event has no message!") + + @overrides(BaseEvent) + def get_session_id(self) -> str: + raise ValueError("Event has no message!") + + @overrides(BaseEvent) + def is_tome(self) -> bool: + return False diff --git a/nonebot/adapters/mirai/event/constants.py b/nonebot/adapters/mirai/event/constants.py new file mode 100644 index 00000000..1ede39a8 --- /dev/null +++ b/nonebot/adapters/mirai/event/constants.py @@ -0,0 +1,31 @@ +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/notice.py b/nonebot/adapters/mirai/event/notice.py new file mode 100644 index 00000000..536bc12b --- /dev/null +++ b/nonebot/adapters/mirai/event/notice.py @@ -0,0 +1,130 @@ +from typing import Optional, Any + +from pydantic import Field + +from .base import Event, SenderGroup, SenderInfo, SenderPermission + + +class BaseNoticeEvent(Event): + pass + + +class BaseMuteEvent(BaseNoticeEvent): + operator: SenderInfo + + +class BotMuteEvent(BaseMuteEvent): + pass + + +class BotUnmuteEvent(BaseMuteEvent): + pass + + +class MemberMuteEvent(BaseMuteEvent): + duration_seconds: int = Field(alias='durationSeconds') + member: SenderInfo + operator: Optional[SenderInfo] = None + + +class MemberUnmuteEvent(BaseMuteEvent): + member: SenderInfo + operator: Optional[SenderInfo] = None + + +class BotJoinGroupEvent(BaseNoticeEvent): + group: SenderGroup + + +class BotLeaveEventActive(BotJoinGroupEvent): + pass + + +class BotLeaveEventKick(BotJoinGroupEvent): + pass + + +class MemberJoinEvent(BaseNoticeEvent): + member: SenderInfo + + +class MemberLeaveEventQuit(MemberJoinEvent): + pass + + +class MemberLeaveEventKick(MemberJoinEvent): + operator: Optional[SenderInfo] = None + + +class FriendRecallEvent(BaseNoticeEvent): + author_id: int = Field(alias='authorId') + message_id: int = Field(alias='messageId') + time: int + operator: int + + +class GroupRecallEvent(FriendRecallEvent): + group: SenderGroup + operator: Optional[SenderInfo] = None + + +class GroupStateChangeEvent(BaseNoticeEvent): + origin: Any + current: Any + group: SenderGroup + operator: Optional[SenderInfo] = None + + +class GroupNameChangeEvent(GroupStateChangeEvent): + origin: str + current: str + + +class GroupEntranceAnnouncementChangeEvent(GroupStateChangeEvent): + origin: str + current: str + + +class GroupMuteAllEvent(GroupStateChangeEvent): + origin: bool + current: bool + + +class GroupAllowAnonymousChatEvent(GroupStateChangeEvent): + origin: bool + current: bool + + +class GroupAllowConfessTalkEvent(GroupStateChangeEvent): + origin: bool + current: bool + + +class GroupAllowMemberInviteEvent(GroupStateChangeEvent): + origin: bool + current: bool + + +class MemberStateChangeEvent(BaseNoticeEvent): + member: SenderInfo + operator: Optional[SenderInfo] = None + + +class MemberCardChangeEvent(MemberStateChangeEvent): + origin: str + current: str + + +class MemberSpecialTitleChangeEvent(MemberStateChangeEvent): + origin: str + current: str + + +class BotGroupPermissionChangeEvent(MemberStateChangeEvent): + origin: SenderPermission + current: SenderPermission + + +class MemberPermissionChangeEvent(MemberStateChangeEvent): + origin: SenderPermission + current: SenderPermission diff --git a/nonebot/adapters/mirai/event/request.py b/nonebot/adapters/mirai/event/request.py new file mode 100644 index 00000000..7e813f29 --- /dev/null +++ b/nonebot/adapters/mirai/event/request.py @@ -0,0 +1 @@ +from .base import Event From 0bb0d16d939aadd9c6a62b28e007067146d98dcc Mon Sep 17 00:00:00 2001 From: Mix Date: Fri, 29 Jan 2021 21:19:13 +0800 Subject: [PATCH 03/37] :construction: basically completed event serialize --- nonebot/adapters/mirai/bot.py | 9 +- nonebot/adapters/mirai/event/__init__.py | 4 + nonebot/adapters/mirai/event/base.py | 51 +++++++++-- nonebot/adapters/mirai/event/constants.py | 31 ------- nonebot/adapters/mirai/event/message.py | 40 +++++++++ nonebot/adapters/mirai/event/notice.py | 22 ++--- nonebot/adapters/mirai/event/request.py | 25 ++++++ nonebot/adapters/mirai/message.py | 101 +++++++++++++++++++++- 8 files changed, 233 insertions(+), 50 deletions(-) delete mode 100644 nonebot/adapters/mirai/event/constants.py create mode 100644 nonebot/adapters/mirai/event/message.py 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()]}>' From 02af1c1227418610a41a470adf3546fa348bb6b1 Mon Sep 17 00:00:00 2001 From: Mix Date: Sat, 30 Jan 2021 05:58:30 +0800 Subject: [PATCH 04/37] :construction: finish forward websocket receive --- nonebot/adapters/mirai/bot.py | 134 ++++++++++++++++++++++++++++--- nonebot/adapters/mirai/config.py | 13 +++ nonebot/drivers/__init__.py | 4 +- 3 files changed, 139 insertions(+), 12 deletions(-) create mode 100644 nonebot/adapters/mirai/config.py diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py index d5034cd4..fba54a69 100644 --- a/nonebot/adapters/mirai/bot.py +++ b/nonebot/adapters/mirai/bot.py @@ -1,22 +1,89 @@ -from pprint import pprint -from typing import Optional +import asyncio +import json +from ipaddress import IPv4Address +from typing import (Any, Callable, Coroutine, Dict, NoReturn, Optional, Set, + TypeVar) + +import httpx +import websockets from nonebot.adapters import Bot as BaseBot from nonebot.adapters import Event as BaseEvent -from nonebot.drivers import Driver, WebSocket +from nonebot.drivers import Driver +from nonebot.drivers import WebSocket as BaseWebSocket +from nonebot.exception import RequestDenied +from nonebot.log import logger from nonebot.message import handle_event from nonebot.typing import overrides +from .config import Config from .event import Event +WebsocketHandlerFunction = Callable[[Dict[str, Any]], Coroutine[Any, Any, None]] +WebsocketHandler_T = TypeVar('WebsocketHandler_T', + bound=WebsocketHandlerFunction) + + +class WebSocket(BaseWebSocket): + + @classmethod + async def new(cls, *, host: IPv4Address, port: int, + session_key: str) -> "WebSocket": + listen_address = httpx.URL(f'ws://{host}:{port}/all', + params={'sessionKey': session_key}) + websocket = await websockets.connect(uri=str(listen_address)) + return cls(websocket) + + @overrides(BaseWebSocket) + def __init__(self, websocket: websockets.WebSocketClientProtocol): + self.event_handlers: Set[WebsocketHandlerFunction] = set() + super().__init__(websocket) + + @property + @overrides(BaseWebSocket) + def websocket(self) -> websockets.WebSocketClientProtocol: + return self._websocket + + @overrides(BaseWebSocket) + async def send(self, data: Dict[str, Any]): + return await self.websocket.send(json.dumps(data)) + + @overrides(BaseWebSocket) + async def receive(self) -> Dict[str, Any]: + received = await self.websocket.recv() + return json.loads(received) + + async def _dispatcher(self): + while not self.websocket.closed: + try: + data = await self.receive() + except websockets.ConnectionClosedOK: + break + except Exception as e: + logger.exception(f'Websocket client listened {self.websocket} ' + f'failed to receive data: {e}') + continue + asyncio.ensure_future( + asyncio.gather(*map(lambda f: f(data), self.event_handlers), + return_exceptions=True)) + + @overrides(BaseWebSocket) + async def accept(self): + asyncio.ensure_future(self._dispatcher()) + + @overrides(BaseWebSocket) + async def close(self): + await self.websocket.close() + + def handle(self, callable: WebsocketHandler_T) -> WebsocketHandler_T: + self.event_handlers.add(callable) + return callable + class MiraiBot(BaseBot): - def __init__(self, - connection_type: str, - self_id: str, - *, - websocket: Optional["WebSocket"] = None): + def __init__(self, connection_type: str, self_id: str, *, + websocket: WebSocket): super().__init__(connection_type, self_id, websocket=websocket) @property @@ -27,8 +94,55 @@ class MiraiBot(BaseBot): @classmethod @overrides(BaseBot) async def check_permission(cls, driver: "Driver", connection_type: str, - headers: dict, body: Optional[dict]) -> str: - return '' + headers: dict, body: Optional[dict]) -> NoReturn: + raise RequestDenied( + status_code=501, + reason=f'Connection {connection_type} not implented') + + @classmethod + @overrides(BaseBot) + def register(cls, driver: "Driver", config: "Config", qq: int): + config = Config.parse_obj(config.dict()) + assert config.auth_key and config.host and config.port, f'Current config {config!r} is invalid' + + super().register(driver, config) # type: ignore + + @driver.on_startup + async def _startup(): + async with httpx.AsyncClient( + base_url=f'http://{config.host}:{config.port}') as client: + response = await client.get('/about') + info = response.json() + logger.debug(f'Mirai API returned info: {info}') + response = await client.post('/auth', + json={'authKey': config.auth_key}) + status = response.json() + assert status['code'] == 0 + session_key = status['session'] + response = await client.post('/verify', + json={ + 'sessionKey': session_key, + 'qq': qq + }) + assert response.json()['code'] == 0 + + websocket = await WebSocket.new( + host=config.host, # type: ignore + port=config.port, # type: ignore + session_key=session_key) + bot = cls(connection_type='forward_ws', + self_id=str(qq), + websocket=websocket) + websocket.handle(bot.handle_message) + driver._clients[str(qq)] = bot + await websocket.accept() + + @driver.on_shutdown + async def _shutdown(): + bot = driver._clients.pop(str(qq), None) + if bot is None: + return + await bot.websocket.close() #type:ignore @overrides(BaseBot) async def handle_message(self, message: dict): diff --git a/nonebot/adapters/mirai/config.py b/nonebot/adapters/mirai/config.py new file mode 100644 index 00000000..942cf9fa --- /dev/null +++ b/nonebot/adapters/mirai/config.py @@ -0,0 +1,13 @@ +from ipaddress import IPv4Address +from typing import Optional + +from pydantic import BaseModel, Extra, Field + + +class Config(BaseModel): + auth_key: Optional[str] = Field(None, alias='mirai_auth_key') + host: Optional[IPv4Address] = Field(None, alias='mirai_host') + port: Optional[int] = Field(None, alias='mirai_port') + + class Config: + extra = Extra.ignore diff --git a/nonebot/drivers/__init__.py b/nonebot/drivers/__init__.py index 986d59a3..134b2078 100644 --- a/nonebot/drivers/__init__.py +++ b/nonebot/drivers/__init__.py @@ -62,7 +62,7 @@ class Driver(abc.ABC): :说明: 已连接的 Bot """ - def register_adapter(self, name: str, adapter: Type["Bot"]): + def register_adapter(self, name: str, adapter: Type["Bot"], **kwargs): """ :说明: @@ -74,7 +74,7 @@ class Driver(abc.ABC): * ``adapter: Type[Bot]``: 适配器 Class """ self._adapters[name] = adapter - adapter.register(self, self.config) + adapter.register(self, self.config, **kwargs) logger.opt( colors=True).debug(f'Succeeded to load adapter "{name}"') From 5de41a18f976838ba70addbfc7fc9b744b5c3940 Mon Sep 17 00:00:00 2001 From: Mix Date: Sat, 30 Jan 2021 06:10:04 +0800 Subject: [PATCH 05/37] :art: sort imports in file --- nonebot/adapters/mirai/bot.py | 1 + nonebot/adapters/mirai/event/base.py | 9 +++++++-- nonebot/adapters/mirai/event/message.py | 7 ++++--- nonebot/adapters/mirai/event/notice.py | 2 +- nonebot/adapters/mirai/message.py | 2 +- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py index fba54a69..6190bedb 100644 --- a/nonebot/adapters/mirai/bot.py +++ b/nonebot/adapters/mirai/bot.py @@ -58,6 +58,7 @@ class WebSocket(BaseWebSocket): try: data = await self.receive() except websockets.ConnectionClosedOK: + logger.debug(f'Websocket connection {self.websocket} closed') break except Exception as e: logger.exception(f'Websocket client listened {self.websocket} ' diff --git a/nonebot/adapters/mirai/event/base.py b/nonebot/adapters/mirai/event/base.py index 451a858e..7a6cae39 100644 --- a/nonebot/adapters/mirai/event/base.py +++ b/nonebot/adapters/mirai/event/base.py @@ -1,12 +1,14 @@ +import json from enum import Enum -from typing import Dict, Any, Optional, Type +from typing import Any, Dict, 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 nonebot.log import logger +from nonebot.typing import overrides class SenderPermission(str, Enum): @@ -104,3 +106,6 @@ class Event(BaseEvent): @overrides(BaseEvent) def is_tome(self) -> bool: return False + + def normalize_dict(self, **kwargs) -> Dict[str, Any]: + return json.loads(self.json(**kwargs)) diff --git a/nonebot/adapters/mirai/event/message.py b/nonebot/adapters/mirai/event/message.py index f680b0a2..9c478e28 100644 --- a/nonebot/adapters/mirai/event/message.py +++ b/nonebot/adapters/mirai/event/message.py @@ -1,10 +1,11 @@ from typing import TYPE_CHECKING from pydantic import Field -from .base import Event, SenderInfo, PrivateSenderInfo + +from nonebot.typing import overrides from ..message import MessageChain -from nonebot.typing import overrides +from .base import Event, PrivateSenderInfo, SenderInfo class MessageEvent(Event): @@ -37,4 +38,4 @@ class FriendMessage(MessageEvent): class TempMessage(MessageEvent): - pass \ No newline at end of file + pass diff --git a/nonebot/adapters/mirai/event/notice.py b/nonebot/adapters/mirai/event/notice.py index ae144b91..b758d9c5 100644 --- a/nonebot/adapters/mirai/event/notice.py +++ b/nonebot/adapters/mirai/event/notice.py @@ -1,4 +1,4 @@ -from typing import Optional, Any +from typing import Any, Optional from pydantic import Field diff --git a/nonebot/adapters/mirai/message.py b/nonebot/adapters/mirai/message.py index 06fc0d28..7562b6be 100644 --- a/nonebot/adapters/mirai/message.py +++ b/nonebot/adapters/mirai/message.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Any, Dict, List, Union, Iterable +from typing import Any, Dict, Iterable, List, Union from pydantic import validate_arguments From e2f837055e457f61105e1e5bfbf53c48df783933 Mon Sep 17 00:00:00 2001 From: Mix Date: Sat, 30 Jan 2021 10:55:06 +0800 Subject: [PATCH 06/37] :heavy_plus_sign: add dependency of websockets --- poetry.lock | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index dc0b527f..2e9e5611 100644 --- a/poetry.lock +++ b/poetry.lock @@ -784,7 +784,7 @@ reference = "aliyun" [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "55439e671ff8c89285f2cf645189c1bf3e3bd53638bbb31ed505727a041d1012" +content-hash = "0038c5b3aa4a382184c1ef5b37a668ce37d8246c8fdf18deb71dccc8bf97be62" [metadata.files] alabaster = [ diff --git a/pyproject.toml b/pyproject.toml index 23f8e799..87a5e573 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ pygtrie = "^2.4.1" fastapi = "^0.63.0" uvicorn = "^0.11.5" pydantic = {extras = ["dotenv", "typing_extensions"], version = "^1.7.3"} +websockets = "^8.1" [tool.poetry.dev-dependencies] yapf = "^0.30.0" From 8b3eb4e0760b348ffec39b48cf82ea084d3a0d1a Mon Sep 17 00:00:00 2001 From: Mix Date: Sat, 30 Jan 2021 13:36:31 +0800 Subject: [PATCH 07/37] :zap: add retry for mirai adapter when websocket connection down --- nonebot/adapters/mirai/bot.py | 118 +++++++++++++++++++++++++--------- 1 file changed, 87 insertions(+), 31 deletions(-) diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py index 6190bedb..70166eff 100644 --- a/nonebot/adapters/mirai/bot.py +++ b/nonebot/adapters/mirai/bot.py @@ -9,6 +9,7 @@ import websockets from nonebot.adapters import Bot as BaseBot from nonebot.adapters import Event as BaseEvent +from nonebot.config import Config from nonebot.drivers import Driver from nonebot.drivers import WebSocket as BaseWebSocket from nonebot.exception import RequestDenied @@ -16,7 +17,7 @@ from nonebot.log import logger from nonebot.message import handle_event from nonebot.typing import overrides -from .config import Config +from .config import Config as MiraiConfig from .event import Event WebsocketHandlerFunction = Callable[[Dict[str, Any]], Coroutine[Any, Any, None]] @@ -24,6 +25,32 @@ WebsocketHandler_T = TypeVar('WebsocketHandler_T', bound=WebsocketHandlerFunction) +async def _ws_authorization(client: httpx.AsyncClient, *, auth_key: str, + qq: int) -> str: + + async def request(method: str, *, path: str, **kwargs) -> Dict[str, Any]: + response = await client.request(method, path, **kwargs) + response.raise_for_status() + return response.json() + + about = await request('GET', path='/about') + logger.opt(colors=True).debug('Mirai API HTTP backend version: ' + f'{about["data"]["version"]}') + + status = await request('POST', path='/auth', json={'authKey': auth_key}) + assert status['code'] == 0 + session_key = status['session'] + + verify = await request('POST', + path='/verify', + json={ + 'sessionKey': session_key, + 'qq': qq + }) + assert verify['code'] == 0, verify['msg'] + return session_key + + class WebSocket(BaseWebSocket): @classmethod @@ -44,6 +71,11 @@ class WebSocket(BaseWebSocket): def websocket(self) -> websockets.WebSocketClientProtocol: return self._websocket + @property + @overrides(BaseWebSocket) + def closed(self) -> bool: + return self.websocket.closed + @overrides(BaseWebSocket) async def send(self, data: Dict[str, Any]): return await self.websocket.send(json.dumps(data)) @@ -54,23 +86,26 @@ class WebSocket(BaseWebSocket): return json.loads(received) async def _dispatcher(self): - while not self.websocket.closed: + while not self.closed: try: data = await self.receive() except websockets.ConnectionClosedOK: logger.debug(f'Websocket connection {self.websocket} closed') break - except Exception as e: + except websockets.ConnectionClosedError: + logger.exception(f'Websocket connection {self.websocket} ' + 'connection closed abnormally:') + break + except json.JSONDecodeError as e: logger.exception(f'Websocket client listened {self.websocket} ' - f'failed to receive data: {e}') + f'failed to decode data: {e}') continue - asyncio.ensure_future( - asyncio.gather(*map(lambda f: f(data), self.event_handlers), - return_exceptions=True)) + asyncio.gather(*map(lambda f: f(data), self.event_handlers), + return_exceptions=True) @overrides(BaseWebSocket) async def accept(self): - asyncio.ensure_future(self._dispatcher()) + asyncio.create_task(self._dispatcher()) @overrides(BaseWebSocket) async def close(self): @@ -92,6 +127,10 @@ class MiraiBot(BaseBot): def type(self) -> str: return "mirai" + @property + def alive(self) -> bool: + return not self.websocket.closed + @classmethod @overrides(BaseBot) async def check_permission(cls, driver: "Driver", connection_type: str, @@ -103,33 +142,26 @@ class MiraiBot(BaseBot): @classmethod @overrides(BaseBot) def register(cls, driver: "Driver", config: "Config", qq: int): - config = Config.parse_obj(config.dict()) - assert config.auth_key and config.host and config.port, f'Current config {config!r} is invalid' + cls.mirai_config = MiraiConfig(**config.dict()) + cls.active = True + assert cls.mirai_config.auth_key is not None + assert cls.mirai_config.host is not None + assert cls.mirai_config.port is not None + super().register(driver, config) - super().register(driver, config) # type: ignore - - @driver.on_startup - async def _startup(): + async def _bot_connection(): async with httpx.AsyncClient( - base_url=f'http://{config.host}:{config.port}') as client: - response = await client.get('/about') - info = response.json() - logger.debug(f'Mirai API returned info: {info}') - response = await client.post('/auth', - json={'authKey': config.auth_key}) - status = response.json() - assert status['code'] == 0 - session_key = status['session'] - response = await client.post('/verify', - json={ - 'sessionKey': session_key, - 'qq': qq - }) - assert response.json()['code'] == 0 + base_url= + f'http://{cls.mirai_config.host}:{cls.mirai_config.port}' + ) as client: + session_key = await _ws_authorization( + client, + auth_key=cls.mirai_config.auth_key, # type: ignore + qq=qq) # type: ignore websocket = await WebSocket.new( - host=config.host, # type: ignore - port=config.port, # type: ignore + host=cls.mirai_config.host, # type: ignore + port=cls.mirai_config.port, # type: ignore session_key=session_key) bot = cls(connection_type='forward_ws', self_id=str(qq), @@ -138,8 +170,32 @@ class MiraiBot(BaseBot): driver._clients[str(qq)] = bot await websocket.accept() + async def _connection_ensure(): + if str(qq) not in driver._clients: + await _bot_connection() + elif not driver._clients[str(qq)].alive: + driver._clients.pop(str(qq), None) + await _bot_connection() + + @driver.on_startup + async def _startup(): + + async def _checker(): + while cls.active: + try: + await _connection_ensure() + except Exception as e: + logger.opt(colors=True).warning( + 'Failed to create mirai connection to ' + f'{qq}, reason: {e}. ' + 'Will retry after 3 seconds') + await asyncio.sleep(3) + + asyncio.create_task(_checker()) + @driver.on_shutdown async def _shutdown(): + cls.active = False bot = driver._clients.pop(str(qq), None) if bot is None: return From 5b3ef53301ee3b125595fb7b0eda653625326b7d Mon Sep 17 00:00:00 2001 From: Mix Date: Sat, 30 Jan 2021 13:45:55 +0800 Subject: [PATCH 08/37] :art: add support for on_bot_* event handler --- nonebot/adapters/mirai/bot.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py index 70166eff..08f6ea4e 100644 --- a/nonebot/adapters/mirai/bot.py +++ b/nonebot/adapters/mirai/bot.py @@ -121,6 +121,8 @@ class MiraiBot(BaseBot): def __init__(self, connection_type: str, self_id: str, *, websocket: WebSocket): super().__init__(connection_type, self_id, websocket=websocket) + websocket.handle(self.handle_message) + self.driver._bot_connect(self) @property @overrides(BaseBot) @@ -213,3 +215,6 @@ class MiraiBot(BaseBot): @overrides(BaseBot) async def send(self, event: "BaseEvent", message: str, **kwargs): return super().send(event, message, **kwargs) + + def __del__(self): + self.driver._bot_disconnect(self) From c82ceefc8b503476fc41d1c5494a91209d2084af Mon Sep 17 00:00:00 2001 From: Mix Date: Sat, 30 Jan 2021 19:11:17 +0800 Subject: [PATCH 09/37] :rewind: revert call method to http post, add api handle --- nonebot/adapters/mirai/__init__.py | 4 +- nonebot/adapters/mirai/bot.py | 233 ++++++++------------------- nonebot/adapters/mirai/bot_ws.py | 220 +++++++++++++++++++++++++ nonebot/adapters/mirai/event/base.py | 1 + 4 files changed, 293 insertions(+), 165 deletions(-) create mode 100644 nonebot/adapters/mirai/bot_ws.py diff --git a/nonebot/adapters/mirai/__init__.py b/nonebot/adapters/mirai/__init__.py index c832d378..991f30fd 100644 --- a/nonebot/adapters/mirai/__init__.py +++ b/nonebot/adapters/mirai/__init__.py @@ -1 +1,3 @@ -from .bot import MiraiBot \ No newline at end of file +from .bot import MiraiBot +from .event import * +from .message import MessageChain, MessageSegment diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py index 08f6ea4e..338dd144 100644 --- a/nonebot/adapters/mirai/bot.py +++ b/nonebot/adapters/mirai/bot.py @@ -1,128 +1,74 @@ -import asyncio -import json +from typing import Any, Dict, Optional, Tuple +from datetime import datetime, timedelta from ipaddress import IPv4Address -from typing import (Any, Callable, Coroutine, Dict, NoReturn, Optional, Set, - TypeVar) import httpx -import websockets from nonebot.adapters import Bot as BaseBot from nonebot.adapters import Event as BaseEvent from nonebot.config import Config -from nonebot.drivers import Driver -from nonebot.drivers import WebSocket as BaseWebSocket +from nonebot.drivers import Driver, WebSocket from nonebot.exception import RequestDenied from nonebot.log import logger from nonebot.message import handle_event from nonebot.typing import overrides from .config import Config as MiraiConfig -from .event import Event - -WebsocketHandlerFunction = Callable[[Dict[str, Any]], Coroutine[Any, Any, None]] -WebsocketHandler_T = TypeVar('WebsocketHandler_T', - bound=WebsocketHandlerFunction) +from .event import Event, FriendMessage, TempMessage, GroupMessage -async def _ws_authorization(client: httpx.AsyncClient, *, auth_key: str, - qq: int) -> str: +class SessionManager: + sessions: Dict[int, Tuple[str, datetime, httpx.AsyncClient]] = {} + session_expiry: timedelta = timedelta(minutes=15) - async def request(method: str, *, path: str, **kwargs) -> Dict[str, Any]: - response = await client.request(method, path, **kwargs) + def __init__(self, session_key: str, client: httpx.AsyncClient): + self.session_key, self.client = session_key, client + + async def post(self, path: str, *, params: Optional[Dict[str, Any]] = None): + params = {**(params or {}), 'sessionKey': self.session_key} + response = await self.client.post(path, json=params) response.raise_for_status() return response.json() - about = await request('GET', path='/about') - logger.opt(colors=True).debug('Mirai API HTTP backend version: ' - f'{about["data"]["version"]}') - - status = await request('POST', path='/auth', json={'authKey': auth_key}) - assert status['code'] == 0 - session_key = status['session'] - - verify = await request('POST', - path='/verify', - json={ - 'sessionKey': session_key, - 'qq': qq - }) - assert verify['code'] == 0, verify['msg'] - return session_key - - -class WebSocket(BaseWebSocket): + @classmethod + async def new(cls, self_id: int, *, host: IPv4Address, port: int, + auth_key: str): + if self_id in cls.sessions: + manager = cls.get(self_id) + if manager is not None: + return manager + client = httpx.AsyncClient(base_url=f'http://{host}:{port}') + response = await client.post('/auth', json={'authKey': auth_key}) + response.raise_for_status() + auth = response.json() + assert auth['code'] == 0 + session_key = auth['session'] + response = await client.post('/verify', + json={ + 'sessionKey': session_key, + 'qq': self_id + }) + assert response.json()['code'] == 0 + cls.sessions[self_id] = session_key, datetime.now(), client + return cls(session_key, client) @classmethod - async def new(cls, *, host: IPv4Address, port: int, - session_key: str) -> "WebSocket": - listen_address = httpx.URL(f'ws://{host}:{port}/all', - params={'sessionKey': session_key}) - websocket = await websockets.connect(uri=str(listen_address)) - return cls(websocket) - - @overrides(BaseWebSocket) - def __init__(self, websocket: websockets.WebSocketClientProtocol): - self.event_handlers: Set[WebsocketHandlerFunction] = set() - super().__init__(websocket) - - @property - @overrides(BaseWebSocket) - def websocket(self) -> websockets.WebSocketClientProtocol: - return self._websocket - - @property - @overrides(BaseWebSocket) - def closed(self) -> bool: - return self.websocket.closed - - @overrides(BaseWebSocket) - async def send(self, data: Dict[str, Any]): - return await self.websocket.send(json.dumps(data)) - - @overrides(BaseWebSocket) - async def receive(self) -> Dict[str, Any]: - received = await self.websocket.recv() - return json.loads(received) - - async def _dispatcher(self): - while not self.closed: - try: - data = await self.receive() - except websockets.ConnectionClosedOK: - logger.debug(f'Websocket connection {self.websocket} closed') - break - except websockets.ConnectionClosedError: - logger.exception(f'Websocket connection {self.websocket} ' - 'connection closed abnormally:') - break - except json.JSONDecodeError as e: - logger.exception(f'Websocket client listened {self.websocket} ' - f'failed to decode data: {e}') - continue - asyncio.gather(*map(lambda f: f(data), self.event_handlers), - return_exceptions=True) - - @overrides(BaseWebSocket) - async def accept(self): - asyncio.create_task(self._dispatcher()) - - @overrides(BaseWebSocket) - async def close(self): - await self.websocket.close() - - def handle(self, callable: WebsocketHandler_T) -> WebsocketHandler_T: - self.event_handlers.add(callable) - return callable + def get(cls, self_id: int): + key, time, client = cls.sessions[self_id] + if datetime.now() - time > cls.session_expiry: + return None + return cls(key, client) class MiraiBot(BaseBot): - def __init__(self, connection_type: str, self_id: str, *, - websocket: WebSocket): + def __init__(self, + connection_type: str, + self_id: str, + *, + websocket: Optional[WebSocket] = None): super().__init__(connection_type, self_id, websocket=websocket) - websocket.handle(self.handle_message) - self.driver._bot_connect(self) + self.api = SessionManager.get(int(self_id)) @property @overrides(BaseBot) @@ -136,85 +82,44 @@ class MiraiBot(BaseBot): @classmethod @overrides(BaseBot) async def check_permission(cls, driver: "Driver", connection_type: str, - headers: dict, body: Optional[dict]) -> NoReturn: - raise RequestDenied( - status_code=501, - reason=f'Connection {connection_type} not implented') + headers: dict, body: Optional[dict]) -> str: + if connection_type == 'ws': + raise RequestDenied( + status_code=501, + reason='Websocket connection is not implemented') + self_id: Optional[str] = headers.get('bot') + if self_id is None: + raise RequestDenied(status_code=400, + reason='Header `Bot` is required.') + self_id = str(self_id).strip() + await SessionManager.new( + int(self_id), + host=cls.mirai_config.host, # type: ignore + port=cls.mirai_config.port, #type: ignore + auth_key=cls.mirai_config.auth_key) # type: ignore + return self_id @classmethod @overrides(BaseBot) - def register(cls, driver: "Driver", config: "Config", qq: int): + def register(cls, driver: "Driver", config: "Config"): cls.mirai_config = MiraiConfig(**config.dict()) - cls.active = True assert cls.mirai_config.auth_key is not None assert cls.mirai_config.host is not None assert cls.mirai_config.port is not None super().register(driver, config) - async def _bot_connection(): - async with httpx.AsyncClient( - base_url= - f'http://{cls.mirai_config.host}:{cls.mirai_config.port}' - ) as client: - session_key = await _ws_authorization( - client, - auth_key=cls.mirai_config.auth_key, # type: ignore - qq=qq) # type: ignore - - websocket = await WebSocket.new( - host=cls.mirai_config.host, # type: ignore - port=cls.mirai_config.port, # type: ignore - session_key=session_key) - bot = cls(connection_type='forward_ws', - self_id=str(qq), - websocket=websocket) - websocket.handle(bot.handle_message) - driver._clients[str(qq)] = bot - await websocket.accept() - - async def _connection_ensure(): - if str(qq) not in driver._clients: - await _bot_connection() - elif not driver._clients[str(qq)].alive: - driver._clients.pop(str(qq), None) - await _bot_connection() - - @driver.on_startup - async def _startup(): - - async def _checker(): - while cls.active: - try: - await _connection_ensure() - except Exception as e: - logger.opt(colors=True).warning( - 'Failed to create mirai connection to ' - f'{qq}, reason: {e}. ' - 'Will retry after 3 seconds') - await asyncio.sleep(3) - - asyncio.create_task(_checker()) - - @driver.on_shutdown - async def _shutdown(): - cls.active = False - bot = driver._clients.pop(str(qq), None) - if bot is None: - return - await bot.websocket.close() #type:ignore - @overrides(BaseBot) async def handle_message(self, message: dict): - event = Event.new(message) - await handle_event(self, event) + await handle_event(bot=self, + event=Event.new({ + **message, + 'self_id': self.self_id, + })) @overrides(BaseBot) async def call_api(self, api: str, **data): - return super().call_api(api, **data) + return await self.api.post('/' + api, params=data) @overrides(BaseBot) async def send(self, event: "BaseEvent", message: str, **kwargs): - return super().send(event, message, **kwargs) - - def __del__(self): - self.driver._bot_disconnect(self) + pass diff --git a/nonebot/adapters/mirai/bot_ws.py b/nonebot/adapters/mirai/bot_ws.py new file mode 100644 index 00000000..d9803c47 --- /dev/null +++ b/nonebot/adapters/mirai/bot_ws.py @@ -0,0 +1,220 @@ +import asyncio +import json +from ipaddress import IPv4Address +from typing import (Any, Callable, Coroutine, Dict, NoReturn, Optional, Set, + TypeVar) + +import httpx +import websockets + +from nonebot.adapters import Bot as BaseBot +from nonebot.adapters import Event as BaseEvent +from nonebot.config import Config +from nonebot.drivers import Driver +from nonebot.drivers import WebSocket as BaseWebSocket +from nonebot.exception import RequestDenied +from nonebot.log import logger +from nonebot.message import handle_event +from nonebot.typing import overrides + +from .config import Config as MiraiConfig +from .event import Event + +WebsocketHandlerFunction = Callable[[Dict[str, Any]], Coroutine[Any, Any, None]] +WebsocketHandler_T = TypeVar('WebsocketHandler_T', + bound=WebsocketHandlerFunction) + + +async def _ws_authorization(client: httpx.AsyncClient, *, auth_key: str, + qq: int) -> str: + + async def request(method: str, *, path: str, **kwargs) -> Dict[str, Any]: + response = await client.request(method, path, **kwargs) + response.raise_for_status() + return response.json() + + about = await request('GET', path='/about') + logger.opt(colors=True).debug('Mirai API HTTP backend version: ' + f'{about["data"]["version"]}') + + status = await request('POST', path='/auth', json={'authKey': auth_key}) + assert status['code'] == 0 + session_key = status['session'] + + verify = await request('POST', + path='/verify', + json={ + 'sessionKey': session_key, + 'qq': qq + }) + assert verify['code'] == 0, verify['msg'] + return session_key + + +class WebSocket(BaseWebSocket): + + @classmethod + async def new(cls, *, host: IPv4Address, port: int, + session_key: str) -> "WebSocket": + listen_address = httpx.URL(f'ws://{host}:{port}/all', + params={'sessionKey': session_key}) + websocket = await websockets.connect(uri=str(listen_address)) + return cls(websocket) + + @overrides(BaseWebSocket) + def __init__(self, websocket: websockets.WebSocketClientProtocol): + self.event_handlers: Set[WebsocketHandlerFunction] = set() + super().__init__(websocket) + + @property + @overrides(BaseWebSocket) + def websocket(self) -> websockets.WebSocketClientProtocol: + return self._websocket + + @property + @overrides(BaseWebSocket) + def closed(self) -> bool: + return self.websocket.closed + + @overrides(BaseWebSocket) + async def send(self, data: Dict[str, Any]): + return await self.websocket.send(json.dumps(data)) + + @overrides(BaseWebSocket) + async def receive(self) -> Dict[str, Any]: + received = await self.websocket.recv() + return json.loads(received) + + async def _dispatcher(self): + while not self.closed: + try: + data = await self.receive() + except websockets.ConnectionClosedOK: + logger.debug(f'Websocket connection {self.websocket} closed') + break + except websockets.ConnectionClosedError: + logger.exception(f'Websocket connection {self.websocket} ' + 'connection closed abnormally:') + break + except json.JSONDecodeError as e: + logger.exception(f'Websocket client listened {self.websocket} ' + f'failed to decode data: {e}') + continue + asyncio.gather(*map(lambda f: f(data), self.event_handlers), + return_exceptions=True) + + @overrides(BaseWebSocket) + async def accept(self): + asyncio.create_task(self._dispatcher()) + + @overrides(BaseWebSocket) + async def close(self): + await self.websocket.close() + + def handle(self, callable: WebsocketHandler_T) -> WebsocketHandler_T: + self.event_handlers.add(callable) + return callable + + +class MiraiWebsocketBot(BaseBot): + + def __init__(self, connection_type: str, self_id: str, *, + websocket: WebSocket): + super().__init__(connection_type, self_id, websocket=websocket) + websocket.handle(self.handle_message) + self.driver._bot_connect(self) + + @property + @overrides(BaseBot) + def type(self) -> str: + return "mirai" + + @property + def alive(self) -> bool: + return not self.websocket.closed + + @classmethod + @overrides(BaseBot) + async def check_permission(cls, driver: "Driver", connection_type: str, + headers: dict, body: Optional[dict]) -> NoReturn: + raise RequestDenied( + status_code=501, + reason=f'Connection {connection_type} not implented') + + @classmethod + @overrides(BaseBot) + def register(cls, driver: "Driver", config: "Config", qq: int): + cls.mirai_config = MiraiConfig(**config.dict()) + cls.active = True + assert cls.mirai_config.auth_key is not None + assert cls.mirai_config.host is not None + assert cls.mirai_config.port is not None + super().register(driver, config) + + async def _bot_connection(): + async with httpx.AsyncClient( + base_url= + f'http://{cls.mirai_config.host}:{cls.mirai_config.port}' + ) as client: + session_key = await _ws_authorization( + client, + auth_key=cls.mirai_config.auth_key, # type: ignore + qq=qq) # type: ignore + + websocket = await WebSocket.new( + host=cls.mirai_config.host, # type: ignore + port=cls.mirai_config.port, # type: ignore + session_key=session_key) + bot = cls(connection_type='forward_ws', + self_id=str(qq), + websocket=websocket) + websocket.handle(bot.handle_message) + driver._clients[str(qq)] = bot + await websocket.accept() + + async def _connection_ensure(): + if str(qq) not in driver._clients: + await _bot_connection() + elif not driver._clients[str(qq)].alive: + driver._clients.pop(str(qq), None) + await _bot_connection() + + @driver.on_startup + async def _startup(): + + async def _checker(): + while cls.active: + try: + await _connection_ensure() + except Exception as e: + logger.opt(colors=True).warning( + 'Failed to create mirai connection to ' + f'{qq}, reason: {e}. ' + 'Will retry after 3 seconds') + await asyncio.sleep(3) + + asyncio.create_task(_checker()) + + @driver.on_shutdown + async def _shutdown(): + cls.active = False + bot = driver._clients.pop(str(qq), None) + if bot is None: + return + await bot.websocket.close() #type:ignore + + @overrides(BaseBot) + async def handle_message(self, message: dict): + 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: "BaseEvent", message: str, **kwargs): + return super().send(event, message, **kwargs) + + def __del__(self): + self.driver._bot_disconnect(self) diff --git a/nonebot/adapters/mirai/event/base.py b/nonebot/adapters/mirai/event/base.py index 7a6cae39..6fbb30ff 100644 --- a/nonebot/adapters/mirai/event/base.py +++ b/nonebot/adapters/mirai/event/base.py @@ -37,6 +37,7 @@ class PrivateSenderInfo(BaseModel): class Event(BaseEvent): + self_id: int type: str @classmethod From 95f27824ee1994753102c39354ccbbaf0f8aa70e Mon Sep 17 00:00:00 2001 From: Mix Date: Sat, 30 Jan 2021 20:40:00 +0800 Subject: [PATCH 10/37] :construction: add api methods define --- nonebot/adapters/mirai/bot.py | 199 +++++++++++++++++++++++- nonebot/adapters/mirai/event/message.py | 28 +++- 2 files changed, 214 insertions(+), 13 deletions(-) diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py index 338dd144..e89eb245 100644 --- a/nonebot/adapters/mirai/bot.py +++ b/nonebot/adapters/mirai/bot.py @@ -1,6 +1,7 @@ -from typing import Any, Dict, Optional, Tuple from datetime import datetime, timedelta +from io import BytesIO from ipaddress import IPv4Address +from typing import Any, Dict, List, NoReturn, Optional, Tuple import httpx @@ -14,7 +15,8 @@ from nonebot.message import handle_event from nonebot.typing import overrides from .config import Config as MiraiConfig -from .event import Event, FriendMessage, TempMessage, GroupMessage +from .event import Event, FriendMessage, GroupMessage, TempMessage +from .message import MessageChain, MessageSegment class SessionManager: @@ -24,12 +26,41 @@ class SessionManager: def __init__(self, session_key: str, client: httpx.AsyncClient): self.session_key, self.client = session_key, client - async def post(self, path: str, *, params: Optional[Dict[str, Any]] = None): + async def post(self, + path: str, + *, + params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: params = {**(params or {}), 'sessionKey': self.session_key} response = await self.client.post(path, json=params) response.raise_for_status() return response.json() + async def request(self, + path: str, + *, + params: Optional[Dict[str, + Any]] = None) -> Dict[str, Any]: + response = await self.client.get(path, + params={ + **(params or {}), 'sessionKey': + self.session_key + }) + response.raise_for_status() + return response.json() + + async def upload(self, path: str, *, type: str, + file: Tuple[str, BytesIO]) -> Dict[str, Any]: + file_type, file_io = file + response = await self.client.post(path, + data={ + 'sessionKey': self.session_key, + 'type': type + }, + files={file_type: file_io}, + timeout=6) + response.raise_for_status() + return response.json() + @classmethod async def new(cls, self_id: int, *, host: IPv4Address, port: int, auth_key: str): @@ -117,9 +148,163 @@ class MiraiBot(BaseBot): })) @overrides(BaseBot) - async def call_api(self, api: str, **data): - return await self.api.post('/' + api, params=data) + async def call_api(self, api: str, **data) -> NoReturn: + raise NotImplementedError @overrides(BaseBot) - async def send(self, event: "BaseEvent", message: str, **kwargs): - pass + async def __getattr__(self, key: str) -> NoReturn: + raise NotImplementedError + + @overrides(BaseBot) + async def send(self, + event: Event, + message: MessageChain, + at_sender: bool = False, + **kwargs): + if isinstance(event, FriendMessage): + return await self.send_friend_message(target=event.sender.id, + message_chain=message) + elif isinstance(event, GroupMessage): + return await self.send_group_message(target=event.sender.group.id, + message_chain=message) + elif isinstance(event, TempMessage): + return await self.send_temp_message(qq=event.sender.id, + group=event.sender.group.id, + message_chain=message) + else: + raise ValueError(f'Unsupported event type {event!r}.') + + async def send_friend_message(self, target: int, + message_chain: MessageChain): + return await self.api.post('sendFriendMessage', + params={ + 'target': target, + 'messageChain': message_chain.export() + }) + + async def send_temp_message(self, qq: int, group: int, + message_chain: MessageChain): + return await self.api.post('sendTempMessage', + params={ + 'qq': qq, + 'group': group, + 'messageChain': message_chain.export() + }) + + async def send_group_message(self, target: int, + message_chain: MessageChain): + return await self.api.post('sendGroupMessage', + params={ + 'target': target, + 'messageChain': message_chain.export() + }) + + async def recall(self, target: int): + return await self.api.post('recall', params={'target': target}) + + async def send_image_message(self, target: int, qq: int, group: int, + urls: List[str]): + return await self.api.post('sendImageMessage', + params={ + 'target': target, + 'qq': qq, + 'group': group, + 'urls': urls + }) + + async def upload_image(self, type: str, img: BytesIO): + return await self.api.upload('uploadImage', + type=type, + file=('img', img)) + + async def upload_voice(self, type: str, voice: BytesIO): + return await self.api.upload('uploadVoice', + type=type, + file=('voice', voice)) + + async def fetch_message(self): + return await self.api.request('fetchMessage') + + async def fetch_latest_message(self): + return await self.api.request('fetchLatestMessage') + + async def peek_message(self, count: int): + return await self.api.request('peekMessage', params={'count': count}) + + async def peek_latest_message(self, count: int): + return await self.api.request('peekLatestMessage', + params={'count': count}) + + async def messsage_from_id(self, id: int): + return await self.api.request('messageFromId', params={'id': id}) + + async def count_message(self): + return await self.api.request('countMessage') + + async def friend_list(self) -> List[Dict[str, Any]]: + return await self.api.request('friendList') # type: ignore + + async def group_list(self) -> List[Dict[str, Any]]: + return await self.api.request('groupList') # type: ignore + + async def member_list(self, target: int) -> List[Dict[str, Any]]: + return await self.api.request('memberList', + params={'target': target}) # type: ignore + + async def mute(self, target: int, member_id: int, time: int): + return await self.api.post('mute', + params={ + 'target': target, + 'memberId': member_id, + 'time': time + }) + + async def unmute(self, target: int, member_id: int): + return await self.api.post('unmute', + params={ + 'target': target, + 'memberId': member_id + }) + + async def kick(self, target: int, member_id: int, msg: str): + return await self.api.post('kick', + params={ + 'target': target, + 'memberId': member_id, + 'msg': msg + }) + + async def quit(self, target: int): + return await self.api.post('quit', params={'target': target}) + + async def mute_all(self, target: int): + return await self.api.post('muteAll', params={'target': target}) + + async def unmute_all(self, target: int): + return await self.api.post('unmuteAll', params={'target': target}) + + async def group_config(self, target: int): + return await self.api.request('groupConfig', params={'target': target}) + + async def modify_group_config(self, target: int, config: Dict[str, Any]): + return await self.api.post('groupConfig', + params={ + 'target': target, + 'config': config + }) + + async def member_info(self, target: int, member_id: int): + return await self.api.request('memberInfo', + params={ + 'target': target, + 'memberId': member_id + }) + + async def modify_member_info(self, target: int, member_id: int, + info: Dict[str, Any]): + return await self.api.post('memberInfo', + params={ + 'target': target, + 'memberId': member_id, + 'info': info + }) diff --git a/nonebot/adapters/mirai/event/message.py b/nonebot/adapters/mirai/event/message.py index 9c478e28..1cfca586 100644 --- a/nonebot/adapters/mirai/event/message.py +++ b/nonebot/adapters/mirai/event/message.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import Any from pydantic import Field @@ -10,7 +10,7 @@ from .base import Event, PrivateSenderInfo, SenderInfo class MessageEvent(Event): message_chain: MessageChain = Field(alias='messageChain') - sender: SenderInfo + sender: Any @overrides(Event) def get_message(self) -> MessageChain: @@ -22,20 +22,36 @@ class MessageEvent(Event): @overrides(Event) def get_user_id(self) -> str: - return str(self.sender.id) + raise NotImplementedError @overrides(Event) def get_session_id(self) -> str: - return self.get_user_id() + raise NotImplementedError class GroupMessage(MessageEvent): - pass + sender: SenderInfo + + @overrides(MessageEvent) + def get_session_id(self) -> str: + return f'group_{self.sender.group.id}_' + self.get_user_id() class FriendMessage(MessageEvent): sender: PrivateSenderInfo + @overrides(MessageEvent) + def get_user_id(self) -> str: + return str(self.sender.id) + + @overrides + def get_session_id(self) -> str: + return 'friend_' + self.get_user_id() + class TempMessage(MessageEvent): - pass + sender: SenderInfo + + @overrides + def get_session_id(self) -> str: + return f'temp_{self.sender.group.id}_' + self.get_user_id() From 73be9151b0c265ca0afc15333f4baa5bc1e2932f Mon Sep 17 00:00:00 2001 From: Mix Date: Sat, 30 Jan 2021 21:51:51 +0800 Subject: [PATCH 11/37] :children_crossing: add factory classmethods in MessageSegment at mirai adapter --- nonebot/adapters/mirai/bot.py | 50 ++++++++++---- nonebot/adapters/mirai/event/message.py | 2 +- nonebot/adapters/mirai/message.py | 86 +++++++++++++++++++++++-- 3 files changed, 118 insertions(+), 20 deletions(-) diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py index e89eb245..2414dca8 100644 --- a/nonebot/adapters/mirai/bot.py +++ b/nonebot/adapters/mirai/bot.py @@ -10,6 +10,7 @@ from nonebot.adapters import Event as BaseEvent from nonebot.config import Config from nonebot.drivers import Driver, WebSocket from nonebot.exception import RequestDenied +from nonebot.exception import ActionFailed as BaseActionFailed from nonebot.log import logger from nonebot.message import handle_event from nonebot.typing import overrides @@ -19,6 +20,17 @@ from .event import Event, FriendMessage, GroupMessage, TempMessage from .message import MessageChain, MessageSegment +class ActionFailed(BaseActionFailed): + + def __init__(self, code: int, message: str = ''): + super().__init__('mirai') + self.code = code + self.message = message + + def __repr__(self): + return f"{self.__class__.__name__}(code={self.code}, message={self.message!r})" + + class SessionManager: sessions: Dict[int, Tuple[str, datetime, httpx.AsyncClient]] = {} session_expiry: timedelta = timedelta(minutes=15) @@ -26,14 +38,22 @@ class SessionManager: def __init__(self, session_key: str, client: httpx.AsyncClient): self.session_key, self.client = session_key, client + @staticmethod + def _raise_code(data: Dict[str, Any]) -> Dict[str, Any]: + code = data.get('code', 0) + logger.debug(f'Mirai API returned data: {data}') + if code != 0: + raise ActionFailed(code, message=data['msg']) + return data + async def post(self, path: str, *, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: params = {**(params or {}), 'sessionKey': self.session_key} - response = await self.client.post(path, json=params) + response = await self.client.post(path, json=params, timeout=3) response.raise_for_status() - return response.json() + return self._raise_code(response.json()) async def request(self, path: str, @@ -44,9 +64,10 @@ class SessionManager: params={ **(params or {}), 'sessionKey': self.session_key - }) + }, + timeout=3) response.raise_for_status() - return response.json() + return self._raise_code(response.json()) async def upload(self, path: str, *, type: str, file: Tuple[str, BytesIO]) -> Dict[str, Any]: @@ -59,7 +80,7 @@ class SessionManager: files={file_type: file_io}, timeout=6) response.raise_for_status() - return response.json() + return self._raise_code(response.json()) @classmethod async def new(cls, self_id: int, *, host: IPv4Address, port: int, @@ -152,7 +173,7 @@ class MiraiBot(BaseBot): raise NotImplementedError @overrides(BaseBot) - async def __getattr__(self, key: str) -> NoReturn: + def __getattr__(self, key: str) -> NoReturn: raise NotImplementedError @overrides(BaseBot) @@ -165,8 +186,10 @@ class MiraiBot(BaseBot): return await self.send_friend_message(target=event.sender.id, message_chain=message) elif isinstance(event, GroupMessage): - return await self.send_group_message(target=event.sender.group.id, - message_chain=message) + return await self.send_group_message( + group=event.sender.group.id, + message_chain=message if not at_sender else + (MessageSegment.at(target=event.sender.id) + message)) elif isinstance(event, TempMessage): return await self.send_temp_message(qq=event.sender.id, group=event.sender.group.id, @@ -191,12 +214,15 @@ class MiraiBot(BaseBot): 'messageChain': message_chain.export() }) - async def send_group_message(self, target: int, - message_chain: MessageChain): + async def send_group_message(self, + group: int, + message_chain: MessageChain, + quote: Optional[int] = None): return await self.api.post('sendGroupMessage', params={ - 'target': target, - 'messageChain': message_chain.export() + 'group': group, + 'messageChain': message_chain.export(), + 'quote': quote }) async def recall(self, target: int): diff --git a/nonebot/adapters/mirai/event/message.py b/nonebot/adapters/mirai/event/message.py index 1cfca586..10574d5e 100644 --- a/nonebot/adapters/mirai/event/message.py +++ b/nonebot/adapters/mirai/event/message.py @@ -18,7 +18,7 @@ class MessageEvent(Event): @overrides(Event) def get_plaintext(self) -> str: - return self.message_chain.__str__() + return self.message_chain.extract_plain_text() @overrides(Event) def get_user_id(self) -> str: diff --git a/nonebot/adapters/mirai/message.py b/nonebot/adapters/mirai/message.py index 7562b6be..ef3949a6 100644 --- a/nonebot/adapters/mirai/message.py +++ b/nonebot/adapters/mirai/message.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Any, Dict, Iterable, List, Union +from typing import Any, Dict, Iterable, List, Optional, Union from pydantic import validate_arguments @@ -31,7 +31,8 @@ class MessageSegment(BaseMessageSegment): @overrides(BaseMessageSegment) @validate_arguments def __init__(self, type: MessageType, **data): - super().__init__(type=type, data=data) + super().__init__(type=type, + data={k: v for k, v in data.items() if v is not None}) @overrides(BaseMessageSegment) def __str__(self) -> str: @@ -60,6 +61,79 @@ class MessageSegment(BaseMessageSegment): def as_dict(self) -> Dict[str, Any]: return {'type': self.type.value, **self.data} + @classmethod + def source(cls, id: int, time: int): + return cls(type=MessageType.SOURCE, id=id, time=time) + + @classmethod + def quote(cls, id: int, group_id: int, sender_id: int, target_id: int, + origin: "MessageChain"): + return cls(type=MessageType.QUOTE, + id=id, + groupId=group_id, + senderId=sender_id, + targetId=target_id, + origin=origin.export()) + + @classmethod + def at(cls, target: int): + return cls(type=MessageType.AT, target=target) + + @classmethod + def at_all(cls): + return cls(type=MessageType.AT_ALL) + + @classmethod + def face(cls, face_id: Optional[int] = None, name: Optional[str] = None): + return cls(type=MessageType.FACE, faceId=face_id, name=name) + + @classmethod + def plain(cls, text: str): + return cls(type=MessageType.PLAIN, text=text) + + @classmethod + def image(cls, + image_id: Optional[str] = None, + url: Optional[str] = None, + path: Optional[str] = None): + return cls(type=MessageType.IMAGE, imageId=image_id, url=url, path=path) + + @classmethod + def flash_image(cls, + image_id: Optional[str] = None, + url: Optional[str] = None, + path: Optional[str] = None): + return cls(type=MessageType.FLASH_IMAGE, + imageId=image_id, + url=url, + path=path) + + @classmethod + def voice(cls, + voice_id: Optional[str] = None, + url: Optional[str] = None, + path: Optional[str] = None): + return cls(type=MessageType.FLASH_IMAGE, + imageId=voice_id, + url=url, + path=path) + + @classmethod + def xml(cls, xml: str): + return cls(type=MessageType.XML, xml=xml) + + @classmethod + def json(cls, json: str): + return cls(type=MessageType.JSON, json=json) + + @classmethod + def app(cls, content: str): + return cls(type=MessageType.APP, content=content) + + @classmethod + def poke(cls, name: str): + return cls(type=MessageType.POKE, name=name) + class MessageChain(BaseMessage): @@ -90,11 +164,9 @@ class MessageChain(BaseMessage): ] 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 + return [ + *map(lambda segment: segment.as_dict(), self.copy()) # type: ignore + ] def __repr__(self) -> str: return f'<{self.__class__.__name__} {[*self.copy()]}>' From 3f56da9245da9d53ab1155511c7e426400709806 Mon Sep 17 00:00:00 2001 From: Mix Date: Sun, 31 Jan 2021 16:02:59 +0800 Subject: [PATCH 12/37] :construction: add support of reverse post and forward ws for mirai adapter --- nonebot/adapters/mirai/__init__.py | 1 + nonebot/adapters/mirai/bot.py | 38 ++++++----- nonebot/adapters/mirai/bot_ws.py | 94 ++++++++-------------------- nonebot/adapters/mirai/event/base.py | 2 +- nonebot/adapters/mirai/message.py | 12 ++-- 5 files changed, 57 insertions(+), 90 deletions(-) diff --git a/nonebot/adapters/mirai/__init__.py b/nonebot/adapters/mirai/__init__.py index 991f30fd..1107af38 100644 --- a/nonebot/adapters/mirai/__init__.py +++ b/nonebot/adapters/mirai/__init__.py @@ -1,3 +1,4 @@ from .bot import MiraiBot +from .bot_ws import MiraiWebsocketBot from .event import * from .message import MessageChain, MessageSegment diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py index 2414dca8..ebb9b768 100644 --- a/nonebot/adapters/mirai/bot.py +++ b/nonebot/adapters/mirai/bot.py @@ -1,19 +1,19 @@ from datetime import datetime, timedelta from io import BytesIO from ipaddress import IPv4Address -from typing import Any, Dict, List, NoReturn, Optional, Tuple +from typing import Any, Dict, List, NoReturn, Optional, Tuple, Union import httpx from nonebot.adapters import Bot as BaseBot -from nonebot.adapters import Event as BaseEvent from nonebot.config import Config from nonebot.drivers import Driver, WebSocket -from nonebot.exception import RequestDenied from nonebot.exception import ActionFailed as BaseActionFailed +from nonebot.exception import RequestDenied from nonebot.log import logger from nonebot.message import handle_event from nonebot.typing import overrides +from nonebot.utils import escape_tag from .config import Config as MiraiConfig from .event import Event, FriendMessage, GroupMessage, TempMessage @@ -41,7 +41,8 @@ class SessionManager: @staticmethod def _raise_code(data: Dict[str, Any]) -> Dict[str, Any]: code = data.get('code', 0) - logger.debug(f'Mirai API returned data: {data}') + logger.opt(colors=True).debug('Mirai API returned data: ' + f'{escape_tag(str(data))}') if code != 0: raise ActionFailed(code, message=data['msg']) return data @@ -85,10 +86,10 @@ class SessionManager: @classmethod async def new(cls, self_id: int, *, host: IPv4Address, port: int, auth_key: str): - if self_id in cls.sessions: - manager = cls.get(self_id) - if manager is not None: - return manager + session = cls.get(self_id) + if session is not None: + return session + client = httpx.AsyncClient(base_url=f'http://{host}:{port}') response = await client.post('/auth', json={'authKey': auth_key}) response.raise_for_status() @@ -102,10 +103,13 @@ class SessionManager: }) assert response.json()['code'] == 0 cls.sessions[self_id] = session_key, datetime.now(), client + return cls(session_key, client) @classmethod def get(cls, self_id: int): + if self_id not in cls.sessions: + return None key, time, client = cls.sessions[self_id] if datetime.now() - time > cls.session_expiry: return None @@ -114,6 +118,7 @@ class SessionManager: class MiraiBot(BaseBot): + @overrides(BaseBot) def __init__(self, connection_type: str, self_id: str, @@ -179,17 +184,20 @@ class MiraiBot(BaseBot): @overrides(BaseBot) async def send(self, event: Event, - message: MessageChain, - at_sender: bool = False, - **kwargs): + message: Union[MessageChain, MessageSegment, str], + at_sender: bool = False): + if isinstance(message, MessageSegment): + message = MessageChain(message) + elif isinstance(message, str): + message = MessageChain(MessageSegment.plain(message)) if isinstance(event, FriendMessage): return await self.send_friend_message(target=event.sender.id, message_chain=message) elif isinstance(event, GroupMessage): - return await self.send_group_message( - group=event.sender.group.id, - message_chain=message if not at_sender else - (MessageSegment.at(target=event.sender.id) + message)) + if at_sender: + message = MessageSegment.at(event.sender.id) + message + return await self.send_group_message(group=event.sender.group.id, + message_chain=message) elif isinstance(event, TempMessage): return await self.send_temp_message(qq=event.sender.id, group=event.sender.group.id, diff --git a/nonebot/adapters/mirai/bot_ws.py b/nonebot/adapters/mirai/bot_ws.py index d9803c47..d20d81dd 100644 --- a/nonebot/adapters/mirai/bot_ws.py +++ b/nonebot/adapters/mirai/bot_ws.py @@ -7,50 +7,21 @@ from typing import (Any, Callable, Coroutine, Dict, NoReturn, Optional, Set, import httpx import websockets -from nonebot.adapters import Bot as BaseBot -from nonebot.adapters import Event as BaseEvent from nonebot.config import Config from nonebot.drivers import Driver from nonebot.drivers import WebSocket as BaseWebSocket from nonebot.exception import RequestDenied from nonebot.log import logger -from nonebot.message import handle_event from nonebot.typing import overrides +from .bot import MiraiBot, SessionManager from .config import Config as MiraiConfig -from .event import Event WebsocketHandlerFunction = Callable[[Dict[str, Any]], Coroutine[Any, Any, None]] WebsocketHandler_T = TypeVar('WebsocketHandler_T', bound=WebsocketHandlerFunction) -async def _ws_authorization(client: httpx.AsyncClient, *, auth_key: str, - qq: int) -> str: - - async def request(method: str, *, path: str, **kwargs) -> Dict[str, Any]: - response = await client.request(method, path, **kwargs) - response.raise_for_status() - return response.json() - - about = await request('GET', path='/about') - logger.opt(colors=True).debug('Mirai API HTTP backend version: ' - f'{about["data"]["version"]}') - - status = await request('POST', path='/auth', json={'authKey': auth_key}) - assert status['code'] == 0 - session_key = status['session'] - - verify = await request('POST', - path='/verify', - json={ - 'sessionKey': session_key, - 'qq': qq - }) - assert verify['code'] == 0, verify['msg'] - return session_key - - class WebSocket(BaseWebSocket): @classmethod @@ -59,6 +30,7 @@ class WebSocket(BaseWebSocket): listen_address = httpx.URL(f'ws://{host}:{port}/all', params={'sessionKey': session_key}) websocket = await websockets.connect(uri=str(listen_address)) + await (await websocket.ping()) return cls(websocket) @overrides(BaseWebSocket) @@ -116,25 +88,24 @@ class WebSocket(BaseWebSocket): return callable -class MiraiWebsocketBot(BaseBot): +class MiraiWebsocketBot(MiraiBot): + @overrides(MiraiBot) def __init__(self, connection_type: str, self_id: str, *, websocket: WebSocket): super().__init__(connection_type, self_id, websocket=websocket) - websocket.handle(self.handle_message) - self.driver._bot_connect(self) @property - @overrides(BaseBot) + @overrides(MiraiBot) def type(self) -> str: - return "mirai" + return "mirai-ws" @property def alive(self) -> bool: return not self.websocket.closed @classmethod - @overrides(BaseBot) + @overrides(MiraiBot) async def check_permission(cls, driver: "Driver", connection_type: str, headers: dict, body: Optional[dict]) -> NoReturn: raise RequestDenied( @@ -142,7 +113,7 @@ class MiraiWebsocketBot(BaseBot): reason=f'Connection {connection_type} not implented') @classmethod - @overrides(BaseBot) + @overrides(MiraiBot) def register(cls, driver: "Driver", config: "Config", qq: int): cls.mirai_config = MiraiConfig(**config.dict()) cls.active = True @@ -152,32 +123,33 @@ class MiraiWebsocketBot(BaseBot): super().register(driver, config) async def _bot_connection(): - async with httpx.AsyncClient( - base_url= - f'http://{cls.mirai_config.host}:{cls.mirai_config.port}' - ) as client: - session_key = await _ws_authorization( - client, - auth_key=cls.mirai_config.auth_key, # type: ignore - qq=qq) # type: ignore - + session: SessionManager = await SessionManager.new( + qq, + host=cls.mirai_config.host, # type: ignore + port=cls.mirai_config.port, # type: ignore + auth_key=cls.mirai_config.auth_key # type: ignore + ) websocket = await WebSocket.new( host=cls.mirai_config.host, # type: ignore port=cls.mirai_config.port, # type: ignore - session_key=session_key) + session_key=session.session_key) bot = cls(connection_type='forward_ws', self_id=str(qq), websocket=websocket) websocket.handle(bot.handle_message) - driver._clients[str(qq)] = bot await websocket.accept() + return bot async def _connection_ensure(): - if str(qq) not in driver._clients: - await _bot_connection() - elif not driver._clients[str(qq)].alive: - driver._clients.pop(str(qq), None) - await _bot_connection() + self_id = str(qq) + if self_id not in driver._clients: + bot = await _bot_connection() + driver._bot_connect(bot) + else: + bot = driver._clients[self_id] + if not bot.alive: + driver._bot_disconnect(bot) + return @driver.on_startup async def _startup(): @@ -202,19 +174,3 @@ class MiraiWebsocketBot(BaseBot): if bot is None: return await bot.websocket.close() #type:ignore - - @overrides(BaseBot) - async def handle_message(self, message: dict): - 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: "BaseEvent", message: str, **kwargs): - return super().send(event, message, **kwargs) - - def __del__(self): - self.driver._bot_disconnect(self) diff --git a/nonebot/adapters/mirai/event/base.py b/nonebot/adapters/mirai/event/base.py index 6fbb30ff..3b6916f5 100644 --- a/nonebot/adapters/mirai/event/base.py +++ b/nonebot/adapters/mirai/event/base.py @@ -86,7 +86,7 @@ class Event(BaseEvent): @overrides(BaseEvent) def get_event_description(self) -> str: - return str(self.dict()) + return str(self.normalize_dict()) @overrides(BaseEvent) def get_message(self) -> BaseMessage: diff --git a/nonebot/adapters/mirai/message.py b/nonebot/adapters/mirai/message.py index ef3949a6..a577a807 100644 --- a/nonebot/adapters/mirai/message.py +++ b/nonebot/adapters/mirai/message.py @@ -135,10 +135,11 @@ class MessageSegment(BaseMessageSegment): return cls(type=MessageType.POKE, name=name) -class MessageChain(BaseMessage): +class MessageChain(BaseMessage): #type:List[MessageSegment] @overrides(BaseMessage) - def __init__(self, message: Union[List[Dict[str, Any]], MessageSegment], + def __init__(self, message: Union[List[Dict[str, Any]], + Iterable[MessageSegment], MessageSegment], **kwargs): super().__init__(**kwargs) if isinstance(message, MessageSegment): @@ -152,15 +153,16 @@ class MessageChain(BaseMessage): @overrides(BaseMessage) def _construct( - self, message: Iterable[Union[Dict[str, Any], MessageSegment]] + self, message: Union[List[Dict[str, Any]], Iterable[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) + lambda x: x + if isinstance(x, MessageSegment) else MessageSegment(**x), + message) ] def export(self) -> List[Dict[str, Any]]: From 20b299c75802aefbf7198a80631a1fe7a1f9de9e Mon Sep 17 00:00:00 2001 From: Mix Date: Sun, 31 Jan 2021 17:01:04 +0800 Subject: [PATCH 13/37] :children_crossing: add .approve and .reject method for request event in mirai adapter --- nonebot/adapters/mirai/event/request.py | 71 +++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/nonebot/adapters/mirai/event/request.py b/nonebot/adapters/mirai/event/request.py index 44a70c17..27fea4d8 100644 --- a/nonebot/adapters/mirai/event/request.py +++ b/nonebot/adapters/mirai/event/request.py @@ -1,7 +1,13 @@ +from typing import TYPE_CHECKING +from typing_extensions import Literal + from pydantic import Field from .base import Event +if TYPE_CHECKING: + from ..bot import MiraiBot as Bot + class RequestEvent(Event): event_id: int = Field(alias='eventId') @@ -13,14 +19,79 @@ class NewFriendRequestEvent(RequestEvent): from_id: int = Field(alias='fromId') group_id: int = Field(0, alias='groupId') + async def approve(self, bot: "Bot"): + return await bot.api.post('/resp/newFriendRequestEvent', + params={ + 'eventId': self.event_id, + 'groupId': self.group_id, + 'fromId': self.from_id, + 'operate': 0 + }) + + async def reject(self, + bot: "Bot", + operate: Literal[1, 2] = 1, + message: str = ''): + assert operate > 0 + return await bot.api.post('/resp/newFriendRequestEvent', + params={ + 'eventId': self.event_id, + 'groupId': self.group_id, + 'fromId': self.from_id, + 'operate': operate, + 'message': message + }) + class MemberJoinRequestEvent(RequestEvent): from_id: int = Field(alias='fromId') group_id: int = Field(alias='groupId') group_name: str = Field(alias='groupName') + async def approve(self, bot: "Bot"): + return await bot.api.post('/resp/memberJoinRequestEvent', + params={ + 'eventId': self.event_id, + 'groupId': self.group_id, + 'fromId': self.from_id, + 'operate': 0 + }) + + async def reject(self, + bot: "Bot", + operate: Literal[1, 2, 3, 4] = 1, + message: str = ''): + assert operate > 0 + return await bot.api.post('/resp/memberJoinRequestEvent', + params={ + 'eventId': self.event_id, + 'groupId': self.group_id, + 'fromId': self.from_id, + 'operate': operate, + 'message': message + }) + class BotInvitedJoinGroupRequestEvent(RequestEvent): from_id: int = Field(alias='fromId') group_id: int = Field(alias='groupId') group_name: str = Field(alias='groupName') + + async def approve(self, bot: "Bot"): + return await bot.api.post('/resp/botInvitedJoinGroupRequestEvent', + params={ + 'eventId': self.event_id, + 'groupId': self.group_id, + 'fromId': self.from_id, + 'operate': 0 + }) + + async def reject(self, bot: "Bot", message: str = ""): + return await bot.api.post('/resp/botInvitedJoinGroupRequestEvent', + params={ + 'eventId': self.event_id, + 'groupId': self.group_id, + 'fromId': self.from_id, + 'operate': 1, + 'message': message + }) From 7b04854b43b9de63ce9929d463fc5b31be666318 Mon Sep 17 00:00:00 2001 From: nonebot Date: Sun, 31 Jan 2021 09:56:46 +0000 Subject: [PATCH 14/37] :memo: update api docs --- docs/api/drivers/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/drivers/README.md b/docs/api/drivers/README.md index 77485ed2..673697b4 100644 --- a/docs/api/drivers/README.md +++ b/docs/api/drivers/README.md @@ -120,7 +120,7 @@ Driver 基类。将后端框架封装,以满足适配器使用。 -### `register_adapter(name, adapter)` +### `register_adapter(name, adapter, **kwargs)` * **说明** From a39785d6d9d27284091e5ff3ba4351a3cdb26c21 Mon Sep 17 00:00:00 2001 From: Mix Date: Sun, 31 Jan 2021 18:00:32 +0800 Subject: [PATCH 15/37] :bulb: add some comments in mirai adapter --- nonebot/adapters/mirai/bot.py | 92 +++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py index ebb9b768..27e91066 100644 --- a/nonebot/adapters/mirai/bot.py +++ b/nonebot/adapters/mirai/bot.py @@ -72,6 +72,7 @@ class SessionManager: async def upload(self, path: str, *, type: str, file: Tuple[str, BytesIO]) -> Dict[str, Any]: + file_type, file_io = file response = await self.client.post(path, data={ @@ -186,6 +187,21 @@ class MiraiBot(BaseBot): event: Event, message: Union[MessageChain, MessageSegment, str], at_sender: bool = False): + """ + :说明: + + 根据 ``event`` 向触发事件的主题发送信息 + + :参数: + + * ``event: Event``: Event对象 + * ``message: Union[MessageChain, MessageSegment, str]``: 要发送的消息 + * ``at_sender: bool``: 是否 @ 事件主题 + + :返回: + + - ``Any``: API 调用返回数据 + """ if isinstance(message, MessageSegment): message = MessageChain(message) elif isinstance(message, str): @@ -207,6 +223,20 @@ class MiraiBot(BaseBot): async def send_friend_message(self, target: int, message_chain: MessageChain): + """ + :说明: + + 使用此方法向指定好友发送消息 + + :参数: + + * ``target: int``: 发送消息目标好友的 QQ 号 + * ``message_chain: MessageChain``: 消息链,是一个消息对象构成的数组 + + :返回: + + - ``Any``: API 调用返回数据 + """ return await self.api.post('sendFriendMessage', params={ 'target': target, @@ -215,6 +245,21 @@ class MiraiBot(BaseBot): async def send_temp_message(self, qq: int, group: int, message_chain: MessageChain): + """ + :说明: + + 使用此方法向临时会话对象发送消息 + + :参数: + + * ``qq: int``: 临时会话对象 QQ 号 + * ``group: int``: 临时会话群号 + * ``message_chain: MessageChain``: 消息链,是一个消息对象构成的数组 + + :返回: + + - ``Any``: API 调用返回数据 + """ return await self.api.post('sendTempMessage', params={ 'qq': qq, @@ -226,6 +271,21 @@ class MiraiBot(BaseBot): group: int, message_chain: MessageChain, quote: Optional[int] = None): + """ + :说明: + + 使用此方法向指定群发送消息 + + :参数: + + * ``group: int``: 发送消息目标群的群号 + * ``message_chain: MessageChain``: 消息链,是一个消息对象构成的数组 + * ``quote: Optional[int]``: 引用一条消息的 message_id 进行回复 + + :返回: + + - ``Any``: API 调用返回数据 + """ return await self.api.post('sendGroupMessage', params={ 'group': group, @@ -234,10 +294,42 @@ class MiraiBot(BaseBot): }) async def recall(self, target: int): + """ + :说明: + + 使用此方法撤回指定消息。对于bot发送的消息,有2分钟时间限制。对于撤回群聊中群员的消息,需要有相应权限 + + :参数: + + * ``target: int``: 需要撤回的消息的message_id + + :返回: + + - ``Any``: API 调用返回数据 + """ return await self.api.post('recall', params={'target': target}) async def send_image_message(self, target: int, qq: int, group: int, urls: List[str]): + """ + :说明: + + 使用此方法向指定对象(群或好友)发送图片消息 + 除非需要通过此手段获取image_id,否则不推荐使用该接口 + + > 当qq和group同时存在时,表示发送临时会话图片,qq为临时会话对象QQ号,group为临时会话发起的群号 + + :参数: + + * ``target: int``: [description] + * ``qq: int``: [description] + * ``group: int``: [description] + * ``urls: List[str]``: [description] + + :返回: + + - ``[type]``: [description] + """ return await self.api.post('sendImageMessage', params={ 'target': target, From 858639bebe592c15fa98ccbbc698ce717e6f0395 Mon Sep 17 00:00:00 2001 From: Mix Date: Sun, 31 Jan 2021 22:43:43 +0800 Subject: [PATCH 16/37] :bulb: :memo: add some comments in code, add document build struct for mirai adapter --- docs_build/README.rst | 1 + docs_build/adapters/mirai.rst | 72 +++++++ nonebot/adapters/mirai/bot.py | 276 ++++++++++++++++++++++-- nonebot/adapters/mirai/event/request.py | 69 ++++++ 4 files changed, 400 insertions(+), 18 deletions(-) create mode 100644 docs_build/adapters/mirai.rst diff --git a/docs_build/README.rst b/docs_build/README.rst index 95ffcc2d..4a273041 100644 --- a/docs_build/README.rst +++ b/docs_build/README.rst @@ -18,3 +18,4 @@ NoneBot Api Reference - `nonebot.adapters `_ - `nonebot.adapters.cqhttp `_ - `nonebot.adapters.ding `_ + - `nonebot.adapters.mirai `_ diff --git a/docs_build/adapters/mirai.rst b/docs_build/adapters/mirai.rst new file mode 100644 index 00000000..1f6e3eaf --- /dev/null +++ b/docs_build/adapters/mirai.rst @@ -0,0 +1,72 @@ +--- +contentSidebar: true +sidebarDepth: 0 +--- + +NoneBot.adapters.mirai 模块 +=========================== + +.. automodule:: nonebot.adapters.mirai + +NoneBot.adapters.mirai.bot 模块 +===================================== + +.. automodule:: nonebot.adapters.mirai.bot + :members: + :show-inheritance: + +NoneBot.adapters.mirai.bot_ws 模块 +===================================== + +.. automodule:: nonebot.adapters.mirai.bot_ws + :members: + :show-inheritance: + +NoneBot.adapters.mirai.config 模块 +===================================== + +.. automodule:: nonebot.adapters.mirai.config + :members: + :show-inheritance: + +NoneBot.adapters.mirai.message 模块 +==================================== + +.. automodule:: nonebot.adapters.mirai.message + :members: + :show-inheritance: + +NoneBot.adapters.mirai.event 模块 +==================================== + +.. automodule:: nonebot.adapters.mirai.event + :members: + :show-inheritance: + +NoneBot.adapters.mirai.event.base 模块 +==================================== + +.. automodule:: nonebot.adapters.mirai.event.base + :members: + :show-inheritance: + +NoneBot.adapters.mirai.event.message 模块 +==================================== + +.. automodule:: nonebot.adapters.mirai.event.message + :members: + :show-inheritance: + +NoneBot.adapters.mirai.event.notice 模块 +==================================== + +.. automodule:: nonebot.adapters.mirai.event.notice + :members: + :show-inheritance: + +NoneBot.adapters.mirai.event.request 模块 +==================================== + +.. automodule:: nonebot.adapters.mirai.event.request + :members: + :show-inheritance: \ No newline at end of file diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py index 27e91066..81c7f7b7 100644 --- a/nonebot/adapters/mirai/bot.py +++ b/nonebot/adapters/mirai/bot.py @@ -22,13 +22,13 @@ from .message import MessageChain, MessageSegment class ActionFailed(BaseActionFailed): - def __init__(self, code: int, message: str = ''): + def __init__(self, **kwargs): super().__init__('mirai') - self.code = code - self.message = message + self.data = kwargs.copy() def __repr__(self): - return f"{self.__class__.__name__}(code={self.code}, message={self.message!r})" + return self.__class__.__name__ + '(%s)' % ', '.join( + map(lambda m: '%s=%r' % m, self.data.items())) class SessionManager: @@ -44,7 +44,7 @@ class SessionManager: logger.opt(colors=True).debug('Mirai API returned data: ' f'{escape_tag(str(data))}') if code != 0: - raise ActionFailed(code, message=data['msg']) + raise ActionFailed(**data) return data async def post(self, @@ -310,7 +310,7 @@ class MiraiBot(BaseBot): return await self.api.post('recall', params={'target': target}) async def send_image_message(self, target: int, qq: int, group: int, - urls: List[str]): + urls: List[str]) -> List[str]: """ :说明: @@ -321,14 +321,14 @@ class MiraiBot(BaseBot): :参数: - * ``target: int``: [description] - * ``qq: int``: [description] - * ``group: int``: [description] - * ``urls: List[str]``: [description] + * ``target: int``: 发送对象的QQ号或群号,可能存在歧义 + * ``qq: int``: 发送对象的QQ号 + * ``group: int``: 发送对象的群号 + * ``urls: List[str]``: 是一个url字符串构成的数组 :返回: - - ``[type]``: [description] + - ``List[str]``: 一个包含图片imageId的数组 """ return await self.api.post('sendImageMessage', params={ @@ -336,48 +336,174 @@ class MiraiBot(BaseBot): 'qq': qq, 'group': group, 'urls': urls - }) + }) # type: ignore async def upload_image(self, type: str, img: BytesIO): + """ + :说明: + + 使用此方法上传图片文件至服务器并返回Image_id + + :参数: + + * ``type: str``: "friend" 或 "group" 或 "temp" + * ``img: BytesIO``: 图片的BytesIO对象 + + :返回: + + - ``Any``: API 调用返回数据 + """ return await self.api.upload('uploadImage', type=type, file=('img', img)) async def upload_voice(self, type: str, voice: BytesIO): + """ + :说明: + + 使用此方法上传语音文件至服务器并返回voice_id + + :参数: + + * ``type: str``: 当前仅支持 "group" + * ``voice: BytesIO``: 语音的BytesIO对象 + + :返回: + + - ``Any``: API 调用返回数据 + """ return await self.api.upload('uploadVoice', type=type, file=('voice', voice)) - async def fetch_message(self): - return await self.api.request('fetchMessage') + async def fetch_message(self, count: int = 10): + """ + :说明: - async def fetch_latest_message(self): - return await self.api.request('fetchLatestMessage') + 使用此方法获取bot接收到的最老消息和最老各类事件 + (会从MiraiApiHttp消息记录中删除) - async def peek_message(self, count: int): + :参数: + + * ``count: int``: 获取消息和事件的数量 + """ + return await self.api.request('fetchMessage', params={'count': count}) + + async def fetch_latest_message(self, count: int = 10): + """ + :说明: + + 使用此方法获取bot接收到的最新消息和最新各类事件 + (会从MiraiApiHttp消息记录中删除) + + :参数: + + * ``count: int``: 获取消息和事件的数量 + """ + return await self.api.request('fetchLatestMessage', + params={'count': count}) + + async def peek_message(self, count: int = 10): + """ + :说明: + + 使用此方法获取bot接收到的最老消息和最老各类事件 + (不会从MiraiApiHttp消息记录中删除) + + :参数: + + * ``count: int``: 获取消息和事件的数量 + """ return await self.api.request('peekMessage', params={'count': count}) - async def peek_latest_message(self, count: int): + async def peek_latest_message(self, count: int = 10): + """ + :说明: + + 使用此方法获取bot接收到的最新消息和最新各类事件 + (不会从MiraiApiHttp消息记录中删除) + + :参数: + + * ``count: int``: 获取消息和事件的数量 + """ return await self.api.request('peekLatestMessage', params={'count': count}) async def messsage_from_id(self, id: int): + """ + :说明: + + 通过messageId获取一条被缓存的消息 + 使用此方法获取bot接收到的消息和各类事件 + + :参数: + + * ``id: int``: 获取消息的message_id + """ return await self.api.request('messageFromId', params={'id': id}) async def count_message(self): + """ + :说明: + + 使用此方法获取bot接收并缓存的消息总数,注意不包含被删除的 + """ return await self.api.request('countMessage') async def friend_list(self) -> List[Dict[str, Any]]: + """ + :说明: + + 使用此方法获取bot的好友列表 + + :返回: + + - ``List[Dict[str, Any]]``: 返回的好友列表数据 + """ return await self.api.request('friendList') # type: ignore async def group_list(self) -> List[Dict[str, Any]]: + """ + :说明: + + 使用此方法获取bot的群列表 + + :返回: + + - ``List[Dict[str, Any]]``: 返回的群列表数据 + """ return await self.api.request('groupList') # type: ignore async def member_list(self, target: int) -> List[Dict[str, Any]]: + """ + :说明: + + 使用此方法获取bot指定群种的成员列表 + + :参数: + + * ``target: int``: 指定群的群号 + + :返回: + + - ``List[Dict[str, Any]]``: 返回的群成员列表数据 + """ return await self.api.request('memberList', params={'target': target}) # type: ignore async def mute(self, target: int, member_id: int, time: int): + """ + :说明: + + 使用此方法指定群禁言指定群员(需要有相关权限) + + :参数: + + * ``target: int``: 指定群的群号 + * ``member_id: int``: 指定群员QQ号 + * ``time: int``: 禁言时长,单位为秒,最多30天 + """ return await self.api.post('mute', params={ 'target': target, @@ -386,6 +512,16 @@ class MiraiBot(BaseBot): }) async def unmute(self, target: int, member_id: int): + """ + :说明: + + 使用此方法指定群解除群成员禁言(需要有相关权限) + + :参数: + + * ``target: int``: 指定群的群号 + * ``member_id: int``: 指定群员QQ号 + """ return await self.api.post('unmute', params={ 'target': target, @@ -393,6 +529,17 @@ class MiraiBot(BaseBot): }) async def kick(self, target: int, member_id: int, msg: str): + """ + :说明: + + 使用此方法移除指定群成员(需要有相关权限) + + :参数: + + * ``target: int``: 指定群的群号 + * ``member_id: int``: 指定群员QQ号 + * ``msg: str``: 信息 + """ return await self.api.post('kick', params={ 'target': target, @@ -401,18 +548,77 @@ class MiraiBot(BaseBot): }) async def quit(self, target: int): + """ + :说明: + + 使用此方法使Bot退出群聊 + + :参数: + + * ``target: int``: 退出的群号 + """ return await self.api.post('quit', params={'target': target}) async def mute_all(self, target: int): + """ + :说明: + + 使用此方法令指定群进行全体禁言(需要有相关权限) + + :参数: + + * ``target: int``: 指定群的群号 + """ return await self.api.post('muteAll', params={'target': target}) async def unmute_all(self, target: int): + """ + :说明: + + 使用此方法令指定群解除全体禁言(需要有相关权限) + + :参数: + + * ``target: int``: 指定群的群号 + """ return await self.api.post('unmuteAll', params={'target': target}) async def group_config(self, target: int): + """ + :说明: + + 使用此方法获取群设置 + + :参数: + + * ``target: int``: 指定群的群号 + + :返回: + + .. code-block:: json + + { + "name": "群名称", + "announcement": "群公告", + "confessTalk": true, + "allowMemberInvite": true, + "autoApprove": true, + "anonymousChat": true + } + """ return await self.api.request('groupConfig', params={'target': target}) async def modify_group_config(self, target: int, config: Dict[str, Any]): + """ + :说明: + + 使用此方法修改群设置(需要有相关权限) + + :参数: + + * ``target: int``: 指定群的群号 + * ``config: Dict[str, Any]``: 群设置, 格式见 ``group_config`` 的返回值 + """ return await self.api.post('groupConfig', params={ 'target': target, @@ -420,6 +626,25 @@ class MiraiBot(BaseBot): }) async def member_info(self, target: int, member_id: int): + """ + :说明: + + 使用此方法获取群员资料 + + :参数: + + * ``target: int``: 指定群的群号 + * ``member_id: int``: 群员QQ号 + + :返回: + + .. code-block:: json + + { + "name": "群名片", + "specialTitle": "群头衔" + } + """ return await self.api.request('memberInfo', params={ 'target': target, @@ -428,6 +653,21 @@ class MiraiBot(BaseBot): async def modify_member_info(self, target: int, member_id: int, info: Dict[str, Any]): + """ + :说明: + + 使用此方法修改群员资料(需要有相关权限) + + :参数: + + * ``target: int``: 指定群的群号 + * ``member_id: int``: 群员QQ号 + * ``info: Dict[str, Any]``: 群员资料, 格式见 ``member_info`` 的返回值 + + :返回: + + - ``[type]``: [description] + """ return await self.api.post('memberInfo', params={ 'target': target, diff --git a/nonebot/adapters/mirai/event/request.py b/nonebot/adapters/mirai/event/request.py index 27fea4d8..39c64298 100644 --- a/nonebot/adapters/mirai/event/request.py +++ b/nonebot/adapters/mirai/event/request.py @@ -20,6 +20,15 @@ class NewFriendRequestEvent(RequestEvent): group_id: int = Field(0, alias='groupId') async def approve(self, bot: "Bot"): + """ + :说明: + + 通过此人的好友申请 + + :参数: + + * ``bot: Bot``: 当前的 ``Bot`` 对象 + """ return await bot.api.post('/resp/newFriendRequestEvent', params={ 'eventId': self.event_id, @@ -32,6 +41,23 @@ class NewFriendRequestEvent(RequestEvent): bot: "Bot", operate: Literal[1, 2] = 1, message: str = ''): + """ + :说明: + + 拒绝此人的好友申请 + + :参数: + + * ``bot: Bot``: 当前的 ``Bot`` 对象 + * ``operate: Literal[1, 2]``: 响应的操作类型 + - ``1``: 拒绝添加好友 + - ``2``: 拒绝添加好友并添加黑名单,不再接收该用户的好友申请 + * ``message: str``: 回复的信息 + + :返回: + + - ``[type]``: [description] + """ assert operate > 0 return await bot.api.post('/resp/newFriendRequestEvent', params={ @@ -49,6 +75,15 @@ class MemberJoinRequestEvent(RequestEvent): group_name: str = Field(alias='groupName') async def approve(self, bot: "Bot"): + """ + :说明: + + 通过此人的加群申请 + + :参数: + + * ``bot: Bot``: 当前的 ``Bot`` 对象 + """ return await bot.api.post('/resp/memberJoinRequestEvent', params={ 'eventId': self.event_id, @@ -61,6 +96,21 @@ class MemberJoinRequestEvent(RequestEvent): bot: "Bot", operate: Literal[1, 2, 3, 4] = 1, message: str = ''): + """ + :说明: + + 拒绝(忽略)此人的加群申请 + + :参数: + + * ``bot: Bot``: 当前的 ``Bot`` 对象 + * ``operate: Literal[1, 2, 3, 4]``: 响应的操作类型 + - ``1``: 拒绝入群 + - ``2``: 忽略请求 + - ``3``: 拒绝入群并添加黑名单,不再接收该用户的入群申请 + - ``4``: 忽略入群并添加黑名单,不再接收该用户的入群申请 + * ``message: str``: 回复的信息 + """ assert operate > 0 return await bot.api.post('/resp/memberJoinRequestEvent', params={ @@ -78,6 +128,15 @@ class BotInvitedJoinGroupRequestEvent(RequestEvent): group_name: str = Field(alias='groupName') async def approve(self, bot: "Bot"): + """ + :说明: + + 通过这份被邀请入群申请 + + :参数: + + * ``bot: Bot``: 当前的 ``Bot`` 对象 + """ return await bot.api.post('/resp/botInvitedJoinGroupRequestEvent', params={ 'eventId': self.event_id, @@ -87,6 +146,16 @@ class BotInvitedJoinGroupRequestEvent(RequestEvent): }) async def reject(self, bot: "Bot", message: str = ""): + """ + :说明: + + 拒绝这份被邀请入群申请 + + :参数: + + * ``bot: Bot``: 当前的 ``Bot`` 对象 + * ``message: str``: 邀请消息 + """ return await bot.api.post('/resp/botInvitedJoinGroupRequestEvent', params={ 'eventId': self.event_id, From ceeb37f8ecdfdc76bc69b05d0f9988749402350c Mon Sep 17 00:00:00 2001 From: nonebot Date: Sun, 31 Jan 2021 14:45:07 +0000 Subject: [PATCH 17/37] :memo: update api docs --- docs/api/README.md | 3 +++ docs/api/adapters/mirai.md | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 docs/api/adapters/mirai.md diff --git a/docs/api/README.md b/docs/api/README.md index 243733f8..36e9803e 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -50,3 +50,6 @@ * [nonebot.adapters.ding](adapters/ding.html) + + + * [nonebot.adapters.mirai](adapters/mirai.html) diff --git a/docs/api/adapters/mirai.md b/docs/api/adapters/mirai.md new file mode 100644 index 00000000..33fbdbf0 --- /dev/null +++ b/docs/api/adapters/mirai.md @@ -0,0 +1,38 @@ +--- +contentSidebar: true +sidebarDepth: 0 +--- + +# NoneBot.adapters.mirai 模块 + +# NoneBot.adapters.mirai.bot 模块 + +# NoneBot.adapters.mirai.bot_ws 模块 + +# NoneBot.adapters.mirai.config 模块 + +# NoneBot.adapters.mirai.message 模块 + + +## _class_ `MessageType` + +基类:`str`, `enum.Enum` + +An enumeration. + +# NoneBot.adapters.mirai.event 模块 + +# NoneBot.adapters.mirai.event.base 模块 + + +## _class_ `SenderPermission` + +基类:`str`, `enum.Enum` + +An enumeration. + +# NoneBot.adapters.mirai.event.message 模块 + +# NoneBot.adapters.mirai.event.notice 模块 + +# NoneBot.adapters.mirai.event.request 模块 From 7c9cbe7b58b4017c4b60ff327ee3904fd799744a Mon Sep 17 00:00:00 2001 From: Mix Date: Mon, 1 Feb 2021 00:01:31 +0800 Subject: [PATCH 18/37] :speech_balloon: :bulb: rename some dataclass, add comments for events in mirai adapter --- docs_build/adapters/mirai.rst | 25 +++++--- nonebot/adapters/mirai/bot.py | 7 +-- nonebot/adapters/mirai/bot_ws.py | 3 + nonebot/adapters/mirai/event/__init__.py | 2 +- nonebot/adapters/mirai/event/base.py | 35 +++++++++--- nonebot/adapters/mirai/event/message.py | 12 ++-- nonebot/adapters/mirai/event/meta.py | 31 ++++++++++ nonebot/adapters/mirai/event/notice.py | 72 ++++++++++++++++-------- nonebot/adapters/mirai/event/request.py | 6 +- 9 files changed, 143 insertions(+), 50 deletions(-) create mode 100644 nonebot/adapters/mirai/event/meta.py diff --git a/docs_build/adapters/mirai.rst b/docs_build/adapters/mirai.rst index 1f6e3eaf..6f15695b 100644 --- a/docs_build/adapters/mirai.rst +++ b/docs_build/adapters/mirai.rst @@ -9,63 +9,70 @@ NoneBot.adapters.mirai 模块 .. automodule:: nonebot.adapters.mirai NoneBot.adapters.mirai.bot 模块 -===================================== +=============================== .. automodule:: nonebot.adapters.mirai.bot :members: :show-inheritance: NoneBot.adapters.mirai.bot_ws 模块 -===================================== +================================== .. automodule:: nonebot.adapters.mirai.bot_ws :members: :show-inheritance: NoneBot.adapters.mirai.config 模块 -===================================== +================================== .. automodule:: nonebot.adapters.mirai.config :members: :show-inheritance: NoneBot.adapters.mirai.message 模块 -==================================== +=================================== .. automodule:: nonebot.adapters.mirai.message :members: :show-inheritance: NoneBot.adapters.mirai.event 模块 -==================================== +================================= .. automodule:: nonebot.adapters.mirai.event :members: :show-inheritance: NoneBot.adapters.mirai.event.base 模块 -==================================== +====================================== .. automodule:: nonebot.adapters.mirai.event.base :members: :show-inheritance: +NoneBot.adapters.mirai.event.meta 模块 +====================================== + +.. automodule:: nonebot.adapters.mirai.event.meta + :members: + :show-inheritance: + NoneBot.adapters.mirai.event.message 模块 -==================================== +========================================= .. automodule:: nonebot.adapters.mirai.event.message :members: :show-inheritance: NoneBot.adapters.mirai.event.notice 模块 -==================================== +========================================= .. automodule:: nonebot.adapters.mirai.event.notice :members: :show-inheritance: NoneBot.adapters.mirai.event.request 模块 -==================================== +========================================= .. automodule:: nonebot.adapters.mirai.event.request :members: diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py index 81c7f7b7..ee718997 100644 --- a/nonebot/adapters/mirai/bot.py +++ b/nonebot/adapters/mirai/bot.py @@ -118,6 +118,9 @@ class SessionManager: class MiraiBot(BaseBot): + """ + mirai-api-http 协议 Bot 适配。 + """ @overrides(BaseBot) def __init__(self, @@ -663,10 +666,6 @@ class MiraiBot(BaseBot): * ``target: int``: 指定群的群号 * ``member_id: int``: 群员QQ号 * ``info: Dict[str, Any]``: 群员资料, 格式见 ``member_info`` 的返回值 - - :返回: - - - ``[type]``: [description] """ return await self.api.post('memberInfo', params={ diff --git a/nonebot/adapters/mirai/bot_ws.py b/nonebot/adapters/mirai/bot_ws.py index d20d81dd..560534b6 100644 --- a/nonebot/adapters/mirai/bot_ws.py +++ b/nonebot/adapters/mirai/bot_ws.py @@ -89,6 +89,9 @@ class WebSocket(BaseWebSocket): class MiraiWebsocketBot(MiraiBot): + """ + mirai-api-http 正向 Websocket 协议 Bot 适配。 + """ @overrides(MiraiBot) def __init__(self, connection_type: str, self_id: str, *, diff --git a/nonebot/adapters/mirai/event/__init__.py b/nonebot/adapters/mirai/event/__init__.py index 903f4eb8..8b96ecca 100644 --- a/nonebot/adapters/mirai/event/__init__.py +++ b/nonebot/adapters/mirai/event/__init__.py @@ -1,4 +1,4 @@ -from .base import Event, SenderInfo, PrivateSenderInfo, SenderGroup +from .base import Event, GroupChatInfo, GroupInfo, UserPermission, PrivateChatInfo 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 3b6916f5..5b1cf7fc 100644 --- a/nonebot/adapters/mirai/event/base.py +++ b/nonebot/adapters/mirai/event/base.py @@ -11,37 +11,53 @@ from nonebot.log import logger from nonebot.typing import overrides -class SenderPermission(str, Enum): +class UserPermission(str, Enum): + """ + 用户权限枚举类 + + - ``OWNER``: 群主 + - ``ADMINISTRATOR``: 群管理 + - ``MEMBER``: 普通群成员 + """ OWNER = 'OWNER' ADMINISTRATOR = 'ADMINISTRATOR' MEMBER = 'MEMBER' -class SenderGroup(BaseModel): +class GroupInfo(BaseModel): id: int name: str - permission: SenderPermission + permission: UserPermission -class SenderInfo(BaseModel): +class GroupChatInfo(BaseModel): id: int name: str = Field(alias='memberName') - permission: SenderPermission - group: SenderGroup + permission: UserPermission + group: GroupInfo -class PrivateSenderInfo(BaseModel): +class PrivateChatInfo(BaseModel): id: int nickname: str remark: str class Event(BaseEvent): + """ + mirai-api-http 协议事件,字段与 mirai-api-http 一致。各事件字段参考 `mirai-api-http 文档`_ + + .. _mirai-api-http 文档: + https://github.com/project-mirai/mirai-api-http/blob/master/docs/EventType.md + """ self_id: int type: str @classmethod def new(cls, data: Dict[str, Any]) -> "Event": + """ + 此事件类的工厂函数, 能够通过事件数据选择合适的子类进行序列化 + """ type = data['type'] def all_subclasses(cls: Type[Event]): @@ -70,7 +86,7 @@ class Event(BaseEvent): @overrides(BaseEvent) def get_type(self) -> Literal["message", "notice", "request", "meta_event"]: - from . import message, notice, request + from . import message, notice, request, meta if isinstance(self, message.MessageEvent): return 'message' elif isinstance(self, notice.NoticeEvent): @@ -109,4 +125,7 @@ class Event(BaseEvent): return False def normalize_dict(self, **kwargs) -> Dict[str, Any]: + """ + 返回可以被json正常反序列化的结构体 + """ return json.loads(self.json(**kwargs)) diff --git a/nonebot/adapters/mirai/event/message.py b/nonebot/adapters/mirai/event/message.py index 10574d5e..6021ea64 100644 --- a/nonebot/adapters/mirai/event/message.py +++ b/nonebot/adapters/mirai/event/message.py @@ -5,10 +5,11 @@ from pydantic import Field from nonebot.typing import overrides from ..message import MessageChain -from .base import Event, PrivateSenderInfo, SenderInfo +from .base import Event, GroupChatInfo, PrivateChatInfo class MessageEvent(Event): + """消息事件基类""" message_chain: MessageChain = Field(alias='messageChain') sender: Any @@ -30,7 +31,8 @@ class MessageEvent(Event): class GroupMessage(MessageEvent): - sender: SenderInfo + """群消息事件""" + sender: GroupChatInfo @overrides(MessageEvent) def get_session_id(self) -> str: @@ -38,7 +40,8 @@ class GroupMessage(MessageEvent): class FriendMessage(MessageEvent): - sender: PrivateSenderInfo + """好友消息事件""" + sender: PrivateChatInfo @overrides(MessageEvent) def get_user_id(self) -> str: @@ -50,7 +53,8 @@ class FriendMessage(MessageEvent): class TempMessage(MessageEvent): - sender: SenderInfo + """临时会话消息事件""" + sender: GroupChatInfo @overrides def get_session_id(self) -> str: diff --git a/nonebot/adapters/mirai/event/meta.py b/nonebot/adapters/mirai/event/meta.py new file mode 100644 index 00000000..e42baf72 --- /dev/null +++ b/nonebot/adapters/mirai/event/meta.py @@ -0,0 +1,31 @@ +from .base import Event + + +class MetaEvent(Event): + """元事件基类""" + qq: int + + +class BotOnlineEvent(MetaEvent): + """Bot登录成功""" + pass + + +class BotOfflineEventActive(MetaEvent): + """Bot主动离线""" + pass + + +class BotOfflineEventForce(MetaEvent): + """Bot被挤下线""" + pass + + +class BotOfflineEventDropped(MetaEvent): + """Bot被服务器断开或因网络问题而掉线""" + pass + + +class BotReloginEvent(MetaEvent): + """Bot主动重新登录""" + pass \ No newline at end of file diff --git a/nonebot/adapters/mirai/event/notice.py b/nonebot/adapters/mirai/event/notice.py index b758d9c5..276b12d1 100644 --- a/nonebot/adapters/mirai/event/notice.py +++ b/nonebot/adapters/mirai/event/notice.py @@ -2,61 +2,74 @@ from typing import Any, Optional from pydantic import Field -from .base import Event, SenderGroup, SenderInfo, SenderPermission +from .base import Event, GroupChatInfo, GroupInfo, UserPermission class NoticeEvent(Event): + """通知事件基类""" pass class MuteEvent(NoticeEvent): - operator: SenderInfo + """禁言类事件基类""" + operator: GroupChatInfo class BotMuteEvent(MuteEvent): + """Bot被禁言""" pass class BotUnmuteEvent(MuteEvent): + """Bot被取消禁言""" pass class MemberMuteEvent(MuteEvent): + """群成员被禁言事件(该成员不是Bot)""" duration_seconds: int = Field(alias='durationSeconds') - member: SenderInfo - operator: Optional[SenderInfo] = None + member: GroupChatInfo + operator: Optional[GroupChatInfo] = None class MemberUnmuteEvent(MuteEvent): - member: SenderInfo - operator: Optional[SenderInfo] = None + """群成员被取消禁言事件(该成员不是Bot)""" + member: GroupChatInfo + operator: Optional[GroupChatInfo] = None class BotJoinGroupEvent(NoticeEvent): - group: SenderGroup + """Bot加入了一个新群""" + group: GroupInfo class BotLeaveEventActive(BotJoinGroupEvent): + """Bot主动退出一个群""" pass class BotLeaveEventKick(BotJoinGroupEvent): + """Bot被踢出一个群""" pass class MemberJoinEvent(NoticeEvent): - member: SenderInfo - - -class MemberLeaveEventQuit(MemberJoinEvent): - pass + """新人入群的事件""" + member: GroupChatInfo class MemberLeaveEventKick(MemberJoinEvent): - operator: Optional[SenderInfo] = None + """成员被踢出群(该成员不是Bot)""" + operator: Optional[GroupChatInfo] = None + + +class MemberLeaveEventQuit(MemberJoinEvent): + """成员主动离群(该成员不是Bot)""" + pass class FriendRecallEvent(NoticeEvent): + """好友消息撤回""" author_id: int = Field(alias='authorId') message_id: int = Field(alias='messageId') time: int @@ -64,67 +77,80 @@ class FriendRecallEvent(NoticeEvent): class GroupRecallEvent(FriendRecallEvent): - group: SenderGroup - operator: Optional[SenderInfo] = None + """群消息撤回""" + group: GroupInfo + operator: Optional[GroupChatInfo] = None class GroupStateChangeEvent(NoticeEvent): + """群变化事件基类""" origin: Any current: Any - group: SenderGroup - operator: Optional[SenderInfo] = None + group: GroupInfo + operator: Optional[GroupChatInfo] = None class GroupNameChangeEvent(GroupStateChangeEvent): + """某个群名改变""" origin: str current: str class GroupEntranceAnnouncementChangeEvent(GroupStateChangeEvent): + """某群入群公告改变""" origin: str current: str class GroupMuteAllEvent(GroupStateChangeEvent): + """全员禁言""" origin: bool current: bool class GroupAllowAnonymousChatEvent(GroupStateChangeEvent): + """匿名聊天""" origin: bool current: bool class GroupAllowConfessTalkEvent(GroupStateChangeEvent): + """坦白说""" origin: bool current: bool class GroupAllowMemberInviteEvent(GroupStateChangeEvent): + """允许群员邀请好友加群""" origin: bool current: bool class MemberStateChangeEvent(NoticeEvent): - member: SenderInfo - operator: Optional[SenderInfo] = None + """群成员变化事件基类""" + member: GroupChatInfo + operator: Optional[GroupChatInfo] = None class MemberCardChangeEvent(MemberStateChangeEvent): + """群名片改动""" origin: str current: str class MemberSpecialTitleChangeEvent(MemberStateChangeEvent): + """群头衔改动(只有群主有操作限权)""" origin: str current: str class BotGroupPermissionChangeEvent(MemberStateChangeEvent): - origin: SenderPermission - current: SenderPermission + """Bot在群里的权限被改变""" + origin: UserPermission + current: UserPermission class MemberPermissionChangeEvent(MemberStateChangeEvent): - origin: SenderPermission - current: SenderPermission + """成员权限改变的事件(该成员不是Bot)""" + origin: UserPermission + current: UserPermission diff --git a/nonebot/adapters/mirai/event/request.py b/nonebot/adapters/mirai/event/request.py index 39c64298..cea13aae 100644 --- a/nonebot/adapters/mirai/event/request.py +++ b/nonebot/adapters/mirai/event/request.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING -from typing_extensions import Literal from pydantic import Field +from typing_extensions import Literal from .base import Event @@ -10,12 +10,14 @@ if TYPE_CHECKING: 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') @@ -70,6 +72,7 @@ class NewFriendRequestEvent(RequestEvent): class MemberJoinRequestEvent(RequestEvent): + """用户入群申请(Bot需要有管理员权限)""" from_id: int = Field(alias='fromId') group_id: int = Field(alias='groupId') group_name: str = Field(alias='groupName') @@ -123,6 +126,7 @@ class MemberJoinRequestEvent(RequestEvent): class BotInvitedJoinGroupRequestEvent(RequestEvent): + """Bot被邀请入群申请""" from_id: int = Field(alias='fromId') group_id: int = Field(alias='groupId') group_name: str = Field(alias='groupName') From 7fdfd89525622e192f9b1e4acf4dfc4408b45f44 Mon Sep 17 00:00:00 2001 From: nonebot Date: Sun, 31 Jan 2021 16:02:54 +0000 Subject: [PATCH 19/37] :memo: update api docs --- docs/api/adapters/mirai.md | 1034 +++++++++++++++++++++++++++++++++++- 1 file changed, 1032 insertions(+), 2 deletions(-) diff --git a/docs/api/adapters/mirai.md b/docs/api/adapters/mirai.md index 33fbdbf0..98b1a640 100644 --- a/docs/api/adapters/mirai.md +++ b/docs/api/adapters/mirai.md @@ -7,8 +7,607 @@ sidebarDepth: 0 # NoneBot.adapters.mirai.bot 模块 + +## _class_ `MiraiBot` + +基类:[`nonebot.adapters.Bot`](README.md#nonebot.adapters.Bot) + +mirai-api-http 协议 Bot 适配。 + + +### _async_ `send(event, message, at_sender=False)` + + +* **说明** + + 根据 `event` 向触发事件的主题发送信息 + + + +* **参数** + + + * `event: Event`: Event对象 + + + * `message: Union[MessageChain, MessageSegment, str]`: 要发送的消息 + + + * `at_sender: bool`: 是否 @ 事件主题 + + + +* **返回** + + + * `Any`: API 调用返回数据 + + + +### _async_ `send_friend_message(target, message_chain)` + + +* **说明** + + 使用此方法向指定好友发送消息 + + + +* **参数** + + + * `target: int`: 发送消息目标好友的 QQ 号 + + + * `message_chain: MessageChain`: 消息链,是一个消息对象构成的数组 + + + +* **返回** + + + * `Any`: API 调用返回数据 + + + +### _async_ `send_temp_message(qq, group, message_chain)` + + +* **说明** + + 使用此方法向临时会话对象发送消息 + + + +* **参数** + + + * `qq: int`: 临时会话对象 QQ 号 + + + * `group: int`: 临时会话群号 + + + * `message_chain: MessageChain`: 消息链,是一个消息对象构成的数组 + + + +* **返回** + + + * `Any`: API 调用返回数据 + + + +### _async_ `send_group_message(group, message_chain, quote=None)` + + +* **说明** + + 使用此方法向指定群发送消息 + + + +* **参数** + + + * `group: int`: 发送消息目标群的群号 + + + * `message_chain: MessageChain`: 消息链,是一个消息对象构成的数组 + + + * `quote: Optional[int]`: 引用一条消息的 message_id 进行回复 + + + +* **返回** + + + * `Any`: API 调用返回数据 + + + +### _async_ `recall(target)` + + +* **说明** + + 使用此方法撤回指定消息。对于bot发送的消息,有2分钟时间限制。对于撤回群聊中群员的消息,需要有相应权限 + + + +* **参数** + + + * `target: int`: 需要撤回的消息的message_id + + + +* **返回** + + + * `Any`: API 调用返回数据 + + + +### _async_ `send_image_message(target, qq, group, urls)` + + +* **说明** + + 使用此方法向指定对象(群或好友)发送图片消息 + 除非需要通过此手段获取image_id,否则不推荐使用该接口 + + > 当qq和group同时存在时,表示发送临时会话图片,qq为临时会话对象QQ号,group为临时会话发起的群号 + + + +* **参数** + + + * `target: int`: 发送对象的QQ号或群号,可能存在歧义 + + + * `qq: int`: 发送对象的QQ号 + + + * `group: int`: 发送对象的群号 + + + * `urls: List[str]`: 是一个url字符串构成的数组 + + + +* **返回** + + + * `List[str]`: 一个包含图片imageId的数组 + + + +### _async_ `upload_image(type, img)` + + +* **说明** + + 使用此方法上传图片文件至服务器并返回Image_id + + + +* **参数** + + + * `type: str`: "friend" 或 "group" 或 "temp" + + + * `img: BytesIO`: 图片的BytesIO对象 + + + +* **返回** + + + * `Any`: API 调用返回数据 + + + +### _async_ `upload_voice(type, voice)` + + +* **说明** + + 使用此方法上传语音文件至服务器并返回voice_id + + + +* **参数** + + + * `type: str`: 当前仅支持 "group" + + + * `voice: BytesIO`: 语音的BytesIO对象 + + + +* **返回** + + + * `Any`: API 调用返回数据 + + + +### _async_ `fetch_message(count=10)` + + +* **说明** + + 使用此方法获取bot接收到的最老消息和最老各类事件 + (会从MiraiApiHttp消息记录中删除) + + + +* **参数** + + + * `count: int`: 获取消息和事件的数量 + + + +### _async_ `fetch_latest_message(count=10)` + + +* **说明** + + 使用此方法获取bot接收到的最新消息和最新各类事件 + (会从MiraiApiHttp消息记录中删除) + + + +* **参数** + + + * `count: int`: 获取消息和事件的数量 + + + +### _async_ `peek_message(count=10)` + + +* **说明** + + 使用此方法获取bot接收到的最老消息和最老各类事件 + (不会从MiraiApiHttp消息记录中删除) + + + +* **参数** + + + * `count: int`: 获取消息和事件的数量 + + + +### _async_ `peek_latest_message(count=10)` + + +* **说明** + + 使用此方法获取bot接收到的最新消息和最新各类事件 + (不会从MiraiApiHttp消息记录中删除) + + + +* **参数** + + + * `count: int`: 获取消息和事件的数量 + + + +### _async_ `messsage_from_id(id)` + + +* **说明** + + 通过messageId获取一条被缓存的消息 + 使用此方法获取bot接收到的消息和各类事件 + + + +* **参数** + + + * `id: int`: 获取消息的message_id + + + +### _async_ `count_message()` + + +* **说明** + + 使用此方法获取bot接收并缓存的消息总数,注意不包含被删除的 + + + +### _async_ `friend_list()` + + +* **说明** + + 使用此方法获取bot的好友列表 + + + +* **返回** + + + * `List[Dict[str, Any]]`: 返回的好友列表数据 + + + +### _async_ `group_list()` + + +* **说明** + + 使用此方法获取bot的群列表 + + + +* **返回** + + + * `List[Dict[str, Any]]`: 返回的群列表数据 + + + +### _async_ `member_list(target)` + + +* **说明** + + 使用此方法获取bot指定群种的成员列表 + + + +* **参数** + + + * `target: int`: 指定群的群号 + + + +* **返回** + + + * `List[Dict[str, Any]]`: 返回的群成员列表数据 + + + +### _async_ `mute(target, member_id, time)` + + +* **说明** + + 使用此方法指定群禁言指定群员(需要有相关权限) + + + +* **参数** + + + * `target: int`: 指定群的群号 + + + * `member_id: int`: 指定群员QQ号 + + + * `time: int`: 禁言时长,单位为秒,最多30天 + + + +### _async_ `unmute(target, member_id)` + + +* **说明** + + 使用此方法指定群解除群成员禁言(需要有相关权限) + + + +* **参数** + + + * `target: int`: 指定群的群号 + + + * `member_id: int`: 指定群员QQ号 + + + +### _async_ `kick(target, member_id, msg)` + + +* **说明** + + 使用此方法移除指定群成员(需要有相关权限) + + + +* **参数** + + + * `target: int`: 指定群的群号 + + + * `member_id: int`: 指定群员QQ号 + + + * `msg: str`: 信息 + + + +### _async_ `quit(target)` + + +* **说明** + + 使用此方法使Bot退出群聊 + + + +* **参数** + + + * `target: int`: 退出的群号 + + + +### _async_ `mute_all(target)` + + +* **说明** + + 使用此方法令指定群进行全体禁言(需要有相关权限) + + + +* **参数** + + + * `target: int`: 指定群的群号 + + + +### _async_ `unmute_all(target)` + + +* **说明** + + 使用此方法令指定群解除全体禁言(需要有相关权限) + + + +* **参数** + + + * `target: int`: 指定群的群号 + + + +### _async_ `group_config(target)` + + +* **说明** + + 使用此方法获取群设置 + + + +* **参数** + + + * `target: int`: 指定群的群号 + + + +* **返回** + + +```json +{ + "name": "群名称", + "announcement": "群公告", + "confessTalk": true, + "allowMemberInvite": true, + "autoApprove": true, + "anonymousChat": true +} +``` + + +### _async_ `modify_group_config(target, config)` + + +* **说明** + + 使用此方法修改群设置(需要有相关权限) + + + +* **参数** + + + * `target: int`: 指定群的群号 + + + * `config: Dict[str, Any]`: 群设置, 格式见 `group_config` 的返回值 + + + +### _async_ `member_info(target, member_id)` + + +* **说明** + + 使用此方法获取群员资料 + + + +* **参数** + + + * `target: int`: 指定群的群号 + + + * `member_id: int`: 群员QQ号 + + + +* **返回** + + +```json +{ + "name": "群名片", + "specialTitle": "群头衔" +} +``` + + +### _async_ `modify_member_info(target, member_id, info)` + + +* **说明** + + 使用此方法修改群员资料(需要有相关权限) + + + +* **参数** + + + * `target: int`: 指定群的群号 + + + * `member_id: int`: 群员QQ号 + + + * `info: Dict[str, Any]`: 群员资料, 格式见 `member_info` 的返回值 + + # NoneBot.adapters.mirai.bot_ws 模块 + +## _class_ `MiraiWebsocketBot` + +基类:`nonebot.adapters.mirai.bot.MiraiBot` + +mirai-api-http 正向 Websocket 协议 Bot 适配。 + # NoneBot.adapters.mirai.config 模块 # NoneBot.adapters.mirai.message 模块 @@ -25,14 +624,445 @@ An enumeration. # NoneBot.adapters.mirai.event.base 模块 -## _class_ `SenderPermission` +## _class_ `UserPermission` 基类:`str`, `enum.Enum` -An enumeration. +用户权限枚举类 + +> +> * `OWNER`: 群主 + + +> * `ADMINISTRATOR`: 群管理 + + +> * `MEMBER`: 普通群成员 + + +## _class_ `Event` + +基类:[`nonebot.adapters.Event`](README.md#nonebot.adapters.Event) + +mirai-api-http 协议事件,字段与 mirai-api-http 一致。各事件字段参考 [mirai-api-http 文档](https://github.com/project-mirai/mirai-api-http/blob/master/docs/EventType.md) + + +### _classmethod_ `new(data)` + +此事件类的工厂函数, 能够通过事件数据选择合适的子类进行序列化 + + +### `normalize_dict(**kwargs)` + +返回可以被json正常反序列化的结构体 + +# NoneBot.adapters.mirai.event.meta 模块 + + +## _class_ `MetaEvent` + +基类:`nonebot.adapters.mirai.event.base.Event` + +元事件基类 + + +## _class_ `BotOnlineEvent` + +基类:`nonebot.adapters.mirai.event.meta.MetaEvent` + +Bot登录成功 + + +## _class_ `BotOfflineEventActive` + +基类:`nonebot.adapters.mirai.event.meta.MetaEvent` + +Bot主动离线 + + +## _class_ `BotOfflineEventForce` + +基类:`nonebot.adapters.mirai.event.meta.MetaEvent` + +Bot被挤下线 + + +## _class_ `BotOfflineEventDropped` + +基类:`nonebot.adapters.mirai.event.meta.MetaEvent` + +Bot被服务器断开或因网络问题而掉线 + + +## _class_ `BotReloginEvent` + +基类:`nonebot.adapters.mirai.event.meta.MetaEvent` + +Bot主动重新登录 # NoneBot.adapters.mirai.event.message 模块 + +## _class_ `MessageEvent` + +基类:`nonebot.adapters.mirai.event.base.Event` + +消息事件基类 + + +## _class_ `GroupMessage` + +基类:`nonebot.adapters.mirai.event.message.MessageEvent` + +群消息事件 + + +## _class_ `FriendMessage` + +基类:`nonebot.adapters.mirai.event.message.MessageEvent` + +好友消息事件 + + +## _class_ `TempMessage` + +基类:`nonebot.adapters.mirai.event.message.MessageEvent` + +临时会话消息事件 + # NoneBot.adapters.mirai.event.notice 模块 + +## _class_ `NoticeEvent` + +基类:`nonebot.adapters.mirai.event.base.Event` + +通知事件基类 + + +## _class_ `MuteEvent` + +基类:`nonebot.adapters.mirai.event.notice.NoticeEvent` + +禁言类事件基类 + + +## _class_ `BotMuteEvent` + +基类:`nonebot.adapters.mirai.event.notice.MuteEvent` + +Bot被禁言 + + +## _class_ `BotUnmuteEvent` + +基类:`nonebot.adapters.mirai.event.notice.MuteEvent` + +Bot被取消禁言 + + +## _class_ `MemberMuteEvent` + +基类:`nonebot.adapters.mirai.event.notice.MuteEvent` + +群成员被禁言事件(该成员不是Bot) + + +## _class_ `MemberUnmuteEvent` + +基类:`nonebot.adapters.mirai.event.notice.MuteEvent` + +群成员被取消禁言事件(该成员不是Bot) + + +## _class_ `BotJoinGroupEvent` + +基类:`nonebot.adapters.mirai.event.notice.NoticeEvent` + +Bot加入了一个新群 + + +## _class_ `BotLeaveEventActive` + +基类:`nonebot.adapters.mirai.event.notice.BotJoinGroupEvent` + +Bot主动退出一个群 + + +## _class_ `BotLeaveEventKick` + +基类:`nonebot.adapters.mirai.event.notice.BotJoinGroupEvent` + +Bot被踢出一个群 + + +## _class_ `MemberJoinEvent` + +基类:`nonebot.adapters.mirai.event.notice.NoticeEvent` + +新人入群的事件 + + +## _class_ `MemberLeaveEventKick` + +基类:`nonebot.adapters.mirai.event.notice.MemberJoinEvent` + +成员被踢出群(该成员不是Bot) + + +## _class_ `MemberLeaveEventQuit` + +基类:`nonebot.adapters.mirai.event.notice.MemberJoinEvent` + +成员主动离群(该成员不是Bot) + + +## _class_ `FriendRecallEvent` + +基类:`nonebot.adapters.mirai.event.notice.NoticeEvent` + +好友消息撤回 + + +## _class_ `GroupRecallEvent` + +基类:`nonebot.adapters.mirai.event.notice.FriendRecallEvent` + +群消息撤回 + + +## _class_ `GroupStateChangeEvent` + +基类:`nonebot.adapters.mirai.event.notice.NoticeEvent` + +群变化事件基类 + + +## _class_ `GroupNameChangeEvent` + +基类:`nonebot.adapters.mirai.event.notice.GroupStateChangeEvent` + +某个群名改变 + + +## _class_ `GroupEntranceAnnouncementChangeEvent` + +基类:`nonebot.adapters.mirai.event.notice.GroupStateChangeEvent` + +某群入群公告改变 + + +## _class_ `GroupMuteAllEvent` + +基类:`nonebot.adapters.mirai.event.notice.GroupStateChangeEvent` + +全员禁言 + + +## _class_ `GroupAllowAnonymousChatEvent` + +基类:`nonebot.adapters.mirai.event.notice.GroupStateChangeEvent` + +匿名聊天 + + +## _class_ `GroupAllowConfessTalkEvent` + +基类:`nonebot.adapters.mirai.event.notice.GroupStateChangeEvent` + +坦白说 + + +## _class_ `GroupAllowMemberInviteEvent` + +基类:`nonebot.adapters.mirai.event.notice.GroupStateChangeEvent` + +允许群员邀请好友加群 + + +## _class_ `MemberStateChangeEvent` + +基类:`nonebot.adapters.mirai.event.notice.NoticeEvent` + +群成员变化事件基类 + + +## _class_ `MemberCardChangeEvent` + +基类:`nonebot.adapters.mirai.event.notice.MemberStateChangeEvent` + +群名片改动 + + +## _class_ `MemberSpecialTitleChangeEvent` + +基类:`nonebot.adapters.mirai.event.notice.MemberStateChangeEvent` + +群头衔改动(只有群主有操作限权) + + +## _class_ `BotGroupPermissionChangeEvent` + +基类:`nonebot.adapters.mirai.event.notice.MemberStateChangeEvent` + +Bot在群里的权限被改变 + + +## _class_ `MemberPermissionChangeEvent` + +基类:`nonebot.adapters.mirai.event.notice.MemberStateChangeEvent` + +成员权限改变的事件(该成员不是Bot) + # NoneBot.adapters.mirai.event.request 模块 + + +## _class_ `RequestEvent` + +基类:`nonebot.adapters.mirai.event.base.Event` + +请求事件基类 + + +## _class_ `NewFriendRequestEvent` + +基类:`nonebot.adapters.mirai.event.request.RequestEvent` + +添加好友申请 + + +### _async_ `approve(bot)` + + +* **说明** + + 通过此人的好友申请 + + + +* **参数** + + + * `bot: Bot`: 当前的 `Bot` 对象 + + + +### _async_ `reject(bot, operate=1, message='')` + + +* **说明** + + 拒绝此人的好友申请 + + + +* **参数** + + + * `bot: Bot`: 当前的 `Bot` 对象 + + + * `operate: Literal[1, 2]`: 响应的操作类型 + - `1`: 拒绝添加好友 + - `2`: 拒绝添加好友并添加黑名单,不再接收该用户的好友申请 + + + * `message: str`: 回复的信息 + + + +* **返回** + + + * `[type]`: [description] + + + +## _class_ `MemberJoinRequestEvent` + +基类:`nonebot.adapters.mirai.event.request.RequestEvent` + +用户入群申请(Bot需要有管理员权限) + + +### _async_ `approve(bot)` + + +* **说明** + + 通过此人的加群申请 + + + +* **参数** + + + * `bot: Bot`: 当前的 `Bot` 对象 + + + +### _async_ `reject(bot, operate=1, message='')` + + +* **说明** + + 拒绝(忽略)此人的加群申请 + + + +* **参数** + + + * `bot: Bot`: 当前的 `Bot` 对象 + + + * `operate: Literal[1, 2, 3, 4]`: 响应的操作类型 + - `1`: 拒绝入群 + - `2`: 忽略请求 + - `3`: 拒绝入群并添加黑名单,不再接收该用户的入群申请 + - `4`: 忽略入群并添加黑名单,不再接收该用户的入群申请 + + + * `message: str`: 回复的信息 + + + +## _class_ `BotInvitedJoinGroupRequestEvent` + +基类:`nonebot.adapters.mirai.event.request.RequestEvent` + +Bot被邀请入群申请 + + +### _async_ `approve(bot)` + + +* **说明** + + 通过这份被邀请入群申请 + + + +* **参数** + + + * `bot: Bot`: 当前的 `Bot` 对象 + + + +### _async_ `reject(bot, message='')` + + +* **说明** + + 拒绝这份被邀请入群申请 + + + +* **参数** + + + * `bot: Bot`: 当前的 `Bot` 对象 + + + * `message: str`: 邀请消息 From 56592fc4134d97a0fe0196b8c0be7d8d2c77bb8d Mon Sep 17 00:00:00 2001 From: Mix Date: Mon, 1 Feb 2021 01:04:30 +0800 Subject: [PATCH 20/37] :construction: :bulb: add comments for message etc. in mirai adapter --- nonebot/adapters/mirai/__init__.py | 10 ++ nonebot/adapters/mirai/bot.py | 77 ++++++++++--- nonebot/adapters/mirai/bot_ws.py | 27 ++++- nonebot/adapters/mirai/config.py | 9 ++ nonebot/adapters/mirai/event/__init__.py | 7 ++ nonebot/adapters/mirai/event/base.py | 4 +- nonebot/adapters/mirai/event/request.py | 4 - nonebot/adapters/mirai/message.py | 140 ++++++++++++++++++++++- 8 files changed, 250 insertions(+), 28 deletions(-) diff --git a/nonebot/adapters/mirai/__init__.py b/nonebot/adapters/mirai/__init__.py index 1107af38..b209657e 100644 --- a/nonebot/adapters/mirai/__init__.py +++ b/nonebot/adapters/mirai/__init__.py @@ -1,3 +1,13 @@ +""" +Mirai-API-HTTP 协议适配 +============================ + +协议详情请看: `mirai-api-http 文档`_ + +.. mirai-api-http 文档: + https://github.com/project-mirai/mirai-api-http/tree/master/docs +""" + from .bot import MiraiBot from .bot_ws import MiraiWebsocketBot from .event import * diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py index ee718997..74c4f602 100644 --- a/nonebot/adapters/mirai/bot.py +++ b/nonebot/adapters/mirai/bot.py @@ -32,6 +32,7 @@ class ActionFailed(BaseActionFailed): class SessionManager: + """Bot会话管理器, 提供API主动调用接口""" sessions: Dict[int, Tuple[str, datetime, httpx.AsyncClient]] = {} session_expiry: timedelta = timedelta(minutes=15) @@ -40,19 +41,39 @@ class SessionManager: @staticmethod def _raise_code(data: Dict[str, Any]) -> Dict[str, Any]: - code = data.get('code', 0) - logger.opt(colors=True).debug('Mirai API returned data: ' - f'{escape_tag(str(data))}') - if code != 0: - raise ActionFailed(**data) + logger.opt(colors=True).debug( + f'Mirai API returned data: {escape_tag(str(data))}') + if isinstance(data, dict) and ('code' in data): + if data['code'] != 0: + raise ActionFailed(**data) return data async def post(self, path: str, *, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - params = {**(params or {}), 'sessionKey': self.session_key} - response = await self.client.post(path, json=params, timeout=3) + """ + :说明: + + 以POST方式主动提交API请求 + + :参数: + + * ``path: str``: 对应API路径 + * ``params: Optional[Dict[str, Any]]``: 请求参数 (无需sessionKey) + + :返回: + + - ``Dict[str, Any]``: API 返回值 + """ + response = await self.client.post( + path, + json={ + **(params or {}), + 'sessionKey': self.session_key, + }, + timeout=3, + ) response.raise_for_status() return self._raise_code(response.json()) @@ -61,12 +82,28 @@ class SessionManager: *, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - response = await self.client.get(path, - params={ - **(params or {}), 'sessionKey': - self.session_key - }, - timeout=3) + """ + :说明: + + 以GET方式主动提交API请求 + + :参数: + + * ``path: str``: 对应API路径 + * ``params: Optional[Dict[str, Any]]``: 请求参数 (无需sessionKey) + + :返回: + + - ``Dict[str, Any]``: API 返回值 + """ + response = await self.client.get( + path, + params={ + **(params or {}), + 'sessionKey': self.session_key, + }, + timeout=3, + ) response.raise_for_status() return self._raise_code(response.json()) @@ -108,11 +145,11 @@ class SessionManager: return cls(session_key, client) @classmethod - def get(cls, self_id: int): + def get(cls, self_id: int, check_expire: bool = True): if self_id not in cls.sessions: return None key, time, client = cls.sessions[self_id] - if datetime.now() - time > cls.session_expiry: + if check_expire and (datetime.now() - time > cls.session_expiry): return None return cls(key, client) @@ -129,7 +166,6 @@ class MiraiBot(BaseBot): *, websocket: Optional[WebSocket] = None): super().__init__(connection_type, self_id, websocket=websocket) - self.api = SessionManager.get(int(self_id)) @property @overrides(BaseBot) @@ -140,6 +176,13 @@ class MiraiBot(BaseBot): def alive(self) -> bool: return not self.websocket.closed + @property + def api(self) -> SessionManager: + """返回该Bot对象的会话管理实例以提供API主动调用""" + api = SessionManager.get(self_id=int(self.self_id)) + assert api is not None, 'SessionManager has not been initialized' + return api + @classmethod @overrides(BaseBot) async def check_permission(cls, driver: "Driver", connection_type: str, @@ -179,10 +222,12 @@ class MiraiBot(BaseBot): @overrides(BaseBot) async def call_api(self, api: str, **data) -> NoReturn: + """由于Mirai的HTTP API特殊性, 该API暂时无法实现""" raise NotImplementedError @overrides(BaseBot) def __getattr__(self, key: str) -> NoReturn: + """由于Mirai的HTTP API特殊性, 该API暂时无法实现""" raise NotImplementedError @overrides(BaseBot) diff --git a/nonebot/adapters/mirai/bot_ws.py b/nonebot/adapters/mirai/bot_ws.py index 560534b6..ccce63b3 100644 --- a/nonebot/adapters/mirai/bot_ws.py +++ b/nonebot/adapters/mirai/bot_ws.py @@ -107,6 +107,12 @@ class MiraiWebsocketBot(MiraiBot): def alive(self) -> bool: return not self.websocket.closed + @property + def api(self) -> SessionManager: + api = SessionManager.get(self_id=int(self.self_id), check_expire=False) + assert api is not None, 'SessionManager has not been initialized' + return api + @classmethod @overrides(MiraiBot) async def check_permission(cls, driver: "Driver", connection_type: str, @@ -118,12 +124,23 @@ class MiraiWebsocketBot(MiraiBot): @classmethod @overrides(MiraiBot) def register(cls, driver: "Driver", config: "Config", qq: int): - cls.mirai_config = MiraiConfig(**config.dict()) - cls.active = True - assert cls.mirai_config.auth_key is not None - assert cls.mirai_config.host is not None - assert cls.mirai_config.port is not None + """ + :说明: + + 注册该Adapter + + :参数: + + * ``driver: Driver``: 程序所使用的``Driver`` + * ``config: Config``: 程序配置对象 + * ``qq: int``: 要使用的Bot的QQ号 **注意: 在使用正向Websocket时必须指定该值!** + + :返回: + + - ``[type]``: [description] + """ super().register(driver, config) + cls.active = True async def _bot_connection(): session: SessionManager = await SessionManager.new( diff --git a/nonebot/adapters/mirai/config.py b/nonebot/adapters/mirai/config.py index 942cf9fa..a907dd17 100644 --- a/nonebot/adapters/mirai/config.py +++ b/nonebot/adapters/mirai/config.py @@ -5,6 +5,15 @@ from pydantic import BaseModel, Extra, Field class Config(BaseModel): + """ + Mirai 配置类 + + :必填: + + - ``mirai_auth_key``: mirai-api-http的auth_key + - ``mirai_host``: mirai-api-http的地址 + - ``mirai_port``: mirai-api-http的端口 + """ auth_key: Optional[str] = Field(None, alias='mirai_auth_key') host: Optional[IPv4Address] = Field(None, alias='mirai_host') port: Optional[int] = Field(None, alias='mirai_port') diff --git a/nonebot/adapters/mirai/event/__init__.py b/nonebot/adapters/mirai/event/__init__.py index 8b96ecca..c0024e19 100644 --- a/nonebot/adapters/mirai/event/__init__.py +++ b/nonebot/adapters/mirai/event/__init__.py @@ -1,3 +1,10 @@ +""" +\:\:\:warning 警告 +事件中为了使代码更加整洁, 我们采用了与PEP8相符的命名规则取代Mirai原有的驼峰命名 + +部分字段可能与文档在符号上不一致 +\:\:\: +""" from .base import Event, GroupChatInfo, GroupInfo, UserPermission, PrivateChatInfo from .message import * from .notice import * diff --git a/nonebot/adapters/mirai/event/base.py b/nonebot/adapters/mirai/event/base.py index 5b1cf7fc..662b856d 100644 --- a/nonebot/adapters/mirai/event/base.py +++ b/nonebot/adapters/mirai/event/base.py @@ -45,9 +45,9 @@ class PrivateChatInfo(BaseModel): class Event(BaseEvent): """ - mirai-api-http 协议事件,字段与 mirai-api-http 一致。各事件字段参考 `mirai-api-http 文档`_ + mirai-api-http 协议事件,字段与 mirai-api-http 一致。各事件字段参考 `mirai-api-http 事件类型`_ - .. _mirai-api-http 文档: + .. _mirai-api-http 事件类型: https://github.com/project-mirai/mirai-api-http/blob/master/docs/EventType.md """ self_id: int diff --git a/nonebot/adapters/mirai/event/request.py b/nonebot/adapters/mirai/event/request.py index cea13aae..18d466ee 100644 --- a/nonebot/adapters/mirai/event/request.py +++ b/nonebot/adapters/mirai/event/request.py @@ -55,10 +55,6 @@ class NewFriendRequestEvent(RequestEvent): - ``1``: 拒绝添加好友 - ``2``: 拒绝添加好友并添加黑名单,不再接收该用户的好友申请 * ``message: str``: 回复的信息 - - :返回: - - - ``[type]``: [description] """ assert operate > 0 return await bot.api.post('/resp/newFriendRequestEvent', diff --git a/nonebot/adapters/mirai/message.py b/nonebot/adapters/mirai/message.py index a577a807..265b3b3b 100644 --- a/nonebot/adapters/mirai/message.py +++ b/nonebot/adapters/mirai/message.py @@ -9,6 +9,7 @@ from nonebot.typing import overrides class MessageType(str, Enum): + """消息类型枚举类""" SOURCE = 'Source' QUOTE = 'Quote' AT = 'At' @@ -25,6 +26,13 @@ class MessageType(str, Enum): class MessageSegment(BaseMessageSegment): + """ + CQHTTP 协议 MessageSegment 适配。具体方法参考 `mirai-api-http 消息类型`_ + + .. _mirai-api-http 消息类型: + https://github.com/project-mirai/mirai-api-http/blob/master/docs/MessageType.md + """ + type: MessageType data: Dict[str, Any] @@ -59,6 +67,7 @@ class MessageSegment(BaseMessageSegment): return self.type == MessageType.PLAIN def as_dict(self) -> Dict[str, Any]: + """导出可以被正常json序列化的结构体""" return {'type': self.type.value, **self.data} @classmethod @@ -68,6 +77,19 @@ class MessageSegment(BaseMessageSegment): @classmethod def quote(cls, id: int, group_id: int, sender_id: int, target_id: int, origin: "MessageChain"): + """ + :说明: + + 生成回复引用消息段 + + :参数: + + * ``id: int``: 被引用回复的原消息的message_id + * ``group_id: int``: 被引用回复的原消息所接收的群号,当为好友消息时为0 + * ``sender_id: int``: 被引用回复的原消息的发送者的QQ号 + * ``target_id: int``: 被引用回复的原消息的接收者者的QQ号(或群号) + * ``origin: MessageChain``: 被引用回复的原消息的消息链对象 + """ return cls(type=MessageType.QUOTE, id=id, groupId=group_id, @@ -77,18 +99,51 @@ class MessageSegment(BaseMessageSegment): @classmethod def at(cls, target: int): + """ + :说明: + + @某个人 + + :参数: + + * ``target: int``: 群员QQ号 + """ return cls(type=MessageType.AT, target=target) @classmethod def at_all(cls): + """ + :说明: + + @全体成员 + """ return cls(type=MessageType.AT_ALL) @classmethod def face(cls, face_id: Optional[int] = None, name: Optional[str] = None): + """ + :说明: + + 发送QQ表情 + + :参数: + + * ``face_id: Optional[int]``: QQ表情编号,可选,优先高于name + * ``name: Optional[str]``: QQ表情拼音,可选 + """ return cls(type=MessageType.FACE, faceId=face_id, name=name) @classmethod def plain(cls, text: str): + """ + :说明: + + 纯文本消息 + + :参数: + + * ``text: str``: 文字消息 + """ return cls(type=MessageType.PLAIN, text=text) @classmethod @@ -96,6 +151,21 @@ class MessageSegment(BaseMessageSegment): image_id: Optional[str] = None, url: Optional[str] = None, path: Optional[str] = None): + """ + :说明: + + 图片消息 + + :参数: + + * ``image_id: Optional[str]``: 图片的image_id,群图片与好友图片格式不同。不为空时将忽略url属性 + * ``url: Optional[str]``: 图片的URL,发送时可作网络图片的链接 + * ``path: Optional[str]``: 图片的路径,发送本地图片 + + :返回: + + - ``[type]``: [description] + """ return cls(type=MessageType.IMAGE, imageId=image_id, url=url, path=path) @classmethod @@ -103,6 +173,15 @@ class MessageSegment(BaseMessageSegment): image_id: Optional[str] = None, url: Optional[str] = None, path: Optional[str] = None): + """ + :说明: + + 闪照消息 + + :参数: + + 同 ``image`` + """ return cls(type=MessageType.FLASH_IMAGE, imageId=image_id, url=url, @@ -113,6 +192,17 @@ class MessageSegment(BaseMessageSegment): voice_id: Optional[str] = None, url: Optional[str] = None, path: Optional[str] = None): + """ + :说明: + + 语音消息 + + :参数: + + * ``voice_id: Optional[str]``: 语音的voice_id,不为空时将忽略url属性 + * ``url: Optional[str]``: 语音的URL,发送时可作网络语音的链接 + * ``path: Optional[str]``: 语音的路径,发送本地语音 + """ return cls(type=MessageType.FLASH_IMAGE, imageId=voice_id, url=url, @@ -120,22 +210,69 @@ class MessageSegment(BaseMessageSegment): @classmethod def xml(cls, xml: str): + """ + :说明: + + XML消息 + + :参数: + + * ``xml: str``: XML文本 + """ return cls(type=MessageType.XML, xml=xml) @classmethod def json(cls, json: str): + """ + :说明: + + Json消息 + + :参数: + + * ``json: str``: Json文本 + """ return cls(type=MessageType.JSON, json=json) @classmethod def app(cls, content: str): + """ + :说明: + + 应用程序消息 + + :参数: + + * ``content: str``: 内容 + """ return cls(type=MessageType.APP, content=content) @classmethod def poke(cls, name: str): + """ + :说明: + + 戳一戳消息 + + :参数: + + * ``name: str``: 戳一戳的类型 + - "Poke": 戳一戳 + - "ShowLove": 比心 + - "Like": 点赞 + - "Heartbroken": 心碎 + - "SixSixSix": 666 + - "FangDaZhao": 放大招 + """ return cls(type=MessageType.POKE, name=name) -class MessageChain(BaseMessage): #type:List[MessageSegment] +class MessageChain(BaseMessage): + """ + Mirai 协议 Messaqge 适配 + + 由于Mirai协议的Message实现较为特殊, 故使用MessageChain命名 + """ @overrides(BaseMessage) def __init__(self, message: Union[List[Dict[str, Any]], @@ -166,6 +303,7 @@ class MessageChain(BaseMessage): #type:List[MessageSegment] ] def export(self) -> List[Dict[str, Any]]: + """导出为可以被正常json序列化的数组""" return [ *map(lambda segment: segment.as_dict(), self.copy()) # type: ignore ] From 923cbd3b8c5a592fccc652e471e2681a583c633c Mon Sep 17 00:00:00 2001 From: nonebot Date: Sun, 31 Jan 2021 17:05:56 +0000 Subject: [PATCH 21/37] :memo: update api docs --- docs/api/adapters/mirai.md | 396 ++++++++++++++++++++++++++++++++++++- 1 file changed, 387 insertions(+), 9 deletions(-) diff --git a/docs/api/adapters/mirai.md b/docs/api/adapters/mirai.md index 98b1a640..9911ffec 100644 --- a/docs/api/adapters/mirai.md +++ b/docs/api/adapters/mirai.md @@ -5,9 +5,79 @@ sidebarDepth: 0 # NoneBot.adapters.mirai 模块 +## Mirai-API-HTTP 协议适配 + +协议详情请看: + +``` +`mirai-api-http 文档`_ +``` + + + # NoneBot.adapters.mirai.bot 模块 +## _class_ `SessionManager` + +基类:`object` + +Bot会话管理器, 提供API主动调用接口 + + +### _async_ `post(path, *, params=None)` + + +* **说明** + + 以POST方式主动提交API请求 + + + +* **参数** + + + * `path: str`: 对应API路径 + + + * `params: Optional[Dict[str, Any]]`: 请求参数 (无需sessionKey) + + + +* **返回** + + + * `Dict[str, Any]`: API 返回值 + + + +### _async_ `request(path, *, params=None)` + + +* **说明** + + 以GET方式主动提交API请求 + + + +* **参数** + + + * `path: str`: 对应API路径 + + + * `params: Optional[Dict[str, Any]]`: 请求参数 (无需sessionKey) + + + +* **返回** + + + * `Dict[str, Any]`: API 返回值 + + + ## _class_ `MiraiBot` 基类:[`nonebot.adapters.Bot`](README.md#nonebot.adapters.Bot) @@ -15,6 +85,16 @@ sidebarDepth: 0 mirai-api-http 协议 Bot 适配。 +### _property_ `api` + +返回该Bot对象的会话管理实例以提供API主动调用 + + +### _async_ `call_api(api, **data)` + +由于Mirai的HTTP API特殊性, 该API暂时无法实现 + + ### _async_ `send(event, message, at_sender=False)` @@ -608,8 +688,57 @@ mirai-api-http 协议 Bot 适配。 mirai-api-http 正向 Websocket 协议 Bot 适配。 + +### _classmethod_ `register(driver, config, qq)` + + +* **说明** + + 注册该Adapter + + + +* **参数** + + + * `driver: Driver`: 程序所使用的\`\`Driver\`\` + + + * `config: Config`: 程序配置对象 + + + * `qq: int`: 要使用的Bot的QQ号 **注意: 在使用正向Websocket时必须指定该值!** + + + +* **返回** + + + * `[type]`: [description] + + # NoneBot.adapters.mirai.config 模块 + +## _class_ `Config` + +基类:`pydantic.main.BaseModel` + +Mirai 配置类 + + +* **必填** + + + * `mirai_auth_key`: mirai-api-http的auth_key + + + * `mirai_host`: mirai-api-http的地址 + + + * `mirai_port`: mirai-api-http的端口 + + # NoneBot.adapters.mirai.message 模块 @@ -617,10 +746,266 @@ mirai-api-http 正向 Websocket 协议 Bot 适配。 基类:`str`, `enum.Enum` -An enumeration. +消息类型枚举类 + + +## _class_ `MessageSegment` + +基类:[`nonebot.adapters.MessageSegment`](README.md#nonebot.adapters.MessageSegment) + +CQHTTP 协议 MessageSegment 适配。具体方法参考 [mirai-api-http 消息类型](https://github.com/project-mirai/mirai-api-http/blob/master/docs/MessageType.md) + + +### `as_dict()` + +导出可以被正常json序列化的结构体 + + +### _classmethod_ `quote(id, group_id, sender_id, target_id, origin)` + + +* **说明** + + 生成回复引用消息段 + + + +* **参数** + + + * `id: int`: 被引用回复的原消息的message_id + + + * `group_id: int`: 被引用回复的原消息所接收的群号,当为好友消息时为0 + + + * `sender_id: int`: 被引用回复的原消息的发送者的QQ号 + + + * `target_id: int`: 被引用回复的原消息的接收者者的QQ号(或群号) + + + * `origin: MessageChain`: 被引用回复的原消息的消息链对象 + + + +### _classmethod_ `at(target)` + + +* **说明** + + @某个人 + + + +* **参数** + + + * `target: int`: 群员QQ号 + + + +### _classmethod_ `at_all()` + + +* **说明** + + @全体成员 + + + +### _classmethod_ `face(face_id=None, name=None)` + + +* **说明** + + 发送QQ表情 + + + +* **参数** + + + * `face_id: Optional[int]`: QQ表情编号,可选,优先高于name + + + * `name: Optional[str]`: QQ表情拼音,可选 + + + +### _classmethod_ `plain(text)` + + +* **说明** + + 纯文本消息 + + + +* **参数** + + + * `text: str`: 文字消息 + + + +### _classmethod_ `image(image_id=None, url=None, path=None)` + + +* **说明** + + 图片消息 + + + +* **参数** + + + * `image_id: Optional[str]`: 图片的image_id,群图片与好友图片格式不同。不为空时将忽略url属性 + + + * `url: Optional[str]`: 图片的URL,发送时可作网络图片的链接 + + + * `path: Optional[str]`: 图片的路径,发送本地图片 + + + +* **返回** + + + * `[type]`: [description] + + + +### _classmethod_ `flash_image(image_id=None, url=None, path=None)` + + +* **说明** + + 闪照消息 + + + +* **参数** + + 同 `image` + + + +### _classmethod_ `voice(voice_id=None, url=None, path=None)` + + +* **说明** + + 语音消息 + + + +* **参数** + + + * `voice_id: Optional[str]`: 语音的voice_id,不为空时将忽略url属性 + + + * `url: Optional[str]`: 语音的URL,发送时可作网络语音的链接 + + + * `path: Optional[str]`: 语音的路径,发送本地语音 + + + +### _classmethod_ `xml(xml)` + + +* **说明** + + XML消息 + + + +* **参数** + + + * `xml: str`: XML文本 + + + +### _classmethod_ `json(json)` + + +* **说明** + + Json消息 + + + +* **参数** + + + * `json: str`: Json文本 + + + +### _classmethod_ `app(content)` + + +* **说明** + + 应用程序消息 + + + +* **参数** + + + * `content: str`: 内容 + + + +### _classmethod_ `poke(name)` + + +* **说明** + + 戳一戳消息 + + + +* **参数** + + + * `name: str`: 戳一戳的类型 + - "Poke": 戳一戳 + - "ShowLove": 比心 + - "Like": 点赞 + - "Heartbroken": 心碎 + - "SixSixSix": 666 + - "FangDaZhao": 放大招 + + + +## _class_ `MessageChain` + +基类:[`nonebot.adapters.Message`](README.md#nonebot.adapters.Message) + +Mirai 协议 Messaqge 适配 + +由于Mirai协议的Message实现较为特殊, 故使用MessageChain命名 + + +### `export()` + +导出为可以被正常json序列化的数组 # NoneBot.adapters.mirai.event 模块 +:::warning 警告 +事件中为了使代码更加整洁, 我们采用了与PEP8相符的命名规则取代Mirai原有的驼峰命名 + +部分字段可能与文档在符号上不一致 +::: + # NoneBot.adapters.mirai.event.base 模块 @@ -644,7 +1029,7 @@ An enumeration. 基类:[`nonebot.adapters.Event`](README.md#nonebot.adapters.Event) -mirai-api-http 协议事件,字段与 mirai-api-http 一致。各事件字段参考 [mirai-api-http 文档](https://github.com/project-mirai/mirai-api-http/blob/master/docs/EventType.md) +mirai-api-http 协议事件,字段与 mirai-api-http 一致。各事件字段参考 [mirai-api-http 事件类型](https://github.com/project-mirai/mirai-api-http/blob/master/docs/EventType.md) ### _classmethod_ `new(data)` @@ -971,13 +1356,6 @@ Bot在群里的权限被改变 -* **返回** - - - * `[type]`: [description] - - - ## _class_ `MemberJoinRequestEvent` 基类:`nonebot.adapters.mirai.event.request.RequestEvent` From 8fe562e864df0ff3012785c99dbb69eb53bdc550 Mon Sep 17 00:00:00 2001 From: Mix Date: Mon, 1 Feb 2021 13:19:37 +0800 Subject: [PATCH 22/37] :bulb: :children_crossing: complete comments and optimize usage in mirai adapter --- nonebot/adapters/mirai/__init__.py | 12 +- nonebot/adapters/mirai/bot.py | 176 +++++++++++++++-------------- nonebot/adapters/mirai/utils.py | 89 +++++++++++++++ 3 files changed, 189 insertions(+), 88 deletions(-) create mode 100644 nonebot/adapters/mirai/utils.py diff --git a/nonebot/adapters/mirai/__init__.py b/nonebot/adapters/mirai/__init__.py index b209657e..75afaff8 100644 --- a/nonebot/adapters/mirai/__init__.py +++ b/nonebot/adapters/mirai/__init__.py @@ -4,8 +4,18 @@ Mirai-API-HTTP 协议适配 协议详情请看: `mirai-api-http 文档`_ -.. mirai-api-http 文档: +\:\:\: tip +该Adapter目前仍然处在早期实验性阶段, 并未经过充分测试 + +如果你在使用过程中遇到了任何问题, 请前往 `Issue页面`_ 为我们提供反馈 +\:\:\: + +.. _mirai-api-http 文档: https://github.com/project-mirai/mirai-api-http/tree/master/docs + +.. _Issue页面 + https://github.com/nonebot/nonebot2/issues + """ from .bot import MiraiBot diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py index 74c4f602..ed0b9ae1 100644 --- a/nonebot/adapters/mirai/bot.py +++ b/nonebot/adapters/mirai/bot.py @@ -1,34 +1,23 @@ from datetime import datetime, timedelta +from functools import wraps from io import BytesIO from ipaddress import IPv4Address -from typing import Any, Dict, List, NoReturn, Optional, Tuple, Union +from typing import (Any, Dict, List, NoReturn, Optional, Tuple, Union) import httpx from nonebot.adapters import Bot as BaseBot from nonebot.config import Config from nonebot.drivers import Driver, WebSocket -from nonebot.exception import ActionFailed as BaseActionFailed -from nonebot.exception import RequestDenied +from nonebot.exception import ApiNotAvailable, RequestDenied from nonebot.log import logger from nonebot.message import handle_event from nonebot.typing import overrides -from nonebot.utils import escape_tag from .config import Config as MiraiConfig from .event import Event, FriendMessage, GroupMessage, TempMessage from .message import MessageChain, MessageSegment - - -class ActionFailed(BaseActionFailed): - - def __init__(self, **kwargs): - super().__init__('mirai') - self.data = kwargs.copy() - - def __repr__(self): - return self.__class__.__name__ + '(%s)' % ', '.join( - map(lambda m: '%s=%r' % m, self.data.items())) +from .utils import catch_network_error, argument_validation class SessionManager: @@ -39,19 +28,11 @@ class SessionManager: def __init__(self, session_key: str, client: httpx.AsyncClient): self.session_key, self.client = session_key, client - @staticmethod - def _raise_code(data: Dict[str, Any]) -> Dict[str, Any]: - logger.opt(colors=True).debug( - f'Mirai API returned data: {escape_tag(str(data))}') - if isinstance(data, dict) and ('code' in data): - if data['code'] != 0: - raise ActionFailed(**data) - return data - + @catch_network_error async def post(self, path: str, *, - params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + params: Optional[Dict[str, Any]] = None) -> Any: """ :说明: @@ -75,13 +56,13 @@ class SessionManager: timeout=3, ) response.raise_for_status() - return self._raise_code(response.json()) + return response.json() + @catch_network_error async def request(self, path: str, *, - params: Optional[Dict[str, - Any]] = None) -> Dict[str, Any]: + params: Optional[Dict[str, Any]] = None) -> Any: """ :说明: @@ -91,10 +72,6 @@ class SessionManager: * ``path: str``: 对应API路径 * ``params: Optional[Dict[str, Any]]``: 请求参数 (无需sessionKey) - - :返回: - - - ``Dict[str, Any]``: API 返回值 """ response = await self.client.get( path, @@ -105,25 +82,34 @@ class SessionManager: timeout=3, ) response.raise_for_status() - return self._raise_code(response.json()) + return response.json() - async def upload(self, path: str, *, type: str, - file: Tuple[str, BytesIO]) -> Dict[str, Any]: + @catch_network_error + async def upload(self, path: str, *, params: Dict[str, Any]) -> Any: + """ + :说明: - file_type, file_io = file - response = await self.client.post(path, - data={ - 'sessionKey': self.session_key, - 'type': type - }, - files={file_type: file_io}, - timeout=6) + 以表单(``multipart/form-data``)形式主动提交API请求 + + :参数: + + * ``path: str``: 对应API路径 + * ``params: Dict[str, Any]``: 请求参数 (无需sessionKey) + """ + files = {k: v for k, v in params.items() if isinstance(v, BytesIO)} + form = {k: v for k, v in params.items() if k not in files} + response = await self.client.post( + path, + data=form, + files=files, + timeout=6, + ) response.raise_for_status() - return self._raise_code(response.json()) + return response.json() @classmethod async def new(cls, self_id: int, *, host: IPv4Address, port: int, - auth_key: str): + auth_key: str) -> "SessionManager": session = cls.get(self_id) if session is not None: return session @@ -145,7 +131,9 @@ class SessionManager: return cls(session_key, client) @classmethod - def get(cls, self_id: int, check_expire: bool = True): + def get(cls, + self_id: int, + check_expire: bool = True) -> Optional["SessionManager"]: if self_id not in cls.sessions: return None key, time, client = cls.sessions[self_id] @@ -157,6 +145,13 @@ class SessionManager: class MiraiBot(BaseBot): """ mirai-api-http 协议 Bot 适配。 + + \:\:\: warning + API中为了使代码更加整洁, 我们采用了与PEP8相符的命名规则取代Mirai原有的驼峰命名 + + 部分字段可能与文档在符号上不一致 + \:\:\: + """ @overrides(BaseBot) @@ -207,9 +202,9 @@ class MiraiBot(BaseBot): @overrides(BaseBot) def register(cls, driver: "Driver", config: "Config"): cls.mirai_config = MiraiConfig(**config.dict()) - assert cls.mirai_config.auth_key is not None - assert cls.mirai_config.host is not None - assert cls.mirai_config.port is not None + if (cls.mirai_config.auth_key and cls.mirai_config.host and + cls.mirai_config.port) is None: + raise ApiNotAvailable('mirai') super().register(driver, config) @overrides(BaseBot) @@ -222,7 +217,12 @@ class MiraiBot(BaseBot): @overrides(BaseBot) async def call_api(self, api: str, **data) -> NoReturn: - """由于Mirai的HTTP API特殊性, 该API暂时无法实现""" + """ + 由于Mirai的HTTP API特殊性, 该API暂时无法实现 + \:\:\: tip + 你可以使用 ``MiraiBot.api`` 中提供的调用方法来代替 + \:\:\: + """ raise NotImplementedError @overrides(BaseBot) @@ -231,6 +231,7 @@ class MiraiBot(BaseBot): raise NotImplementedError @overrides(BaseBot) + @argument_validation async def send(self, event: Event, message: Union[MessageChain, MessageSegment, str], @@ -245,10 +246,6 @@ class MiraiBot(BaseBot): * ``event: Event``: Event对象 * ``message: Union[MessageChain, MessageSegment, str]``: 要发送的消息 * ``at_sender: bool``: 是否 @ 事件主题 - - :返回: - - - ``Any``: API 调用返回数据 """ if isinstance(message, MessageSegment): message = MessageChain(message) @@ -269,6 +266,7 @@ class MiraiBot(BaseBot): else: raise ValueError(f'Unsupported event type {event!r}.') + @argument_validation async def send_friend_message(self, target: int, message_chain: MessageChain): """ @@ -280,10 +278,6 @@ class MiraiBot(BaseBot): * ``target: int``: 发送消息目标好友的 QQ 号 * ``message_chain: MessageChain``: 消息链,是一个消息对象构成的数组 - - :返回: - - - ``Any``: API 调用返回数据 """ return await self.api.post('sendFriendMessage', params={ @@ -291,6 +285,7 @@ class MiraiBot(BaseBot): 'messageChain': message_chain.export() }) + @argument_validation async def send_temp_message(self, qq: int, group: int, message_chain: MessageChain): """ @@ -303,10 +298,6 @@ class MiraiBot(BaseBot): * ``qq: int``: 临时会话对象 QQ 号 * ``group: int``: 临时会话群号 * ``message_chain: MessageChain``: 消息链,是一个消息对象构成的数组 - - :返回: - - - ``Any``: API 调用返回数据 """ return await self.api.post('sendTempMessage', params={ @@ -315,6 +306,7 @@ class MiraiBot(BaseBot): 'messageChain': message_chain.export() }) + @argument_validation async def send_group_message(self, group: int, message_chain: MessageChain, @@ -329,10 +321,6 @@ class MiraiBot(BaseBot): * ``group: int``: 发送消息目标群的群号 * ``message_chain: MessageChain``: 消息链,是一个消息对象构成的数组 * ``quote: Optional[int]``: 引用一条消息的 message_id 进行回复 - - :返回: - - - ``Any``: API 调用返回数据 """ return await self.api.post('sendGroupMessage', params={ @@ -341,6 +329,7 @@ class MiraiBot(BaseBot): 'quote': quote }) + @argument_validation async def recall(self, target: int): """ :说明: @@ -350,13 +339,10 @@ class MiraiBot(BaseBot): :参数: * ``target: int``: 需要撤回的消息的message_id - - :返回: - - - ``Any``: API 调用返回数据 """ return await self.api.post('recall', params={'target': target}) + @argument_validation async def send_image_message(self, target: int, qq: int, group: int, urls: List[str]) -> List[str]: """ @@ -384,8 +370,9 @@ class MiraiBot(BaseBot): 'qq': qq, 'group': group, 'urls': urls - }) # type: ignore + }) + @argument_validation async def upload_image(self, type: str, img: BytesIO): """ :说明: @@ -396,15 +383,14 @@ class MiraiBot(BaseBot): * ``type: str``: "friend" 或 "group" 或 "temp" * ``img: BytesIO``: 图片的BytesIO对象 - - :返回: - - - ``Any``: API 调用返回数据 """ return await self.api.upload('uploadImage', - type=type, - file=('img', img)) + params={ + 'type': type, + 'img': img + }) + @argument_validation async def upload_voice(self, type: str, voice: BytesIO): """ :说明: @@ -415,15 +401,14 @@ class MiraiBot(BaseBot): * ``type: str``: 当前仅支持 "group" * ``voice: BytesIO``: 语音的BytesIO对象 - - :返回: - - - ``Any``: API 调用返回数据 """ return await self.api.upload('uploadVoice', - type=type, - file=('voice', voice)) + params={ + 'type': type, + 'voice': voice + }) + @argument_validation async def fetch_message(self, count: int = 10): """ :说明: @@ -437,6 +422,7 @@ class MiraiBot(BaseBot): """ return await self.api.request('fetchMessage', params={'count': count}) + @argument_validation async def fetch_latest_message(self, count: int = 10): """ :说明: @@ -451,6 +437,7 @@ class MiraiBot(BaseBot): return await self.api.request('fetchLatestMessage', params={'count': count}) + @argument_validation async def peek_message(self, count: int = 10): """ :说明: @@ -464,6 +451,7 @@ class MiraiBot(BaseBot): """ return await self.api.request('peekMessage', params={'count': count}) + @argument_validation async def peek_latest_message(self, count: int = 10): """ :说明: @@ -478,6 +466,7 @@ class MiraiBot(BaseBot): return await self.api.request('peekLatestMessage', params={'count': count}) + @argument_validation async def messsage_from_id(self, id: int): """ :说明: @@ -491,6 +480,7 @@ class MiraiBot(BaseBot): """ return await self.api.request('messageFromId', params={'id': id}) + @argument_validation async def count_message(self): """ :说明: @@ -499,6 +489,7 @@ class MiraiBot(BaseBot): """ return await self.api.request('countMessage') + @argument_validation async def friend_list(self) -> List[Dict[str, Any]]: """ :说明: @@ -509,8 +500,9 @@ class MiraiBot(BaseBot): - ``List[Dict[str, Any]]``: 返回的好友列表数据 """ - return await self.api.request('friendList') # type: ignore + return await self.api.request('friendList') + @argument_validation async def group_list(self) -> List[Dict[str, Any]]: """ :说明: @@ -521,8 +513,9 @@ class MiraiBot(BaseBot): - ``List[Dict[str, Any]]``: 返回的群列表数据 """ - return await self.api.request('groupList') # type: ignore + return await self.api.request('groupList') + @argument_validation async def member_list(self, target: int) -> List[Dict[str, Any]]: """ :说明: @@ -537,9 +530,9 @@ class MiraiBot(BaseBot): - ``List[Dict[str, Any]]``: 返回的群成员列表数据 """ - return await self.api.request('memberList', - params={'target': target}) # type: ignore + return await self.api.request('memberList', params={'target': target}) + @argument_validation async def mute(self, target: int, member_id: int, time: int): """ :说明: @@ -559,6 +552,7 @@ class MiraiBot(BaseBot): 'time': time }) + @argument_validation async def unmute(self, target: int, member_id: int): """ :说明: @@ -576,6 +570,7 @@ class MiraiBot(BaseBot): 'memberId': member_id }) + @argument_validation async def kick(self, target: int, member_id: int, msg: str): """ :说明: @@ -595,6 +590,7 @@ class MiraiBot(BaseBot): 'msg': msg }) + @argument_validation async def quit(self, target: int): """ :说明: @@ -607,6 +603,7 @@ class MiraiBot(BaseBot): """ return await self.api.post('quit', params={'target': target}) + @argument_validation async def mute_all(self, target: int): """ :说明: @@ -619,6 +616,7 @@ class MiraiBot(BaseBot): """ return await self.api.post('muteAll', params={'target': target}) + @argument_validation async def unmute_all(self, target: int): """ :说明: @@ -631,6 +629,7 @@ class MiraiBot(BaseBot): """ return await self.api.post('unmuteAll', params={'target': target}) + @argument_validation async def group_config(self, target: int): """ :说明: @@ -656,6 +655,7 @@ class MiraiBot(BaseBot): """ return await self.api.request('groupConfig', params={'target': target}) + @argument_validation async def modify_group_config(self, target: int, config: Dict[str, Any]): """ :说明: @@ -673,6 +673,7 @@ class MiraiBot(BaseBot): 'config': config }) + @argument_validation async def member_info(self, target: int, member_id: int): """ :说明: @@ -699,6 +700,7 @@ class MiraiBot(BaseBot): 'memberId': member_id }) + @argument_validation async def modify_member_info(self, target: int, member_id: int, info: Dict[str, Any]): """ diff --git a/nonebot/adapters/mirai/utils.py b/nonebot/adapters/mirai/utils.py new file mode 100644 index 00000000..0a4b4a1b --- /dev/null +++ b/nonebot/adapters/mirai/utils.py @@ -0,0 +1,89 @@ +from functools import wraps +from typing import Callable, Coroutine, TypeVar + +import httpx +from pydantic import ValidationError, validate_arguments, Extra + +import nonebot.exception as exception +from nonebot.log import logger +from nonebot.utils import escape_tag + +_AsyncCallable = TypeVar("_AsyncCallable", bound=Callable[..., Coroutine]) +_AnyCallable = TypeVar("_AnyCallable", bound=Callable) + + +class ActionFailed(exception.ActionFailed): + """ + :说明: + + API 请求成功返回数据,但 API 操作失败。 + """ + + def __init__(self, **kwargs): + super().__init__('mirai') + self.data = kwargs.copy() + + def __repr__(self): + return self.__class__.__name__ + '(%s)' % ', '.join( + map(lambda m: '%s=%r' % m, self.data.items())) + + +class InvalidArgument(exception.AdapterException): + """ + :说明: + + 调用API的参数出错 + """ + + def __init__(self, **kwargs): + super().__init__('mirai') + + +def catch_network_error(function: _AsyncCallable) -> _AsyncCallable: + """ + :说明: + + 捕捉函数抛出的httpx网络异常并释放``NetworkError``异常 + 处理返回数据, 在code不为0时释放``ActionFailed``异常 + + \:\:\: warning + 此装饰器只支持使用了httpx的异步函数 + \:\:\: + """ + + @wraps(function) + async def wrapper(*args, **kwargs): + try: + data = await function(*args, **kwargs) + except httpx.HTTPError: + raise exception.NetworkError('mirai') + logger.opt(colors=True).debug('Mirai API returned data: ' + f'{escape_tag(str(data))}') + if isinstance(data, dict): + if data.get('code', 0) != 0: + raise ActionFailed(**data) + return data + + return wrapper # type: ignore + + +def argument_validation(function: _AnyCallable) -> _AnyCallable: + """ + :说明: + + 通过函数签名中的类型注解来对传入参数进行运行时校验 + 会在参数出错时释放``InvalidArgument``异常 + """ + function = validate_arguments(config={ + 'arbitrary_types_allowed': True, + 'extra': Extra.forbid + })(function) + + @wraps(function) + def wrapper(*args, **kwargs): + try: + return function(*args, **kwargs) + except ValidationError: + raise InvalidArgument + + return wrapper # type: ignore From 35d34a787b131cda40c9222562a7e4effcc86907 Mon Sep 17 00:00:00 2001 From: Mix Date: Mon, 1 Feb 2021 13:20:09 +0800 Subject: [PATCH 23/37] :memo: update document building struct to fit changes in mirai adapter --- docs/.vuepress/config.js | 116 ++++++++++++++++++---------------- docs_build/adapters/mirai.rst | 7 ++ 2 files changed, 67 insertions(+), 56 deletions(-) diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 516d7ce0..6d684ad9 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -1,11 +1,11 @@ const path = require("path"); -module.exports = context => ({ +module.exports = (context) => ({ base: process.env.VUEPRESS_BASE || "/", title: "NoneBot", description: "基于 酷Q 的 Python 异步 QQ 机器人框架", markdown: { - lineNumbers: true + lineNumbers: true, }, /** * Extra tags to be injected to the page HTML `` @@ -21,26 +21,26 @@ module.exports = context => ({ ["meta", { name: "apple-mobile-web-app-capable", content: "yes" }], [ "meta", - { name: "apple-mobile-web-app-status-bar-style", content: "black" } + { name: "apple-mobile-web-app-status-bar-style", content: "black" }, ], [ "link", - { rel: "apple-touch-icon", href: "/icons/apple-touch-icon-180x180.png" } + { rel: "apple-touch-icon", href: "/icons/apple-touch-icon-180x180.png" }, ], [ "link", { rel: "mask-icon", href: "/icons/safari-pinned-tab.svg", - color: "#ea5252" - } + color: "#ea5252", + }, ], [ "meta", { name: "msapplication-TileImage", - content: "/icons/mstile-150x150.png" - } + content: "/icons/mstile-150x150.png", + }, ], ["meta", { name: "msapplication-TileColor", content: "#ea5252" }], [ @@ -48,16 +48,16 @@ module.exports = context => ({ { rel: "stylesheet", href: - "https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5/css/all.min.css" - } - ] + "https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5/css/all.min.css", + }, + ], ], locales: { "/": { lang: "zh-CN", title: "NoneBot", - description: "基于 酷Q 的 Python 异步 QQ 机器人框架" - } + description: "基于 酷Q 的 Python 异步 QQ 机器人框架", + }, }, theme: "nonebot", @@ -83,7 +83,7 @@ module.exports = context => ({ { text: "进阶", link: "/advanced/" }, { text: "API", link: "/api/" }, { text: "插件广场", link: "/plugin-store" }, - { text: "更新日志", link: "/changelog" } + { text: "更新日志", link: "/changelog" }, ], sidebarDepth: 2, sidebar: { @@ -97,8 +97,8 @@ module.exports = context => ({ "installation", "getting-started", "creating-a-project", - "basic-configuration" - ] + "basic-configuration", + ], }, { title: "编写插件", @@ -109,15 +109,15 @@ module.exports = context => ({ "creating-a-plugin", "creating-a-matcher", "creating-a-handler", - "end-or-start" - ] + "end-or-start", + ], }, { title: "协议适配", collapsable: false, sidebar: "auto", - children: ["cqhttp-guide", "ding-guide"] - } + children: ["cqhttp-guide", "ding-guide"], + }, ], "/advanced/": [ { @@ -130,15 +130,15 @@ module.exports = context => ({ "permission", "runtime-hook", "export-and-require", - "overloaded-handlers" - ] + "overloaded-handlers", + ], }, { title: "发布", collapsable: false, sidebar: "auto", - children: ["publish-plugin"] - } + children: ["publish-plugin"], + }, ], "/api/": [ { @@ -148,74 +148,78 @@ module.exports = context => ({ children: [ { title: "nonebot 模块", - path: "nonebot" + path: "nonebot", }, { title: "nonebot.config 模块", - path: "config" + path: "config", }, { title: "nonebot.plugin 模块", - path: "plugin" + path: "plugin", }, { title: "nonebot.message 模块", - path: "message" + path: "message", }, { title: "nonebot.matcher 模块", - path: "matcher" + path: "matcher", }, { title: "nonebot.rule 模块", - path: "rule" + path: "rule", }, { title: "nonebot.permission 模块", - path: "permission" + path: "permission", }, { title: "nonebot.log 模块", - path: "log" + path: "log", }, { title: "nonebot.utils 模块", - path: "utils" + path: "utils", }, { title: "nonebot.typing 模块", - path: "typing" + path: "typing", }, { title: "nonebot.exception 模块", - path: "exception" + path: "exception", }, { title: "nonebot.drivers 模块", - path: "drivers/" + path: "drivers/", }, { title: "nonebot.drivers.fastapi 模块", - path: "drivers/fastapi" + path: "drivers/fastapi", }, { title: "nonebot.adapters 模块", - path: "adapters/" + path: "adapters/", }, { title: "nonebot.adapters.cqhttp 模块", - path: "adapters/cqhttp" + path: "adapters/cqhttp", }, { title: "nonebot.adapters.ding 模块", - path: "adapters/ding" - } - ] - } - ] - } - } - } + path: "adapters/ding", + }, + { + title: "nonebot.adapters.mirai 模块", + path: "adapters/mirai", + }, + ], + }, + ], + }, + }, + }, }, plugins: [ @@ -227,9 +231,9 @@ module.exports = context => ({ serviceWorker: true, updatePopup: { message: "发现新内容", - buttonText: "刷新" - } - } + buttonText: "刷新", + }, + }, ], [ "versioning", @@ -238,16 +242,16 @@ module.exports = context => ({ pagesSourceDir: path.resolve(context.sourceDir, "..", "pages"), onNewVersion(version, versionDestPath) { console.log(`Created version ${version} in ${versionDestPath}`); - } - } + }, + }, ], [ "container", { type: "vue", before: '
',
-        after: "
" - } - ] - ] + after: "", + }, + ], + ], }); diff --git a/docs_build/adapters/mirai.rst b/docs_build/adapters/mirai.rst index 6f15695b..a2f6a9c6 100644 --- a/docs_build/adapters/mirai.rst +++ b/docs_build/adapters/mirai.rst @@ -36,6 +36,13 @@ NoneBot.adapters.mirai.message 模块 :members: :show-inheritance: +NoneBot.adapters.mirai.utils 模块 +=================================== + +.. automodule:: nonebot.adapters.mirai.utils + :members: + :show-inheritance: + NoneBot.adapters.mirai.event 模块 ================================= From 6a273a8eeabec6a1d725e71defa18e0a603238a9 Mon Sep 17 00:00:00 2001 From: nonebot Date: Mon, 1 Feb 2021 05:21:33 +0000 Subject: [PATCH 24/37] :memo: update api docs --- docs/api/adapters/mirai.md | 190 +++++++++++++++++++++---------------- 1 file changed, 108 insertions(+), 82 deletions(-) diff --git a/docs/api/adapters/mirai.md b/docs/api/adapters/mirai.md index 9911ffec..c320d75c 100644 --- a/docs/api/adapters/mirai.md +++ b/docs/api/adapters/mirai.md @@ -7,15 +7,21 @@ sidebarDepth: 0 ## Mirai-API-HTTP 协议适配 -协议详情请看: +协议详情请看: [mirai-api-http 文档](https://github.com/project-mirai/mirai-api-http/tree/master/docs) + +::: tip +该Adapter目前仍然处在早期实验性阶段, 并未经过充分测试 + +如果你在使用过程中遇到了任何问题, 请前往 ``` -`mirai-api-http 文档`_ +`Issue页面`_ ``` + 为我们提供反馈 +::: - + # NoneBot.adapters.mirai.bot 模块 @@ -71,10 +77,22 @@ Bot会话管理器, 提供API主动调用接口 -* **返回** +### _async_ `upload(path, *, params)` + + +* **说明** + + 以表单(`multipart/form-data`)形式主动提交API请求 + + + +* **参数** - * `Dict[str, Any]`: API 返回值 + * `path: str`: 对应API路径 + + + * `params: Dict[str, Any]`: 请求参数 (无需sessionKey) @@ -84,6 +102,12 @@ Bot会话管理器, 提供API主动调用接口 mirai-api-http 协议 Bot 适配。 +::: warning +API中为了使代码更加整洁, 我们采用了与PEP8相符的命名规则取代Mirai原有的驼峰命名 + +部分字段可能与文档在符号上不一致 +::: + ### _property_ `api` @@ -93,9 +117,12 @@ mirai-api-http 协议 Bot 适配。 ### _async_ `call_api(api, **data)` 由于Mirai的HTTP API特殊性, 该API暂时无法实现 +::: tip +你可以使用 `MiraiBot.api` 中提供的调用方法来代替 +::: -### _async_ `send(event, message, at_sender=False)` +### `send(event, message, at_sender=False)` * **说明** @@ -117,14 +144,7 @@ mirai-api-http 协议 Bot 适配。 -* **返回** - - - * `Any`: API 调用返回数据 - - - -### _async_ `send_friend_message(target, message_chain)` +### `send_friend_message(target, message_chain)` * **说明** @@ -143,14 +163,7 @@ mirai-api-http 协议 Bot 适配。 -* **返回** - - - * `Any`: API 调用返回数据 - - - -### _async_ `send_temp_message(qq, group, message_chain)` +### `send_temp_message(qq, group, message_chain)` * **说明** @@ -172,14 +185,7 @@ mirai-api-http 协议 Bot 适配。 -* **返回** - - - * `Any`: API 调用返回数据 - - - -### _async_ `send_group_message(group, message_chain, quote=None)` +### `send_group_message(group, message_chain, quote=None)` * **说明** @@ -201,14 +207,7 @@ mirai-api-http 协议 Bot 适配。 -* **返回** - - - * `Any`: API 调用返回数据 - - - -### _async_ `recall(target)` +### `recall(target)` * **说明** @@ -224,14 +223,7 @@ mirai-api-http 协议 Bot 适配。 -* **返回** - - - * `Any`: API 调用返回数据 - - - -### _async_ `send_image_message(target, qq, group, urls)` +### `send_image_message(target, qq, group, urls)` * **说明** @@ -266,7 +258,7 @@ mirai-api-http 协议 Bot 适配。 -### _async_ `upload_image(type, img)` +### `upload_image(type, img)` * **说明** @@ -285,14 +277,7 @@ mirai-api-http 协议 Bot 适配。 -* **返回** - - - * `Any`: API 调用返回数据 - - - -### _async_ `upload_voice(type, voice)` +### `upload_voice(type, voice)` * **说明** @@ -311,14 +296,7 @@ mirai-api-http 协议 Bot 适配。 -* **返回** - - - * `Any`: API 调用返回数据 - - - -### _async_ `fetch_message(count=10)` +### `fetch_message(count=10)` * **说明** @@ -335,7 +313,7 @@ mirai-api-http 协议 Bot 适配。 -### _async_ `fetch_latest_message(count=10)` +### `fetch_latest_message(count=10)` * **说明** @@ -352,7 +330,7 @@ mirai-api-http 协议 Bot 适配。 -### _async_ `peek_message(count=10)` +### `peek_message(count=10)` * **说明** @@ -369,7 +347,7 @@ mirai-api-http 协议 Bot 适配。 -### _async_ `peek_latest_message(count=10)` +### `peek_latest_message(count=10)` * **说明** @@ -386,7 +364,7 @@ mirai-api-http 协议 Bot 适配。 -### _async_ `messsage_from_id(id)` +### `messsage_from_id(id)` * **说明** @@ -403,7 +381,7 @@ mirai-api-http 协议 Bot 适配。 -### _async_ `count_message()` +### `count_message()` * **说明** @@ -412,7 +390,7 @@ mirai-api-http 协议 Bot 适配。 -### _async_ `friend_list()` +### `friend_list()` * **说明** @@ -428,7 +406,7 @@ mirai-api-http 协议 Bot 适配。 -### _async_ `group_list()` +### `group_list()` * **说明** @@ -444,7 +422,7 @@ mirai-api-http 协议 Bot 适配。 -### _async_ `member_list(target)` +### `member_list(target)` * **说明** @@ -467,7 +445,7 @@ mirai-api-http 协议 Bot 适配。 -### _async_ `mute(target, member_id, time)` +### `mute(target, member_id, time)` * **说明** @@ -489,7 +467,7 @@ mirai-api-http 协议 Bot 适配。 -### _async_ `unmute(target, member_id)` +### `unmute(target, member_id)` * **说明** @@ -508,7 +486,7 @@ mirai-api-http 协议 Bot 适配。 -### _async_ `kick(target, member_id, msg)` +### `kick(target, member_id, msg)` * **说明** @@ -530,7 +508,7 @@ mirai-api-http 协议 Bot 适配。 -### _async_ `quit(target)` +### `quit(target)` * **说明** @@ -546,7 +524,7 @@ mirai-api-http 协议 Bot 适配。 -### _async_ `mute_all(target)` +### `mute_all(target)` * **说明** @@ -562,7 +540,7 @@ mirai-api-http 协议 Bot 适配。 -### _async_ `unmute_all(target)` +### `unmute_all(target)` * **说明** @@ -578,7 +556,7 @@ mirai-api-http 协议 Bot 适配。 -### _async_ `group_config(target)` +### `group_config(target)` * **说明** @@ -609,7 +587,7 @@ mirai-api-http 协议 Bot 适配。 ``` -### _async_ `modify_group_config(target, config)` +### `modify_group_config(target, config)` * **说明** @@ -628,7 +606,7 @@ mirai-api-http 协议 Bot 适配。 -### _async_ `member_info(target, member_id)` +### `member_info(target, member_id)` * **说明** @@ -658,7 +636,7 @@ mirai-api-http 协议 Bot 适配。 ``` -### _async_ `modify_member_info(target, member_id, info)` +### `modify_member_info(target, member_id, info)` * **说明** @@ -998,6 +976,54 @@ Mirai 协议 Messaqge 适配 导出为可以被正常json序列化的数组 +# NoneBot.adapters.mirai.utils 模块 + + +## _exception_ `ActionFailed` + +基类:[`nonebot.exception.ActionFailed`](../exception.md#nonebot.exception.ActionFailed) + + +* **说明** + + API 请求成功返回数据,但 API 操作失败。 + + + +## _exception_ `InvalidArgument` + +基类:[`nonebot.exception.AdapterException`](../exception.md#nonebot.exception.AdapterException) + + +* **说明** + + 调用API的参数出错 + + + +## `catch_network_error(function)` + + +* **说明** + + 捕捉函数抛出的httpx网络异常并释放\`\`NetworkError\`\`异常 + 处理返回数据, 在code不为0时释放\`\`ActionFailed\`\`异常 + + +::: warning +此装饰器只支持使用了httpx的异步函数 +::: + + +## `argument_validation(function)` + + +* **说明** + + 通过函数签名中的类型注解来对传入参数进行运行时校验 + 会在参数出错时释放\`\`InvalidArgument\`\`异常 + + # NoneBot.adapters.mirai.event 模块 :::warning 警告 From d2a62ebd3dd73c23c60d9cff27b9a52169b557a8 Mon Sep 17 00:00:00 2001 From: Mix Date: Mon, 1 Feb 2021 13:50:14 +0800 Subject: [PATCH 25/37] :pencil: :bulb: fix some typo and style in mirai adapter --- nonebot/adapters/mirai/__init__.py | 2 +- nonebot/adapters/mirai/bot.py | 7 +++++-- nonebot/adapters/mirai/bot_ws.py | 1 - nonebot/adapters/mirai/event/__init__.py | 7 ++++--- nonebot/adapters/mirai/event/base.py | 10 ++++++---- nonebot/adapters/mirai/event/request.py | 16 ++++++++++------ nonebot/adapters/mirai/utils.py | 8 +++++--- 7 files changed, 31 insertions(+), 20 deletions(-) diff --git a/nonebot/adapters/mirai/__init__.py b/nonebot/adapters/mirai/__init__.py index 75afaff8..15bc12d0 100644 --- a/nonebot/adapters/mirai/__init__.py +++ b/nonebot/adapters/mirai/__init__.py @@ -13,7 +13,7 @@ Mirai-API-HTTP 协议适配 .. _mirai-api-http 文档: https://github.com/project-mirai/mirai-api-http/tree/master/docs -.. _Issue页面 +.. _Issue页面: https://github.com/nonebot/nonebot2/issues """ diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py index ed0b9ae1..6c4023e2 100644 --- a/nonebot/adapters/mirai/bot.py +++ b/nonebot/adapters/mirai/bot.py @@ -218,7 +218,10 @@ class MiraiBot(BaseBot): @overrides(BaseBot) async def call_api(self, api: str, **data) -> NoReturn: """ + \:\:\: danger 由于Mirai的HTTP API特殊性, 该API暂时无法实现 + \:\:\: + \:\:\: tip 你可以使用 ``MiraiBot.api`` 中提供的调用方法来代替 \:\:\: @@ -239,13 +242,13 @@ class MiraiBot(BaseBot): """ :说明: - 根据 ``event`` 向触发事件的主题发送信息 + 根据 ``event`` 向触发事件的主体发送信息 :参数: * ``event: Event``: Event对象 * ``message: Union[MessageChain, MessageSegment, str]``: 要发送的消息 - * ``at_sender: bool``: 是否 @ 事件主题 + * ``at_sender: bool``: 是否 @ 事件主体 """ if isinstance(message, MessageSegment): message = MessageChain(message) diff --git a/nonebot/adapters/mirai/bot_ws.py b/nonebot/adapters/mirai/bot_ws.py index ccce63b3..c10382ba 100644 --- a/nonebot/adapters/mirai/bot_ws.py +++ b/nonebot/adapters/mirai/bot_ws.py @@ -15,7 +15,6 @@ from nonebot.log import logger from nonebot.typing import overrides from .bot import MiraiBot, SessionManager -from .config import Config as MiraiConfig WebsocketHandlerFunction = Callable[[Dict[str, Any]], Coroutine[Any, Any, None]] WebsocketHandler_T = TypeVar('WebsocketHandler_T', diff --git a/nonebot/adapters/mirai/event/__init__.py b/nonebot/adapters/mirai/event/__init__.py index c0024e19..cc763f65 100644 --- a/nonebot/adapters/mirai/event/__init__.py +++ b/nonebot/adapters/mirai/event/__init__.py @@ -1,11 +1,12 @@ """ -\:\:\:warning 警告 +\:\:\: warning 事件中为了使代码更加整洁, 我们采用了与PEP8相符的命名规则取代Mirai原有的驼峰命名 部分字段可能与文档在符号上不一致 \:\:\: """ -from .base import Event, GroupChatInfo, GroupInfo, UserPermission, PrivateChatInfo +from .base import (Event, GroupChatInfo, GroupInfo, PrivateChatInfo, + UserPermission) from .message import * from .notice import * -from .request import * \ No newline at end of file +from .request import * diff --git a/nonebot/adapters/mirai/event/base.py b/nonebot/adapters/mirai/event/base.py index 662b856d..1362bd74 100644 --- a/nonebot/adapters/mirai/event/base.py +++ b/nonebot/adapters/mirai/event/base.py @@ -13,11 +13,13 @@ from nonebot.typing import overrides class UserPermission(str, Enum): """ - 用户权限枚举类 + :说明: - - ``OWNER``: 群主 - - ``ADMINISTRATOR``: 群管理 - - ``MEMBER``: 普通群成员 + 用户权限枚举类 + + * ``OWNER``: 群主 + * ``ADMINISTRATOR``: 群管理 + * ``MEMBER``: 普通群成员 """ OWNER = 'OWNER' ADMINISTRATOR = 'ADMINISTRATOR' diff --git a/nonebot/adapters/mirai/event/request.py b/nonebot/adapters/mirai/event/request.py index 18d466ee..623c52dd 100644 --- a/nonebot/adapters/mirai/event/request.py +++ b/nonebot/adapters/mirai/event/request.py @@ -52,8 +52,10 @@ class NewFriendRequestEvent(RequestEvent): * ``bot: Bot``: 当前的 ``Bot`` 对象 * ``operate: Literal[1, 2]``: 响应的操作类型 - - ``1``: 拒绝添加好友 - - ``2``: 拒绝添加好友并添加黑名单,不再接收该用户的好友申请 + + * ``1``: 拒绝添加好友 + * ``2``: 拒绝添加好友并添加黑名单,不再接收该用户的好友申请 + * ``message: str``: 回复的信息 """ assert operate > 0 @@ -104,10 +106,12 @@ class MemberJoinRequestEvent(RequestEvent): * ``bot: Bot``: 当前的 ``Bot`` 对象 * ``operate: Literal[1, 2, 3, 4]``: 响应的操作类型 - - ``1``: 拒绝入群 - - ``2``: 忽略请求 - - ``3``: 拒绝入群并添加黑名单,不再接收该用户的入群申请 - - ``4``: 忽略入群并添加黑名单,不再接收该用户的入群申请 + + * ``1``: 拒绝入群 + * ``2``: 忽略请求 + * ``3``: 拒绝入群并添加黑名单,不再接收该用户的入群申请 + * ``4``: 忽略入群并添加黑名单,不再接收该用户的入群申请 + * ``message: str``: 回复的信息 """ assert operate > 0 diff --git a/nonebot/adapters/mirai/utils.py b/nonebot/adapters/mirai/utils.py index 0a4b4a1b..30adf42d 100644 --- a/nonebot/adapters/mirai/utils.py +++ b/nonebot/adapters/mirai/utils.py @@ -43,8 +43,9 @@ def catch_network_error(function: _AsyncCallable) -> _AsyncCallable: """ :说明: - 捕捉函数抛出的httpx网络异常并释放``NetworkError``异常 - 处理返回数据, 在code不为0时释放``ActionFailed``异常 + 捕捉函数抛出的httpx网络异常并释放 ``NetworkError`` 异常 + + 处理返回数据, 在code不为0时释放 ``ActionFailed`` 异常 \:\:\: warning 此装饰器只支持使用了httpx的异步函数 @@ -72,7 +73,8 @@ def argument_validation(function: _AnyCallable) -> _AnyCallable: :说明: 通过函数签名中的类型注解来对传入参数进行运行时校验 - 会在参数出错时释放``InvalidArgument``异常 + + 会在参数出错时释放 ``InvalidArgument`` 异常 """ function = validate_arguments(config={ 'arbitrary_types_allowed': True, From 6c0b20e5b7694ddc05c3797ff26c962aac392e61 Mon Sep 17 00:00:00 2001 From: nonebot Date: Mon, 1 Feb 2021 05:51:45 +0000 Subject: [PATCH 26/37] :memo: update api docs --- docs/api/adapters/mirai.md | 54 ++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/docs/api/adapters/mirai.md b/docs/api/adapters/mirai.md index c320d75c..b669222b 100644 --- a/docs/api/adapters/mirai.md +++ b/docs/api/adapters/mirai.md @@ -12,16 +12,9 @@ sidebarDepth: 0 ::: tip 该Adapter目前仍然处在早期实验性阶段, 并未经过充分测试 -如果你在使用过程中遇到了任何问题, 请前往 - -``` -`Issue页面`_ -``` - - 为我们提供反馈 +如果你在使用过程中遇到了任何问题, 请前往 [Issue页面](https://github.com/nonebot/nonebot2/issues) 为我们提供反馈 ::: - # NoneBot.adapters.mirai.bot 模块 @@ -116,7 +109,10 @@ API中为了使代码更加整洁, 我们采用了与PEP8相符的命名规则 ### _async_ `call_api(api, **data)` +::: danger 由于Mirai的HTTP API特殊性, 该API暂时无法实现 +::: + ::: tip 你可以使用 `MiraiBot.api` 中提供的调用方法来代替 ::: @@ -127,7 +123,7 @@ API中为了使代码更加整洁, 我们采用了与PEP8相符的命名规则 * **说明** - 根据 `event` 向触发事件的主题发送信息 + 根据 `event` 向触发事件的主体发送信息 @@ -140,7 +136,7 @@ API中为了使代码更加整洁, 我们采用了与PEP8相符的命名规则 * `message: Union[MessageChain, MessageSegment, str]`: 要发送的消息 - * `at_sender: bool`: 是否 @ 事件主题 + * `at_sender: bool`: 是否 @ 事件主体 @@ -1006,8 +1002,9 @@ Mirai 协议 Messaqge 适配 * **说明** - 捕捉函数抛出的httpx网络异常并释放\`\`NetworkError\`\`异常 - 处理返回数据, 在code不为0时释放\`\`ActionFailed\`\`异常 + 捕捉函数抛出的httpx网络异常并释放 `NetworkError` 异常 + + 处理返回数据, 在code不为0时释放 `ActionFailed` 异常 ::: warning @@ -1021,12 +1018,13 @@ Mirai 协议 Messaqge 适配 * **说明** 通过函数签名中的类型注解来对传入参数进行运行时校验 - 会在参数出错时释放\`\`InvalidArgument\`\`异常 + + 会在参数出错时释放 `InvalidArgument` 异常 # NoneBot.adapters.mirai.event 模块 -:::warning 警告 +::: warning 事件中为了使代码更加整洁, 我们采用了与PEP8相符的命名规则取代Mirai原有的驼峰命名 部分字段可能与文档在符号上不一致 @@ -1039,6 +1037,10 @@ Mirai 协议 Messaqge 适配 基类:`str`, `enum.Enum` + +* **说明** + + 用户权限枚举类 > @@ -1374,8 +1376,12 @@ Bot在群里的权限被改变 * `operate: Literal[1, 2]`: 响应的操作类型 - - `1`: 拒绝添加好友 - - `2`: 拒绝添加好友并添加黑名单,不再接收该用户的好友申请 + + + * `1`: 拒绝添加好友 + + + * `2`: 拒绝添加好友并添加黑名单,不再接收该用户的好友申请 * `message: str`: 回复的信息 @@ -1421,10 +1427,18 @@ Bot在群里的权限被改变 * `operate: Literal[1, 2, 3, 4]`: 响应的操作类型 - - `1`: 拒绝入群 - - `2`: 忽略请求 - - `3`: 拒绝入群并添加黑名单,不再接收该用户的入群申请 - - `4`: 忽略入群并添加黑名单,不再接收该用户的入群申请 + + + * `1`: 拒绝入群 + + + * `2`: 忽略请求 + + + * `3`: 拒绝入群并添加黑名单,不再接收该用户的入群申请 + + + * `4`: 忽略入群并添加黑名单,不再接收该用户的入群申请 * `message: str`: 回复的信息 From 5a63827f2243f2226f5079a0e80e8c9eca5d7e95 Mon Sep 17 00:00:00 2001 From: Mix Date: Mon, 1 Feb 2021 14:24:45 +0800 Subject: [PATCH 27/37] :speech_balloon: :bulb: rename MiraiBot to Bot, fix a comment style --- nonebot/adapters/mirai/__init__.py | 4 ++-- nonebot/adapters/mirai/bot.py | 2 +- nonebot/adapters/mirai/bot_ws.py | 17 +++++++++-------- nonebot/adapters/mirai/event/__init__.py | 17 +++++++++++++++++ nonebot/adapters/mirai/event/request.py | 2 +- nonebot/adapters/mirai/message.py | 14 ++++++++------ 6 files changed, 38 insertions(+), 18 deletions(-) diff --git a/nonebot/adapters/mirai/__init__.py b/nonebot/adapters/mirai/__init__.py index 15bc12d0..c083a8a7 100644 --- a/nonebot/adapters/mirai/__init__.py +++ b/nonebot/adapters/mirai/__init__.py @@ -18,7 +18,7 @@ Mirai-API-HTTP 协议适配 """ -from .bot import MiraiBot -from .bot_ws import MiraiWebsocketBot +from .bot import Bot +from .bot_ws import WebsocketBot from .event import * from .message import MessageChain, MessageSegment diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py index 6c4023e2..3182d7a8 100644 --- a/nonebot/adapters/mirai/bot.py +++ b/nonebot/adapters/mirai/bot.py @@ -142,7 +142,7 @@ class SessionManager: return cls(key, client) -class MiraiBot(BaseBot): +class Bot(BaseBot): """ mirai-api-http 协议 Bot 适配。 diff --git a/nonebot/adapters/mirai/bot_ws.py b/nonebot/adapters/mirai/bot_ws.py index c10382ba..fdf35c0c 100644 --- a/nonebot/adapters/mirai/bot_ws.py +++ b/nonebot/adapters/mirai/bot_ws.py @@ -14,7 +14,7 @@ from nonebot.exception import RequestDenied from nonebot.log import logger from nonebot.typing import overrides -from .bot import MiraiBot, SessionManager +from .bot import SessionManager, Bot WebsocketHandlerFunction = Callable[[Dict[str, Any]], Coroutine[Any, Any, None]] WebsocketHandler_T = TypeVar('WebsocketHandler_T', @@ -71,8 +71,9 @@ class WebSocket(BaseWebSocket): logger.exception(f'Websocket client listened {self.websocket} ' f'failed to decode data: {e}') continue - asyncio.gather(*map(lambda f: f(data), self.event_handlers), - return_exceptions=True) + asyncio.gather( + *map(lambda f: f(data), self.event_handlers), #type: ignore + return_exceptions=True) @overrides(BaseWebSocket) async def accept(self): @@ -87,18 +88,18 @@ class WebSocket(BaseWebSocket): return callable -class MiraiWebsocketBot(MiraiBot): +class WebsocketBot(Bot): """ mirai-api-http 正向 Websocket 协议 Bot 适配。 """ - @overrides(MiraiBot) + @overrides(Bot) def __init__(self, connection_type: str, self_id: str, *, websocket: WebSocket): super().__init__(connection_type, self_id, websocket=websocket) @property - @overrides(MiraiBot) + @overrides(Bot) def type(self) -> str: return "mirai-ws" @@ -113,7 +114,7 @@ class MiraiWebsocketBot(MiraiBot): return api @classmethod - @overrides(MiraiBot) + @overrides(Bot) async def check_permission(cls, driver: "Driver", connection_type: str, headers: dict, body: Optional[dict]) -> NoReturn: raise RequestDenied( @@ -121,7 +122,7 @@ class MiraiWebsocketBot(MiraiBot): reason=f'Connection {connection_type} not implented') @classmethod - @overrides(MiraiBot) + @overrides(Bot) def register(cls, driver: "Driver", config: "Config", qq: int): """ :说明: diff --git a/nonebot/adapters/mirai/event/__init__.py b/nonebot/adapters/mirai/event/__init__.py index cc763f65..1cf92096 100644 --- a/nonebot/adapters/mirai/event/__init__.py +++ b/nonebot/adapters/mirai/event/__init__.py @@ -10,3 +10,20 @@ from .base import (Event, GroupChatInfo, GroupInfo, PrivateChatInfo, from .message import * from .notice import * from .request import * + +__all__ = [ + 'Event', 'GroupChatInfo', 'GroupInfo', 'PrivateChatInfo', 'UserPermission', + 'MessageChain', 'MessageEvent', 'GroupMessage', 'FriendMessage', + 'TempMessage', 'NoticeEvent', 'MuteEvent', 'BotMuteEvent', 'BotUnmuteEvent', + 'MemberMuteEvent', 'MemberUnmuteEvent', 'BotJoinGroupEvent', + 'BotLeaveEventActive', 'BotLeaveEventKick', 'MemberJoinEvent', + 'MemberLeaveEventKick', 'MemberLeaveEventQuit', 'FriendRecallEvent', + 'GroupRecallEvent', 'GroupStateChangeEvent', 'GroupNameChangeEvent', + 'GroupEntranceAnnouncementChangeEvent', 'GroupMuteAllEvent', + 'GroupAllowAnonymousChatEvent', 'GroupAllowConfessTalkEvent', + 'GroupAllowMemberInviteEvent', 'MemberStateChangeEvent', + 'MemberCardChangeEvent', 'MemberSpecialTitleChangeEvent', + 'BotGroupPermissionChangeEvent', 'MemberPermissionChangeEvent', + 'RequestEvent', 'NewFriendRequestEvent', 'MemberJoinRequestEvent', + 'BotInvitedJoinGroupRequestEvent' +] diff --git a/nonebot/adapters/mirai/event/request.py b/nonebot/adapters/mirai/event/request.py index 623c52dd..3bf82f01 100644 --- a/nonebot/adapters/mirai/event/request.py +++ b/nonebot/adapters/mirai/event/request.py @@ -6,7 +6,7 @@ from typing_extensions import Literal from .base import Event if TYPE_CHECKING: - from ..bot import MiraiBot as Bot + from ..bot import Bot class RequestEvent(Event): diff --git a/nonebot/adapters/mirai/message.py b/nonebot/adapters/mirai/message.py index 265b3b3b..f1aff156 100644 --- a/nonebot/adapters/mirai/message.py +++ b/nonebot/adapters/mirai/message.py @@ -257,12 +257,14 @@ class MessageSegment(BaseMessageSegment): :参数: * ``name: str``: 戳一戳的类型 - - "Poke": 戳一戳 - - "ShowLove": 比心 - - "Like": 点赞 - - "Heartbroken": 心碎 - - "SixSixSix": 666 - - "FangDaZhao": 放大招 + + * ``Poke``: 戳一戳 + * ``ShowLove``: 比心 + * ``Like``: 点赞 + * ``Heartbroken``: 心碎 + * ``SixSixSix``: 666 + * ``FangDaZhao``: 放大招 + """ return cls(type=MessageType.POKE, name=name) From c0fa137fed42529712fd6df3b267435a4357d28b Mon Sep 17 00:00:00 2001 From: Mix Date: Mon, 1 Feb 2021 16:31:51 +0800 Subject: [PATCH 28/37] :children_crossing: add support of rule `to_me` in mirai adapter --- nonebot/adapters/mirai/bot.py | 24 ++++++--- nonebot/adapters/mirai/event/base.py | 4 +- nonebot/adapters/mirai/event/message.py | 21 +++++++- nonebot/adapters/mirai/utils.py | 71 +++++++++++++++++++++++-- 4 files changed, 107 insertions(+), 13 deletions(-) diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py index 3182d7a8..0a1262c7 100644 --- a/nonebot/adapters/mirai/bot.py +++ b/nonebot/adapters/mirai/bot.py @@ -13,11 +13,12 @@ from nonebot.exception import ApiNotAvailable, RequestDenied from nonebot.log import logger from nonebot.message import handle_event from nonebot.typing import overrides +from nonebot.utils import escape_tag from .config import Config as MiraiConfig from .event import Event, FriendMessage, GroupMessage, TempMessage from .message import MessageChain, MessageSegment -from .utils import catch_network_error, argument_validation +from .utils import catch_network_error, argument_validation, check_tome, Log class SessionManager: @@ -209,11 +210,22 @@ class Bot(BaseBot): @overrides(BaseBot) async def handle_message(self, message: dict): - await handle_event(bot=self, - event=Event.new({ - **message, - 'self_id': self.self_id, - })) + Log.debug(f'received message {message}') + try: + await handle_event( + bot=self, + event=await check_tome( + bot=self, + event=Event.new({ + **message, + 'self_id': self.self_id, + }), + ), + ) + except Exception as e: + logger.opt(colors=True, exception=e).exception( + 'Failed to handle message ' + f'{escape_tag(str(message))}: ') @overrides(BaseBot) async def call_api(self, api: str, **data) -> NoReturn: diff --git a/nonebot/adapters/mirai/event/base.py b/nonebot/adapters/mirai/event/base.py index 1362bd74..4a7b3809 100644 --- a/nonebot/adapters/mirai/event/base.py +++ b/nonebot/adapters/mirai/event/base.py @@ -80,8 +80,8 @@ class Event(BaseEvent): 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.') + f'Failed to parse {data} to class {event_class.__name__}: ' + f'{e.errors()!r}. Fallback to parent class.') event_class = event_class.__base__ raise ValueError(f'Failed to serialize {data}.') diff --git a/nonebot/adapters/mirai/event/message.py b/nonebot/adapters/mirai/event/message.py index 6021ea64..26d534d4 100644 --- a/nonebot/adapters/mirai/event/message.py +++ b/nonebot/adapters/mirai/event/message.py @@ -33,11 +33,20 @@ class MessageEvent(Event): class GroupMessage(MessageEvent): """群消息事件""" sender: GroupChatInfo + to_me: bool = False @overrides(MessageEvent) def get_session_id(self) -> str: return f'group_{self.sender.group.id}_' + self.get_user_id() + @overrides(MessageEvent) + def get_user_id(self) -> str: + return str(self.sender.id) + + @overrides(MessageEvent) + def is_tome(self) -> bool: + return self.to_me + class FriendMessage(MessageEvent): """好友消息事件""" @@ -47,15 +56,23 @@ class FriendMessage(MessageEvent): def get_user_id(self) -> str: return str(self.sender.id) - @overrides + @overrides(MessageEvent) def get_session_id(self) -> str: return 'friend_' + self.get_user_id() + @overrides(MessageEvent) + def is_tome(self) -> bool: + return True + class TempMessage(MessageEvent): """临时会话消息事件""" sender: GroupChatInfo - @overrides + @overrides(MessageEvent) def get_session_id(self) -> str: return f'temp_{self.sender.group.id}_' + self.get_user_id() + + @overrides(MessageEvent) + def is_tome(self) -> bool: + return True diff --git a/nonebot/adapters/mirai/utils.py b/nonebot/adapters/mirai/utils.py index 30adf42d..cb2b5e2d 100644 --- a/nonebot/adapters/mirai/utils.py +++ b/nonebot/adapters/mirai/utils.py @@ -1,17 +1,44 @@ +import re from functools import wraps -from typing import Callable, Coroutine, TypeVar +from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional, TypeVar import httpx -from pydantic import ValidationError, validate_arguments, Extra +from pydantic import Extra, ValidationError, validate_arguments import nonebot.exception as exception from nonebot.log import logger -from nonebot.utils import escape_tag +from nonebot.utils import escape_tag, logger_wrapper + +from .event import Event, GroupMessage +from .message import MessageSegment, MessageType + +if TYPE_CHECKING: + from .bot import Bot _AsyncCallable = TypeVar("_AsyncCallable", bound=Callable[..., Coroutine]) _AnyCallable = TypeVar("_AnyCallable", bound=Callable) +class Log: + _log = logger_wrapper('MIRAI') + + @classmethod + def info(cls, message: Any): + cls._log('INFO', str(message)) + + @classmethod + def debug(cls, message: Any): + cls._log('DEBUG', str(message)) + + @classmethod + def warn(cls, message: Any): + cls._log('WARNING', str(message)) + + @classmethod + def error(cls, message: Any, exception: Optional[Exception] = None): + cls._log('ERROR', str(message), exception=exception) + + class ActionFailed(exception.ActionFailed): """ :说明: @@ -89,3 +116,41 @@ def argument_validation(function: _AnyCallable) -> _AnyCallable: raise InvalidArgument return wrapper # type: ignore + + +async def check_tome(bot: "Bot", event: "Event") -> "Event": + if not isinstance(event, GroupMessage): + return event + + def _is_at(event: GroupMessage) -> bool: + for segment in event.message_chain: + segment: MessageSegment + if segment.type != MessageType.AT: + continue + if segment.data['target'] == event.self_id: + return True + return False + + def _is_nick(event: GroupMessage) -> bool: + text = event.get_plaintext() + if not text: + return False + nick_regex = '|'.join( + {i.strip() for i in bot.config.nickname if i.strip()}) + matched = re.search(rf"^({nick_regex})([\s,,]*|$)", text, re.IGNORECASE) + if matched is None: + return False + Log.info(f'User is calling me {matched.group(1)}') + return True + + def _is_reply(event: GroupMessage) -> bool: + for segment in event.message_chain: + segment: MessageSegment + if segment.type != MessageType.QUOTE: + continue + if segment.data['senderId'] == event.self_id: + return True + return False + + event.to_me = any([_is_at(event), _is_reply(event), _is_nick(event)]) + return event From da1218221ce2d9f506464f6b6a64e05df85b3782 Mon Sep 17 00:00:00 2001 From: Mix Date: Mon, 1 Feb 2021 16:37:42 +0800 Subject: [PATCH 29/37] :white_check_mark: add specified test for mirai adapter --- tests/.env.dev | 4 ++++ tests/bot.py | 2 ++ tests/test_plugins/test_mirai.py | 13 +++++++++++++ 3 files changed, 19 insertions(+) create mode 100644 tests/test_plugins/test_mirai.py diff --git a/tests/.env.dev b/tests/.env.dev index 9b69f65a..33e6f835 100644 --- a/tests/.env.dev +++ b/tests/.env.dev @@ -11,3 +11,7 @@ COMMAND_SEP=["/", "."] CUSTOM_CONFIG1=config in env CUSTOM_CONFIG3= + +MIRAI_AUTH_KEY=12345678 +MIRAI_HOST=127.0.0.1 +MIRAI_PORT=8080 \ No newline at end of file diff --git a/tests/bot.py b/tests/bot.py index 6e45e051..849aee27 100644 --- a/tests/bot.py +++ b/tests/bot.py @@ -6,6 +6,7 @@ sys.path.insert(0, os.path.abspath("..")) import nonebot from nonebot.adapters.cqhttp import Bot from nonebot.adapters.ding import Bot as DingBot +from nonebot.adapters.mirai import Bot as MiraiBot from nonebot.log import logger, default_format # test custom log @@ -20,6 +21,7 @@ app = nonebot.get_asgi() driver = nonebot.get_driver() driver.register_adapter("cqhttp", Bot) driver.register_adapter("ding", DingBot) +driver.register_adapter("mirai", MiraiBot) # load builtin plugin nonebot.load_builtin_plugins() diff --git a/tests/test_plugins/test_mirai.py b/tests/test_plugins/test_mirai.py new file mode 100644 index 00000000..a5da93ae --- /dev/null +++ b/tests/test_plugins/test_mirai.py @@ -0,0 +1,13 @@ +from nonebot.plugin import on_message +from nonebot.adapters.mirai import Bot, MessageEvent + +message_test = on_message() + + +@message_test.handle() +async def _message(bot: Bot, event: MessageEvent): + text = event.get_plaintext() + if not text: + return + reversed_text = ''.join(reversed(text)) + await bot.send(event, reversed_text, at_sender=True) From f2ab618083f5d43c32d914d2269c1eeec38a1ee0 Mon Sep 17 00:00:00 2001 From: nonebot Date: Mon, 1 Feb 2021 08:39:38 +0000 Subject: [PATCH 30/37] :memo: update api docs --- docs/api/adapters/mirai.md | 448 ++++++++++++++++++++++++++++++++++++- 1 file changed, 439 insertions(+), 9 deletions(-) diff --git a/docs/api/adapters/mirai.md b/docs/api/adapters/mirai.md index b669222b..34aae0c9 100644 --- a/docs/api/adapters/mirai.md +++ b/docs/api/adapters/mirai.md @@ -89,7 +89,7 @@ Bot会话管理器, 提供API主动调用接口 -## _class_ `MiraiBot` +## _class_ `Bot` 基类:[`nonebot.adapters.Bot`](README.md#nonebot.adapters.Bot) @@ -656,9 +656,9 @@ API中为了使代码更加整洁, 我们采用了与PEP8相符的命名规则 # NoneBot.adapters.mirai.bot_ws 模块 -## _class_ `MiraiWebsocketBot` +## _class_ `WebsocketBot` -基类:`nonebot.adapters.mirai.bot.MiraiBot` +基类:`nonebot.adapters.mirai.bot.Bot` mirai-api-http 正向 Websocket 协议 Bot 适配。 @@ -950,12 +950,24 @@ CQHTTP 协议 MessageSegment 适配。具体方法参考 [mirai-api-http 消息 * `name: str`: 戳一戳的类型 - - "Poke": 戳一戳 - - "ShowLove": 比心 - - "Like": 点赞 - - "Heartbroken": 心碎 - - "SixSixSix": 666 - - "FangDaZhao": 放大招 + + + * `Poke`: 戳一戳 + + + * `ShowLove`: 比心 + + + * `Like`: 点赞 + + + * `Heartbroken`: 心碎 + + + * `SixSixSix`: 666 + + + * `FangDaZhao`: 放大招 @@ -1030,6 +1042,424 @@ Mirai 协议 Messaqge 适配 部分字段可能与文档在符号上不一致 ::: + +## _class_ `Event` + +基类:[`nonebot.adapters.Event`](README.md#nonebot.adapters.Event) + +mirai-api-http 协议事件,字段与 mirai-api-http 一致。各事件字段参考 [mirai-api-http 事件类型](https://github.com/project-mirai/mirai-api-http/blob/master/docs/EventType.md) + + +### _classmethod_ `new(data)` + +此事件类的工厂函数, 能够通过事件数据选择合适的子类进行序列化 + + +### `normalize_dict(**kwargs)` + +返回可以被json正常反序列化的结构体 + + +## _class_ `UserPermission` + +基类:`str`, `enum.Enum` + + +* **说明** + + +用户权限枚举类 + +> +> * `OWNER`: 群主 + + +> * `ADMINISTRATOR`: 群管理 + + +> * `MEMBER`: 普通群成员 + + +## _class_ `MessageChain` + +基类:[`nonebot.adapters.Message`](README.md#nonebot.adapters.Message) + +Mirai 协议 Messaqge 适配 + +由于Mirai协议的Message实现较为特殊, 故使用MessageChain命名 + + +### `export()` + +导出为可以被正常json序列化的数组 + + +## _class_ `MessageEvent` + +基类:`nonebot.adapters.mirai.event.base.Event` + +消息事件基类 + + +## _class_ `GroupMessage` + +基类:`nonebot.adapters.mirai.event.message.MessageEvent` + +群消息事件 + + +## _class_ `FriendMessage` + +基类:`nonebot.adapters.mirai.event.message.MessageEvent` + +好友消息事件 + + +## _class_ `TempMessage` + +基类:`nonebot.adapters.mirai.event.message.MessageEvent` + +临时会话消息事件 + + +## _class_ `NoticeEvent` + +基类:`nonebot.adapters.mirai.event.base.Event` + +通知事件基类 + + +## _class_ `MuteEvent` + +基类:`nonebot.adapters.mirai.event.notice.NoticeEvent` + +禁言类事件基类 + + +## _class_ `BotMuteEvent` + +基类:`nonebot.adapters.mirai.event.notice.MuteEvent` + +Bot被禁言 + + +## _class_ `BotUnmuteEvent` + +基类:`nonebot.adapters.mirai.event.notice.MuteEvent` + +Bot被取消禁言 + + +## _class_ `MemberMuteEvent` + +基类:`nonebot.adapters.mirai.event.notice.MuteEvent` + +群成员被禁言事件(该成员不是Bot) + + +## _class_ `MemberUnmuteEvent` + +基类:`nonebot.adapters.mirai.event.notice.MuteEvent` + +群成员被取消禁言事件(该成员不是Bot) + + +## _class_ `BotJoinGroupEvent` + +基类:`nonebot.adapters.mirai.event.notice.NoticeEvent` + +Bot加入了一个新群 + + +## _class_ `BotLeaveEventActive` + +基类:`nonebot.adapters.mirai.event.notice.BotJoinGroupEvent` + +Bot主动退出一个群 + + +## _class_ `BotLeaveEventKick` + +基类:`nonebot.adapters.mirai.event.notice.BotJoinGroupEvent` + +Bot被踢出一个群 + + +## _class_ `MemberJoinEvent` + +基类:`nonebot.adapters.mirai.event.notice.NoticeEvent` + +新人入群的事件 + + +## _class_ `MemberLeaveEventKick` + +基类:`nonebot.adapters.mirai.event.notice.MemberJoinEvent` + +成员被踢出群(该成员不是Bot) + + +## _class_ `MemberLeaveEventQuit` + +基类:`nonebot.adapters.mirai.event.notice.MemberJoinEvent` + +成员主动离群(该成员不是Bot) + + +## _class_ `FriendRecallEvent` + +基类:`nonebot.adapters.mirai.event.notice.NoticeEvent` + +好友消息撤回 + + +## _class_ `GroupRecallEvent` + +基类:`nonebot.adapters.mirai.event.notice.FriendRecallEvent` + +群消息撤回 + + +## _class_ `GroupStateChangeEvent` + +基类:`nonebot.adapters.mirai.event.notice.NoticeEvent` + +群变化事件基类 + + +## _class_ `GroupNameChangeEvent` + +基类:`nonebot.adapters.mirai.event.notice.GroupStateChangeEvent` + +某个群名改变 + + +## _class_ `GroupEntranceAnnouncementChangeEvent` + +基类:`nonebot.adapters.mirai.event.notice.GroupStateChangeEvent` + +某群入群公告改变 + + +## _class_ `GroupMuteAllEvent` + +基类:`nonebot.adapters.mirai.event.notice.GroupStateChangeEvent` + +全员禁言 + + +## _class_ `GroupAllowAnonymousChatEvent` + +基类:`nonebot.adapters.mirai.event.notice.GroupStateChangeEvent` + +匿名聊天 + + +## _class_ `GroupAllowConfessTalkEvent` + +基类:`nonebot.adapters.mirai.event.notice.GroupStateChangeEvent` + +坦白说 + + +## _class_ `GroupAllowMemberInviteEvent` + +基类:`nonebot.adapters.mirai.event.notice.GroupStateChangeEvent` + +允许群员邀请好友加群 + + +## _class_ `MemberStateChangeEvent` + +基类:`nonebot.adapters.mirai.event.notice.NoticeEvent` + +群成员变化事件基类 + + +## _class_ `MemberCardChangeEvent` + +基类:`nonebot.adapters.mirai.event.notice.MemberStateChangeEvent` + +群名片改动 + + +## _class_ `MemberSpecialTitleChangeEvent` + +基类:`nonebot.adapters.mirai.event.notice.MemberStateChangeEvent` + +群头衔改动(只有群主有操作限权) + + +## _class_ `BotGroupPermissionChangeEvent` + +基类:`nonebot.adapters.mirai.event.notice.MemberStateChangeEvent` + +Bot在群里的权限被改变 + + +## _class_ `MemberPermissionChangeEvent` + +基类:`nonebot.adapters.mirai.event.notice.MemberStateChangeEvent` + +成员权限改变的事件(该成员不是Bot) + + +## _class_ `RequestEvent` + +基类:`nonebot.adapters.mirai.event.base.Event` + +请求事件基类 + + +## _class_ `NewFriendRequestEvent` + +基类:`nonebot.adapters.mirai.event.request.RequestEvent` + +添加好友申请 + + +### _async_ `approve(bot)` + + +* **说明** + + 通过此人的好友申请 + + + +* **参数** + + + * `bot: Bot`: 当前的 `Bot` 对象 + + + +### _async_ `reject(bot, operate=1, message='')` + + +* **说明** + + 拒绝此人的好友申请 + + + +* **参数** + + + * `bot: Bot`: 当前的 `Bot` 对象 + + + * `operate: Literal[1, 2]`: 响应的操作类型 + + + * `1`: 拒绝添加好友 + + + * `2`: 拒绝添加好友并添加黑名单,不再接收该用户的好友申请 + + + * `message: str`: 回复的信息 + + + +## _class_ `MemberJoinRequestEvent` + +基类:`nonebot.adapters.mirai.event.request.RequestEvent` + +用户入群申请(Bot需要有管理员权限) + + +### _async_ `approve(bot)` + + +* **说明** + + 通过此人的加群申请 + + + +* **参数** + + + * `bot: Bot`: 当前的 `Bot` 对象 + + + +### _async_ `reject(bot, operate=1, message='')` + + +* **说明** + + 拒绝(忽略)此人的加群申请 + + + +* **参数** + + + * `bot: Bot`: 当前的 `Bot` 对象 + + + * `operate: Literal[1, 2, 3, 4]`: 响应的操作类型 + + + * `1`: 拒绝入群 + + + * `2`: 忽略请求 + + + * `3`: 拒绝入群并添加黑名单,不再接收该用户的入群申请 + + + * `4`: 忽略入群并添加黑名单,不再接收该用户的入群申请 + + + * `message: str`: 回复的信息 + + + +## _class_ `BotInvitedJoinGroupRequestEvent` + +基类:`nonebot.adapters.mirai.event.request.RequestEvent` + +Bot被邀请入群申请 + + +### _async_ `approve(bot)` + + +* **说明** + + 通过这份被邀请入群申请 + + + +* **参数** + + + * `bot: Bot`: 当前的 `Bot` 对象 + + + +### _async_ `reject(bot, message='')` + + +* **说明** + + 拒绝这份被邀请入群申请 + + + +* **参数** + + + * `bot: Bot`: 当前的 `Bot` 对象 + + + * `message: str`: 邀请消息 + + # NoneBot.adapters.mirai.event.base 模块 From ad3a08f514018ecc563804b345330877d7fd4296 Mon Sep 17 00:00:00 2001 From: Mix Date: Mon, 1 Feb 2021 16:53:06 +0800 Subject: [PATCH 31/37] :bulb: :wastebasket: remove some invalid comments in mirai adapter --- nonebot/adapters/mirai/bot_ws.py | 4 ---- nonebot/adapters/mirai/message.py | 4 ---- 2 files changed, 8 deletions(-) diff --git a/nonebot/adapters/mirai/bot_ws.py b/nonebot/adapters/mirai/bot_ws.py index fdf35c0c..9dabe356 100644 --- a/nonebot/adapters/mirai/bot_ws.py +++ b/nonebot/adapters/mirai/bot_ws.py @@ -134,10 +134,6 @@ class WebsocketBot(Bot): * ``driver: Driver``: 程序所使用的``Driver`` * ``config: Config``: 程序配置对象 * ``qq: int``: 要使用的Bot的QQ号 **注意: 在使用正向Websocket时必须指定该值!** - - :返回: - - - ``[type]``: [description] """ super().register(driver, config) cls.active = True diff --git a/nonebot/adapters/mirai/message.py b/nonebot/adapters/mirai/message.py index f1aff156..26fb198c 100644 --- a/nonebot/adapters/mirai/message.py +++ b/nonebot/adapters/mirai/message.py @@ -161,10 +161,6 @@ class MessageSegment(BaseMessageSegment): * ``image_id: Optional[str]``: 图片的image_id,群图片与好友图片格式不同。不为空时将忽略url属性 * ``url: Optional[str]``: 图片的URL,发送时可作网络图片的链接 * ``path: Optional[str]``: 图片的路径,发送本地图片 - - :返回: - - - ``[type]``: [description] """ return cls(type=MessageType.IMAGE, imageId=image_id, url=url, path=path) From f446411f084f6b81f1f278dfe57cb7bd51eadac5 Mon Sep 17 00:00:00 2001 From: Mix Date: Mon, 1 Feb 2021 18:28:27 +0800 Subject: [PATCH 32/37] :memo: add start guide for mirai adapter --- docs/.vuepress/config.js | 2 +- docs/api/adapters/mirai.md | 14 --- docs/guide/getting-started.md | 1 + docs/guide/mirai-guide.md | 187 ++++++++++++++++++++++++++++++++++ 4 files changed, 189 insertions(+), 15 deletions(-) create mode 100644 docs/guide/mirai-guide.md diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 6d684ad9..889b4bfe 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -116,7 +116,7 @@ module.exports = (context) => ({ title: "协议适配", collapsable: false, sidebar: "auto", - children: ["cqhttp-guide", "ding-guide"], + children: ["cqhttp-guide", "ding-guide", "mirai-guide"], }, ], "/advanced/": [ diff --git a/docs/api/adapters/mirai.md b/docs/api/adapters/mirai.md index 34aae0c9..1b2e709d 100644 --- a/docs/api/adapters/mirai.md +++ b/docs/api/adapters/mirai.md @@ -684,13 +684,6 @@ mirai-api-http 正向 Websocket 协议 Bot 适配。 * `qq: int`: 要使用的Bot的QQ号 **注意: 在使用正向Websocket时必须指定该值!** - -* **返回** - - - * `[type]`: [description] - - # NoneBot.adapters.mirai.config 模块 @@ -845,13 +838,6 @@ CQHTTP 协议 MessageSegment 适配。具体方法参考 [mirai-api-http 消息 -* **返回** - - - * `[type]`: [description] - - - ### _classmethod_ `flash_image(image_id=None, url=None, path=None)` diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index d35665a2..71445c39 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -63,6 +63,7 @@ python bot.py - [配置 CQHTTP](./cqhttp-guide.md) - [配置钉钉](./ding-guide.md) +- [配置 mirai-api-http](./mirai-guide.md) NoneBot 接受的上报地址与 `Driver` 有关,默认使用的 `FastAPI Driver` 所接受的上报地址有: diff --git a/docs/guide/mirai-guide.md b/docs/guide/mirai-guide.md new file mode 100644 index 00000000..d3544179 --- /dev/null +++ b/docs/guide/mirai-guide.md @@ -0,0 +1,187 @@ +# Mirai-API-HTTP 协议使用指南 + +::: warning + +Mirai-API-HTTP 的适配现在仍然处于早期阶段, 可能没有进行过充分的测试 + +请在生产环境中谨慎使用 + +::: + +::: tip + +为了你的使用之旅更加顺畅, 我们建议您在配置之前具有以下的前置知识 + +- 对服务端/客户端(C/S)模型的基本了解 +- 对 Web 服务配置基础的认知 +- 对`YAML`语法的一点点了解 + +::: + +**为了便捷起见, 以下内容均以缩写 `MAH` 代替 `mirai-api-http`** + +## 配置 MAH 客户端 + +正如你可能刚刚在[CQHTTP 协议使用指南](./cqhttp-guide.md)中所读到的: + +> 单纯运行 NoneBot 实例并不会产生任何效果,因为此刻 QQ 这边还不知道 NoneBot 的存在,也就无法把消息发送给它,因此现在需要使用一个无头 QQ 来把消息等事件上报给 NoneBot。 + +这次, 我们将采用在实现上有别于 onebot即 CQHTTP协议的另外一种无头 QQ API 协议, 即 MAH + +为了配置 MAH 端, 我们现在需要移步到[MAH 的项目地址](https://github.com/project-mirai/mirai-api-http), 来看看它是如何配置的 + +根据[项目提供的 README](https://github.com/project-mirai/mirai-api-http/blob/056beedba31d6ad06426997a1d3fde861a7f8ba3/README.md),配置 MAH 大概需要以下几步 + +1. 下载并安装 Java 运行环境, 你可以有以下几种选择: + + - [由 Oracle 提供的 Java 运行环境](https://java.com/zh-CN/download/manual.jsp) **在没有特殊需求的情况下推荐** + - [由 Zulu 编译的 OpenJRE 环境](https://www.azul.com/downloads/zulu-community/?version=java-8-lts&architecture=x86-64-bit&package=jre) + +2. 下载[Mirai Console Loader](https://github.com/iTXTech/mirai-console-loader) + + - 请按照文档 README 中的步骤下载并安装 + +3. 安装 MAH: + + - 在 Mirai Console Loader 目录下执行该指令 + + - ```shell + ./mcl --update-package net.mamoe:mirai-api-http --channel stable --type plugin + ``` + + 注意: 该指令的前缀`./mcl`可能根据操作系统以及使用 java 环境的不同而变化 + +4. 修改配置文件 + + ::: tip + + 在此之前, 你可能需要了解我们为 MAH 设计的两种通信方式 + + - 正向 Websocket + - NoneBot 作为纯粹的客户端,通过 websocket 监听事件下发 + - 优势 + 1. 网络配置简单, 特别是在使用 Docker 等网络隔离的容器时 + 2. 在初步测试中连接性较好 + - 劣势 + 1. 与 NoneBot 本身的架构不同, 可能稳定性较差 + 2. 需要在注册 adapter 时显式指定 qq, 对于需要开源的程序来讲不利 + - POST 消息上报 + - NoneBot 在接受消息上报时作为服务端, 发送消息时作为客户端 + - 优势 + 1. 与 NoneBot 本身架构相符, 性能和稳定性较强 + 2. 无需在任何地方指定 QQ, 即插即用 + - 劣势 + 1. 由于同时作为客户端和服务端, 配置较为复杂 + 2. 在测试中网络连接性较差 (未确认原因) + + ::: + + - 这是当使用正向 Websocket 时的配置举例 + + - MAH 的`setting.yml`文件 + + - ```yaml + # 省略了部分无需修改的部分 + + host: "0.0.0.0" # 监听地址 + port: 8080 # 监听端口 + authKey: 1234567890 # 访问密钥, 最少八位 + enableWebsocket: true # 必须为true + ``` + + - `.env`文件 + + - ```shell + MIRAI_AUTH_KEY=1234567890 + MIRAI_HOST=127.0.0.1 # 当MAH运行在本机时 + MIRAI_PORT=8080 # MAH的监听端口 + ``` + + - `bot.py`文件 + + - ```python + import nonebot + from nonebot.adapters.mirai import WebsocketBot + + nonebot.init() + nonebot.get_driver().register_adapter('mirai-ws', WebsocketBot, qq=12345678) # qq参数需要填在mah中登录的qq + nonebot.load_builtin_plugins() # 加载 nonebot 内置插件 + nonebot.run() + ``` + + - 这是当使用 POST 消息上报时的配置文件 + + - MAH 的`setting.yml`文件 + + - ```yaml + # 省略了部分无需修改的部分 + + host: '0.0.0.0' # 监听地址 + port: 8080 # 监听端口 + authKey: 1234567890 # 访问密钥, 最少八位 + + ## 消息上报 + report: + enable: true # 必须为true + groupMessage: + report: true # 群消息上报 + friendMessage: + report: true # 好友消息上报 + tempMessage: + report: true # 临时会话上报 + eventMessage: + report: true # 事件上报 + destinations: + - 'http://127.0.0.1:2333/mirai/http' #上报地址, 请按照实际情况修改 + # 上报时的额外Header + extraHeaders: {} + ``` + + - `.env`文件 + + - ```shell + HOST=127.0.0.1 # 当MAH运行在本机时 + PORT=2333 + + MIRAI_AUTH_KEY=1234567890 + MIRAI_HOST=127.0.0.1 # 当MAH运行在本机时 + MIRAI_PORT=8080 # MAH的监听端口 + ``` + + - `bot.py`文件 + + - ```python + import nonebot + from nonebot.adapters.mirai import Bot + + nonebot.init() + nonebot.get_driver().register_adapter('mirai', Bot) + nonebot.load_builtin_plugins() # 加载 nonebot 内置插件 + nonebot.run() + ``` + +## 历史性的第一次对话 + +现在, 先启动 NoneBot, 再启动 MAH + +如果你的配置文件一切正常, 你将在控制台看到类似于下列的日志 + +```log +02-01 18:25:12 [INFO] nonebot | NoneBot is initializing... +02-01 18:25:12 [INFO] nonebot | Current Env: prod +02-01 18:25:12 [DEBUG] nonebot | Loaded Config: {'driver': 'nonebot.drivers.fastapi', 'host': IPv4Address('127.0.0.1'), 'port': 8080, 'debug': True, 'api_root': {}, 'api_timeout': 30.0, 'access_token': None, 'secret': None, 'superusers': set(), 'nickname': set(), 'command_start': {'/'}, 'command_sep': {'.'}, 'session_expire_timeout': datetime.timedelta(seconds=120), 'mirai_port': 8080, 'environment': 'prod', 'mirai_auth_key': 12345678, 'mirai_host': '127.0.0.1'} +02-01 18:25:12 [DEBUG] nonebot | Succeeded to load adapter "mirai" +02-01 18:25:12 [INFO] nonebot | Succeeded to import "nonebot.plugins.echo" +02-01 18:25:12 [INFO] nonebot | Running NoneBot... +02-01 18:25:12 [DEBUG] nonebot | Loaded adapters: mirai +02-01 18:25:12 [INFO] uvicorn | Started server process [183155] +02-01 18:25:12 [INFO] uvicorn | Waiting for application startup. +02-01 18:25:12 [INFO] uvicorn | Application startup complete. +02-01 18:25:12 [INFO] uvicorn | Uvicorn running on http://127.0.0.1:2333 (Press CTRL+C to quit) +02-01 18:25:14 [INFO] uvicorn | 127.0.0.1:37794 - "POST /mirai/http HTTP/1.1" 204 +02-01 18:25:14 [DEBUG] nonebot | MIRAI | received message {'type': 'BotOnlineEvent', 'qq': 1234567} +02-01 18:25:14 [INFO] nonebot | MIRAI 1234567 | [BotOnlineEvent]: {'self_id': 1234567, 'type': 'BotOnlineEvent', 'qq': 1234567} +02-01 18:25:14 [DEBUG] nonebot | Checking for matchers in priority 1... +``` + +恭喜你, 你的配置已经成功! From d6ae1ca01cd06856dc712f2ddcce4287bef857cb Mon Sep 17 00:00:00 2001 From: Mix Date: Mon, 1 Feb 2021 19:16:36 +0800 Subject: [PATCH 33/37] :page_facing_up: add agpl v3 license for mirai adapter --- docs/api/adapters/mirai.md | 6 + docs/guide/mirai-guide.md | 8 + nonebot/adapters/mirai/LICENSE | 661 +++++++++++++++++++++++++++++ nonebot/adapters/mirai/__init__.py | 9 + 4 files changed, 684 insertions(+) create mode 100644 nonebot/adapters/mirai/LICENSE diff --git a/docs/api/adapters/mirai.md b/docs/api/adapters/mirai.md index 1b2e709d..3e53e9e7 100644 --- a/docs/api/adapters/mirai.md +++ b/docs/api/adapters/mirai.md @@ -15,6 +15,12 @@ sidebarDepth: 0 如果你在使用过程中遇到了任何问题, 请前往 [Issue页面](https://github.com/nonebot/nonebot2/issues) 为我们提供反馈 ::: +::: danger +Mirai-API-HTTP 的适配器以 [AGPLv3许可](https://opensource.org/licenses/AGPL-3.0) 单独开源 + +这意味着在使用该适配器时需要\*\*开源您的完整程序代码\*\* +::: + # NoneBot.adapters.mirai.bot 模块 diff --git a/docs/guide/mirai-guide.md b/docs/guide/mirai-guide.md index d3544179..bd11083c 100644 --- a/docs/guide/mirai-guide.md +++ b/docs/guide/mirai-guide.md @@ -18,6 +18,14 @@ Mirai-API-HTTP 的适配现在仍然处于早期阶段, 可能没有进行过充 ::: +::: danger + +Mirai-API-HTTP 的适配器以 [AGPLv3 许可](https://opensource.org/licenses/AGPL-3.0) 单独开源 + +这意味着在使用该适配器时需要 **以该许可开源您的完整程序代码** + +::: + **为了便捷起见, 以下内容均以缩写 `MAH` 代替 `mirai-api-http`** ## 配置 MAH 客户端 diff --git a/nonebot/adapters/mirai/LICENSE b/nonebot/adapters/mirai/LICENSE new file mode 100644 index 00000000..be3f7b28 --- /dev/null +++ b/nonebot/adapters/mirai/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/nonebot/adapters/mirai/__init__.py b/nonebot/adapters/mirai/__init__.py index c083a8a7..2b09e365 100644 --- a/nonebot/adapters/mirai/__init__.py +++ b/nonebot/adapters/mirai/__init__.py @@ -10,12 +10,21 @@ Mirai-API-HTTP 协议适配 如果你在使用过程中遇到了任何问题, 请前往 `Issue页面`_ 为我们提供反馈 \:\:\: +\:\:\: danger +Mirai-API-HTTP 的适配器以 `AGPLv3许可`_ 单独开源 + +这意味着在使用该适配器时需要 **以该许可开源您的完整程序代码** +\:\:\: + .. _mirai-api-http 文档: https://github.com/project-mirai/mirai-api-http/tree/master/docs .. _Issue页面: https://github.com/nonebot/nonebot2/issues +.. _AGPLv3许可: + https://opensource.org/licenses/AGPL-3.0 + """ from .bot import Bot From 9b79c83c3d8af6a8e461c01d74f38c289a7f0295 Mon Sep 17 00:00:00 2001 From: nonebot Date: Mon, 1 Feb 2021 11:30:11 +0000 Subject: [PATCH 34/37] :memo: update api docs --- docs/api/adapters/mirai.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/adapters/mirai.md b/docs/api/adapters/mirai.md index 3e53e9e7..4b568152 100644 --- a/docs/api/adapters/mirai.md +++ b/docs/api/adapters/mirai.md @@ -18,7 +18,7 @@ sidebarDepth: 0 ::: danger Mirai-API-HTTP 的适配器以 [AGPLv3许可](https://opensource.org/licenses/AGPL-3.0) 单独开源 -这意味着在使用该适配器时需要\*\*开源您的完整程序代码\*\* +这意味着在使用该适配器时需要 **以该许可开源您的完整程序代码** ::: # NoneBot.adapters.mirai.bot 模块 From 00858416f93bec661a6638841dbdb978a8fbe344 Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Mon, 1 Feb 2021 20:58:12 +0800 Subject: [PATCH 35/37] :art: format code and bump dependency --- docs/.vuepress/config.js | 114 ++++++++++++++++++------------------ nonebot/drivers/__init__.py | 4 +- poetry.lock | 48 +++++++-------- pyproject.toml | 2 +- 4 files changed, 84 insertions(+), 84 deletions(-) diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 889b4bfe..3b011655 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -1,11 +1,11 @@ const path = require("path"); -module.exports = (context) => ({ +module.exports = context => ({ base: process.env.VUEPRESS_BASE || "/", title: "NoneBot", description: "基于 酷Q 的 Python 异步 QQ 机器人框架", markdown: { - lineNumbers: true, + lineNumbers: true }, /** * Extra tags to be injected to the page HTML `` @@ -21,26 +21,26 @@ module.exports = (context) => ({ ["meta", { name: "apple-mobile-web-app-capable", content: "yes" }], [ "meta", - { name: "apple-mobile-web-app-status-bar-style", content: "black" }, + { name: "apple-mobile-web-app-status-bar-style", content: "black" } ], [ "link", - { rel: "apple-touch-icon", href: "/icons/apple-touch-icon-180x180.png" }, + { rel: "apple-touch-icon", href: "/icons/apple-touch-icon-180x180.png" } ], [ "link", { rel: "mask-icon", href: "/icons/safari-pinned-tab.svg", - color: "#ea5252", - }, + color: "#ea5252" + } ], [ "meta", { name: "msapplication-TileImage", - content: "/icons/mstile-150x150.png", - }, + content: "/icons/mstile-150x150.png" + } ], ["meta", { name: "msapplication-TileColor", content: "#ea5252" }], [ @@ -48,16 +48,16 @@ module.exports = (context) => ({ { rel: "stylesheet", href: - "https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5/css/all.min.css", - }, - ], + "https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5/css/all.min.css" + } + ] ], locales: { "/": { lang: "zh-CN", title: "NoneBot", - description: "基于 酷Q 的 Python 异步 QQ 机器人框架", - }, + description: "基于 酷Q 的 Python 异步 QQ 机器人框架" + } }, theme: "nonebot", @@ -83,7 +83,7 @@ module.exports = (context) => ({ { text: "进阶", link: "/advanced/" }, { text: "API", link: "/api/" }, { text: "插件广场", link: "/plugin-store" }, - { text: "更新日志", link: "/changelog" }, + { text: "更新日志", link: "/changelog" } ], sidebarDepth: 2, sidebar: { @@ -97,8 +97,8 @@ module.exports = (context) => ({ "installation", "getting-started", "creating-a-project", - "basic-configuration", - ], + "basic-configuration" + ] }, { title: "编写插件", @@ -109,15 +109,15 @@ module.exports = (context) => ({ "creating-a-plugin", "creating-a-matcher", "creating-a-handler", - "end-or-start", - ], + "end-or-start" + ] }, { title: "协议适配", collapsable: false, sidebar: "auto", - children: ["cqhttp-guide", "ding-guide", "mirai-guide"], - }, + children: ["cqhttp-guide", "ding-guide", "mirai-guide"] + } ], "/advanced/": [ { @@ -130,15 +130,15 @@ module.exports = (context) => ({ "permission", "runtime-hook", "export-and-require", - "overloaded-handlers", - ], + "overloaded-handlers" + ] }, { title: "发布", collapsable: false, sidebar: "auto", - children: ["publish-plugin"], - }, + children: ["publish-plugin"] + } ], "/api/": [ { @@ -148,78 +148,78 @@ module.exports = (context) => ({ children: [ { title: "nonebot 模块", - path: "nonebot", + path: "nonebot" }, { title: "nonebot.config 模块", - path: "config", + path: "config" }, { title: "nonebot.plugin 模块", - path: "plugin", + path: "plugin" }, { title: "nonebot.message 模块", - path: "message", + path: "message" }, { title: "nonebot.matcher 模块", - path: "matcher", + path: "matcher" }, { title: "nonebot.rule 模块", - path: "rule", + path: "rule" }, { title: "nonebot.permission 模块", - path: "permission", + path: "permission" }, { title: "nonebot.log 模块", - path: "log", + path: "log" }, { title: "nonebot.utils 模块", - path: "utils", + path: "utils" }, { title: "nonebot.typing 模块", - path: "typing", + path: "typing" }, { title: "nonebot.exception 模块", - path: "exception", + path: "exception" }, { title: "nonebot.drivers 模块", - path: "drivers/", + path: "drivers/" }, { title: "nonebot.drivers.fastapi 模块", - path: "drivers/fastapi", + path: "drivers/fastapi" }, { title: "nonebot.adapters 模块", - path: "adapters/", + path: "adapters/" }, { title: "nonebot.adapters.cqhttp 模块", - path: "adapters/cqhttp", + path: "adapters/cqhttp" }, { title: "nonebot.adapters.ding 模块", - path: "adapters/ding", + path: "adapters/ding" }, { title: "nonebot.adapters.mirai 模块", - path: "adapters/mirai", - }, - ], - }, - ], - }, - }, - }, + path: "adapters/mirai" + } + ] + } + ] + } + } + } }, plugins: [ @@ -231,9 +231,9 @@ module.exports = (context) => ({ serviceWorker: true, updatePopup: { message: "发现新内容", - buttonText: "刷新", - }, - }, + buttonText: "刷新" + } + } ], [ "versioning", @@ -242,16 +242,16 @@ module.exports = (context) => ({ pagesSourceDir: path.resolve(context.sourceDir, "..", "pages"), onNewVersion(version, versionDestPath) { console.log(`Created version ${version} in ${versionDestPath}`); - }, - }, + } + } ], [ "container", { type: "vue", before: '
',
-        after: "
", - }, - ], - ], + after: "" + } + ] + ] }); diff --git a/nonebot/drivers/__init__.py b/nonebot/drivers/__init__.py index 134b2078..986d59a3 100644 --- a/nonebot/drivers/__init__.py +++ b/nonebot/drivers/__init__.py @@ -62,7 +62,7 @@ class Driver(abc.ABC): :说明: 已连接的 Bot """ - def register_adapter(self, name: str, adapter: Type["Bot"], **kwargs): + def register_adapter(self, name: str, adapter: Type["Bot"]): """ :说明: @@ -74,7 +74,7 @@ class Driver(abc.ABC): * ``adapter: Type[Bot]``: 适配器 Class """ self._adapters[name] = adapter - adapter.register(self, self.config, **kwargs) + adapter.register(self, self.config) logger.opt( colors=True).debug(f'Succeeded to load adapter "{name}"') diff --git a/poetry.lock b/poetry.lock index 2e9e5611..9523412b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -143,7 +143,7 @@ reference = "aliyun" [[package]] name = "httpcore" -version = "0.12.2" +version = "0.12.3" description = "A minimal low-level HTTP client." category = "main" optional = false @@ -228,7 +228,7 @@ reference = "aliyun" [[package]] name = "jinja2" -version = "2.11.2" +version = "2.11.3" description = "A very fast and expressive template engine." category = "dev" optional = false @@ -280,7 +280,7 @@ reference = "aliyun" [[package]] name = "packaging" -version = "20.8" +version = "20.9" description = "Core utilities for Python packages" category = "dev" optional = false @@ -334,7 +334,7 @@ reference = "aliyun" [[package]] name = "pygments" -version = "2.7.3" +version = "2.7.4" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false @@ -347,7 +347,7 @@ reference = "aliyun" [[package]] name = "pygtrie" -version = "2.4.1" +version = "2.4.2" description = "A pure Python trie data structure implementation." category = "main" optional = false @@ -457,8 +457,8 @@ reference = "aliyun" [[package]] name = "snowballstemmer" -version = "2.0.0" -description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." +version = "2.1.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." category = "dev" optional = false python-versions = "*" @@ -470,7 +470,7 @@ reference = "aliyun" [[package]] name = "sphinx" -version = "3.4.1" +version = "3.4.3" description = "Python documentation generator" category = "dev" optional = false @@ -687,7 +687,7 @@ reference = "aliyun" [[package]] name = "urllib3" -version = "1.26.2" +version = "1.26.3" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false @@ -828,8 +828,8 @@ html2text = [ {file = "html2text-2020.1.16.tar.gz", hash = "sha256:e296318e16b059ddb97f7a8a1d6a5c1d7af4544049a01e261731d2d5cc277bbb"}, ] httpcore = [ - {file = "httpcore-0.12.2-py3-none-any.whl", hash = "sha256:420700af11db658c782f7e8fda34f9dcd95e3ee93944dd97d78cb70247e0cd06"}, - {file = "httpcore-0.12.2.tar.gz", hash = "sha256:dd1d762d4f7c2702149d06be2597c35fb154c5eff9789a8c5823fbcf4d2978d6"}, + {file = "httpcore-0.12.3-py3-none-any.whl", hash = "sha256:93e822cd16c32016b414b789aeff4e855d0ccbfc51df563ee34d4dbadbb3bcdc"}, + {file = "httpcore-0.12.3.tar.gz", hash = "sha256:37ae835fb370049b2030c3290e12ed298bf1473c41bb72ca4aa78681eba9b7c9"}, ] httptools = [ {file = "httptools-0.1.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce"}, @@ -858,8 +858,8 @@ imagesize = [ {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] jinja2 = [ - {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, - {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, + {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, + {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, ] loguru = [ {file = "loguru-0.5.3-py3-none-any.whl", hash = "sha256:f8087ac396b5ee5f67c963b495d615ebbceac2796379599820e324419d53667c"}, @@ -901,8 +901,8 @@ markupsafe = [ {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] packaging = [ - {file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"}, - {file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"}, + {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, + {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, ] pydantic = [ {file = "pydantic-1.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c59ea046aea25be14dc22d69c97bee629e6d48d2b2ecb724d7fe8806bf5f61cd"}, @@ -933,11 +933,11 @@ pydash = [ {file = "pydash-4.9.2.tar.gz", hash = "sha256:11d8f3c92d92a004e042fdb226b10dba28f4e311546b0de89d983e91539d5e55"}, ] pygments = [ - {file = "Pygments-2.7.3-py3-none-any.whl", hash = "sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08"}, - {file = "Pygments-2.7.3.tar.gz", hash = "sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716"}, + {file = "Pygments-2.7.4-py3-none-any.whl", hash = "sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435"}, + {file = "Pygments-2.7.4.tar.gz", hash = "sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337"}, ] pygtrie = [ - {file = "pygtrie-2.4.1.tar.gz", hash = "sha256:4367b87d92eaf475107421dce0295a9d4d72156702908c96c430a426b654aee7"}, + {file = "pygtrie-2.4.2.tar.gz", hash = "sha256:43205559d28863358dbbf25045029f58e2ab357317a59b11f11ade278ac64692"}, ] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, @@ -964,12 +964,12 @@ sniffio = [ {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, ] snowballstemmer = [ - {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, - {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, + {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, + {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, ] sphinx = [ - {file = "Sphinx-3.4.1-py3-none-any.whl", hash = "sha256:aeef652b14629431c82d3fe994ce39ead65b3fe87cf41b9a3714168ff8b83376"}, - {file = "Sphinx-3.4.1.tar.gz", hash = "sha256:e450cb205ff8924611085183bf1353da26802ae73d9251a8fcdf220a8f8712ef"}, + {file = "Sphinx-3.4.3-py3-none-any.whl", hash = "sha256:c314c857e7cd47c856d2c5adff514ac2e6495f8b8e0f886a8a37e9305dfea0d8"}, + {file = "Sphinx-3.4.3.tar.gz", hash = "sha256:41cad293f954f7d37f803d97eb184158cfd90f51195131e94875bc07cd08b93c"}, ] sphinx-markdown-builder = [] sphinxcontrib-applehelp = [ @@ -1012,8 +1012,8 @@ untokenize = [ {file = "untokenize-0.1.1.tar.gz", hash = "md5:50d325dff09208c624cc603fad33bb0d"}, ] urllib3 = [ - {file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"}, - {file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"}, + {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"}, + {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"}, ] uvicorn = [ {file = "uvicorn-0.11.8-py3-none-any.whl", hash = "sha256:4b70ddb4c1946e39db9f3082d53e323dfd50634b95fd83625d778729ef1730ef"}, diff --git a/pyproject.toml b/pyproject.toml index 87a5e573..39854210 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,8 +29,8 @@ loguru = "^0.5.1" pygtrie = "^2.4.1" fastapi = "^0.63.0" uvicorn = "^0.11.5" -pydantic = {extras = ["dotenv", "typing_extensions"], version = "^1.7.3"} websockets = "^8.1" +pydantic = {extras = ["dotenv", "typing_extensions"], version = "^1.7.3"} [tool.poetry.dev-dependencies] yapf = "^0.30.0" From 02a780f3b0f4b783e9b8343ff441471774553d4b Mon Sep 17 00:00:00 2001 From: nonebot Date: Mon, 1 Feb 2021 13:00:25 +0000 Subject: [PATCH 36/37] :memo: update api docs --- docs/api/drivers/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/drivers/README.md b/docs/api/drivers/README.md index 673697b4..77485ed2 100644 --- a/docs/api/drivers/README.md +++ b/docs/api/drivers/README.md @@ -120,7 +120,7 @@ Driver 基类。将后端框架封装,以满足适配器使用。 -### `register_adapter(name, adapter, **kwargs)` +### `register_adapter(name, adapter)` * **说明** From 27c6457c2053d38a7a7f40f2167c206b41c890d8 Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Mon, 1 Feb 2021 21:11:41 +0800 Subject: [PATCH 37/37] :memo: update README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 10633bdd..1bb43a02 100644 --- a/README.md +++ b/README.md @@ -110,3 +110,9 @@ NoneBot2 的驱动框架 `Driver` 以及通信协议 `Adapter` 均可**自定义 如果你在使用过程中发现任何问题,可以 [提交 issue](https://github.com/nonebot/nonebot2/issues/new) 或自行 fork 修改后提交 pull request。 如果你要提交 pull request,请确保你的代码风格和项目已有的代码保持一致,遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/),变量命名清晰,有适当的注释。 + +## 许可证 + +`NoneBot` 采用 `MIT` 协议开源,协议文件参考 [LICENSE](./LICENSE)。 + +特别的,由于 `mirai` 使用 `AGPLv3` 协议并要求使用 `mirai` 的软件同样以 `AGPLv3` 协议开源,本项目 `mirai` 适配器部分(即 [`nonebot/adapters/mirai/`](./nonebot/adapters/mirai/) 目录)以 `AGPLv3` 协议开源,协议文件参考 [LICENSE](./nonebot/adapters/mirai/LICENSE)。