diff --git a/nonebot/adapters/__init__.py b/nonebot/adapters/__init__.py index 9d0346af..30d6a788 100644 --- a/nonebot/adapters/__init__.py +++ b/nonebot/adapters/__init__.py @@ -1,8 +1,47 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +from typing import Any, Dict, Optional + +from nonebot.config import Config + class BaseBot(object): - def __init__(self): + def __init__(self, type: str, config: Config, *, websocket=None): raise NotImplementedError + + async def handle_message(self, message: dict): + raise NotImplementedError + + async def call_api(self, api: str, data: dict): + raise NotImplementedError + + +class BaseMessageSegment(dict): + + def __init__(self, + d: Optional[Dict[str, Any]] = None, + *, + type_: Optional[str] = None, + data: Optional[Dict[str, str]] = None): + super().__init__() + if isinstance(d, dict) and d.get('type'): + self.update(d) + elif type_: + self.type = type_ + self.data = data + else: + raise ValueError('the "type" field cannot be None or empty') + + def __str__(self): + raise NotImplementedError + + +class BaseMessage(list): + + def __init__(self, message: str = None): + raise NotImplementedError + + def __str__(self): + return ''.join((str(seg) for seg in self)) diff --git a/nonebot/adapters/coolq.py b/nonebot/adapters/coolq.py index 113be4a3..8e7630c1 100644 --- a/nonebot/adapters/coolq.py +++ b/nonebot/adapters/coolq.py @@ -1,10 +1,40 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from nonebot.adapters import BaseBot +import httpx + +from nonebot.event import Event +from nonebot.config import Config +from nonebot.adapters import BaseBot, BaseMessage, BaseMessageSegment +from nonebot.message import handle_event class Bot(BaseBot): - def __init__(self): - pass + def __init__(self, type_: str, config: Config, *, websocket=None): + if type_ not in ["http", "websocket"]: + raise ValueError("Unsupported connection type") + self.type = type_ + self.config = config + self.websocket = websocket + + async def handle_message(self, message: dict): + # TODO: convert message into event + event = Event.from_payload(message) + + # TODO: Handle Meta Event + await handle_event(self, event) + + async def call_api(self, api: str, data: dict): + if self.type == "websocket": + pass + elif self.type == "http": + pass + + +class MessageSegment(BaseMessageSegment): + pass + + +class Message(BaseMessage): + pass diff --git a/nonebot/drivers/__init__.py b/nonebot/drivers/__init__.py index 15d8a8f1..b8f6c207 100644 --- a/nonebot/drivers/__init__.py +++ b/nonebot/drivers/__init__.py @@ -4,10 +4,12 @@ from typing import Optional from ipaddress import IPv4Address +from nonebot.config import Config + class BaseDriver(object): - def __init__(self): + def __init__(self, config: Config): raise NotImplementedError @property @@ -34,3 +36,6 @@ class BaseDriver(object): async def _handle_ws_reverse(self): raise NotImplementedError + + async def _handle_http_api(self): + raise NotImplementedError diff --git a/nonebot/drivers/fastapi.py b/nonebot/drivers/fastapi.py index fde0c146..a30fc87e 100644 --- a/nonebot/drivers/fastapi.py +++ b/nonebot/drivers/fastapi.py @@ -7,15 +7,18 @@ from typing import Optional from ipaddress import IPv4Address import uvicorn +from starlette.websockets import WebSocketDisconnect from fastapi import Body, FastAPI, WebSocket from nonebot.log import logger +from nonebot.config import Config from nonebot.drivers import BaseDriver +from nonebot.adapters.coolq import Bot as CoolQBot class Driver(BaseDriver): - def __init__(self, config): + def __init__(self, config: Config): self._server_app = FastAPI( debug=config.debug, openapi_url=None, @@ -25,8 +28,10 @@ class Driver(BaseDriver): self.config = config - self._server_app.post("/coolq/")(self._handle_http) - self._server_app.websocket("/coolq/ws")(self._handle_ws_reverse) + self._server_app.post("/{adapter}/")(self._handle_http) + self._server_app.post("/{adapter}/http")(self._handle_http) + self._server_app.websocket("/{adapter}/ws")(self._handle_ws_reverse) + self._server_app.websocket("/{adapter}/ws/")(self._handle_ws_reverse) @property def server_app(self): @@ -81,11 +86,16 @@ class Driver(BaseDriver): log_config=LOGGING_CONFIG, **kwargs) - async def _handle_http(self, data: dict = Body(...)): + async def _handle_http(self, adapter: str, data: dict = Body(...)): + # TODO: Check authorization logger.debug(f"Received message: {data}") + if adapter == "coolq": + bot = CoolQBot("http", self.config) + await bot.handle_message(data) return {"status": 200, "message": "success"} - async def _handle_ws_reverse(self, websocket: WebSocket): + async def _handle_ws_reverse(self, adapter: str, websocket: WebSocket): + # TODO: Check authorization await websocket.accept() while True: try: @@ -93,5 +103,11 @@ class Driver(BaseDriver): except json.decoder.JSONDecodeError as e: logger.exception(e) continue + except WebSocketDisconnect: + logger.error("WebSocket Disconnect") + return logger.debug(f"Received message: {data}") + if adapter == "coolq": + bot = CoolQBot("websocket", self.config, websocket=websocket) + await bot.handle_message(data) diff --git a/nonebot/matcher.py b/nonebot/matcher.py index 43914679..89e22b56 100644 --- a/nonebot/matcher.py +++ b/nonebot/matcher.py @@ -94,7 +94,6 @@ class Matcher: def _decorator(func: Handler) -> Handler: - @wraps(func) async def _handler(bot, event: Event, state: dict): raise PausedException diff --git a/nonebot/message.py b/nonebot/message.py index f554261e..032615b8 100644 --- a/nonebot/message.py +++ b/nonebot/message.py @@ -6,13 +6,19 @@ from nonebot.event import Event from nonebot.matcher import matchers -async def handle_message(bot, event: Event): +async def handle_event(bot, event: Event): # TODO: PreProcess for priority in sorted(matchers.keys()): for index in range(len(matchers[priority])): Matcher = matchers[priority][index] - if not Matcher.check_rule(event): + try: + if not Matcher.check_rule(event): + continue + except Exception as e: + logger.error( + f"Rule check failed for matcher {Matcher}. Ignored.") + logger.exception(e) continue matcher = Matcher() @@ -22,5 +28,6 @@ async def handle_message(bot, event: Event): try: await matcher.run(bot, event) except Exception as e: + logger.error(f"Running matcher {matcher} failed.") logger.exception(e) return diff --git a/poetry.lock b/poetry.lock index f5277e89..433f5c55 100644 --- a/poetry.lock +++ b/poetry.lock @@ -60,7 +60,7 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" [[package]] -category = "dev" +category = "main" description = "Python package for providing Mozilla's CA Bundle." name = "certifi" optional = false @@ -73,7 +73,7 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" [[package]] -category = "dev" +category = "main" description = "Universal encoding detector for Python 2 and 3" name = "chardet" optional = false @@ -161,6 +161,49 @@ reference = "aliyun" type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" +[[package]] +category = "main" +description = "HTTP/2 State-Machine based protocol implementation" +name = "h2" +optional = false +python-versions = "*" +version = "3.2.0" + +[package.dependencies] +hpack = ">=3.0,<4" +hyperframe = ">=5.2.0,<6" + +[package.source] +reference = "aliyun" +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" + +[[package]] +category = "main" +description = "Pure-Python HPACK header compression" +name = "hpack" +optional = false +python-versions = "*" +version = "3.0.0" + +[package.source] +reference = "aliyun" +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" + +[[package]] +category = "main" +description = "Chromium HSTS Preload list as a Python package and updated daily" +name = "hstspreload" +optional = false +python-versions = ">=3.6" +version = "2020.7.7" + +[package.source] +reference = "aliyun" +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" + [[package]] category = "dev" description = "Turn HTML into equivalent Markdown-structured text." @@ -174,6 +217,24 @@ reference = "aliyun" type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" +[[package]] +category = "main" +description = "A minimal low-level HTTP client." +name = "httpcore" +optional = false +python-versions = ">=3.6" +version = "0.9.1" + +[package.dependencies] +h11 = ">=0.8,<0.10" +h2 = ">=3.0.0,<4.0.0" +sniffio = ">=1.0.0,<2.0.0" + +[package.source] +reference = "aliyun" +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" + [[package]] category = "main" description = "A collection of framework independent HTTP protocol utils." @@ -192,7 +253,42 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" [[package]] -category = "dev" +category = "main" +description = "The next generation HTTP client." +name = "httpx" +optional = false +python-versions = ">=3.6" +version = "0.13.3" + +[package.dependencies] +certifi = "*" +chardet = ">=3.0.0,<4.0.0" +hstspreload = "*" +httpcore = ">=0.9.0,<0.10.0" +idna = ">=2.0.0,<3.0.0" +rfc3986 = ">=1.3,<2" +sniffio = "*" + +[package.source] +reference = "aliyun" +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" + +[[package]] +category = "main" +description = "HTTP/2 framing layer for Python" +name = "hyperframe" +optional = false +python-versions = "*" +version = "5.2.0" + +[package.source] +reference = "aliyun" +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" + +[[package]] +category = "main" description = "Internationalized Domain Names in Applications (IDNA)" name = "idna" optional = false @@ -383,6 +479,22 @@ reference = "aliyun" type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" +[[package]] +category = "main" +description = "Validating URI References per RFC 3986" +name = "rfc3986" +optional = false +python-versions = "*" +version = "1.4.0" + +[package.extras] +idna2008 = ["idna"] + +[package.source] +reference = "aliyun" +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" + [[package]] category = "main" description = "Python 2 and 3 compatibility utilities" @@ -396,6 +508,19 @@ reference = "aliyun" type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" +[[package]] +category = "main" +description = "Sniff out which async library your code is running under" +name = "sniffio" +optional = false +python-versions = ">=3.5" +version = "1.1.0" + +[package.source] +reference = "aliyun" +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" + [[package]] category = "dev" description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." @@ -713,7 +838,7 @@ url = "https://mirrors.aliyun.com/pypi/simple" scheduler = ["apscheduler"] [metadata] -content-hash = "5dc37a3a06ef422bb885c2f6b09179964e083f2005bf8873349431ebc4508152" +content-hash = "e98c07c170a70d27c0cb15dd6b4cfd10e678a1df55bd3c3bebbbe26035b88a24" python-versions = "^3.7" [metadata.files] @@ -757,10 +882,26 @@ h11 = [ {file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"}, {file = "h11-0.9.0.tar.gz", hash = "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1"}, ] +h2 = [ + {file = "h2-3.2.0-py2.py3-none-any.whl", hash = "sha256:61e0f6601fa709f35cdb730863b4e5ec7ad449792add80d1410d4174ed139af5"}, + {file = "h2-3.2.0.tar.gz", hash = "sha256:875f41ebd6f2c44781259005b157faed1a5031df3ae5aa7bcb4628a6c0782f14"}, +] +hpack = [ + {file = "hpack-3.0.0-py2.py3-none-any.whl", hash = "sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89"}, + {file = "hpack-3.0.0.tar.gz", hash = "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"}, +] +hstspreload = [ + {file = "hstspreload-2020.7.7-py3-none-any.whl", hash = "sha256:051a752188c3422558a1302be99b742a2dd9d7568419614104ae7e87233f9d63"}, + {file = "hstspreload-2020.7.7.tar.gz", hash = "sha256:3e1b107d6c865fc28f0f023456f193f2e916d14bca5a16c93fe440bef90c7c58"}, +] html2text = [ {file = "html2text-2020.1.16-py3-none-any.whl", hash = "sha256:c7c629882da0cf377d66f073329ccf34a12ed2adf0169b9285ae4e63ef54c82b"}, {file = "html2text-2020.1.16.tar.gz", hash = "sha256:e296318e16b059ddb97f7a8a1d6a5c1d7af4544049a01e261731d2d5cc277bbb"}, ] +httpcore = [ + {file = "httpcore-0.9.1-py3-none-any.whl", hash = "sha256:9850fe97a166a794d7e920590d5ec49a05488884c9fc8b5dba8561effab0c2a0"}, + {file = "httpcore-0.9.1.tar.gz", hash = "sha256:ecc5949310d9dae4de64648a4ce529f86df1f232ce23dcfefe737c24d21dfbe9"}, +] httptools = [ {file = "httptools-0.1.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce"}, {file = "httptools-0.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4"}, @@ -775,6 +916,14 @@ httptools = [ {file = "httptools-0.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be"}, {file = "httptools-0.1.1.tar.gz", hash = "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce"}, ] +httpx = [ + {file = "httpx-0.13.3-py3-none-any.whl", hash = "sha256:32d930858eab677bc29a742aaa4f096de259f1c78c68a90ad11f5c3c04f08335"}, + {file = "httpx-0.13.3.tar.gz", hash = "sha256:3642bd13e90b80ba8a243a730275eb10a4c26ec96f5fc16b87e458d4ab21efae"}, +] +hyperframe = [ + {file = "hyperframe-5.2.0-py2.py3-none-any.whl", hash = "sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40"}, + {file = "hyperframe-5.2.0.tar.gz", hash = "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"}, +] idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, @@ -869,10 +1018,18 @@ requests = [ {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, ] +rfc3986 = [ + {file = "rfc3986-1.4.0-py2.py3-none-any.whl", hash = "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"}, + {file = "rfc3986-1.4.0.tar.gz", hash = "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d"}, +] six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, ] +sniffio = [ + {file = "sniffio-1.1.0-py3-none-any.whl", hash = "sha256:20ed6d5b46f8ae136d00b9dcb807615d83ed82ceea6b2058cecb696765246da5"}, + {file = "sniffio-1.1.0.tar.gz", hash = "sha256:8e3810100f69fe0edd463d02ad407112542a11ffdc29f67db2bf3771afb87a21"}, +] snowballstemmer = [ {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, diff --git a/pyproject.toml b/pyproject.toml index d24450cb..1ed34686 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ fastapi = "^0.58.1" uvicorn = "^0.11.5" pydantic = {extras = ["dotenv"], version = "^1.5.1"} apscheduler = { version = "^3.6.3", optional = true } +httpx = "^0.13.3" [tool.poetry.dev-dependencies] yapf = "^0.30.0" diff --git a/tests/bot.py b/tests/bot.py index 282fbb2f..72e3238c 100644 --- a/tests/bot.py +++ b/tests/bot.py @@ -13,8 +13,6 @@ nonebot.init() app = nonebot.get_asgi() nonebot.load_plugins("test_plugins") -print(nonebot.get_loaded_plugins()) -print(matchers) if __name__ == "__main__": nonebot.run(app="bot:app") diff --git a/tests/test_plugins/test_matcher.py b/tests/test_plugins/test_matcher.py index 5a1bb3ea..08472417 100644 --- a/tests/test_plugins/test_matcher.py +++ b/tests/test_plugins/test_matcher.py @@ -10,4 +10,12 @@ test_matcher = on_message(Rule(), state={"default": 1}) @test_matcher.handle() async def test_handler(bot, event: Event, state: dict): - print(state) + print("Test Matcher Received:", event) + print("Current State:", state) + state["message1"] = event.get("raw_message") + + +@test_matcher.receive() +async def test_receive(bot, event: Event, state: dict): + print("Test Matcher Received next time:", event) + print("Current State:", state)