From 2e7c86dc23e9500e6660fd52fd1549b47e088f67 Mon Sep 17 00:00:00 2001 From: AsakuraMizu <0xWATERx0@gmail.com> Date: Sun, 23 Aug 2020 20:01:58 +0800 Subject: [PATCH 1/5] fix command type --- nonebot/plugin.py | 2 +- nonebot/rule.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nonebot/plugin.py b/nonebot/plugin.py index 63e06793..e49c5775 100644 --- a/nonebot/plugin.py +++ b/nonebot/plugin.py @@ -142,7 +142,7 @@ def on_endswith(msg: str, startswith(msg), permission, **kwargs) -def on_command(cmd: Union[str, Tuple[str]], +def on_command(cmd: Union[str, Tuple[str, ...]], rule: Optional[Union[Rule, RuleChecker]] = None, permission: Permission = Permission(), **kwargs) -> Type[Matcher]: diff --git a/nonebot/rule.py b/nonebot/rule.py index c13e2f1d..39b1ec6f 100644 --- a/nonebot/rule.py +++ b/nonebot/rule.py @@ -105,7 +105,7 @@ def keyword(msg: str) -> Rule: return Rule(_keyword) -def command(command: Tuple[str]) -> Rule: +def command(command: Tuple[str, ...]) -> Rule: config = get_driver().config command_start = config.command_start command_sep = config.command_sep From d66259da2baac4e9145bdb40fa7530a7baa433d1 Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Tue, 25 Aug 2020 11:36:06 +0800 Subject: [PATCH 2/5] change project settings --- package.json | 17 ++++++----------- pyproject.toml | 8 ++++---- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 06e5231f..ecc13c8c 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,14 @@ { "name": "nonebot", "version": "2.0.0", - "description": "An asynchronous QQ bot framework.", - "homepage": "https://nonebot.cqp.moe/", + "description": "An asynchronous python bot framework.", + "homepage": "https://docs.nonebot.dev/", "main": "index.js", "contributors": [{ - "name": "Richard Chien", - "email": "richardchienthebest@gmail.com" - }, - { - "name": "yanyongyu", - "email": "yanyongyu_1@126.com" - } - ], - "repository": "https://github.com/nonebot/nonebot/nonebot", + "name": "yanyongyu", + "email": "yanyongyu_1@126.com" + }], + "repository": "https://github.com/nonebot/nonebot/", "bugs": { "url": "https://github.com/nonebot/nonebot/issues" }, diff --git a/pyproject.toml b/pyproject.toml index a5a0c6f7..5f222f53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,13 @@ [tool.poetry] name = "nonebot" version = "2.0.0" -description = "An asynchronous QQ bot framework." -authors = ["Richard Chien ", "yanyongyu "] +description = "An asynchronous python bot framework." +authors = ["yanyongyu "] license = "MIT" readme = "README.md" -homepage = "https://nonebot.cqp.moe/" +homepage = "https://docs.nonebot.dev/" repository = "https://github.com/nonebot/nonebot" -documentation = "https://nonebot.cqp.moe/" +documentation = "https://docs.nonebot.dev/" keywords = ["bot", "qq", "qqbot", "mirai", "coolq"] classifiers = [ "Development Status :: 5 - Production/Stable", From c5ea8bc1c31c2f2eb6460b6718c7d7a7368a469c Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Tue, 25 Aug 2020 15:23:10 +0800 Subject: [PATCH 3/5] add contextvars and fix mutable default args --- nonebot/adapters/__init__.py | 14 +++++ nonebot/adapters/cqhttp.py | 40 +++++++++++++ nonebot/matcher.py | 58 ++++++++++++++----- nonebot/plugin.py | 30 +++++----- .../test_plugins/test_package/test_command.py | 3 +- 5 files changed, 112 insertions(+), 33 deletions(-) diff --git a/nonebot/adapters/__init__.py b/nonebot/adapters/__init__.py index b12652d9..2903b0f9 100644 --- a/nonebot/adapters/__init__.py +++ b/nonebot/adapters/__init__.py @@ -42,6 +42,10 @@ class BaseBot(abc.ABC): async def call_api(self, api: str, data: dict): raise NotImplementedError + @abc.abstractmethod + async def send(self, *args, **kwargs): + raise NotImplementedError + # TODO: improve event class BaseEvent(abc.ABC): @@ -102,6 +106,16 @@ class BaseEvent(abc.ABC): def user_id(self, value) -> None: raise NotImplementedError + @property + @abc.abstractmethod + def group_id(self) -> Optional[int]: + raise NotImplementedError + + @group_id.setter + @abc.abstractmethod + def group_id(self, value) -> None: + raise NotImplementedError + @property @abc.abstractmethod def to_me(self) -> Optional[bool]: diff --git a/nonebot/adapters/cqhttp.py b/nonebot/adapters/cqhttp.py index a62d8546..b6332d8e 100644 --- a/nonebot/adapters/cqhttp.py +++ b/nonebot/adapters/cqhttp.py @@ -223,6 +223,36 @@ class Bot(BaseBot): except httpx.HTTPError: raise NetworkError("HTTP request failed") + @overrides(BaseBot) + async def send(self, event: "Event", message: Union[str, "Message", + "MessageSegment"], + **kwargs) -> Union[Any, NoReturn]: + msg = message if isinstance(message, Message) else Message(message) + + at_sender = kwargs.pop("at_sender", False) and bool(event.user_id) + + params = {} + if event.user_id: + params["user_id"] = event.user_id + if event.group_id: + params["group_id"] = event.group_id + params.update(kwargs) + + if "message_type" not in params: + if "group_id" in params: + params["message_type"] = "group" + elif "user_id" in params: + params["message_type"] = "private" + else: + raise ValueError("Cannot guess message type to reply!") + + if at_sender and params["message_type"] != "private": + params["message"] = MessageSegment.at(params["user_id"]) + \ + MessageSegment.text(" ") + msg + else: + params["message"] = msg + return await self.send_msg(**params) + class Event(BaseEvent): @@ -277,6 +307,16 @@ class Event(BaseEvent): def user_id(self, value) -> None: self._raw_event["user_id"] = value + @property + @overrides(BaseEvent) + def group_id(self) -> Optional[int]: + return self._raw_event.get("group_id") + + @group_id.setter + @overrides(BaseEvent) + def group_id(self, value) -> None: + self._raw_event["group_id"] = value + @property @overrides(BaseEvent) def to_me(self) -> Optional[bool]: diff --git a/nonebot/matcher.py b/nonebot/matcher.py index 92fffbc1..6ef7d8f1 100644 --- a/nonebot/matcher.py +++ b/nonebot/matcher.py @@ -6,14 +6,17 @@ import inspect from functools import wraps from datetime import datetime from collections import defaultdict +from contextvars import Context, ContextVar, copy_context from nonebot.rule import Rule from nonebot.permission import Permission, USER -from nonebot.typing import Bot, Event, Handler, ArgsParser -from nonebot.typing import Type, List, Dict, Callable, Optional, NoReturn +from nonebot.typing import Type, List, Dict, Union, Callable, Optional, NoReturn +from nonebot.typing import Bot, Event, Handler, Message, ArgsParser, MessageSegment from nonebot.exception import PausedException, RejectedException, FinishedException matchers: Dict[int, List[Type["Matcher"]]] = defaultdict(list) +current_bot: ContextVar = ContextVar("current_bot") +current_event: ContextVar = ContextVar("current_event") class Matcher: @@ -51,12 +54,12 @@ class Matcher: type_: str = "", rule: Rule = Rule(), permission: Permission = Permission(), - handlers: list = [], + handlers: Optional[list] = None, temp: bool = False, priority: int = 1, block: bool = False, *, - default_state: dict = {}, + default_state: Optional[dict] = None, expire_time: Optional[datetime] = None) -> Type["Matcher"]: """创建新的 Matcher @@ -69,12 +72,12 @@ class Matcher: "type": type_, "rule": rule, "permission": permission, - "handlers": handlers, + "handlers": handlers or [], "temp": temp, "expire_time": expire_time, "priority": priority, "block": block, - "_default_state": default_state + "_default_state": default_state or {} }) matchers[priority].append(NewMatcher) @@ -117,12 +120,12 @@ class Matcher: def receive(cls) -> Callable[[Handler], Handler]: """接收一条新消息并处理""" - async def _handler(bot: Bot, event: Event, state: dict) -> NoReturn: + async def _receive(bot: Bot, event: Event, state: dict) -> NoReturn: raise PausedException if cls.handlers: # 已有前置handlers则接受一条新的消息,否则视为接收初始消息 - cls.handlers.append(_handler) + cls.handlers.append(_receive) def _decorator(func: Handler) -> Handler: if not cls.handlers or cls.handlers[-1] is not func: @@ -144,8 +147,7 @@ class Matcher: if key not in state: state["_current_key"] = key if prompt: - await bot.send_private_msg(user_id=event.user_id, - message=prompt) + await bot.send(event=event, message=prompt) raise PausedException async def _key_parser(bot: Bot, event: Event, state: dict): @@ -176,19 +178,42 @@ class Matcher: return _decorator @classmethod - def finish(cls) -> NoReturn: + async def finish( + cls, + prompt: Optional[Union[str, Message, + MessageSegment]] = None) -> NoReturn: + bot: Bot = current_bot.get() + event: Event = current_event.get() + if prompt: + await bot.send(event=event, message=prompt) raise FinishedException @classmethod - def pause(cls) -> NoReturn: + async def pause( + cls, + prompt: Optional[Union[str, Message, + MessageSegment]] = None) -> NoReturn: + bot: Bot = current_bot.get() + event: Event = current_event.get() + if prompt: + await bot.send(event=event, message=prompt) raise PausedException @classmethod - def reject(cls) -> NoReturn: + async def reject( + cls, + prompt: Optional[Union[str, Message, + MessageSegment]] = None) -> NoReturn: + bot: Bot = current_bot.get() + event: Event = current_event.get() + if prompt: + await bot.send(event=event, message=prompt) raise RejectedException # 运行handlers async def run(self, bot: Bot, event: Event, state: dict): + b_t = current_bot.set(bot) + e_t = current_event.set(event) try: # Refresh preprocess state self.state.update(state) @@ -214,7 +239,6 @@ class Matcher: block=True, default_state=self.state, expire_time=datetime.now() + bot.config.session_expire_timeout) - return except PausedException: Matcher.new( self.type, @@ -226,6 +250,8 @@ class Matcher: block=True, default_state=self.state, expire_time=datetime.now() + bot.config.session_expire_timeout) - return except FinishedException: - return + pass + finally: + current_bot.reset(b_t) + current_event.reset(e_t) diff --git a/nonebot/plugin.py b/nonebot/plugin.py index c6584615..0ea04046 100644 --- a/nonebot/plugin.py +++ b/nonebot/plugin.py @@ -31,11 +31,11 @@ class Plugin(object): def on(rule: Union[Rule, RuleChecker] = Rule(), permission: Permission = Permission(), *, - handlers=[], - temp=False, + handlers: Optional[list] = None, + temp: bool = False, priority: int = 1, block: bool = False, - state={}) -> Type[Matcher]: + state: Optional[dict] = None) -> Type[Matcher]: matcher = Matcher.new("", Rule() & rule, permission, @@ -50,11 +50,11 @@ def on(rule: Union[Rule, RuleChecker] = Rule(), def on_metaevent(rule: Union[Rule, RuleChecker] = Rule(), *, - handlers=[], - temp=False, + handlers: Optional[list] = None, + temp: bool = False, priority: int = 1, block: bool = False, - state={}) -> Type[Matcher]: + state: Optional[dict] = None) -> Type[Matcher]: matcher = Matcher.new("meta_event", Rule() & rule, Permission(), @@ -70,11 +70,11 @@ def on_metaevent(rule: Union[Rule, RuleChecker] = Rule(), def on_message(rule: Union[Rule, RuleChecker] = Rule(), permission: Permission = Permission(), *, - handlers=[], - temp=False, + handlers: Optional[list] = None, + temp: bool = False, priority: int = 1, block: bool = True, - state={}) -> Type[Matcher]: + state: Optional[dict] = None) -> Type[Matcher]: matcher = Matcher.new("message", Rule() & rule, permission, @@ -89,11 +89,11 @@ def on_message(rule: Union[Rule, RuleChecker] = Rule(), def on_notice(rule: Union[Rule, RuleChecker] = Rule(), *, - handlers=[], - temp=False, + handlers: Optional[list] = None, + temp: bool = False, priority: int = 1, block: bool = False, - state={}) -> Type[Matcher]: + state: Optional[dict] = None) -> Type[Matcher]: matcher = Matcher.new("notice", Rule() & rule, Permission(), @@ -108,11 +108,11 @@ def on_notice(rule: Union[Rule, RuleChecker] = Rule(), def on_request(rule: Union[Rule, RuleChecker] = Rule(), *, - handlers=[], - temp=False, + handlers: Optional[list] = None, + temp: bool = False, priority: int = 1, block: bool = False, - state={}) -> Type[Matcher]: + state: Optional[dict] = None) -> Type[Matcher]: matcher = Matcher.new("request", Rule() & rule, Permission(), diff --git a/tests/test_plugins/test_package/test_command.py b/tests/test_plugins/test_package/test_command.py index 57551450..c2b5f334 100644 --- a/tests/test_plugins/test_package/test_command.py +++ b/tests/test_plugins/test_package/test_command.py @@ -24,6 +24,5 @@ async def test_handler(bot: Bot, event: Event, state: dict): async def test_handler(bot: Bot, event: Event, state: dict): print("[!] Command 帮助:", state["help"]) if state["help"] not in ["test1", "test2"]: - await bot.send_private_msg(message=f"{state['help']} 不支持,请重新输入!") - test_command.reject() + await test_command.reject(f"{state['help']} 不支持,请重新输入!") await bot.send_private_msg(message=f"{state['help']} 帮助:\n...") From cef3a8236e9ccad65ccede29c3336e424d1a5667 Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Tue, 25 Aug 2020 18:02:18 +0800 Subject: [PATCH 4/5] add more logs --- nonebot/__init__.py | 1 + nonebot/adapters/__init__.py | 18 +++++++- nonebot/adapters/cqhttp.py | 23 ++++++++++ nonebot/drivers/fastapi.py | 83 ++++++++++++++++++++++++------------ nonebot/message.py | 20 ++++++++- 5 files changed, 115 insertions(+), 30 deletions(-) diff --git a/nonebot/__init__.py b/nonebot/__init__.py index 117dda4c..f5578073 100644 --- a/nonebot/__init__.py +++ b/nonebot/__init__.py @@ -147,6 +147,7 @@ def init(*, _env_file: Optional[str] = None, **kwargs): """ global _driver env = Env() + logger.debug(f"Current Env: {env.environment}") config = Config(**kwargs, _env_file=_env_file or f".env.{env.environment}") logger.setLevel(logging.DEBUG if config.debug else logging.INFO) diff --git a/nonebot/adapters/__init__.py b/nonebot/adapters/__init__.py index 2903b0f9..6e08553f 100644 --- a/nonebot/adapters/__init__.py +++ b/nonebot/adapters/__init__.py @@ -54,18 +54,32 @@ class BaseEvent(abc.ABC): self._raw_event = raw_event def __repr__(self) -> str: - # TODO: pretty print - return f"" + return f"" @property def raw_event(self) -> dict: return self._raw_event + @property + @abc.abstractmethod + def id(self) -> int: + raise NotImplementedError + + @property + @abc.abstractmethod + def name(self) -> str: + raise NotImplementedError + @property @abc.abstractmethod def self_id(self) -> str: raise NotImplementedError + @property + @abc.abstractmethod + def time(self) -> int: + raise NotImplementedError + @property @abc.abstractmethod def type(self) -> str: diff --git a/nonebot/adapters/cqhttp.py b/nonebot/adapters/cqhttp.py index b6332d8e..9e5ba061 100644 --- a/nonebot/adapters/cqhttp.py +++ b/nonebot/adapters/cqhttp.py @@ -168,9 +168,14 @@ class Bot(BaseBot): if not message: return + if "post_type" not in message: + ResultStore.add_result(message) + return + event = Event(message) # Check whether user is calling me + # TODO: Check reply _check_at_me(self, event) _check_nickname(self, event) @@ -262,11 +267,29 @@ class Event(BaseEvent): super().__init__(raw_event) + @property + @overrides(BaseEvent) + def id(self) -> Optional[int]: + return self._raw_event.get("message_id") or self._raw_event.get("flag") + + @property + @overrides(BaseEvent) + def name(self) -> str: + n = self.type + "." + self.detail_type + if self.sub_type: + n += "." + self.sub_type + return n + @property @overrides(BaseEvent) def self_id(self) -> str: return str(self._raw_event["self_id"]) + @property + @overrides(BaseEvent) + def time(self) -> int: + return self._raw_event["time"] + @property @overrides(BaseEvent) def type(self) -> str: diff --git a/nonebot/drivers/fastapi.py b/nonebot/drivers/fastapi.py index 3eb6b9bc..7dcb0527 100644 --- a/nonebot/drivers/fastapi.py +++ b/nonebot/drivers/fastapi.py @@ -1,19 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +import hmac import json import logging import uvicorn +from fastapi.responses import Response from fastapi import Body, status, Header, FastAPI, Depends, HTTPException from starlette.websockets import WebSocketDisconnect, WebSocket as FastAPIWebSocket from nonebot.log import logger from nonebot.config import Env, Config from nonebot.utils import DataclassEncoder -from nonebot.adapters.cqhttp import Bot as CQBot from nonebot.drivers import BaseDriver, BaseWebSocket -from nonebot.typing import Union, Optional, Callable, overrides +from nonebot.typing import Optional, Callable, overrides def get_auth_bearer(access_token: Optional[str] = Header( @@ -116,28 +117,50 @@ class Driver(BaseDriver): **kwargs) @overrides(BaseDriver) - async def _handle_http( - self, - adapter: str, - data: dict = Body(...), - x_self_id: str = Header(None), - access_token: Optional[str] = Depends(get_auth_bearer)): - secret = self.config.secret - if secret is not None and secret != access_token: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, - detail="Not authenticated", - headers={"WWW-Authenticate": "Bearer"}) + async def _handle_http(self, + adapter: str, + data: dict = Body(...), + x_self_id: Optional[str] = Header(None), + x_signature: Optional[str] = Header(None)): + # 检查self_id + if not x_self_id: + logger.warning("Missing X-Self-ID Header") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, + detail="Missing X-Self-ID Header") - # Create Bot Object + # 检查签名 + secret = self.config.secret + if secret: + if not x_signature: + logger.warning("Missing Signature Header") + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, + detail="Missing Signature") + sig = hmac.new(secret.encode("utf-8"), + json.dumps(data).encode(), "sha1").hexdigest() + if x_signature != "sha1=" + sig: + logger.warning("Signature Header is invalid") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, + detail="Signature is invalid") + + if not isinstance(data, dict): + logger.warning("Data received is invalid") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) + + if x_self_id in self._clients: + logger.warning("There's already a reverse websocket api connection," + "so the event may be handled twice.") + + # 创建 Bot 对象 if adapter in self._adapters: BotClass = self._adapters[adapter] bot = BotClass(self, "http", self.config, x_self_id) else: + logger.warning("Unknown adapter") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="adapter not found") await bot.handle_message(data) - return {"status": 200, "message": "success"} + return Response("", 204) @overrides(BaseDriver) async def _handle_ws_reverse( @@ -146,19 +169,21 @@ class Driver(BaseDriver): websocket: FastAPIWebSocket, x_self_id: str = Header(None), access_token: Optional[str] = Depends(get_auth_bearer)): + ws = WebSocket(websocket) + secret = self.config.secret if secret is not None and secret != access_token: - await websocket.close(code=status.WS_1008_POLICY_VIOLATION) - - websocket = WebSocket(websocket) + logger.warning("Authorization Header is invalid" + if access_token else "Missing Authorization Header") + await ws.close(code=status.WS_1008_POLICY_VIOLATION) if not x_self_id: - logger.error(f"Error Connection Unkown: self_id {x_self_id}") - await websocket.close(code=status.WS_1008_POLICY_VIOLATION) + logger.warning(f"Missing X-Self-ID Header") + await ws.close(code=status.WS_1008_POLICY_VIOLATION) if x_self_id in self._clients: - logger.error(f"Error Connection Conflict: self_id {x_self_id}") - await websocket.close(code=status.WS_1008_POLICY_VIOLATION) + logger.warning(f"Connection Conflict: self_id {x_self_id}") + await ws.close(code=status.WS_1008_POLICY_VIOLATION) # Create Bot Object if adapter in self._adapters: @@ -167,17 +192,18 @@ class Driver(BaseDriver): "websocket", self.config, x_self_id, - websocket=websocket) + websocket=ws) else: + logger.warning("Unknown adapter") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="adapter not found") - await websocket.accept() + await ws.accept() self._clients[x_self_id] = bot try: - while not websocket.closed: - data = await websocket.receive() + while not ws.closed: + data = await ws.receive() if not data: continue @@ -213,8 +239,11 @@ class WebSocket(BaseWebSocket): data = None try: data = await self.websocket.receive_json() + if not isinstance(data, dict): + data = None + raise ValueError except ValueError: - logger.debug("Received an invalid json message.") + logger.warning("Received an invalid json message.") except WebSocketDisconnect: self._closed = True logger.error("WebSocket disconnected by peer.") diff --git a/nonebot/message.py b/nonebot/message.py index 0e883438..d405f6f8 100644 --- a/nonebot/message.py +++ b/nonebot/message.py @@ -53,15 +53,31 @@ async def _run_matcher(Matcher: Type[Matcher], bot: Bot, event: Event, async def handle_event(bot: Bot, event: Event): + log_msg = f"{bot.type.upper()} Bot {event.self_id} [{event.name}]: " + if event.type == "message": + log_msg += f"Message {event.id} from " + log_msg += str(event.user_id) + if event.detail_type == "group": + log_msg += f"@[群:{event.group_id}]: " + log_msg += repr(str(event.message)) + elif event.type == "notice": + log_msg += f"Notice {event.raw_event}" + elif event.type == "request": + log_msg += f"Request {event.raw_event}" + elif event.type == "meta_event": + log_msg += f"MetaEvent {event.raw_event}" + logger.info(log_msg) + coros = [] state = {} for preprocessor in _event_preprocessors: coros.append(preprocessor(bot, event, state)) if coros: try: + logger.debug("Running PreProcessors...") await asyncio.gather(*coros) except IgnoredException: - logger.info(f"Event {event} is ignored") + logger.info(f"Event {event.name} is ignored") return # Trie Match @@ -77,6 +93,7 @@ async def handle_event(bot: Bot, event: Event): for matcher in matchers[priority] ] + logger.debug(f"Checking for all matchers in priority {priority}...") results = await asyncio.gather(*pending_tasks, return_exceptions=True) i = 0 @@ -85,6 +102,7 @@ async def handle_event(bot: Bot, event: Event): e_list = result.exceptions if StopPropagation in e_list: break_flag = True + logger.debug("Stop event propafation") if ExpiredException in e_list: del matchers[priority][index - i] i += 1 From 24e03ed0e76fde41fa7c7b340046b1108975434d Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Wed, 26 Aug 2020 14:43:27 +0800 Subject: [PATCH 5/5] :arrow_up: Update to cqhttp v11 --- nonebot/adapters/__init__.py | 2 +- nonebot/adapters/cqhttp.py | 169 +++++++++++++++++++---------------- package.json | 10 ++- 3 files changed, 98 insertions(+), 83 deletions(-) diff --git a/nonebot/adapters/__init__.py b/nonebot/adapters/__init__.py index 6e08553f..efc34acc 100644 --- a/nonebot/adapters/__init__.py +++ b/nonebot/adapters/__init__.py @@ -179,7 +179,7 @@ class BaseEvent(abc.ABC): @dataclass class BaseMessageSegment(abc.ABC): type: str - data: Dict[str, str] = field(default_factory=lambda: {}) + data: Dict[str, Union[str, list]] = field(default_factory=lambda: {}) @abc.abstractmethod def __str__(self): diff --git a/nonebot/adapters/cqhttp.py b/nonebot/adapters/cqhttp.py index 9e5ba061..bb9ec12d 100644 --- a/nonebot/adapters/cqhttp.py +++ b/nonebot/adapters/cqhttp.py @@ -1,5 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +""" +CQHTTP (OneBot) v11 协议适配 +============================ + +协议详情请看: `CQHTTP`_ | `OneBot`_ + +.. _CQHTTP: + http://cqhttp.cc/ +.. _OneBot: + https://github.com/howmanybots/onebot +""" import re import sys @@ -38,8 +49,8 @@ def unescape(s: str) -> str: .replace("&", "&") -def _b2s(b: bool) -> str: - return str(b).lower() +def _b2s(b: Optional[bool]) -> Optional[str]: + return b if b is None else str(b).lower() def _check_at_me(bot: "Bot", event: "Event"): @@ -389,14 +400,8 @@ class Event(BaseEvent): class MessageSegment(BaseMessageSegment): @overrides(BaseMessageSegment) - def __init__(self, type: str, data: Dict[str, str]) -> None: - if type == "at" and data.get("qq") == "all": - type = "at_all" - data.clear() - elif type == "shake": - type = "poke" - data = {"type": "Poke"} - elif type == "text": + def __init__(self, type: str, data: Dict[str, Union[str, list]]) -> None: + if type == "text": data["text"] = unescape(data["text"]) super().__init__(type=type, data=data) @@ -406,16 +411,11 @@ class MessageSegment(BaseMessageSegment): data = self.data.copy() # process special types - if type_ == "at_all": - type_ = "at" - data = {"qq": "all"} - elif type_ == "poke": - type_ = "shake" - data.clear() - elif type_ == "text": + if type_ == "text": return escape(data.get("text", ""), escape_comma=False) - params = ",".join([f"{k}={escape(str(v))}" for k, v in data.items()]) + params = ",".join( + [f"{k}={escape(str(v))}" for k, v in data.items() if v is not None]) return f"[CQ:{type_}{',' if params else ''}{params}]" @overrides(BaseMessageSegment) @@ -423,17 +423,13 @@ class MessageSegment(BaseMessageSegment): return Message(self) + other @staticmethod - def anonymous(ignore_failure: bool = False) -> "MessageSegment": + def anonymous(ignore_failure: Optional[bool] = None) -> "MessageSegment": return MessageSegment("anonymous", {"ignore": _b2s(ignore_failure)}) @staticmethod def at(user_id: Union[int, str]) -> "MessageSegment": return MessageSegment("at", {"qq": str(user_id)}) - @staticmethod - def at_all() -> "MessageSegment": - return MessageSegment("at_all") - @staticmethod def contact_group(group_id: int) -> "MessageSegment": return MessageSegment("contact", {"type": "group", "id": str(group_id)}) @@ -442,23 +438,43 @@ class MessageSegment(BaseMessageSegment): def contact_user(user_id: int) -> "MessageSegment": return MessageSegment("contact", {"type": "qq", "id": str(user_id)}) + @staticmethod + def dice() -> "MessageSegment": + return MessageSegment("dice", {}) + @staticmethod def face(id_: int) -> "MessageSegment": return MessageSegment("face", {"id": str(id_)}) @staticmethod def forward(id_: str) -> "MessageSegment": + logger.warning("Forward Message only can be received!") return MessageSegment("forward", {"id": id_}) @staticmethod - def image(file: str) -> "MessageSegment": - return MessageSegment("image", {"file": file}) + def image(file: str, + type_: Optional[str] = None, + cache: bool = True, + proxy: bool = True, + timeout: Optional[int] = None) -> "MessageSegment": + return MessageSegment( + "image", { + "file": file, + "type": type_, + "cache": cache, + "proxy": proxy, + "timeout": timeout + }) + + @staticmethod + def json(data: str) -> "MessageSegment": + return MessageSegment("json", {"data": data}) @staticmethod def location(latitude: float, longitude: float, - title: str = "", - content: str = "") -> "MessageSegment": + title: Optional[str] = None, + content: Optional[str] = None) -> "MessageSegment": return MessageSegment( "location", { "lat": str(latitude), @@ -468,36 +484,18 @@ class MessageSegment(BaseMessageSegment): }) @staticmethod - def magic_face(type_: str) -> "MessageSegment": - if type_ not in ["dice", "rpc"]: - raise ValueError( - f"Coolq doesn't support magic face type {type_}. Supported types: dice, rpc." - ) - return MessageSegment("magic_face", {"type": type_}) + def music(type_: str, id_: int) -> "MessageSegment": + return MessageSegment("music", {"type": type_, "id": id_}) @staticmethod - def music(type_: str, - id_: int, - style: Optional[int] = None) -> "MessageSegment": - if style is None: - return MessageSegment("music", {"type": type_, "id": id_}) - else: - return MessageSegment("music", { - "type": type_, - "id": id_, - "style": style - }) - - @staticmethod - def music_custom(type_: str, - url: str, + def music_custom(url: str, audio: str, title: str, - content: str = "", - img_url: str = "") -> "MessageSegment": + content: Optional[str] = None, + img_url: Optional[str] = None) -> "MessageSegment": return MessageSegment( "music", { - "type": type_, + "type": "custom", "url": url, "audio": audio, "title": title, @@ -510,35 +508,43 @@ class MessageSegment(BaseMessageSegment): return MessageSegment("node", {"id": str(id_)}) @staticmethod - def node_custom(name: str, uin: int, - content: "Message") -> "MessageSegment": + def node_custom(user_id: int, nickname: str, + content: Union[str, "Message"]) -> "MessageSegment": return MessageSegment("node", { - "name": name, - "uin": str(uin), - "content": str(content) + "user_id": str(user_id), + "nickname": nickname, + "content": content }) @staticmethod - def poke(type_: str = "Poke") -> "MessageSegment": - if type_ not in ["Poke"]: - raise ValueError( - f"Coolq doesn't support poke type {type_}. Supported types: Poke." - ) - return MessageSegment("poke", {"type": type_}) + def poke(type_: str, id_: str) -> "MessageSegment": + return MessageSegment("poke", {"type": type_, "id": id_}) @staticmethod - def record(file: str, magic: bool = False) -> "MessageSegment": + def record(file: str, + magic: Optional[bool] = None, + cache: Optional[bool] = None, + proxy: Optional[bool] = None, + timeout: Optional[int] = None) -> "MessageSegment": return MessageSegment("record", {"file": file, "magic": _b2s(magic)}) @staticmethod - def replay(id_: int) -> "MessageSegment": - return MessageSegment("replay", {"id": str(id_)}) + def reply(id_: int) -> "MessageSegment": + return MessageSegment("reply", {"id": str(id_)}) + + @staticmethod + def rps() -> "MessageSegment": + return MessageSegment("rps", {}) + + @staticmethod + def shake() -> "MessageSegment": + return MessageSegment("shake", {}) @staticmethod def share(url: str = "", title: str = "", - content: str = "", - img_url: str = "") -> "MessageSegment": + content: Optional[str] = None, + img_url: Optional[str] = None) -> "MessageSegment": return MessageSegment("share", { "url": url, "title": title, @@ -550,6 +556,22 @@ class MessageSegment(BaseMessageSegment): def text(text: str) -> "MessageSegment": return MessageSegment("text", {"text": text}) + @staticmethod + def video(file: str, + cache: Optional[bool] = None, + proxy: Optional[bool] = None, + timeout: Optional[int] = None) -> "MessageSegment": + return MessageSegment("video", { + "file": file, + "cache": cache, + "proxy": proxy, + "timeout": timeout + }) + + @staticmethod + def xml(data: str) -> "MessageSegment": + return MessageSegment("xml", {"data": data}) + class Message(BaseMessage): @@ -564,7 +586,7 @@ class Message(BaseMessage): yield MessageSegment(seg["type"], seg.get("data") or {}) return - def _iter_message() -> Iterable[Tuple[str, str]]: + def _iter_message(msg: str) -> Iterable[Tuple[str, str]]: text_begin = 0 for cqcode in re.finditer( r"\[CQ:(?P[a-zA-Z0-9-_.]+)" @@ -577,7 +599,7 @@ class Message(BaseMessage): yield cqcode.group("type"), cqcode.group("params").lstrip(",") yield "text", unescape(msg[text_begin:]) - for type_, data in _iter_message(): + for type_, data in _iter_message(msg): if type_ == "text": if data: # only yield non-empty text segment @@ -589,13 +611,4 @@ class Message(BaseMessage): filter(lambda x: x, ( x.lstrip() for x in data.split(",")))) } - if type_ == "at" and data["qq"] == "all": - type_ = "at_all" - data.clear() - elif type_ in ["dice", "rpc"]: - type_ = "magic_face" - data["type"] = type_ - elif type_ == "shake": - type_ = "poke" - data["type"] = "Poke" yield MessageSegment(type_, data) diff --git a/package.json b/package.json index ecc13c8c..0baf5f4d 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,12 @@ "description": "An asynchronous python bot framework.", "homepage": "https://docs.nonebot.dev/", "main": "index.js", - "contributors": [{ - "name": "yanyongyu", - "email": "yanyongyu_1@126.com" - }], + "contributors": [ + { + "name": "yanyongyu", + "email": "yanyongyu_1@126.com" + } + ], "repository": "https://github.com/nonebot/nonebot/", "bugs": { "url": "https://github.com/nonebot/nonebot/issues"